mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-11-05 12:48:14 +00:00
Compare commits
250 Commits
9ef4f91ec4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7330aeb8ec | |||
| d3aad632c0 | |||
| d1bad3d7a6 | |||
| 43056a8b92 | |||
| 0bf286f62a | |||
| df8390f386 | |||
| 48557b06e3 | |||
| 1cff5778d3 | |||
| 60e2c972d6 | |||
| 637de6a190 | |||
| f5efbce205 | |||
| d6f3618d70 | |||
| 773655efb5 | |||
| 7bc9f7abd9 | |||
| ec7b8662dd | |||
| d1ccfd9cdd | |||
| d61c81634c | |||
| 265f815b48 | |||
| f8e5110730 | |||
| 37b213f96a | |||
| 5ef525eac9 | |||
| 295ae7e477 | |||
| c67ccc1df6 | |||
| cb483f60d1 | |||
| 2be73502ca | |||
| 57d5269b07 | |||
| 1eefdea050 | |||
| 561160504e | |||
| 9a4bf91276 | |||
| 468b6e734c | |||
| 83cb94b6ff | |||
| 6857295969 | |||
| 8ab398f679 | |||
| 31133ddd90 | |||
| 783b1e152d | |||
| eca567fefd | |||
| 905f461ee8 | |||
| 9f0b259ba9 | |||
| 06e4323faa | |||
| 3d99226f37 | |||
| 73ba09fbe2 | |||
| 01ea9b76ce | |||
| c22acf202f | |||
| 61e138c1a6 | |||
| 07c8e036ec | |||
| 0b36059cd2 | |||
| d76e384ae3 | |||
| e6f4f3a6a4 | |||
| a80b26ed9e | |||
| 45ec7b0ead | |||
| ec396d130c | |||
| 93c2fbedd7 | |||
| d006f0ba5e | |||
| dd43722e02 | |||
| 05d7ddc491 | |||
| e54436821c | |||
| ed73a37795 | |||
| adff9271fd | |||
| 2f0fb2cb69 | |||
| 6abf2629e0 | |||
| 6a8e0f38d8 | |||
| ae618cbf19 | |||
| c835ca8f2c | |||
| 087175a3c7 | |||
| 3da645f3b8 | |||
| a996e2190f | |||
| 7dccffd52d | |||
| 853f2c3e2d | |||
| b2978a3141 | |||
| 0e0b703ccd | |||
| 0b86b2f057 | |||
| 80e048a274 | |||
| 2610aec293 | |||
| 07db162368 | |||
| a526d1adc4 | |||
| ca95079111 | |||
| e410d66cb4 | |||
| ab48cf522f | |||
| 41c12bdc12 | |||
| aae463b602 | |||
| bb50551533 | |||
| 098099b41e | |||
| 0a7d767252 | |||
| d88599f76c | |||
| 4d9890406e | |||
| 59b652958f | |||
| a327adf8db | |||
| 7a38cb90fb | |||
| 9d6cf03f5b | |||
| 9439ac7f76 | |||
| 23353ac878 | |||
| 8beda2d45d | |||
| 5773409bd7 | |||
| b3ea962338 | |||
| b9fbf92461 | |||
| 6824e444b0 | |||
| 5cdcc18a99 | |||
| e7702948b8 | |||
| 09a4c243d7 | |||
| 1d5a50abf2 | |||
| 0d99c7f297 | |||
| 0a17e54d8c | |||
| bf94338845 | |||
| 5d42b78b3d | |||
| 26a1992d84 | |||
| 2439beb95a | |||
| 251f7b227d | |||
| 3fbb9c38a8 | |||
| 29e8b3a590 | |||
| 27b89d8fb6 | |||
| 55f2d15e93 | |||
| aa19a97ed6 | |||
| c06d1c4d17 | |||
| 66f294537d | |||
| a9097a3ec3 | |||
| fc59c64273 | |||
| dbbb3510f3 | |||
| eb3bf543a4 | |||
| 4f5602c791 | |||
| 75d476267e | |||
| c3e5db7f2e | |||
| dfd2d243b7 | |||
| 78ad2ea4b6 | |||
| c362e160fc | |||
| a044028e03 | |||
| 7405883b48 | |||
| 85db0a40db | |||
| 8af39c32ec | |||
| 31e86ac0fc | |||
| 4d223f1784 | |||
| 926def3d01 | |||
| 083b7d2914 | |||
| 73a38e0b2b | |||
| e3c0880e98 | |||
| a817d964e4 | |||
| 7572134e9d | |||
| 97af4990aa | |||
| b6d0535173 | |||
| 27d33435f8 | |||
| 3cc4014edf | |||
| 63da669c33 | |||
| fb04a4c7a0 | |||
| 2968ac7f0a | |||
| 1daa53017e | |||
| 9082443753 | |||
| bcee1fecdf | |||
| 0602148caa | |||
| cbfb991e79 | |||
| fa7b1400bd | |||
| c7cae93597 | |||
| 6ea0d09f14 | |||
| 5e4cda0ac9 | |||
| 1d29617f85 | |||
| 7c5ad8e6a1 | |||
| a26538d1b3 | |||
| f55b0ca797 | |||
| 6f3522dc28 | |||
| 5186eb5714 | |||
| 73bcdcaf45 | |||
| 9e402c863f | |||
| 84865d61b8 | |||
| 423850d3e6 | |||
| 598f4e854a | |||
| 1f99a6b84b | |||
| 189aaaa9ec | |||
| ca52dcda43 | |||
| 4f59e8e48b | |||
| a993c153dd | |||
| 8d6ebb4693 | |||
| 567babfdfc | |||
| 18e5f001d0 | |||
| 7d9cb5820f | |||
| c181c7f6cd | |||
| 929cddec0e | |||
| 9ba0efc1a1 | |||
| 9bf77e1e35 | |||
| 426ba32c11 | |||
| ff7b7aeb2d | |||
| c523d8d8d4 | |||
| 12d05ef013 | |||
| 3cbf37d774 | |||
| fc99c72f86 | |||
| 3211dd7cea | |||
| c07a9835fc | |||
| f4cf55b3c8 | |||
| 1b91ddeac2 | |||
| b638d00d73 | |||
| 75c36a1d71 | |||
| 7a119c3175 | |||
| 3e6193ffce | |||
| 9d8e06015f | |||
| 5daf3387bf | |||
| 6da7f28370 | |||
| 208848579d | |||
| d8c73e9fc3 | |||
| 10b20cc3c4 | |||
| 790c184e66 | |||
| 93d165fa4c | |||
| 1f3abb95af | |||
| 7ca3a73f21 | |||
| 08720a43c1 | |||
| 1baed62078 | |||
| 963e1aea21 | |||
| a819a05737 | |||
| 4cb58bec0f | |||
| 002f45d1df | |||
| cbc4dad1d1 | |||
| 70d395ed15 | |||
| e20a709f04 | |||
| d129f71cef | |||
| 4cb428274a | |||
| 97e2d440b2 | |||
| 588cd1959f | |||
| 5d1210d651 | |||
| aeab7e7358 | |||
| fa6bb67a66 | |||
| 3dc2fbd47c | |||
| 4b56ab3d18 | |||
| 8e934677ff | |||
| 0a927f49a2 | |||
| e6803e5614 | |||
| 6cf6c74802 | |||
| 734b8764f2 | |||
| 3edb66f444 | |||
| 181b2d0542 | |||
| 78ebf4d075 | |||
| d523629cdd | |||
| 08ac8b6a9d | |||
| 79db2419a6 | |||
| c424afa935 | |||
| 974a83fe6e | |||
| 0168167769 | |||
| 1c7152ceb2 | |||
| 2a98b265bc | |||
| 14d1362dc8 | |||
| a4a8061998 | |||
| 96ded68ef4 | |||
| 2d8967d559 | |||
| 5e616d3962 | |||
| 0f85d27a4d | |||
| c6677ca61b | |||
| 83ce88a048 | |||
| 7d150fa021 | |||
| 2806aab89e | |||
| 61772d5916 | |||
| a10ba78a5a | |||
| 6854acf204 | |||
| 54d4eeb1ab | |||
| 52fb7accac | |||
| d4c62dbf72 |
6
.github/workflows/test-cli.yml
vendored
6
.github/workflows/test-cli.yml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
- name: Clean build artifacts
|
||||
run: |
|
||||
docker run --rm infinito:latest make clean
|
||||
docker run --rm infinito:latest infinito make clean
|
||||
|
||||
- name: Generate project outputs
|
||||
run: |
|
||||
docker run --rm infinito:latest make build
|
||||
docker run --rm infinito:latest infinito make build
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
docker run --rm infinito:latest make test
|
||||
docker run --rm infinito:latest infinito make test
|
||||
|
||||
@@ -59,11 +59,4 @@ RUN INFINITO_PATH=$(pkgmgr path infinito) && \
|
||||
ln -sf "$INFINITO_PATH"/main.py /usr/local/bin/infinito && \
|
||||
chmod +x /usr/local/bin/infinito
|
||||
|
||||
# 10) Run integration tests
|
||||
# This needed to be deactivated becaus it doesn't work with gitthub workflow
|
||||
#RUN INFINITO_PATH=$(pkgmgr path infinito) && \
|
||||
# cd "$INFINITO_PATH" && \
|
||||
# make test
|
||||
|
||||
ENTRYPOINT ["infinito"]
|
||||
CMD ["--help"]
|
||||
CMD sh -c "infinito --help && exec tail -f /dev/null"
|
||||
|
||||
2
Makefile
2
Makefile
@@ -73,7 +73,7 @@ messy-test:
|
||||
@echo "🧪 Running Python tests…"
|
||||
PYTHONPATH=. python -m unittest discover -s tests
|
||||
@echo "📑 Checking Ansible syntax…"
|
||||
ansible-playbook playbook.yml --syntax-check
|
||||
ansible-playbook -i localhost, -c local $(foreach f,$(wildcard group_vars/all/*.yml),-e @$(f)) playbook.yml --syntax-check
|
||||
|
||||
install: build
|
||||
@echo "⚙️ Install complete."
|
||||
|
||||
12
ansible.cfg
12
ansible.cfg
@@ -1,5 +1,6 @@
|
||||
[defaults]
|
||||
# --- Performance & Behavior ---
|
||||
pipelining = True
|
||||
forks = 25
|
||||
strategy = linear
|
||||
gathering = smart
|
||||
@@ -14,19 +15,14 @@ stdout_callback = yaml
|
||||
callbacks_enabled = profile_tasks,timer
|
||||
|
||||
# --- Plugin paths ---
|
||||
filter_plugins = ./filter_plugins
|
||||
filter_plugins = ./filter_plugins
|
||||
lookup_plugins = ./lookup_plugins
|
||||
module_utils = ./module_utils
|
||||
|
||||
[ssh_connection]
|
||||
# Multiplexing: safer socket path in HOME instead of /tmp
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=20s -o ControlPath=~/.ssh/ansible-%h-%p-%r \
|
||||
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=accept-new \
|
||||
-o PreferredAuthentications=publickey,password,keyboard-interactive
|
||||
|
||||
# Pipelining boosts speed; works fine if sudoers does not enforce "requiretty"
|
||||
ssh_args = -o ControlMaster=auto -o ControlPersist=20s -o ControlPath=~/.ssh/ansible-%h-%p-%r -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o StrictHostKeyChecking=accept-new -o PreferredAuthentications=publickey,password,keyboard-interactive
|
||||
pipelining = True
|
||||
scp_if_ssh = smart
|
||||
transfer_method = smart
|
||||
|
||||
[persistent_connection]
|
||||
connect_timeout = 30
|
||||
|
||||
@@ -83,6 +83,13 @@ class DefaultsGenerator:
|
||||
print(f"Error during rendering: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Sort applications by application key for stable output
|
||||
apps = result.get("defaults_applications", {})
|
||||
if isinstance(apps, dict) and apps:
|
||||
result["defaults_applications"] = {
|
||||
k: apps[k] for k in sorted(apps.keys())
|
||||
}
|
||||
|
||||
# Write output
|
||||
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.output_file.open("w", encoding="utf-8") as f:
|
||||
|
||||
@@ -220,6 +220,10 @@ def main():
|
||||
print(f"Error building user entries: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Sort users by key for deterministic output
|
||||
if isinstance(users, dict) and users:
|
||||
users = OrderedDict(sorted(users.items()))
|
||||
|
||||
# Convert OrderedDict into plain dict for YAML
|
||||
default_users = {'default_users': users}
|
||||
plain_data = dictify(default_users)
|
||||
|
||||
@@ -17,6 +17,7 @@ def run_ansible_playbook(
|
||||
password_file=None,
|
||||
verbose=0,
|
||||
skip_build=False,
|
||||
skip_tests=False,
|
||||
logs=False
|
||||
):
|
||||
start_time = datetime.datetime.now()
|
||||
@@ -56,9 +57,8 @@ def run_ansible_playbook(
|
||||
except subprocess.CalledProcessError:
|
||||
print("\n❌ Inventory validation failed. Deployment aborted.\n", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Tests are controlled via MODE_TEST
|
||||
if modes.get("MODE_TEST", False):
|
||||
|
||||
if not skip_tests:
|
||||
print("\n🧪 Running tests (make messy-test)...\n")
|
||||
subprocess.run(["make", "messy-test"], check=True)
|
||||
|
||||
@@ -255,6 +255,12 @@ def main():
|
||||
action="store_true",
|
||||
help="Skip running 'make build' before deployment.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--skip-tests",
|
||||
action="store_true",
|
||||
help="Skip running 'make messy-tests' before deployment.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--id",
|
||||
@@ -301,6 +307,7 @@ def main():
|
||||
password_file=args.password_file,
|
||||
verbose=args.verbose,
|
||||
skip_build=args.skip_build,
|
||||
skip_tests=args.skip_tests,
|
||||
logs=args.logs,
|
||||
)
|
||||
|
||||
|
||||
60
docker-compose.yml
Normal file
60
docker-compose.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
infinito:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
network: host
|
||||
pull_policy: never
|
||||
container_name: infinito_nexus
|
||||
image: infinito_nexus
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- data:/var/lib/docker/volumes/
|
||||
- backups:/Backups/
|
||||
- letsencrypt:/etc/letsencrypt/
|
||||
ports:
|
||||
# --- Mail services (classic + secure) ---
|
||||
- "${BIND_IP:-127.0.0.1}:25:25" # SMTP
|
||||
- "${BIND_IP:-127.0.0.1}:110:110" # POP3
|
||||
- "${BIND_IP:-127.0.0.1}:143:143" # IMAP
|
||||
- "${BIND_IP:-127.0.0.1}:465:465" # SMTPS
|
||||
- "${BIND_IP:-127.0.0.1}:587:587" # Submission (SMTP)
|
||||
- "${BIND_IP:-127.0.0.1}:993:993" # IMAPS (bound to public IP)
|
||||
- "${BIND_IP:-127.0.0.1}:995:995" # POP3S
|
||||
- "${BIND_IP:-127.0.0.1}:4190:4190" # Sieve (ManageSieve)
|
||||
|
||||
# --- Web / API services ---
|
||||
- "${BIND_IP:-127.0.0.1}:80:80" # HTTP
|
||||
- "${BIND_IP:-127.0.0.1}:443:443" # HTTPS
|
||||
- "${BIND_IP:-127.0.0.1}:8448:8448" # Matrix federation port
|
||||
|
||||
# --- TURN / STUN (UDP + TCP) ---
|
||||
- "${BIND_IP:-127.0.0.1}:3478-3480:3478-3480/udp" # TURN/STUN UDP
|
||||
- "${BIND_IP:-127.0.0.1}:3478-3480:3478-3480" # TURN/STUN TCP
|
||||
|
||||
# --- Streaming / RTMP ---
|
||||
- "${BIND_IP:-127.0.0.1}:1935:1935" # Peertube
|
||||
|
||||
# --- Custom / application ports ---
|
||||
- "${BIND_IP:-127.0.0.1}:2201:2201" # Gitea
|
||||
- "${BIND_IP:-127.0.0.1}:2202:2202" # Gitlab
|
||||
- "${BIND_IP:-127.0.0.1}:2203:22" # SSH
|
||||
- "${BIND_IP:-127.0.0.1}:33552:33552"
|
||||
|
||||
# --- Consecutive ranges ---
|
||||
- "${BIND_IP:-127.0.0.1}:48081-48083:48081-48083"
|
||||
- "${BIND_IP:-127.0.0.1}:48087:48087"
|
||||
volumes:
|
||||
data:
|
||||
backups:
|
||||
letsencrypt:
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: ${SUBNET:-172.30.0.0/24}
|
||||
gateway: ${GATEWAY:-172.30.0.1}
|
||||
3
env.sample
Normal file
3
env.sample
Normal file
@@ -0,0 +1,3 @@
|
||||
BIND_IP=127.0.0.1
|
||||
SUBNET=172.30.0.0/24
|
||||
GATEWAY=172.30.0.1
|
||||
79
filter_plugins/active_docker.py
Normal file
79
filter_plugins/active_docker.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ansible filter to count active docker services for current host.
|
||||
|
||||
Active means:
|
||||
- application key is in group_names
|
||||
- application key matches prefix regex (default: ^(web-|svc-).* )
|
||||
- under applications[app]['docker']['services'] each service is counted if:
|
||||
- 'enabled' is True, OR
|
||||
- 'enabled' is missing/undefined (treated as active)
|
||||
|
||||
Returns an integer. If ensure_min_one=True, returns at least 1.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, Mapping, Iterable
|
||||
|
||||
|
||||
def _is_mapping(x: Any) -> bool:
|
||||
# be liberal: Mapping covers dict-like; fallback to dict check
|
||||
try:
|
||||
return isinstance(x, Mapping)
|
||||
except Exception:
|
||||
return isinstance(x, dict)
|
||||
|
||||
|
||||
def active_docker_container_count(applications: Mapping[str, Any],
|
||||
group_names: Iterable[str],
|
||||
prefix_regex: str = r'^(web-|svc-).*',
|
||||
ensure_min_one: bool = False) -> int:
|
||||
if not _is_mapping(applications):
|
||||
return 1 if ensure_min_one else 0
|
||||
|
||||
group_set = set(group_names or [])
|
||||
try:
|
||||
pattern = re.compile(prefix_regex)
|
||||
except re.error:
|
||||
pattern = re.compile(r'^(web-|svc-).*') # fallback
|
||||
|
||||
count = 0
|
||||
|
||||
for app_key, app_val in applications.items():
|
||||
# host selection + name prefix
|
||||
if app_key not in group_set:
|
||||
continue
|
||||
if not pattern.match(str(app_key)):
|
||||
continue
|
||||
|
||||
docker = app_val.get('docker') if _is_mapping(app_val) else None
|
||||
services = docker.get('services') if _is_mapping(docker) else None
|
||||
if not _is_mapping(services):
|
||||
# sometimes roles define a single service name string; ignore
|
||||
continue
|
||||
|
||||
for _svc_name, svc_cfg in services.items():
|
||||
if not _is_mapping(svc_cfg):
|
||||
# allow shorthand like: service: {} or image string -> counts as enabled
|
||||
count += 1
|
||||
continue
|
||||
enabled = svc_cfg.get('enabled', True)
|
||||
if isinstance(enabled, bool):
|
||||
if enabled:
|
||||
count += 1
|
||||
else:
|
||||
# non-bool enabled -> treat "truthy" as enabled
|
||||
if bool(enabled):
|
||||
count += 1
|
||||
|
||||
if ensure_min_one and count < 1:
|
||||
return 1
|
||||
return count
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
# usage: {{ applications | active_docker_container_count(group_names) }}
|
||||
'active_docker_container_count': active_docker_container_count,
|
||||
}
|
||||
@@ -10,9 +10,23 @@ from module_utils.config_utils import get_app_conf
|
||||
from module_utils.get_url import get_url
|
||||
|
||||
|
||||
def _dedup_preserve(seq):
|
||||
"""Return a list with stable order and unique items."""
|
||||
seen = set()
|
||||
out = []
|
||||
for x in seq:
|
||||
if x not in seen:
|
||||
seen.add(x)
|
||||
out.append(x)
|
||||
return out
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
"""
|
||||
Custom filters for Content Security Policy generation and CSP-related utilities.
|
||||
Jinja filters for building a robust, CSP3-aware Content-Security-Policy header.
|
||||
Safari/CSP2 compatibility is ensured by merging the -elem/-attr variants into the base
|
||||
directives (style-src, script-src). We intentionally do NOT mirror back into -elem/-attr
|
||||
to allow true CSP3 granularity on modern browsers.
|
||||
"""
|
||||
|
||||
def filters(self):
|
||||
@@ -61,11 +75,14 @@ class FilterModule(object):
|
||||
"""
|
||||
Returns CSP flag tokens (e.g., "'unsafe-eval'", "'unsafe-inline'") for a directive,
|
||||
merging sane defaults with app config.
|
||||
Default: 'unsafe-inline' is enabled for style-src and style-src-elem.
|
||||
|
||||
Defaults:
|
||||
- For styles we enable 'unsafe-inline' by default (style-src, style-src-elem, style-src-attr),
|
||||
because many apps rely on inline styles / style attributes.
|
||||
- For scripts we do NOT enable 'unsafe-inline' by default.
|
||||
"""
|
||||
# Defaults that apply to all apps
|
||||
default_flags = {}
|
||||
if directive in ('style-src', 'style-src-elem'):
|
||||
if directive in ('style-src', 'style-src-elem', 'style-src-attr'):
|
||||
default_flags = {'unsafe-inline': True}
|
||||
|
||||
configured = get_app_conf(
|
||||
@@ -76,7 +93,6 @@ class FilterModule(object):
|
||||
{}
|
||||
)
|
||||
|
||||
# Merge defaults with configured flags (configured overrides defaults)
|
||||
merged = {**default_flags, **configured}
|
||||
|
||||
tokens = []
|
||||
@@ -131,77 +147,148 @@ class FilterModule(object):
|
||||
):
|
||||
"""
|
||||
Builds the Content-Security-Policy header value dynamically based on application settings.
|
||||
- Flags (e.g., 'unsafe-eval', 'unsafe-inline') are read from server.csp.flags.<directive>,
|
||||
with sane defaults applied in get_csp_flags (always 'unsafe-inline' for style-src and style-src-elem).
|
||||
- Inline hashes are read from server.csp.hashes.<directive>.
|
||||
- Whitelists are read from server.csp.whitelist.<directive>.
|
||||
- Inline hashes are added only if the final tokens do NOT include 'unsafe-inline'.
|
||||
|
||||
Key points:
|
||||
- CSP3-aware: supports base/elem/attr for styles and scripts.
|
||||
- Safari/CSP2 fallback: base directives (style-src, script-src) always include
|
||||
the union of their -elem/-attr variants.
|
||||
- We do NOT mirror back into -elem/-attr; finer CSP3 rules remain effective
|
||||
on modern browsers if you choose to use them.
|
||||
- If the app explicitly disables a token on the *base* (e.g. style-src.unsafe-inline: false),
|
||||
that token is removed from the merged base even if present in elem/attr.
|
||||
- Inline hashes are added ONLY if that directive does NOT include 'unsafe-inline'.
|
||||
- Whitelists/flags/hashes read from:
|
||||
server.csp.whitelist.<directive>
|
||||
server.csp.flags.<directive>
|
||||
server.csp.hashes.<directive>
|
||||
- “Smart defaults”:
|
||||
* internal CDN for style/script elem and connect
|
||||
* Matomo endpoints (if feature enabled) for script-elem/connect
|
||||
* Simpleicons (if feature enabled) for connect
|
||||
* reCAPTCHA (if feature enabled) for script-elem/frame-src
|
||||
* frame-ancestors extended for desktop/logout/keycloak if enabled
|
||||
"""
|
||||
try:
|
||||
directives = [
|
||||
'default-src', # Fallback source list for content types not explicitly listed
|
||||
'connect-src', # Allowed URLs for XHR, WebSockets, EventSource, fetch()
|
||||
'frame-ancestors', # Who may embed this page
|
||||
'frame-src', # Sources for nested browsing contexts (e.g., <iframe>)
|
||||
'script-src', # Sources for script execution
|
||||
'script-src-elem', # Sources for <script> elements
|
||||
'style-src', # Sources for inline styles and <style>/<link> elements
|
||||
'style-src-elem', # Sources for <style> and <link rel="stylesheet">
|
||||
'font-src', # Sources for fonts
|
||||
'worker-src', # Sources for workers
|
||||
'manifest-src', # Sources for web app manifests
|
||||
'media-src', # Sources for audio and video
|
||||
'default-src',
|
||||
'connect-src',
|
||||
'frame-ancestors',
|
||||
'frame-src',
|
||||
'script-src',
|
||||
'script-src-elem',
|
||||
'script-src-attr',
|
||||
'style-src',
|
||||
'style-src-elem',
|
||||
'style-src-attr',
|
||||
'font-src',
|
||||
'worker-src',
|
||||
'manifest-src',
|
||||
'media-src',
|
||||
]
|
||||
|
||||
parts = []
|
||||
tokens_by_dir = {}
|
||||
explicit_flags_by_dir = {}
|
||||
|
||||
for directive in directives:
|
||||
# Collect explicit flags (to later respect explicit "False" on base during merge)
|
||||
explicit_flags = get_app_conf(
|
||||
applications,
|
||||
application_id,
|
||||
'server.csp.flags.' + directive,
|
||||
False,
|
||||
{}
|
||||
)
|
||||
explicit_flags_by_dir[directive] = explicit_flags
|
||||
|
||||
tokens = ["'self'"]
|
||||
|
||||
# 1) Load flags (includes defaults from get_csp_flags)
|
||||
# 1) Flags (with sane defaults)
|
||||
flags = self.get_csp_flags(applications, application_id, directive)
|
||||
tokens += flags
|
||||
|
||||
# 2) Allow fetching from internal CDN by default for selected directives
|
||||
if directive in ['script-src-elem', 'connect-src', 'style-src-elem']:
|
||||
# 2) Internal CDN defaults for selected directives
|
||||
if directive in ('script-src-elem', 'connect-src', 'style-src-elem', 'style-src'):
|
||||
tokens.append(get_url(domains, 'web-svc-cdn', web_protocol))
|
||||
|
||||
# 3) Matomo integration if feature is enabled
|
||||
if directive in ['script-src-elem', 'connect-src']:
|
||||
# 3) Matomo (if enabled)
|
||||
if directive in ('script-src-elem', 'connect-src'):
|
||||
if self.is_feature_enabled(applications, matomo_feature_name, application_id):
|
||||
tokens.append(get_url(domains, 'web-app-matomo', web_protocol))
|
||||
|
||||
# 4) ReCaptcha integration (scripts + frames) if feature is enabled
|
||||
# 4) Simpleicons (if enabled) – typically used via connect-src (fetch)
|
||||
if directive == 'connect-src':
|
||||
if self.is_feature_enabled(applications, 'simpleicons', application_id):
|
||||
tokens.append(get_url(domains, 'web-svc-simpleicons', web_protocol))
|
||||
|
||||
# 5) reCAPTCHA (if enabled) – scripts + frames
|
||||
if self.is_feature_enabled(applications, 'recaptcha', application_id):
|
||||
if directive in ['script-src-elem', 'frame-src']:
|
||||
if directive in ('script-src-elem', 'frame-src'):
|
||||
tokens.append('https://www.gstatic.com')
|
||||
tokens.append('https://www.google.com')
|
||||
|
||||
# 5) Frame ancestors handling (desktop + logout support)
|
||||
# 6) Frame ancestors (desktop + logout)
|
||||
if directive == 'frame-ancestors':
|
||||
if self.is_feature_enabled(applications, 'desktop', application_id):
|
||||
# Allow being embedded by the desktop app domain (and potentially its parent)
|
||||
# Allow being embedded by the desktop app domain's site
|
||||
domain = domains.get('web-app-desktop')[0]
|
||||
sld_tld = ".".join(domain.split(".")[-2:]) # e.g., example.com
|
||||
tokens.append(f"{sld_tld}")
|
||||
if self.is_feature_enabled(applications, 'logout', application_id):
|
||||
# Allow embedding via logout proxy and Keycloak app
|
||||
tokens.append(get_url(domains, 'web-svc-logout', web_protocol))
|
||||
tokens.append(get_url(domains, 'web-app-keycloak', web_protocol))
|
||||
|
||||
# 6) Custom whitelist entries
|
||||
# 7) Custom whitelist
|
||||
tokens += self.get_csp_whitelist(applications, application_id, directive)
|
||||
|
||||
# 7) Add inline content hashes ONLY if final tokens do NOT include 'unsafe-inline'
|
||||
# (Check tokens, not flags, to include defaults and later modifications.)
|
||||
# 8) Inline hashes (only if this directive does NOT include 'unsafe-inline')
|
||||
if "'unsafe-inline'" not in tokens:
|
||||
for snippet in self.get_csp_inline_content(applications, application_id, directive):
|
||||
tokens.append(self.get_csp_hash(snippet))
|
||||
|
||||
# Append directive
|
||||
parts.append(f"{directive} {' '.join(tokens)};")
|
||||
tokens_by_dir[directive] = _dedup_preserve(tokens)
|
||||
|
||||
# 8) Static img-src directive (kept permissive for data/blob and any host)
|
||||
# ----------------------------------------------------------
|
||||
# CSP3 families → ensure CSP2 fallback (Safari-safe)
|
||||
# Merge style/script families so base contains union of elem/attr.
|
||||
# Respect explicit disables on the base (e.g. unsafe-inline=False).
|
||||
# Do NOT mirror back into elem/attr (keep granularity).
|
||||
# ----------------------------------------------------------
|
||||
def _strip_if_disabled(unioned_tokens, explicit_flags, name):
|
||||
"""
|
||||
Remove a token (e.g. 'unsafe-inline') from the unioned token list
|
||||
if it is explicitly disabled in the base directive flags.
|
||||
"""
|
||||
if isinstance(explicit_flags, dict) and explicit_flags.get(name) is False:
|
||||
tok = f"'{name}'"
|
||||
return [t for t in unioned_tokens if t != tok]
|
||||
return unioned_tokens
|
||||
|
||||
def merge_family(base_key, elem_key, attr_key):
|
||||
base = tokens_by_dir.get(base_key, [])
|
||||
elem = tokens_by_dir.get(elem_key, [])
|
||||
attr = tokens_by_dir.get(attr_key, [])
|
||||
union = _dedup_preserve(base + elem + attr)
|
||||
|
||||
# Respect explicit disables on the base
|
||||
explicit_base = explicit_flags_by_dir.get(base_key, {})
|
||||
# The most relevant flags for script/style:
|
||||
for flag_name in ('unsafe-inline', 'unsafe-eval'):
|
||||
union = _strip_if_disabled(union, explicit_base, flag_name)
|
||||
|
||||
tokens_by_dir[base_key] = union # write back only to base
|
||||
|
||||
merge_family('style-src', 'style-src-elem', 'style-src-attr')
|
||||
merge_family('script-src', 'script-src-elem', 'script-src-attr')
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Assemble header
|
||||
# ----------------------------------------------------------
|
||||
parts = []
|
||||
for directive in directives:
|
||||
if directive in tokens_by_dir:
|
||||
parts.append(f"{directive} {' '.join(tokens_by_dir[directive])};")
|
||||
|
||||
# Keep permissive img-src for data/blob + any host (as before)
|
||||
parts.append("img-src * data: blob:;")
|
||||
|
||||
return ' '.join(parts)
|
||||
|
||||
@@ -7,7 +7,7 @@ class FilterModule(object):
|
||||
def filters(self):
|
||||
return {'domain_mappings': self.domain_mappings}
|
||||
|
||||
def domain_mappings(self, apps, PRIMARY_DOMAIN):
|
||||
def domain_mappings(self, apps, primary_domain, auto_build_alias):
|
||||
"""
|
||||
Build a flat list of redirect mappings for all apps:
|
||||
- source: each alias domain
|
||||
@@ -43,7 +43,7 @@ class FilterModule(object):
|
||||
domains_cfg = cfg.get('server',{}).get('domains',{})
|
||||
entry = domains_cfg.get('canonical')
|
||||
if entry is None:
|
||||
canonical_map[app_id] = [default_domain(app_id, PRIMARY_DOMAIN)]
|
||||
canonical_map[app_id] = [default_domain(app_id, primary_domain)]
|
||||
elif isinstance(entry, dict):
|
||||
canonical_map[app_id] = list(entry.values())
|
||||
elif isinstance(entry, list):
|
||||
@@ -61,11 +61,11 @@ class FilterModule(object):
|
||||
alias_map[app_id] = []
|
||||
continue
|
||||
if isinstance(domains_cfg, dict) and not domains_cfg:
|
||||
alias_map[app_id] = [default_domain(app_id, PRIMARY_DOMAIN)]
|
||||
alias_map[app_id] = [default_domain(app_id, primary_domain)]
|
||||
continue
|
||||
|
||||
aliases = parse_entry(domains_cfg, 'aliases', app_id) or []
|
||||
default = default_domain(app_id, PRIMARY_DOMAIN)
|
||||
default = default_domain(app_id, primary_domain)
|
||||
has_aliases = 'aliases' in domains_cfg
|
||||
has_canonical = 'canonical' in domains_cfg
|
||||
|
||||
@@ -74,7 +74,7 @@ class FilterModule(object):
|
||||
aliases.append(default)
|
||||
elif has_canonical:
|
||||
canon = canonical_map.get(app_id, [])
|
||||
if default not in canon and default not in aliases:
|
||||
if default not in canon and default not in aliases and auto_build_alias:
|
||||
aliases.append(default)
|
||||
|
||||
alias_map[app_id] = aliases
|
||||
@@ -84,7 +84,7 @@ class FilterModule(object):
|
||||
mappings = []
|
||||
for app_id, sources in alias_map.items():
|
||||
canon_list = canonical_map.get(app_id, [])
|
||||
target = canon_list[0] if canon_list else default_domain(app_id, PRIMARY_DOMAIN)
|
||||
target = canon_list[0] if canon_list else default_domain(app_id, primary_domain)
|
||||
for src in sources:
|
||||
if src == target:
|
||||
# skip self-redirects
|
||||
|
||||
@@ -4,7 +4,7 @@ class FilterModule(object):
|
||||
def filters(self):
|
||||
return {'generate_all_domains': self.generate_all_domains}
|
||||
|
||||
def generate_all_domains(self, domains_dict, include_www=True):
|
||||
def generate_all_domains(self, domains_dict, include_www:bool=True):
|
||||
"""
|
||||
Transform a dict of domains (values: str, list, dict) into a flat list,
|
||||
optionally add 'www.' prefixes, dedupe and sort alphabetically.
|
||||
|
||||
@@ -20,9 +20,10 @@ def get_docker_paths(application_id: str, path_docker_compose_instances: str) ->
|
||||
'config': f"{base}config/",
|
||||
},
|
||||
'files': {
|
||||
'env': f"{base}.env/env",
|
||||
'docker_compose': f"{base}docker-compose.yml",
|
||||
'dockerfile': f"{base}Dockerfile",
|
||||
'env': f"{base}.env/env",
|
||||
'docker_compose': f"{base}docker-compose.yml",
|
||||
'docker_compose_override': f"{base}docker-compose.override.yml",
|
||||
'dockerfile': f"{base}Dockerfile",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
filter_plugins/jvm_filters.py
Normal file
77
filter_plugins/jvm_filters.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys, os, re
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from module_utils.config_utils import get_app_conf
|
||||
from module_utils.entity_name_utils import get_entity_name
|
||||
|
||||
_UNIT_RE = re.compile(r'^\s*(\d+(?:\.\d+)?)\s*([kKmMgGtT]?[bB]?)?\s*$')
|
||||
_FACTORS = {
|
||||
'': 1, 'b': 1,
|
||||
'k': 1024, 'kb': 1024,
|
||||
'm': 1024**2, 'mb': 1024**2,
|
||||
'g': 1024**3, 'gb': 1024**3,
|
||||
't': 1024**4, 'tb': 1024**4,
|
||||
}
|
||||
|
||||
def _to_bytes(v: str) -> int:
|
||||
if v is None:
|
||||
raise AnsibleFilterError("jvm_filters: size value is None")
|
||||
s = str(v).strip()
|
||||
m = _UNIT_RE.match(s)
|
||||
if not m:
|
||||
raise AnsibleFilterError(f"jvm_filters: invalid size '{v}'")
|
||||
num, unit = m.group(1), (m.group(2) or '').lower()
|
||||
try:
|
||||
val = float(num)
|
||||
except ValueError as e:
|
||||
raise AnsibleFilterError(f"jvm_filters: invalid numeric size '{v}'") from e
|
||||
factor = _FACTORS.get(unit)
|
||||
if factor is None:
|
||||
raise AnsibleFilterError(f"jvm_filters: unknown unit in '{v}'")
|
||||
return int(val * factor)
|
||||
|
||||
def _to_mb(v: str) -> int:
|
||||
return max(0, _to_bytes(v) // (1024 * 1024))
|
||||
|
||||
def _svc(app_id: str) -> str:
|
||||
return get_entity_name(app_id)
|
||||
|
||||
def _mem_limit_mb(apps: dict, app_id: str) -> int:
|
||||
svc = _svc(app_id)
|
||||
raw = get_app_conf(apps, app_id, f"docker.services.{svc}.mem_limit")
|
||||
mb = _to_mb(raw)
|
||||
if mb <= 0:
|
||||
raise AnsibleFilterError(f"jvm_filters: mem_limit for '{svc}' must be > 0 MB (got '{raw}')")
|
||||
return mb
|
||||
|
||||
def _mem_res_mb(apps: dict, app_id: str) -> int:
|
||||
svc = _svc(app_id)
|
||||
raw = get_app_conf(apps, app_id, f"docker.services.{svc}.mem_reservation")
|
||||
mb = _to_mb(raw)
|
||||
if mb <= 0:
|
||||
raise AnsibleFilterError(f"jvm_filters: mem_reservation for '{svc}' must be > 0 MB (got '{raw}')")
|
||||
return mb
|
||||
|
||||
def jvm_max_mb(apps: dict, app_id: str) -> int:
|
||||
"""Xmx = min( floor(0.7*limit), limit-1024, 12288 ) with floor at 1024 MB."""
|
||||
limit_mb = _mem_limit_mb(apps, app_id)
|
||||
c1 = (limit_mb * 7) // 10
|
||||
c2 = max(0, limit_mb - 1024)
|
||||
c3 = 12288
|
||||
return max(1024, min(c1, c2, c3))
|
||||
|
||||
def jvm_min_mb(apps: dict, app_id: str) -> int:
|
||||
"""Xms = min( floor(Xmx/2), mem_reservation, Xmx ) with floor at 512 MB."""
|
||||
xmx = jvm_max_mb(apps, app_id)
|
||||
res = _mem_res_mb(apps, app_id)
|
||||
return max(512, min(xmx // 2, res, xmx))
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
"jvm_max_mb": jvm_max_mb,
|
||||
"jvm_min_mb": jvm_min_mb,
|
||||
}
|
||||
40
filter_plugins/resource_filter.py
Normal file
40
filter_plugins/resource_filter.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# filter_plugins/resource_filter.py
|
||||
from __future__ import annotations
|
||||
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from module_utils.config_utils import get_app_conf, AppConfigKeyError, ConfigEntryNotSetError # noqa: F401
|
||||
from module_utils.entity_name_utils import get_entity_name
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
|
||||
|
||||
def resource_filter(
|
||||
applications: dict,
|
||||
application_id: str,
|
||||
key: str,
|
||||
service_name: str,
|
||||
hard_default,
|
||||
):
|
||||
"""
|
||||
Lookup order:
|
||||
1) docker.services.<service_name or get_entity_name(application_id)>.<key>
|
||||
2) hard_default (mandatory)
|
||||
|
||||
- service_name may be "" → will resolve to get_entity_name(application_id).
|
||||
- hard_default is mandatory (no implicit None).
|
||||
- required=False always.
|
||||
"""
|
||||
try:
|
||||
primary_service = service_name if service_name != "" else get_entity_name(application_id)
|
||||
return get_app_conf(applications, application_id, f"docker.services.{primary_service}.{key}", False, hard_default)
|
||||
except (AppConfigKeyError, ConfigEntryNotSetError) as e:
|
||||
raise AnsibleFilterError(str(e))
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
def filters(self):
|
||||
return {
|
||||
"resource_filter": resource_filter,
|
||||
}
|
||||
@@ -32,8 +32,10 @@ WEBSOCKET_PROTOCOL: "{{ 'wss' if WEB_PROTOCOL == 'https' else 'ws' }}"
|
||||
# WWW-Redirect to None WWW-Domains enabled
|
||||
WWW_REDIRECT_ENABLED: "{{ ('web-opt-rdr-www' in group_names) | bool }}"
|
||||
|
||||
AUTO_BUILD_ALIASES: False # If enabled it creates an alias domain for each web application by the entity name, recommended to set to false to safge domain space
|
||||
|
||||
# Domain
|
||||
PRIMARY_DOMAIN: "localhost" # Primary Domain of the server
|
||||
PRIMARY_DOMAIN: "localhost" # Primary Domain of the server
|
||||
|
||||
DNS_PROVIDER: cloudflare # The DNS Provider\Registrar for the domain
|
||||
|
||||
@@ -58,7 +60,7 @@ DOCKER_WHITELISTET_ANON_VOLUMES: []
|
||||
|
||||
# Asyn Confitguration
|
||||
ASYNC_ENABLED: "{{ not MODE_DEBUG | bool }}" # Activate async, deactivated for debugging
|
||||
ASYNC_TIME: "{{ 300 if ASYNC_ENABLED | bool else omit }}" # Run for mnax 5min
|
||||
ASYNC_TIME: "{{ 300 if ASYNC_ENABLED | bool else omit }}" # Run for max 5min
|
||||
ASYNC_POLL: "{{ 0 if ASYNC_ENABLED | bool else 10 }}" # Don't wait for task
|
||||
|
||||
# default value if not set via CLI (-e) or in playbook vars
|
||||
@@ -74,8 +76,9 @@ _applications_nextcloud_oidc_flavor: >-
|
||||
False,
|
||||
'oidc_login'
|
||||
if applications
|
||||
| get_app_conf('web-app-nextcloud','features.ldap',False, True)
|
||||
else 'sociallogin'
|
||||
| get_app_conf('web-app-nextcloud','features.ldap',False, True, True)
|
||||
else 'sociallogin',
|
||||
True
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -84,4 +87,4 @@ _applications_nextcloud_oidc_flavor: >-
|
||||
RBAC:
|
||||
GROUP:
|
||||
NAME: "/roles" # Name of the group which holds the RBAC roles
|
||||
CLAIM: "groups" # Name of the claim containing the RBAC groups
|
||||
CLAIM: "groups" # Name of the claim containing the RBAC groups
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Mode
|
||||
|
||||
# The following modes can be combined with each other
|
||||
MODE_TEST: false # Executes test routines instead of productive routines
|
||||
MODE_DUMMY: false # Executes dummy/test routines instead of productive routines
|
||||
MODE_UPDATE: true # Executes updates
|
||||
MODE_DEBUG: false # This enables debugging in ansible and in the apps, You SHOULD NOT enable this on production servers
|
||||
MODE_RESET: false # Cleans up all Infinito.Nexus files. It's necessary to run to whole playbook and not particial roles when using this function.
|
||||
MODE_CLEANUP: "{{ MODE_DEBUG | bool }}" # Cleanup unused files and configurations
|
||||
MODE_CLEANUP: true # Cleanup unused files and configurations
|
||||
MODE_ASSERT: "{{ MODE_DEBUG | bool }}" # Executes validation tasks during the run.
|
||||
MODE_BACKUP: true # Executes the Backup before the deployment
|
||||
|
||||
@@ -29,4 +29,31 @@ NGINX:
|
||||
IMAGE: "/tmp/cache_nginx_image/" # Directory which nginx uses to cache images
|
||||
USER: "http" # Default nginx user in ArchLinux
|
||||
|
||||
# Effective CPUs (float) across proxy and the current app
|
||||
WEBSERVER_CPUS_EFFECTIVE: >-
|
||||
{{
|
||||
[
|
||||
(applications | resource_filter('svc-prx-openresty', 'cpus', service_name | default(''), RESOURCE_CPUS)) | float,
|
||||
(applications | resource_filter(application_id, 'cpus', service_name | default(''), RESOURCE_CPUS)) | float
|
||||
] | min
|
||||
}}
|
||||
|
||||
# Nginx requires an integer for worker_processes:
|
||||
# - if cpus < 1 → 1
|
||||
# - else → floor to int
|
||||
WEBSERVER_WORKER_PROCESSES: >-
|
||||
{{
|
||||
1 if (WEBSERVER_CPUS_EFFECTIVE | float) < 1
|
||||
else (WEBSERVER_CPUS_EFFECTIVE | float | int)
|
||||
}}
|
||||
|
||||
# worker_connections from pids_limit (use the smaller one), with correct key/defaults
|
||||
WEBSERVER_WORKER_CONNECTIONS: >-
|
||||
{{
|
||||
[
|
||||
(applications | resource_filter('svc-prx-openresty', 'pids_limit', service_name | default(''), RESOURCE_PIDS_LIMIT)) | int,
|
||||
(applications | resource_filter(application_id, 'pids_limit', service_name | default(''), RESOURCE_PIDS_LIMIT)) | int
|
||||
] | min
|
||||
}}
|
||||
|
||||
# @todo It propably makes sense to distinguish between target and source mount path, so that the config files can be stored in the openresty volumes folder
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# Service Timers
|
||||
|
||||
## Meta
|
||||
@@ -6,12 +5,12 @@ SYS_TIMER_ALL_ENABLED: "{{ MODE_DEBUG }}" # Runtime Var
|
||||
|
||||
## Server Tact Variables
|
||||
|
||||
HOURS_SERVER_AWAKE: "0..23" # Ours in which the server is "awake" (100% working). Rest of the time is reserved for maintanance
|
||||
HOURS_SERVER_AWAKE: "6..23" # Ours in which the server is "awake" (100% working). Rest of the time is reserved for maintanance
|
||||
RANDOMIZED_DELAY_SEC: "5min" # Random delay for systemd timers to avoid peak loads.
|
||||
|
||||
## Timeouts for all services
|
||||
SYS_TIMEOUT_DOCKER_RPR_HARD: "10min"
|
||||
SYS_TIMEOUT_DOCKER_RPR_SOFT: "{{ SYS_TIMEOUT_DOCKER_RPR_HARD }}"
|
||||
SYS_TIMEOUT_DOCKER_RPR_SOFT: "{{ SYS_TIMEOUT_DOCKER_RPR_HARD }}"
|
||||
SYS_TIMEOUT_CLEANUP_SERVICES: "15min"
|
||||
SYS_TIMEOUT_DOCKER_UPDATE: "20min"
|
||||
SYS_TIMEOUT_STORAGE_OPTIMIZER: "{{ SYS_TIMEOUT_DOCKER_UPDATE }}"
|
||||
@@ -24,29 +23,29 @@ SYS_SCHEDULE_HEALTH_BTRFS: "*-*-* 00:00:00"
|
||||
SYS_SCHEDULE_HEALTH_JOURNALCTL: "*-*-* 00:00:00" # Check once per day the journalctl for errors
|
||||
SYS_SCHEDULE_HEALTH_DISC_SPACE: "*-*-* 06,12,18,00:00:00" # Check four times per day if there is sufficient disc space
|
||||
SYS_SCHEDULE_HEALTH_DOCKER_CONTAINER: "*-*-* {{ HOURS_SERVER_AWAKE }}:00:00" # Check once per hour if the docker containers are healthy
|
||||
SYS_SCHEDULE_HEALTH_DOCKER_VOLUMES: "*-*-* {{ HOURS_SERVER_AWAKE }}:15:00" # Check once per hour if the docker volumes are healthy
|
||||
SYS_SCHEDULE_HEALTH_CSP_CRAWLER: "*-*-* {{ HOURS_SERVER_AWAKE }}:30:00" # Check once per hour if all CSP are fullfilled available
|
||||
SYS_SCHEDULE_HEALTH_NGINX: "*-*-* {{ HOURS_SERVER_AWAKE }}:45:00" # Check once per hour if all webservices are available
|
||||
SYS_SCHEDULE_HEALTH_DOCKER_VOLUMES: "*-*-* {{ HOURS_SERVER_AWAKE }}:00:00" # Check once per hour if the docker volumes are healthy
|
||||
SYS_SCHEDULE_HEALTH_CSP_CRAWLER: "*-*-* {{ HOURS_SERVER_AWAKE }}:00:00" # Check once per hour if all CSP are fullfilled available
|
||||
SYS_SCHEDULE_HEALTH_NGINX: "*-*-* {{ HOURS_SERVER_AWAKE }}:00:00" # Check once per hour if all webservices are available
|
||||
SYS_SCHEDULE_HEALTH_MSMTP: "*-*-* 00:00:00" # Check once per day SMTP Server
|
||||
|
||||
### Schedule for cleanup tasks
|
||||
SYS_SCHEDULE_CLEANUP_BACKUPS: "*-*-* 00,06,12,18:30:00" # Cleanup backups every 6 hours, MUST be called before disc space cleanup
|
||||
SYS_SCHEDULE_CLEANUP_DISC_SPACE: "*-*-* 07,13,19,01:30:00" # Cleanup disc space every 6 hours
|
||||
SYS_SCHEDULE_CLEANUP_CERTS: "*-*-* 12,00:45:00" # Deletes and revokes unused certs
|
||||
SYS_SCHEDULE_CLEANUP_FAILED_BACKUPS: "*-*-* 12:00:00" # Clean up failed docker backups every noon
|
||||
SYS_SCHEDULE_CLEANUP_CERTS: "*-*-* 20:00" # Deletes and revokes unused certs once per day
|
||||
SYS_SCHEDULE_CLEANUP_FAILED_BACKUPS: "*-*-* 21:00" # Clean up failed docker backups once per day
|
||||
SYS_SCHEDULE_CLEANUP_BACKUPS: "*-*-* 22:00" # Cleanup backups once per day, MUST be called before disc space cleanup
|
||||
SYS_SCHEDULE_CLEANUP_DISC_SPACE: "*-*-* 23:00" # Cleanup disc space once per day
|
||||
|
||||
### Schedule for repair services
|
||||
SYS_SCHEDULE_REPAIR_BTRFS_AUTO_BALANCER: "Sat *-*-01..07 00:00:00" # Execute btrfs auto balancer every first Saturday of a month
|
||||
SYS_SCHEDULE_REPAIR_DOCKER_HARD: "Sun *-*-* 08:00:00" # Restart docker instances every Sunday at 8:00 AM
|
||||
SYS_SCHEDULE_REPAIR_DOCKER_HARD: "Sun *-*-* 00:00:00" # Restart docker instances every Sunday
|
||||
|
||||
### Schedule for backup tasks
|
||||
SYS_SCHEDULE_BACKUP_DOCKER_TO_LOCAL: "*-*-* 03:30:00"
|
||||
SYS_SCHEDULE_BACKUP_REMOTE_TO_LOCAL: "*-*-* 21:30:00"
|
||||
SYS_SCHEDULE_BACKUP_REMOTE_TO_LOCAL: "*-*-* 00:30:00" # Pull Backup of the previous day
|
||||
SYS_SCHEDULE_BACKUP_DOCKER_TO_LOCAL: "*-*-* 01:00:00" # Backup the current day
|
||||
|
||||
### Schedule for Maintenance Tasks
|
||||
SYS_SCHEDULE_MAINTANANCE_LETSENCRYPT_RENEW: "*-*-* 12,00:30:00" # Renew Mailu certificates twice per day
|
||||
SYS_SCHEDULE_MAINTANANCE_LETSENCRYPT_DEPLOY: "*-*-* 13,01:30:00" # Deploy letsencrypt certificates twice per day to docker containers
|
||||
SYS_SCHEDULE_MAINTANANCE_NEXTCLOUD: "22" # Do nextcloud maintanace between 22:00 and 02:00
|
||||
SYS_SCHEDULE_MAINTANANCE_LETSENCRYPT_RENEW: "*-*-* 10,22:00:00" # Renew Mailu certificates twice per day
|
||||
SYS_SCHEDULE_MAINTANANCE_LETSENCRYPT_DEPLOY: "*-*-* 11,23:00:00" # Deploy letsencrypt certificates twice per day to docker containers
|
||||
SYS_SCHEDULE_MAINTANANCE_NEXTCLOUD: "21" # Do nextcloud maintanace between 21:00 and 01:00
|
||||
|
||||
### Animation
|
||||
SYS_SCHEDULE_ANIMATION_KEYBOARD_COLOR: "*-*-* *:*:00" # Change the keyboard color every minute
|
||||
@@ -104,6 +104,18 @@ defaults_networks:
|
||||
subnet: 192.168.103.224/28
|
||||
web-app-xwiki:
|
||||
subnet: 192.168.103.240/28
|
||||
web-app-openwebui:
|
||||
subnet: 192.168.104.0/28
|
||||
web-app-flowise:
|
||||
subnet: 192.168.104.16/28
|
||||
web-app-minio:
|
||||
subnet: 192.168.104.32/28
|
||||
web-svc-coturn:
|
||||
subnet: 192.168.104.48/28
|
||||
web-app-mini-qr:
|
||||
subnet: 192.168.104.64/28
|
||||
web-app-shopware:
|
||||
subnet: 192.168.104.80/28
|
||||
|
||||
# /24 Networks / 254 Usable Clients
|
||||
web-app-bigbluebutton:
|
||||
@@ -116,3 +128,5 @@ defaults_networks:
|
||||
subnet: 192.168.201.0/24
|
||||
svc-db-openldap:
|
||||
subnet: 192.168.202.0/24
|
||||
svc-ai-ollama:
|
||||
subnet: 192.168.203.0/24 # Big network to bridge applications into ai
|
||||
|
||||
@@ -75,21 +75,36 @@ ports:
|
||||
web-app-bluesky_view: 8051
|
||||
web-app-magento: 8052
|
||||
web-app-bridgy-fed: 8053
|
||||
web-app-xwiki: 8054
|
||||
web-app-xwiki: 8054
|
||||
web-app-openwebui: 8055
|
||||
web-app-flowise: 8056
|
||||
web-app-minio_api: 8057
|
||||
web-app-minio_console: 8058
|
||||
web-app-mini-qr: 8059
|
||||
web-app-shopware: 8060
|
||||
web-app-bigbluebutton: 48087 # This port is predefined by bbb. @todo Try to change this to a 8XXX port
|
||||
public:
|
||||
# The following ports should be changed to 22 on the subdomain via stream mapping
|
||||
ssh:
|
||||
web-app-gitea: 2201
|
||||
web-app-gitlab: 2202
|
||||
web-app-gitea: 2201
|
||||
web-app-gitlab: 2202
|
||||
ldaps:
|
||||
svc-db-openldap: 636
|
||||
stun:
|
||||
web-app-bigbluebutton: 3478 # Not sure if it's right placed here or if it should be moved to localhost section
|
||||
# Occupied by BBB: 3479
|
||||
web-app-nextcloud: 3480
|
||||
turn:
|
||||
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
|
||||
svc-db-openldap: 636
|
||||
stun_turn:
|
||||
web-app-bigbluebutton: 3478 # Not sure if it's right placed here or if it should be moved to localhost section
|
||||
# Occupied by BBB: 3479
|
||||
web-app-nextcloud: 3480
|
||||
web-svc-coturn: 3481
|
||||
stun_turn_tls:
|
||||
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-svc-coturn: 5351
|
||||
federation:
|
||||
web-app-matrix_synapse: 8448
|
||||
relay_port_ranges:
|
||||
web-svc-coturn_start: 20000
|
||||
web-svc-coturn_end: 39999
|
||||
web-app-bigbluebutton_start: 40000
|
||||
web-app-bigbluebutton_end: 49999
|
||||
web-app-nextcloud_start: 50000
|
||||
web-app-nextcloud_end: 59999
|
||||
|
||||
3
group_vars/all/17_ai.yml
Normal file
3
group_vars/all/17_ai.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
# URL of Local Ollama Container
|
||||
OLLAMA_BASE_LOCAL_URL: "http://{{ applications | get_app_conf('svc-ai-ollama', 'docker.services.ollama.name') }}:{{ applications | get_app_conf('svc-ai-ollama', 'docker.services.ollama.port') }}"
|
||||
OLLAMA_LOCAL_ENABLED: "{{ applications | get_app_conf(application_id, 'features.local_ai') }}"
|
||||
47
group_vars/all/18_resource.yml
Normal file
47
group_vars/all/18_resource.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Host resources
|
||||
RESOURCE_HOST_CPUS: "{{ ansible_processor_vcpus | int }}"
|
||||
RESOURCE_HOST_MEM: "{{ (ansible_memtotal_mb | int) // 1024 }}"
|
||||
|
||||
# Reserve for OS
|
||||
RESOURCE_HOST_RESERVE_CPU: 2
|
||||
RESOURCE_HOST_RESERVE_MEM: 4
|
||||
|
||||
# Available for apps
|
||||
RESOURCE_AVAIL_CPUS: "{{ (RESOURCE_HOST_CPUS | int) - (RESOURCE_HOST_RESERVE_CPU | int) }}"
|
||||
RESOURCE_AVAIL_MEM: "{{ (RESOURCE_HOST_MEM | int) - (RESOURCE_HOST_RESERVE_MEM | int) }}"
|
||||
|
||||
# Count active docker services (only roles starting with web- or svc-; service counts if enabled==true OR enabled is undefined)
|
||||
RESOURCE_ACTIVE_DOCKER_CONTAINER_COUNT: >-
|
||||
{{
|
||||
applications
|
||||
| active_docker_container_count(group_names, '^(web-|svc-).*', ensure_min_one=True)
|
||||
}}
|
||||
|
||||
# Per-container fair share (numbers!), later we append 'g' only for the string fields in compose
|
||||
RESOURCE_CPUS_NUM: >-
|
||||
{{
|
||||
[
|
||||
(
|
||||
((RESOURCE_AVAIL_CPUS | float) / (RESOURCE_ACTIVE_DOCKER_CONTAINER_COUNT | float))
|
||||
| round(2)
|
||||
),
|
||||
0.5
|
||||
] | max
|
||||
}}
|
||||
|
||||
RESOURCE_MEM_RESERVATION_NUM: >-
|
||||
{{
|
||||
(((RESOURCE_AVAIL_MEM | float) / (RESOURCE_ACTIVE_DOCKER_CONTAINER_COUNT | float)) * 0.7)
|
||||
| round(1)
|
||||
}}
|
||||
RESOURCE_MEM_LIMIT_NUM: >-
|
||||
{{
|
||||
(((RESOURCE_AVAIL_MEM | float) / (RESOURCE_ACTIVE_DOCKER_CONTAINER_COUNT | float)) * 1.0)
|
||||
| round(1)
|
||||
}}
|
||||
|
||||
# Final strings with units for compose defaults (keep numbers above for math elsewhere if needed)
|
||||
RESOURCE_CPUS: "{{ RESOURCE_CPUS_NUM }}"
|
||||
RESOURCE_MEM_RESERVATION: "{{ RESOURCE_MEM_RESERVATION_NUM }}g"
|
||||
RESOURCE_MEM_LIMIT: "{{ RESOURCE_MEM_LIMIT_NUM }}g"
|
||||
RESOURCE_PIDS_LIMIT: 512
|
||||
@@ -6,6 +6,7 @@ __metaclass__ = type
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
class CertUtils:
|
||||
_domain_cert_mapping = None
|
||||
@@ -22,6 +23,30 @@ class CertUtils:
|
||||
except subprocess.CalledProcessError:
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def run_openssl_dates(cert_path):
|
||||
"""
|
||||
Returns (not_before_ts, not_after_ts) as POSIX timestamps or (None, None) on failure.
|
||||
"""
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
['openssl', 'x509', '-in', cert_path, '-noout', '-startdate', '-enddate'],
|
||||
universal_newlines=True
|
||||
)
|
||||
nb, na = None, None
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('notBefore='):
|
||||
nb = line.split('=', 1)[1].strip()
|
||||
elif line.startswith('notAfter='):
|
||||
na = line.split('=', 1)[1].strip()
|
||||
def _parse(openssl_dt):
|
||||
# OpenSSL format example: "Oct 10 12:34:56 2025 GMT"
|
||||
return int(datetime.strptime(openssl_dt, "%b %d %H:%M:%S %Y %Z").timestamp())
|
||||
return (_parse(nb) if nb else None, _parse(na) if na else None)
|
||||
except Exception:
|
||||
return (None, None)
|
||||
|
||||
@staticmethod
|
||||
def extract_sans(cert_text):
|
||||
dns_entries = []
|
||||
@@ -59,7 +84,6 @@ class CertUtils:
|
||||
else:
|
||||
return domain == san
|
||||
|
||||
|
||||
@classmethod
|
||||
def build_snapshot(cls, cert_base_path):
|
||||
snapshot = []
|
||||
@@ -82,6 +106,17 @@ class CertUtils:
|
||||
|
||||
@classmethod
|
||||
def refresh_cert_mapping(cls, cert_base_path, debug=False):
|
||||
"""
|
||||
Build mapping: SAN -> list of entries
|
||||
entry = {
|
||||
'folder': str,
|
||||
'cert_path': str,
|
||||
'mtime': float,
|
||||
'not_before': int|None,
|
||||
'not_after': int|None,
|
||||
'is_wildcard': bool
|
||||
}
|
||||
"""
|
||||
cert_files = cls.list_cert_files(cert_base_path)
|
||||
mapping = {}
|
||||
for cert_path in cert_files:
|
||||
@@ -90,46 +125,82 @@ class CertUtils:
|
||||
continue
|
||||
sans = cls.extract_sans(cert_text)
|
||||
folder = os.path.basename(os.path.dirname(cert_path))
|
||||
try:
|
||||
mtime = os.stat(cert_path).st_mtime
|
||||
except FileNotFoundError:
|
||||
mtime = 0.0
|
||||
nb, na = cls.run_openssl_dates(cert_path)
|
||||
|
||||
for san in sans:
|
||||
if san not in mapping:
|
||||
mapping[san] = folder
|
||||
entry = {
|
||||
'folder': folder,
|
||||
'cert_path': cert_path,
|
||||
'mtime': mtime,
|
||||
'not_before': nb,
|
||||
'not_after': na,
|
||||
'is_wildcard': san.startswith('*.'),
|
||||
}
|
||||
mapping.setdefault(san, []).append(entry)
|
||||
|
||||
cls._domain_cert_mapping = mapping
|
||||
if debug:
|
||||
print(f"[DEBUG] Refreshed domain-to-cert mapping: {mapping}")
|
||||
print(f"[DEBUG] Refreshed domain-to-cert mapping (counts): "
|
||||
f"{ {k: len(v) for k, v in mapping.items()} }")
|
||||
|
||||
@classmethod
|
||||
def ensure_cert_mapping(cls, cert_base_path, debug=False):
|
||||
if cls._domain_cert_mapping is None or cls.snapshot_changed(cert_base_path):
|
||||
cls.refresh_cert_mapping(cert_base_path, debug)
|
||||
|
||||
@staticmethod
|
||||
def _score_entry(entry):
|
||||
"""
|
||||
Return tuple used for sorting newest-first:
|
||||
(not_before or -inf, mtime)
|
||||
"""
|
||||
nb = entry.get('not_before')
|
||||
mtime = entry.get('mtime', 0.0)
|
||||
return (nb if nb is not None else -1, mtime)
|
||||
|
||||
@classmethod
|
||||
def find_cert_for_domain(cls, domain, cert_base_path, debug=False):
|
||||
cls.ensure_cert_mapping(cert_base_path, debug)
|
||||
|
||||
exact_match = None
|
||||
wildcard_match = None
|
||||
candidates_exact = []
|
||||
candidates_wild = []
|
||||
|
||||
for san, folder in cls._domain_cert_mapping.items():
|
||||
for san, entries in cls._domain_cert_mapping.items():
|
||||
if san == domain:
|
||||
exact_match = folder
|
||||
break
|
||||
if san.startswith('*.'):
|
||||
candidates_exact.extend(entries)
|
||||
elif san.startswith('*.'):
|
||||
base = san[2:]
|
||||
if domain.count('.') == base.count('.') + 1 and domain.endswith('.' + base):
|
||||
wildcard_match = folder
|
||||
candidates_wild.extend(entries)
|
||||
|
||||
if exact_match:
|
||||
if debug:
|
||||
print(f"[DEBUG] Exact match for {domain} found in {exact_match}")
|
||||
return exact_match
|
||||
def _pick_newest(entries):
|
||||
if not entries:
|
||||
return None
|
||||
# newest by (not_before, mtime)
|
||||
best = max(entries, key=cls._score_entry)
|
||||
return best
|
||||
|
||||
if wildcard_match:
|
||||
if debug:
|
||||
print(f"[DEBUG] Wildcard match for {domain} found in {wildcard_match}")
|
||||
return wildcard_match
|
||||
best_exact = _pick_newest(candidates_exact)
|
||||
best_wild = _pick_newest(candidates_wild)
|
||||
|
||||
if best_exact and debug:
|
||||
print(f"[DEBUG] Best exact match for {domain}: {best_exact['folder']} "
|
||||
f"(not_before={best_exact['not_before']}, mtime={best_exact['mtime']})")
|
||||
if best_wild and debug:
|
||||
print(f"[DEBUG] Best wildcard match for {domain}: {best_wild['folder']} "
|
||||
f"(not_before={best_wild['not_before']}, mtime={best_wild['mtime']})")
|
||||
|
||||
# Prefer exact if it exists; otherwise wildcard
|
||||
chosen = best_exact or best_wild
|
||||
|
||||
if chosen:
|
||||
return chosen['folder']
|
||||
|
||||
if debug:
|
||||
print(f"[DEBUG] No certificate folder found for {domain}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class ConfigEntryNotSetError(AppConfigKeyError):
|
||||
pass
|
||||
|
||||
|
||||
def get_app_conf(applications, application_id, config_path, strict=True, default=None):
|
||||
def get_app_conf(applications, application_id, config_path, strict=True, default=None, skip_missing_app=False):
|
||||
# Path to the schema file for this application
|
||||
schema_path = os.path.join('roles', application_id, 'schema', 'main.yml')
|
||||
|
||||
@@ -133,6 +133,9 @@ def get_app_conf(applications, application_id, config_path, strict=True, default
|
||||
try:
|
||||
obj = applications[application_id]
|
||||
except KeyError:
|
||||
if skip_missing_app:
|
||||
# Simply return default instead of failing
|
||||
return default if default is not None else False
|
||||
raise AppConfigKeyError(
|
||||
f"Application ID '{application_id}' not found in applications dict.\n"
|
||||
f"path_trace: {path_trace}\n"
|
||||
|
||||
@@ -142,7 +142,8 @@ class InventoryManager:
|
||||
"""
|
||||
if algorithm == "random_hex":
|
||||
return secrets.token_hex(64)
|
||||
|
||||
if algorithm == "random_hex_32":
|
||||
return secrets.token_hex(32)
|
||||
if algorithm == "sha256":
|
||||
return hashlib.sha256(secrets.token_bytes(32)).hexdigest()
|
||||
if algorithm == "sha1":
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
- name: Execute {{ SOFTWARE_NAME }} Play
|
||||
- name: "Execute {{ SOFTWARE_NAME }} Play"
|
||||
hosts: all
|
||||
tasks:
|
||||
- name: "Load 'constructor' tasks"
|
||||
include_tasks: "tasks/stages/01_constructor.yml"
|
||||
- name: "Load '{{host_type}}' tasks"
|
||||
include_tasks: "tasks/stages/02_{{host_type}}.yml"
|
||||
- name: "Load '{{ host_type }}' tasks"
|
||||
include_tasks: "tasks/stages/02_{{ host_type }}.yml"
|
||||
- name: "Load 'destructor' tasks"
|
||||
include_tasks: "tasks/stages/03_destructor.yml"
|
||||
become: true
|
||||
@@ -3,4 +3,7 @@ collections:
|
||||
- name: community.general
|
||||
- name: hetzner.hcloud
|
||||
yay:
|
||||
- python-simpleaudio
|
||||
- python-simpleaudio
|
||||
- python-numpy
|
||||
pacman:
|
||||
- ansible
|
||||
@@ -148,6 +148,16 @@ roles:
|
||||
description: "Network setup (DNS, Let's Encrypt HTTP, WireGuard, etc.)"
|
||||
icon: "fas fa-globe"
|
||||
invokable: true
|
||||
ai:
|
||||
title: "AI Services"
|
||||
description: "Core AI building blocks—model serving, OpenAI-compatible gateways, vector databases, orchestration, and chat UIs."
|
||||
icon: "fas fa-brain"
|
||||
invokable: true
|
||||
bkp:
|
||||
title: "Backup Services"
|
||||
description: "Service-level backup and recovery components—handling automated data snapshots, remote backups, synchronization services, and backup orchestration across databases, files, and containers."
|
||||
icon: "fas fa-database"
|
||||
invokable: true
|
||||
user:
|
||||
title: "Users & Access"
|
||||
description: "User accounts & access control"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
- name: Link homefolders to cloud
|
||||
ansible.builtin.file:
|
||||
src: "{{nextcloud_cloud_directory}}{{item}}"
|
||||
dest: "{{nextcloud_user_home_directory}}{{item}}"
|
||||
src: "{{nextcloud_cloud_directory}}{{ item }}"
|
||||
dest: "{{nextcloud_user_home_directory}}{{ item }}"
|
||||
owner: "{{ users[desktop_username].username }}"
|
||||
group: "{{ users[desktop_username].username }}"
|
||||
state: link
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
- name: Setup locale.gen
|
||||
template:
|
||||
template:
|
||||
src: locale.gen.j2
|
||||
dest: /etc/locale.gen
|
||||
|
||||
- name: Setup locale.conf
|
||||
template:
|
||||
template:
|
||||
src: locale.conf.j2
|
||||
dest: /etc/locale.conf
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
#de_BE@euro ISO-8859-15
|
||||
#de_CH.UTF-8 UTF-8
|
||||
#de_CH ISO-8859-1
|
||||
de_DE.UTF-8 UTF-8
|
||||
#de_DE.UTF-8 UTF-8
|
||||
#de_DE ISO-8859-1
|
||||
#de_DE@euro ISO-8859-15
|
||||
#de_IT.UTF-8 UTF-8
|
||||
|
||||
4
roles/dev-yay/defaults/main.yml
Normal file
4
roles/dev-yay/defaults/main.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
AUR_HELPER: yay
|
||||
AUR_BUILDER_USER: aur_builder
|
||||
AUR_BUILDER_GROUP: wheel
|
||||
AUR_BUILDER_SUDOERS_PATH: /etc/sudoers.d/11-install-aur_builder
|
||||
@@ -6,42 +6,53 @@
|
||||
- dev-git
|
||||
- dev-base-devel
|
||||
|
||||
- name: install yay
|
||||
- name: Install yay build prerequisites
|
||||
community.general.pacman:
|
||||
name:
|
||||
- base-devel
|
||||
- patch
|
||||
state: present
|
||||
|
||||
- name: Create the `aur_builder` user
|
||||
- name: Create the AUR builder user
|
||||
become: true
|
||||
ansible.builtin.user:
|
||||
name: aur_builder
|
||||
name: "{{ AUR_BUILDER_USER }}"
|
||||
create_home: yes
|
||||
group: wheel
|
||||
group: "{{ AUR_BUILDER_GROUP }}"
|
||||
|
||||
- name: Allow the `aur_builder` user to run `sudo pacman` without a password
|
||||
- name: Allow AUR builder to run pacman without password
|
||||
become: true
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sudoers.d/11-install-aur_builder
|
||||
line: 'aur_builder ALL=(ALL) NOPASSWD: /usr/bin/pacman'
|
||||
path: "{{ AUR_BUILDER_SUDOERS_PATH }}"
|
||||
line: '{{ AUR_BUILDER_USER }} ALL=(ALL) NOPASSWD: /usr/bin/pacman'
|
||||
create: yes
|
||||
validate: 'visudo -cf %s'
|
||||
|
||||
- name: Clone yay from AUR
|
||||
become: true
|
||||
become_user: aur_builder
|
||||
become_user: "{{ AUR_BUILDER_USER }}"
|
||||
git:
|
||||
repo: https://aur.archlinux.org/yay.git
|
||||
dest: /home/aur_builder/yay
|
||||
dest: "/home/{{ AUR_BUILDER_USER }}/yay"
|
||||
clone: yes
|
||||
update: yes
|
||||
|
||||
- name: Build and install yay
|
||||
become: true
|
||||
become_user: aur_builder
|
||||
become_user: "{{ AUR_BUILDER_USER }}"
|
||||
shell: |
|
||||
cd /home/aur_builder/yay
|
||||
cd /home/{{ AUR_BUILDER_USER }}/yay
|
||||
makepkg -si --noconfirm
|
||||
args:
|
||||
creates: /usr/bin/yay
|
||||
|
||||
- name: upgrade the system using yay, only act on AUR packages.
|
||||
become: true
|
||||
become_user: "{{ AUR_BUILDER_USER }}"
|
||||
kewlfft.aur.aur:
|
||||
upgrade: yes
|
||||
use: "{{ AUR_HELPER }}"
|
||||
aur_only: yes
|
||||
when: MODE_UPDATE | bool
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
- block:
|
||||
- include_tasks: 01_core.yml
|
||||
- set_fact:
|
||||
run_once_dev_yay: true
|
||||
when: run_once_dev_yay is not defined
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
docker_compose_skipp_file_creation: false # If set to true the file creation will be skipped
|
||||
docker_pull_git_repository: false # Activates docker repository download and routine
|
||||
docker_compose_flush_handlers: false # Set to true in the vars/main.yml of the including role to autoflush after docker compose routine
|
||||
docker_compose_file_creation_enabled: true # If set to true the file creation will be skipped
|
||||
docker_pull_git_repository: false # Activates docker repository download and routine
|
||||
docker_compose_flush_handlers: false # Set to true in the vars/main.yml of the including role to autoflush after docker compose routine
|
||||
@@ -1,15 +1,18 @@
|
||||
- name: Set default docker_repository_path
|
||||
set_fact:
|
||||
docker_repository_path: "{{docker_compose.directories.services}}repository/"
|
||||
docker_repository_path: "{{ [ docker_compose.directories.services, 'repository/' ] | path_join }}"
|
||||
|
||||
- name: pull docker repository
|
||||
git:
|
||||
repo: "{{ docker_repository_address }}"
|
||||
dest: "{{ docker_repository_path }}"
|
||||
version: "{{ docker_repository_branch | default('main') }}"
|
||||
depth: 1
|
||||
update: yes
|
||||
recursive: yes
|
||||
repo: "{{ docker_repository_address }}"
|
||||
dest: "{{ docker_repository_path }}"
|
||||
version: "{{ docker_repository_branch | default('main') }}"
|
||||
single_branch: yes
|
||||
depth: 1
|
||||
update: yes
|
||||
recursive: yes
|
||||
force: yes
|
||||
accept_hostkey: yes
|
||||
notify:
|
||||
- docker compose build
|
||||
- docker compose up
|
||||
|
||||
@@ -28,6 +28,21 @@
|
||||
- env_template is failed
|
||||
- "'Could not find or access' not in env_template.msg"
|
||||
|
||||
- name: "Create (optional) '{{ docker_compose.files.docker_compose_override }}'"
|
||||
template:
|
||||
src: "{{ item }}"
|
||||
dest: "{{ docker_compose.files.docker_compose_override }}"
|
||||
mode: '770'
|
||||
force: yes
|
||||
notify: docker compose up
|
||||
register: docker_compose_override_template
|
||||
loop:
|
||||
- "{{ application_id | abs_role_path_by_application_id }}/templates/docker-compose.override.yml.j2"
|
||||
- "{{ application_id | abs_role_path_by_application_id }}/files/docker-compose.override.yml"
|
||||
failed_when:
|
||||
- docker_compose_override_template is failed
|
||||
- "'Could not find or access' not in docker_compose_override_template.msg"
|
||||
|
||||
- name: "Create (obligatoric) '{{ docker_compose.files.docker_compose }}'"
|
||||
template:
|
||||
src: "docker-compose.yml.j2"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
include_tasks: "04_files.yml"
|
||||
- name: "Ensure that {{ docker_compose.directories.instance }} is up"
|
||||
include_tasks: "05_ensure_up.yml"
|
||||
when: not docker_compose_skipp_file_creation | bool
|
||||
when: docker_compose_file_creation_enabled | bool
|
||||
|
||||
- name: "flush docker compose for '{{ application_id }}'"
|
||||
meta: flush_handlers
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{# This template needs to be included in docker-compose.yml #}
|
||||
networks:
|
||||
{# Central RDMS-Database Network #}
|
||||
{% if
|
||||
(applications | get_app_conf(application_id, 'features.central_database', False) and database_type is defined) or
|
||||
application_id in ['svc-db-mariadb','svc-db-postgres']
|
||||
@@ -7,6 +8,7 @@ networks:
|
||||
{{ applications | get_app_conf('svc-db-' ~ database_type, 'docker.network') }}:
|
||||
external: true
|
||||
{% endif %}
|
||||
{# Central LDAP Network #}
|
||||
{% if
|
||||
applications | get_app_conf(application_id, 'features.ldap', False) and
|
||||
applications | get_app_conf('svc-db-openldap', 'network.docker', False)
|
||||
@@ -14,7 +16,13 @@ networks:
|
||||
{{ applications | get_app_conf('svc-db-openldap', 'docker.network') }}:
|
||||
external: true
|
||||
{% endif %}
|
||||
{% if not application_id.startswith('svc-db-') %}
|
||||
{# Central AI Network #}
|
||||
{% if applications | get_app_conf(application_id, 'features.local_ai', False) %}
|
||||
{{ applications | get_app_conf('svc-ai-ollama', 'docker.network') }}:
|
||||
external: true
|
||||
{% endif %}
|
||||
{# Default Network #}
|
||||
{% if not application_id.startswith('svc-db-') and not application_id.startswith('svc-ai-') %}
|
||||
default:
|
||||
{% if
|
||||
application_id in networks.local and
|
||||
@@ -25,7 +33,7 @@ networks:
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: {{networks.local[application_id].subnet}}
|
||||
- subnet: {{ networks.local[application_id].subnet }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ "\n" }}
|
||||
@@ -1,4 +1,6 @@
|
||||
users:
|
||||
blackhole:
|
||||
description: "Everything what will be send to this user will disapear"
|
||||
username: "blackhole"
|
||||
username: "blackhole"
|
||||
roles:
|
||||
- mail-bot
|
||||
@@ -1,2 +1,4 @@
|
||||
# @See https://chatgpt.com/share/67a23d18-fb54-800f-983c-d6d00752b0b4
|
||||
docker_compose: "{{ application_id | get_docker_paths(PATH_DOCKER_COMPOSE_INSTANCES) }}"
|
||||
docker_compose: "{{ application_id | get_docker_paths(PATH_DOCKER_COMPOSE_INSTANCES) }}"
|
||||
docker_compose_command_base: "docker compose --env-file {{ docker_compose.files.env }}"
|
||||
docker_compose_command_exec: "{{ docker_compose_command_base }} exec"
|
||||
@@ -1,11 +1,13 @@
|
||||
{# Base for docker services #}
|
||||
|
||||
restart: {{ DOCKER_RESTART_POLICY }}
|
||||
restart: {{ docker_restart_policy | default(DOCKER_RESTART_POLICY) }}
|
||||
{% if application_id | has_env %}
|
||||
env_file:
|
||||
- "{{ docker_compose.files.env }}"
|
||||
{% endif %}
|
||||
logging:
|
||||
driver: journald
|
||||
|
||||
{% filter indent(4) %}
|
||||
{% include 'roles/docker-container/templates/resource.yml.j2' %}
|
||||
{% endfilter %}
|
||||
{{ "\n" }}
|
||||
6
roles/docker-container/templates/build.yml.j2
Normal file
6
roles/docker-container/templates/build.yml.j2
Normal file
@@ -0,0 +1,6 @@
|
||||
{# integrate it into service sections to be build by Dockerfile #}
|
||||
pull_policy: never
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
{# pass Arguments here #}
|
||||
31
roles/docker-container/templates/healthcheck/http.yml.j2
Normal file
31
roles/docker-container/templates/healthcheck/http.yml.j2
Normal file
@@ -0,0 +1,31 @@
|
||||
{# ------------------------------------------------------------------------------
|
||||
Healthcheck: HTTP Local
|
||||
------------------------------------------------------------------------------
|
||||
This template defines a generic HTTP healthcheck for containers exposing
|
||||
a web service on a local port (e.g., Nginx, Apache, PHP-FPM, Shopware, etc.).
|
||||
|
||||
It uses `wget` or `curl` (as fallback) to test if the container responds on
|
||||
http://127.0.0.1:{{ container_port }}/. If the request succeeds, Docker marks
|
||||
the container as "healthy"; otherwise, as "unhealthy".
|
||||
|
||||
Parameters:
|
||||
- container_port: The internal port the service listens on.
|
||||
|
||||
Timing:
|
||||
- interval: 30s → Check every 30 seconds
|
||||
- timeout: 5s → Each check must complete within 5 seconds
|
||||
- retries: 5 → Mark unhealthy after 5 consecutive failures
|
||||
- start_period: 20s → Grace period before health checks begin
|
||||
|
||||
Usage:
|
||||
{% filter indent(4) %}
|
||||
{% include 'roles/docker-container/templates/healthcheck/http.yml.j2' %}
|
||||
{% endfilter %}
|
||||
------------------------------------------------------------------------------
|
||||
#}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:{{ container_port }}/ >/dev/null || curl -fsS http://127.0.0.1:{{ container_port }}/ >/dev/null"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
@@ -1,15 +1,25 @@
|
||||
{# This template needs to be included in docker-compose.yml containers #}
|
||||
networks:
|
||||
{# Central RDMS-Database Network #}
|
||||
{% if
|
||||
(applications | get_app_conf(application_id, 'features.central_database', False) and database_type is defined) or
|
||||
application_id in ['svc-db-mariadb','svc-db-postgres']
|
||||
%}
|
||||
{{ applications | get_app_conf('svc-db-' ~ database_type, 'docker.network') }}:
|
||||
{% if application_id in ['svc-db-mariadb','svc-db-postgres'] %}
|
||||
aliases:
|
||||
- {{ database_type }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# Central LDAP Network #}
|
||||
{% if applications | get_app_conf(application_id, 'features.ldap', False) and applications | get_app_conf('svc-db-openldap', 'network.docker') %}
|
||||
{{ applications | get_app_conf('svc-db-openldap', 'docker.network') }}:
|
||||
{% endif %}
|
||||
{% if application_id != 'svc-db-openldap' %}
|
||||
{# Central AI Network #}
|
||||
{% if applications | get_app_conf(application_id, 'features.local_ai', False) %}
|
||||
{{ applications | get_app_conf('svc-ai-ollama', 'docker.network') }}:
|
||||
{% endif %}
|
||||
{% if not application_id.startswith('svc-db-') and not application_id.startswith('svc-ai-') %}
|
||||
default:
|
||||
{% endif %}
|
||||
{{ "\n" }}
|
||||
4
roles/docker-container/templates/resource.yml.j2
Normal file
4
roles/docker-container/templates/resource.yml.j2
Normal file
@@ -0,0 +1,4 @@
|
||||
cpus: {{ applications | resource_filter(application_id, 'cpus', service_name | default(''), RESOURCE_CPUS) }}
|
||||
mem_reservation: {{ applications | resource_filter(application_id, 'mem_reservation', service_name | default(''), RESOURCE_MEM_RESERVATION) }}
|
||||
mem_limit: {{ applications | resource_filter(application_id, 'mem_limit', service_name | default(''), RESOURCE_MEM_LIMIT) }}
|
||||
pids_limit: {{ applications | resource_filter(application_id, 'pids_limit', service_name | default(''), RESOURCE_PIDS_LIMIT) }}
|
||||
@@ -4,7 +4,7 @@
|
||||
run_once_pkgmgr_install: true
|
||||
when: run_once_pkgmgr_install is not defined
|
||||
|
||||
- name: update {{ package_name }}
|
||||
- name: "update {{ package_name }}"
|
||||
ansible.builtin.shell: |
|
||||
source ~/.venvs/pkgmgr/bin/activate
|
||||
pkgmgr update {{ package_name }} --dependencies --clone-mode https
|
||||
|
||||
23
roles/svc-ai-ollama/README.md
Normal file
23
roles/svc-ai-ollama/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
# Ollama
|
||||
|
||||
## Description
|
||||
|
||||
**Ollama** is a local model server that runs open LLMs on your hardware and exposes a simple HTTP API. It’s the backbone for privacy-first AI: prompts and data stay on your machines.
|
||||
|
||||
## Overview
|
||||
|
||||
After the first model pull, Ollama serves models to clients like Open WebUI (for chat) and Flowise (for workflows). Models are cached locally for quick reuse and can run fully offline when required.
|
||||
|
||||
## Features
|
||||
|
||||
* Run popular open models (chat, code, embeddings) locally
|
||||
* Simple, predictable HTTP API for developers
|
||||
* Local caching to avoid repeated downloads
|
||||
* Works seamlessly with Open WebUI and Flowise
|
||||
* Offline-capable for air-gapped deployments
|
||||
|
||||
## Further Resources
|
||||
|
||||
* Ollama — [https://ollama.com](https://ollama.com)
|
||||
* Ollama Model Library — [https://ollama.com/library](https://ollama.com/library)
|
||||
22
roles/svc-ai-ollama/config/main.yml
Normal file
22
roles/svc-ai-ollama/config/main.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
features:
|
||||
local_ai: true # Needs to be set so that network is loaded
|
||||
docker:
|
||||
services:
|
||||
ollama:
|
||||
backup:
|
||||
no_stop_required: true
|
||||
image: ollama/ollama
|
||||
version: latest
|
||||
name: ollama
|
||||
port: 11434
|
||||
cpus: "4.0"
|
||||
mem_reservation: "6g"
|
||||
mem_limit: "8g"
|
||||
pids_limit: 2048
|
||||
volumes:
|
||||
models: "ollama_models"
|
||||
network: "ollama"
|
||||
preload_models:
|
||||
- "llama3:latest"
|
||||
- "mistral:latest"
|
||||
- "nomic-embed-text:latest"
|
||||
25
roles/svc-ai-ollama/meta/main.yml
Normal file
25
roles/svc-ai-ollama/meta/main.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Installs Ollama — a local model server for running open LLMs with a simple HTTP API."
|
||||
license: "Infinito.Nexus NonCommercial License"
|
||||
license_url: "https://s.infinito.nexus/license"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
galaxy_tags:
|
||||
- ai
|
||||
- llm
|
||||
- inference
|
||||
- offline
|
||||
- privacy
|
||||
- self-hosted
|
||||
- ollama
|
||||
repository: "https://s.infinito.nexus/code"
|
||||
issue_tracker_url: "https://s.infinito.nexus/issues"
|
||||
documentation: "https://s.infinito.nexus/code/"
|
||||
logo:
|
||||
class: "fa-solid fa-microchip"
|
||||
run_after: []
|
||||
dependencies: []
|
||||
38
roles/svc-ai-ollama/tasks/01_core.yml
Normal file
38
roles/svc-ai-ollama/tasks/01_core.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
- name: create docker network for Ollama, so that other applications can access it
|
||||
community.docker.docker_network:
|
||||
name: "{{ OLLAMA_NETWORK }}"
|
||||
state: present
|
||||
ipam_config:
|
||||
- subnet: "{{ networks.local[application_id].subnet }}"
|
||||
|
||||
- name: Include dependency 'sys-svc-docker'
|
||||
include_role:
|
||||
name: sys-svc-docker
|
||||
when: run_once_sys_svc_docker is not defined
|
||||
|
||||
- name: "include docker-compose role"
|
||||
include_role:
|
||||
name: docker-compose
|
||||
vars:
|
||||
docker_compose_flush_handlers: true
|
||||
|
||||
- name: Pre-pull Ollama models
|
||||
vars:
|
||||
_cmd: "docker exec -i {{ OLLAMA_CONTAINER }} ollama pull {{ model }}"
|
||||
shell: "{{ _cmd }}"
|
||||
register: pull_result
|
||||
loop: "{{ OLLAMA_PRELOAD_MODELS }}"
|
||||
loop_control:
|
||||
loop_var: model
|
||||
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
||||
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
||||
changed_when: >
|
||||
(not (ASYNC_ENABLED | bool)) and (
|
||||
'downloaded' in (pull_result.stdout | default('')) or
|
||||
'pulling manifest' in (pull_result.stdout | default(''))
|
||||
)
|
||||
failed_when: >
|
||||
(pull_result.rc | default(0)) != 0 and
|
||||
('up to date' not in (pull_result.stdout | default('')))
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
5
roles/svc-ai-ollama/tasks/main.yml
Normal file
5
roles/svc-ai-ollama/tasks/main.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
- block:
|
||||
- include_tasks: 01_core.yml
|
||||
vars:
|
||||
flush_handlers: true
|
||||
when: run_once_svc_ai_ollama is not defined
|
||||
17
roles/svc-ai-ollama/templates/docker-compose.yml.j2
Normal file
17
roles/svc-ai-ollama/templates/docker-compose.yml.j2
Normal file
@@ -0,0 +1,17 @@
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
|
||||
ollama:
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
image: {{ OLLAMA_IMAGE }}:{{ OLLAMA_VERSION }}
|
||||
container_name: {{ OLLAMA_CONTAINER }}
|
||||
expose:
|
||||
- "{{ OLLAMA_PORT }}"
|
||||
volumes:
|
||||
- ollama_models:/root/.ollama
|
||||
{% include 'roles/docker-container/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
ollama_models:
|
||||
name: {{ OLLAMA_VOLUME }}
|
||||
16
roles/svc-ai-ollama/vars/main.yml
Normal file
16
roles/svc-ai-ollama/vars/main.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# General
|
||||
application_id: "svc-ai-ollama"
|
||||
|
||||
# Docker
|
||||
docker_compose_flush_handlers: true
|
||||
|
||||
# Ollama
|
||||
# https://ollama.com/
|
||||
OLLAMA_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.ollama.version') }}"
|
||||
OLLAMA_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.ollama.image') }}"
|
||||
OLLAMA_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.ollama.name') }}"
|
||||
OLLAMA_PORT: "{{ applications | get_app_conf(application_id, 'docker.services.ollama.port') }}"
|
||||
OLLAMA_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.models') }}"
|
||||
OLLAMA_NETWORK: "{{ applications | get_app_conf(application_id, 'docker.network') }}"
|
||||
OLLAMA_PRELOAD_MODELS: "{{ applications | get_app_conf(application_id, 'preload_models') }}"
|
||||
|
||||
0
roles/svc-bkp-rmt-2-loc/__init__.py
Normal file
0
roles/svc-bkp-rmt-2-loc/__init__.py
Normal file
0
roles/svc-bkp-rmt-2-loc/files/__init__.py
Normal file
0
roles/svc-bkp-rmt-2-loc/files/__init__.py
Normal file
132
roles/svc-bkp-rmt-2-loc/files/pull-specific-host.py
Normal file
132
roles/svc-bkp-rmt-2-loc/files/pull-specific-host.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import sys
|
||||
|
||||
|
||||
def run_command(command, capture_output=True, check=False, shell=True):
|
||||
"""Run a shell command and return its output as string."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=capture_output,
|
||||
shell=shell,
|
||||
text=True,
|
||||
check=check
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
if capture_output:
|
||||
print(e.stdout)
|
||||
print(e.stderr)
|
||||
raise
|
||||
|
||||
|
||||
def pull_backups(hostname: str):
|
||||
print(f"pulling backups from: {hostname}")
|
||||
errors = 0
|
||||
|
||||
print("loading meta data...")
|
||||
remote_host = f"backup@{hostname}"
|
||||
print(f"host address: {remote_host}")
|
||||
|
||||
remote_machine_id = run_command(f'ssh "{remote_host}" sha256sum /etc/machine-id')[:64]
|
||||
print(f"remote machine id: {remote_machine_id}")
|
||||
|
||||
general_backup_machine_dir = f"/Backups/{remote_machine_id}/"
|
||||
print(f"backup dir: {general_backup_machine_dir}")
|
||||
|
||||
try:
|
||||
remote_backup_types = run_command(
|
||||
f'ssh "{remote_host}" "find {general_backup_machine_dir} -maxdepth 1 -type d -execdir basename {{}} ;"'
|
||||
).splitlines()
|
||||
print(f"backup types: {' '.join(remote_backup_types)}")
|
||||
except subprocess.CalledProcessError:
|
||||
sys.exit(1)
|
||||
|
||||
for backup_type in remote_backup_types:
|
||||
if backup_type == remote_machine_id:
|
||||
continue
|
||||
|
||||
print(f"backup type: {backup_type}")
|
||||
|
||||
general_backup_type_dir = f"{general_backup_machine_dir}{backup_type}/"
|
||||
general_versions_dir = general_backup_type_dir
|
||||
|
||||
# local previous version
|
||||
try:
|
||||
local_previous_version_dir = run_command(f"ls -d {general_versions_dir}* | tail -1")
|
||||
except subprocess.CalledProcessError:
|
||||
local_previous_version_dir = ""
|
||||
print(f"last local backup: {local_previous_version_dir}")
|
||||
|
||||
# remote versions
|
||||
remote_backup_versions = run_command(
|
||||
f'ssh "{remote_host}" "ls -d /Backups/{remote_machine_id}/backup-docker-to-local/*"'
|
||||
).splitlines()
|
||||
print(f"remote backup versions: {' '.join(remote_backup_versions)}")
|
||||
|
||||
remote_last_backup_dir = remote_backup_versions[-1] if remote_backup_versions else ""
|
||||
print(f"last remote backup: {remote_last_backup_dir}")
|
||||
|
||||
remote_source_path = f"{remote_host}:{remote_last_backup_dir}/"
|
||||
print(f"source path: {remote_source_path}")
|
||||
|
||||
local_backup_destination_path = remote_last_backup_dir
|
||||
print(f"backup destination: {local_backup_destination_path}")
|
||||
|
||||
print("creating local backup destination folder...")
|
||||
os.makedirs(local_backup_destination_path, exist_ok=True)
|
||||
|
||||
rsync_command = (
|
||||
f'rsync -abP --delete --delete-excluded --rsync-path="sudo rsync" '
|
||||
f'--link-dest="{local_previous_version_dir}" "{remote_source_path}" "{local_backup_destination_path}"'
|
||||
)
|
||||
print("starting backup...")
|
||||
print(f"executing: {rsync_command}")
|
||||
|
||||
retry_count = 0
|
||||
max_retries = 12
|
||||
retry_delay = 300 # 5 minutes
|
||||
last_retry_start = 0
|
||||
max_retry_duration = 43200 # 12 hours
|
||||
|
||||
rsync_exit_code = 1
|
||||
while retry_count < max_retries:
|
||||
print(f"Retry attempt: {retry_count + 1}")
|
||||
if retry_count > 0:
|
||||
current_time = int(time.time())
|
||||
last_retry_duration = current_time - last_retry_start
|
||||
if last_retry_duration >= max_retry_duration:
|
||||
print("Last retry took more than 12 hours, increasing max retries to 12.")
|
||||
max_retries = 12
|
||||
last_retry_start = int(time.time())
|
||||
rsync_exit_code = os.system(rsync_command)
|
||||
if rsync_exit_code == 0:
|
||||
break
|
||||
retry_count += 1
|
||||
time.sleep(retry_delay)
|
||||
|
||||
if rsync_exit_code != 0:
|
||||
print(f"Error: rsync failed after {max_retries} attempts")
|
||||
errors += 1
|
||||
|
||||
sys.exit(errors)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Pull backups from a remote backup host via rsync."
|
||||
)
|
||||
parser.add_argument(
|
||||
"hostname",
|
||||
help="Hostname from which backup should be pulled"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
pull_backups(args.hostname)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/bin/bash
|
||||
# @param $1 hostname from which backup should be pulled
|
||||
|
||||
echo "pulling backups from: $1" &&
|
||||
|
||||
# error counter
|
||||
errors=0 &&
|
||||
|
||||
echo "loading meta data..." &&
|
||||
|
||||
remote_host="backup@$1" &&
|
||||
echo "host address: $remote_host" &&
|
||||
|
||||
remote_machine_id="$( (ssh "$remote_host" sha256sum /etc/machine-id) | head -c 64 )" &&
|
||||
echo "remote machine id: $remote_machine_id" &&
|
||||
|
||||
general_backup_machine_dir="/Backups/$remote_machine_id/" &&
|
||||
echo "backup dir: $general_backup_machine_dir" &&
|
||||
|
||||
remote_backup_types="$(ssh "$remote_host" "find $general_backup_machine_dir -maxdepth 1 -type d -execdir basename {} ;")" &&
|
||||
echo "backup types: $remote_backup_types" || exit 1
|
||||
|
||||
for backup_type in $remote_backup_types; do
|
||||
if [ "$backup_type" != "$remote_machine_id" ]; then
|
||||
echo "backup type: $backup_type" &&
|
||||
|
||||
general_backup_type_dir="$general_backup_machine_dir""$backup_type/" &&
|
||||
general_versions_dir="$general_backup_type_dir" &&
|
||||
local_previous_version_dir="$(ls -d $general_versions_dir* | tail -1)" &&
|
||||
echo "last local backup: $local_previous_version_dir" &&
|
||||
|
||||
remote_backup_versions="$(ssh "$remote_host" ls -d "$general_backup_type_dir"\*)" &&
|
||||
echo "remote backup versions: $remote_backup_versions" &&
|
||||
|
||||
|
||||
remote_last_backup_dir=$(echo "$remote_backup_versions" | tail -1) &&
|
||||
echo "last remote backup: $remote_last_backup_dir" &&
|
||||
|
||||
remote_source_path="$remote_host:$remote_last_backup_dir/" &&
|
||||
echo "source path: $remote_source_path" &&
|
||||
|
||||
local_backup_destination_path=$remote_last_backup_dir &&
|
||||
echo "backup destination: $local_backup_destination_path" &&
|
||||
|
||||
echo "creating local backup destination folder..." &&
|
||||
mkdir -vp "$local_backup_destination_path" &&
|
||||
|
||||
echo "starting backup..."
|
||||
rsync_command='rsync -abP --delete --delete-excluded --rsync-path="sudo rsync" --link-dest="'$local_previous_version_dir'" "'$remote_source_path'" "'$local_backup_destination_path'"'
|
||||
|
||||
echo "executing: $rsync_command"
|
||||
|
||||
retry_count=0
|
||||
max_retries=12
|
||||
retry_delay=300 # Retry delay in seconds (5 minutes)
|
||||
last_retry_start=0
|
||||
max_retry_duration=43200 # Maximum duration for a single retry attempt (12 hours)
|
||||
|
||||
while [[ $retry_count -lt $max_retries ]]; do
|
||||
echo "Retry attempt: $((retry_count + 1))"
|
||||
if [[ $retry_count -gt 0 ]]; then
|
||||
current_time=$(date +%s)
|
||||
last_retry_duration=$((current_time - last_retry_start))
|
||||
if [[ $last_retry_duration -ge $max_retry_duration ]]; then
|
||||
echo "Last retry took more than 12 hours, increasing max retries to 12."
|
||||
max_retries=12
|
||||
fi
|
||||
fi
|
||||
last_retry_start=$(date +%s)
|
||||
eval "$rsync_command"
|
||||
rsync_exit_code=$?
|
||||
if [[ $rsync_exit_code -eq 0 ]]; then
|
||||
break
|
||||
fi
|
||||
retry_count=$((retry_count + 1))
|
||||
sleep $retry_delay
|
||||
done
|
||||
|
||||
if [[ $rsync_exit_code -ne 0 ]]; then
|
||||
echo "Error: rsync failed after $max_retries attempts"
|
||||
((errors += 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
exit $errors;
|
||||
@@ -10,15 +10,15 @@
|
||||
- include_tasks: utils/run_once.yml
|
||||
when: run_once_svc_bkp_rmt_2_loc is not defined
|
||||
|
||||
- name: "create {{ DOCKER_BACKUP_REMOTE_2_LOCAL_DIR }}"
|
||||
- name: "Create Directory '{{ DOCKER_BACKUP_REMOTE_2_LOCAL_DIR }}'"
|
||||
file:
|
||||
path: "{{ DOCKER_BACKUP_REMOTE_2_LOCAL_DIR }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: create svc-bkp-rmt-2-loc.sh
|
||||
- name: "Deploy '{{ DOCKER_BACKUP_REMOTE_2_LOCAL_SCRIPT }}'"
|
||||
copy:
|
||||
src: svc-bkp-rmt-2-loc.sh
|
||||
src: "{{ DOCKER_BACKUP_REMOTE_2_LOCAL_FILE }}"
|
||||
dest: "{{ DOCKER_BACKUP_REMOTE_2_LOCAL_SCRIPT }}"
|
||||
mode: "0755"
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
hosts="{{ DOCKER_BACKUP_REMOTE_2_LOCAL_BACKUP_PROVIDERS | join(' ') }}";
|
||||
errors=0
|
||||
for host in $hosts; do
|
||||
bash {{ DOCKER_BACKUP_REMOTE_2_LOCAL_SCRIPT }} $host || ((errors+=1));
|
||||
python {{ DOCKER_BACKUP_REMOTE_2_LOCAL_SCRIPT }} $host || ((errors+=1));
|
||||
done;
|
||||
exit $errors;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# General
|
||||
application_id: svc-bkp-rmt-2-loc
|
||||
system_service_id: "{{ application_id }}"
|
||||
system_service_id: "{{ application_id }}"
|
||||
|
||||
# Role Specific
|
||||
DOCKER_BACKUP_REMOTE_2_LOCAL_DIR: '{{ PATH_ADMINISTRATOR_SCRIPTS }}{{ application_id }}/'
|
||||
DOCKER_BACKUP_REMOTE_2_LOCAL_SCRIPT: "{{ DOCKER_BACKUP_REMOTE_2_LOCAL_DIR }}svc-bkp-rmt-2-loc.sh"
|
||||
DOCKER_BACKUP_REMOTE_2_LOCAL_FILE: 'pull-specific-host.py'
|
||||
DOCKER_BACKUP_REMOTE_2_LOCAL_SCRIPT: "{{ [ DOCKER_BACKUP_REMOTE_2_LOCAL_DIR , DOCKER_BACKUP_REMOTE_2_LOCAL_FILE ] | path_join }}"
|
||||
DOCKER_BACKUP_REMOTE_2_LOCAL_BACKUP_PROVIDERS: "{{ applications | get_app_conf(application_id, 'backup_providers') }}"
|
||||
@@ -1,11 +1,16 @@
|
||||
docker:
|
||||
services:
|
||||
mariadb:
|
||||
version: "latest"
|
||||
image: "mariadb"
|
||||
name: "mariadb"
|
||||
version: "latest"
|
||||
image: "mariadb"
|
||||
name: "mariadb"
|
||||
backup:
|
||||
database_routine: true
|
||||
# Performance Variables aren't used yet, but will be in the future as soon as an docker file is implemented
|
||||
cpus: "2.0"
|
||||
mem_reservation: "2g"
|
||||
mem_limit: "4g"
|
||||
pids_limit: 1024
|
||||
network: "mariadb"
|
||||
volumes:
|
||||
data: "mariadb_data"
|
||||
@@ -5,9 +5,14 @@ network:
|
||||
docker:
|
||||
services:
|
||||
openldap:
|
||||
image: "bitnami/openldap"
|
||||
image: "bitnamilegacy/openldap"
|
||||
name: "openldap"
|
||||
version: "latest"
|
||||
cpus: 1.25
|
||||
# Optimized up to 5k user
|
||||
mem_reservation: 1g
|
||||
mem_limit: 1.5g
|
||||
pids_limit: 1024
|
||||
network: "openldap"
|
||||
volumes:
|
||||
data: "openldap_data"
|
||||
|
||||
40
roles/svc-db-openldap/handlers/main.yml
Normal file
40
roles/svc-db-openldap/handlers/main.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
- name: Load memberof module from file in OpenLDAP container
|
||||
shell: >
|
||||
docker exec -i {{ OPENLDAP_CONTAINER }} ldapmodify -Y EXTERNAL -H ldapi:/// -f "{{ [OPENLDAP_LDIF_PATH_DOCKER, 'configuration/01_member_of_configuration.ldif' ] | path_join }}"
|
||||
listen:
|
||||
- "Import configuration LDIF files"
|
||||
# @todo Remove the following ignore errors when setting up a new server
|
||||
# Just here because debugging would take to much time
|
||||
ignore_errors: true
|
||||
|
||||
- name: Refint Module Activation for OpenLDAP
|
||||
shell: >
|
||||
docker exec -i {{ OPENLDAP_CONTAINER }} ldapadd -Y EXTERNAL -H ldapi:/// -f "{{ [ OPENLDAP_LDIF_PATH_DOCKER, 'configuration/02_member_of_configuration.ldif' ] | path_join }}"
|
||||
listen:
|
||||
- "Import configuration LDIF files"
|
||||
register: ldapadd_result
|
||||
failed_when: ldapadd_result.rc not in [0, 68]
|
||||
# @todo Remove the following ignore errors when setting up a new server
|
||||
# Just here because debugging would take to much time
|
||||
ignore_errors: true
|
||||
|
||||
- name: Refint Overlay Configuration for OpenLDAP
|
||||
shell: >
|
||||
docker exec -i {{ OPENLDAP_CONTAINER }} ldapmodify -Y EXTERNAL -H ldapi:/// -f "{{ [ OPENLDAP_LDIF_PATH_DOCKER, 'configuration/03_member_of_configuration.ldif' ] | path_join }}"
|
||||
listen:
|
||||
- "Import configuration LDIF files"
|
||||
register: ldapadd_result
|
||||
failed_when: ldapadd_result.rc not in [0, 68]
|
||||
# @todo Remove the following ignore errors when setting up a new server
|
||||
# Just here because debugging would take to much time
|
||||
ignore_errors: true
|
||||
|
||||
- name: "Import users, groups, etc. to LDAP"
|
||||
shell: >
|
||||
docker exec -i {{ OPENLDAP_CONTAINER }} ldapadd -x -D "{{ LDAP.DN.ADMINISTRATOR.DATA }}" -w "{{ LDAP.BIND_CREDENTIAL }}" -c -f "{{ [ OPENLDAP_LDIF_PATH_DOCKER, 'groups', (item | basename | regex_replace('\.j2$', '')) ] | path_join }}"
|
||||
register: ldapadd_result
|
||||
changed_when: "'adding new entry' in ldapadd_result.stdout"
|
||||
failed_when: ldapadd_result.rc not in [0, 20, 68, 65]
|
||||
listen:
|
||||
- "Import groups LDIF files"
|
||||
loop: "{{ query('fileglob', role_path ~ '/templates/ldif/groups/*.j2') | sort }}"
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
- name: "Query available LDAP databases"
|
||||
shell: |
|
||||
docker exec {{ openldap_name }} \
|
||||
docker exec {{ OPENLDAP_CONTAINER }} \
|
||||
ldapsearch -Y EXTERNAL -H ldapi:/// -LLL -b cn=config "(olcDatabase=*)" dn
|
||||
register: ldap_databases
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
|
||||
- name: "Generate hash for Database Admin password"
|
||||
shell: |
|
||||
docker exec {{ openldap_name }} \
|
||||
docker exec {{ OPENLDAP_CONTAINER }} \
|
||||
slappasswd -s "{{ LDAP.BIND_CREDENTIAL }}"
|
||||
register: database_admin_pw_hash
|
||||
|
||||
- name: "Reset Database Admin password in LDAP (olcRootPW)"
|
||||
shell: |
|
||||
docker exec -i {{ openldap_name }} ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
|
||||
docker exec -i {{ OPENLDAP_CONTAINER }} ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
|
||||
dn: {{ data_backend_dn }}
|
||||
changetype: modify
|
||||
replace: olcRootPW
|
||||
@@ -42,13 +42,13 @@
|
||||
|
||||
- name: "Generate hash for Configuration Admin password"
|
||||
shell: |
|
||||
docker exec {{ openldap_name }} \
|
||||
docker exec {{ OPENLDAP_CONTAINER }} \
|
||||
slappasswd -s "{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}"
|
||||
register: config_admin_pw_hash
|
||||
|
||||
- name: "Reset Configuration Admin password in LDAP (olcRootPW)"
|
||||
shell: |
|
||||
docker exec -i {{ openldap_name }} ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
|
||||
docker exec -i {{ OPENLDAP_CONTAINER }} ldapmodify -Y EXTERNAL -H ldapi:/// <<EOF
|
||||
dn: {{ config_backend_dn }}
|
||||
changetype: modify
|
||||
replace: olcRootPW
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- name: Ensure LDAP users exist
|
||||
community.general.ldap_entry:
|
||||
dn: "{{ LDAP.USER.ATTRIBUTES.ID }}={{ item.key }},{{ LDAP.DN.OU.USERS }}"
|
||||
server_uri: "{{ openldap_server_uri }}"
|
||||
server_uri: "{{ OPENLDAP_SERVER_URI }}"
|
||||
bind_dn: "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
|
||||
bind_pw: "{{ LDAP.BIND_CREDENTIAL }}"
|
||||
objectClass: "{{ LDAP.USER.OBJECTS.STRUCTURAL }}"
|
||||
@@ -30,7 +30,7 @@
|
||||
- name: Ensure required objectClass values and mail address are present
|
||||
community.general.ldap_attrs:
|
||||
dn: "{{ LDAP.USER.ATTRIBUTES.ID }}={{ item.key }},{{ LDAP.DN.OU.USERS }}"
|
||||
server_uri: "{{ openldap_server_uri }}"
|
||||
server_uri: "{{ OPENLDAP_SERVER_URI }}"
|
||||
bind_dn: "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
|
||||
bind_pw: "{{ LDAP.BIND_CREDENTIAL }}"
|
||||
attributes:
|
||||
@@ -46,7 +46,7 @@
|
||||
- name: "Ensure container for application roles exists"
|
||||
community.general.ldap_entry:
|
||||
dn: "{{ LDAP.DN.OU.ROLES }}"
|
||||
server_uri: "{{ openldap_server_uri }}"
|
||||
server_uri: "{{ OPENLDAP_SERVER_URI }}"
|
||||
bind_dn: "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
|
||||
bind_pw: "{{ LDAP.BIND_CREDENTIAL }}"
|
||||
objectClass: organizationalUnit
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- name: Gather all users with their current objectClass list
|
||||
community.general.ldap_search:
|
||||
server_uri: "{{ openldap_server_uri }}"
|
||||
server_uri: "{{ OPENLDAP_SERVER_URI }}"
|
||||
bind_dn: "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
|
||||
bind_pw: "{{ LDAP.BIND_CREDENTIAL }}"
|
||||
dn: "{{ LDAP.DN.OU.USERS }}"
|
||||
@@ -14,16 +14,16 @@
|
||||
|
||||
- name: Add only missing auxiliary classes
|
||||
community.general.ldap_attrs:
|
||||
server_uri: "{{ openldap_server_uri }}"
|
||||
server_uri: "{{ OPENLDAP_SERVER_URI }}"
|
||||
bind_dn: "{{ LDAP.DN.ADMINISTRATOR.DATA }}"
|
||||
bind_pw: "{{ LDAP.BIND_CREDENTIAL }}"
|
||||
dn: "{{ item.dn }}"
|
||||
attributes:
|
||||
objectClass: "{{ missing_auxiliary }}"
|
||||
state: present
|
||||
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
||||
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
||||
loop: "{{ ldap_users_with_classes.results }}"
|
||||
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
||||
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
||||
loop: "{{ ldap_users_with_classes.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.dn }}"
|
||||
vars:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- name: "Create LDIF files at {{ openldap_ldif_host_path }}{{ folder }}"
|
||||
- name: "Create LDIF files at {{ OPENLDAP_LDIF_PATH_HOST }}{{ folder }}"
|
||||
template:
|
||||
src: "{{ item }}"
|
||||
dest: "{{ openldap_ldif_host_path }}{{ folder }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
|
||||
dest: "{{ OPENLDAP_LDIF_PATH_HOST }}{{ folder }}/{{ item | basename | regex_replace('\\.j2$', '') }}"
|
||||
mode: "0770"
|
||||
loop: >-
|
||||
{{
|
||||
@@ -1,25 +1,25 @@
|
||||
---
|
||||
|
||||
- name: "include docker-compose role"
|
||||
include_role:
|
||||
include_role:
|
||||
name: docker-compose
|
||||
|
||||
- name: Create {{ domains | get_domain(application_id) }}.conf if LDAP is exposed to internet
|
||||
template:
|
||||
template:
|
||||
src: "nginx.stream.conf.j2"
|
||||
dest: "{{ NGINX.DIRECTORIES.STREAMS }}{{ domains | get_domain(application_id) }}.conf"
|
||||
notify: restart openresty
|
||||
when: applications | get_app_conf(application_id, 'network.public', True) | bool
|
||||
when: OPENLDAP_NETWORK_SWITCH_PUBLIC | bool
|
||||
|
||||
- name: Remove {{ domains | get_domain(application_id) }}.conf if LDAP is not exposed to internet
|
||||
file:
|
||||
path: "{{ NGINX.DIRECTORIES.STREAMS }}{{ domains | get_domain(application_id) }}.conf"
|
||||
state: absent
|
||||
when: not applications | get_app_conf(application_id, 'network.public', True) | bool
|
||||
when: not OPENLDAP_NETWORK_SWITCH_PUBLIC | bool
|
||||
|
||||
- name: create docker network for LDAP, so that other applications can access it
|
||||
community.docker.docker_network:
|
||||
name: "{{ openldap_network }}"
|
||||
name: "{{ OPENLDAP_NETWORK }}"
|
||||
state: present
|
||||
ipam_config:
|
||||
- subnet: "{{ networks.local[application_id].subnet }}"
|
||||
@@ -37,23 +37,23 @@
|
||||
- name: "Reset LDAP Credentials"
|
||||
include_tasks: 01_credentials.yml
|
||||
when:
|
||||
- applications | get_app_conf(application_id, 'network.local')
|
||||
- applications | get_app_conf(application_id, 'provisioning.credentials', True)
|
||||
- OPENLDAP_NETWORK_SWITCH_LOCAL | bool
|
||||
- applications | get_app_conf(application_id, 'provisioning.credentials')
|
||||
|
||||
- name: "create directory {{openldap_ldif_host_path}}{{item}}"
|
||||
- name: "create directory {{ OPENLDAP_LDIF_PATH_HOST }}{{ item }}"
|
||||
file:
|
||||
path: "{{openldap_ldif_host_path}}{{item}}"
|
||||
path: "{{ OPENLDAP_LDIF_PATH_HOST }}{{ item }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
loop: "{{openldap_ldif_types}}"
|
||||
loop: "{{ OPENLDAP_LDIF_TYPES }}"
|
||||
|
||||
- name: "Import LDIF Configuration"
|
||||
include_tasks: ldifs_creation.yml
|
||||
include_tasks: _ldifs_creation.yml
|
||||
loop:
|
||||
- configuration
|
||||
loop_control:
|
||||
loop_var: folder
|
||||
when: applications | get_app_conf(application_id, 'provisioning.configuration', True)
|
||||
when: applications | get_app_conf(application_id, 'provisioning.configuration')
|
||||
|
||||
- name: flush LDIF handlers
|
||||
meta: flush_handlers
|
||||
@@ -66,20 +66,22 @@
|
||||
|
||||
- name: "Include Schemas (if enabled)"
|
||||
include_tasks: 02_schemas.yml
|
||||
when: applications | get_app_conf(application_id, 'provisioning.schemas', True)
|
||||
when: applications | get_app_conf(application_id, 'provisioning.schemas')
|
||||
|
||||
- name: "Import LDAP Entries (if enabled)"
|
||||
include_tasks: 03_users.yml
|
||||
when: applications | get_app_conf(application_id, 'provisioning.users', True)
|
||||
when: applications | get_app_conf(application_id, 'provisioning.users')
|
||||
|
||||
- name: "Import LDIF Data (if enabled)"
|
||||
include_tasks: ldifs_creation.yml
|
||||
include_tasks: _ldifs_creation.yml
|
||||
loop:
|
||||
- groups
|
||||
loop_control:
|
||||
loop_var: folder
|
||||
when: applications | get_app_conf(application_id, 'provisioning.groups', True)
|
||||
when: applications | get_app_conf(application_id, 'provisioning.groups')
|
||||
|
||||
- meta: flush_handlers
|
||||
|
||||
- name: "Add Objects to all users"
|
||||
include_tasks: 04_update.yml
|
||||
when: applications | get_app_conf(application_id, 'provisioning.update', True)
|
||||
when: applications | get_app_conf(application_id, 'provisioning.update')
|
||||
@@ -13,9 +13,9 @@
|
||||
- "( 1.3.6.1.4.1.99999.2 NAME '{{ LDAP.USER.OBJECTS.AUXILIARY.NEXTCLOUD_USER }}' DESC 'Auxiliary class for Nextcloud attributes' AUXILIARY MAY ( {{ LDAP.USER.ATTRIBUTES.NEXTCLOUD_QUOTA }} ) )"
|
||||
command: >
|
||||
ldapsm
|
||||
-s {{ openldap_server_uri }}
|
||||
-D '{{ openldap_bind_dn }}'
|
||||
-W '{{ openldap_bind_pw }}'
|
||||
-s {{ OPENLDAP_SERVER_URI }}
|
||||
-D '{{ OPENLDAP_BIND_DN }}'
|
||||
-W '{{ OPENLDAP_BIND_PW }}'
|
||||
-n {{ schema_name }}
|
||||
{% for at in attribute_defs %}
|
||||
-a "{{ at }}"
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
command: >
|
||||
ldapsm
|
||||
-s {{ openldap_server_uri }}
|
||||
-D '{{ openldap_bind_dn }}'
|
||||
-W '{{ openldap_bind_pw }}'
|
||||
-s {{ OPENLDAP_SERVER_URI }}
|
||||
-D '{{ OPENLDAP_BIND_DN }}'
|
||||
-W '{{ OPENLDAP_BIND_PW }}'
|
||||
-n {{ schema_name }}
|
||||
{% for at in attribute_defs %}
|
||||
-a "{{ at }}"
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{% include 'roles/docker-compose/templates/base.yml.j2' %}
|
||||
|
||||
application:
|
||||
image: "{{ openldap_image }}:{{ openldap_version }}"
|
||||
container_name: "{{ openldap_name }}"
|
||||
image: "{{ OPENLDAP_IMAGE }}:{{ OPENLDAP_VERSION }}"
|
||||
container_name: "{{ OPENLDAP_CONTAINER }}"
|
||||
{% include 'roles/docker-container/templates/base.yml.j2' %}
|
||||
{% if openldap_network_expose_local %}
|
||||
{% if OPENLDAP_NETWORK_EXPOSE_LOCAL | bool %}
|
||||
ports:
|
||||
- 127.0.0.1:{{ports.localhost.ldap['svc-db-openldap']}}:{{openldap_docker_port_open}}
|
||||
- 127.0.0.1:{{ports.localhost.ldap['svc-db-openldap']}}:{{ OPENLDAP_DOCKER_PORT_OPEN }}
|
||||
{% endif %}
|
||||
volumes:
|
||||
- 'data:/bitnami/openldap'
|
||||
- '{{openldap_ldif_host_path}}:{{ openldap_ldif_docker_path }}:ro'
|
||||
- '{{ OPENLDAP_LDIF_PATH_HOST }}:{{ OPENLDAP_LDIF_PATH_DOCKER }}:ro'
|
||||
healthcheck:
|
||||
test: >
|
||||
bash -c '
|
||||
ldapsearch -x -H ldap://localhost:{{ openldap_docker_port_open }} \
|
||||
ldapsearch -x -H ldap://localhost:{{ OPENLDAP_DOCKER_PORT_OPEN }} \
|
||||
-D "{{ LDAP.DN.ADMINISTRATOR.DATA }}" -w "{{ LDAP.BIND_CREDENTIAL }}" -b "{{ LDAP.DN.ROOT }}" > /dev/null \
|
||||
&& ldapsearch -Y EXTERNAL -H ldapi:/// \
|
||||
-b cn=config "(&(objectClass=olcOverlayConfig)(olcOverlay=memberof))" \
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
{% include 'roles/docker-compose/templates/volumes.yml.j2' %}
|
||||
data:
|
||||
name: "{{ openldap_volume }}"
|
||||
name: "{{ OPENLDAP_VOLUME }}"
|
||||
|
||||
{% include 'roles/docker-compose/templates/networks.yml.j2' %}
|
||||
@@ -3,24 +3,24 @@
|
||||
|
||||
# GENERAL
|
||||
## Admin (Data)
|
||||
LDAP_ADMIN_USERNAME= {{ applications | get_app_conf(application_id, 'users.administrator.username') }} # LDAP database admin user.
|
||||
LDAP_ADMIN_PASSWORD= {{ LDAP.BIND_CREDENTIAL }} # LDAP database admin password.
|
||||
LDAP_ADMIN_USERNAME= {{ applications | get_app_conf(application_id, 'users.administrator.username') }} # LDAP database admin user.
|
||||
LDAP_ADMIN_PASSWORD= {{ LDAP.BIND_CREDENTIAL }} # LDAP database admin password.
|
||||
|
||||
## Users
|
||||
LDAP_USERS= ' ' # Comma separated list of LDAP users to create in the default LDAP tree. Default: user01,user02
|
||||
LDAP_PASSWORDS= ' ' # Comma separated list of passwords to use for LDAP users. Default: bitnami1,bitnami2
|
||||
LDAP_ROOT= {{ LDAP.DN.ROOT }} # LDAP baseDN (or suffix) of the LDAP tree. Default: dc=example,dc=org
|
||||
LDAP_USERS= ' ' # Comma separated list of LDAP users to create in the default LDAP tree. Default: user01,user02
|
||||
LDAP_PASSWORDS= ' ' # Comma separated list of passwords to use for LDAP users. Default: bitnami1,bitnami2
|
||||
LDAP_ROOT= {{ LDAP.DN.ROOT }} # LDAP baseDN (or suffix) of the LDAP tree. Default: dc=example,dc=org
|
||||
|
||||
## Admin (Config)
|
||||
LDAP_ADMIN_DN= {{LDAP.DN.ADMINISTRATOR.DATA}}
|
||||
LDAP_ADMIN_DN= {{ LDAP.DN.ADMINISTRATOR.DATA }}
|
||||
LDAP_CONFIG_ADMIN_ENABLED= yes
|
||||
LDAP_CONFIG_ADMIN_USERNAME= {{ applications | get_app_conf(application_id, 'users.administrator.username') }}
|
||||
LDAP_CONFIG_ADMIN_PASSWORD= {{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}
|
||||
|
||||
# Network
|
||||
LDAP_PORT_NUMBER= {{openldap_docker_port_open}} # Route to default port
|
||||
LDAP_ENABLE_TLS= no # Using nginx proxy for tls
|
||||
LDAP_LDAPS_PORT_NUMBER= {{openldap_docker_port_secure}} # Port used for TLS secure traffic. Priviledged port is supported (e.g. 636). Default: 1636 (non privileged port).
|
||||
LDAP_PORT_NUMBER= {{ OPENLDAP_DOCKER_PORT_OPEN }} # Route to default port
|
||||
LDAP_ENABLE_TLS= no # Using nginx proxy for tls
|
||||
LDAP_LDAPS_PORT_NUMBER= {{ OPENLDAP_DOCKER_PORT_SECURE }} # Port used for TLS secure traffic. Priviledged port is supported (e.g. 636). Default: 1636 (non privileged port).
|
||||
|
||||
# Security
|
||||
LDAP_ALLOW_ANON_BINDING= no # Allow anonymous bindings to the LDAP server. Default: yes.
|
||||
LDAP_ALLOW_ANON_BINDING= no # Allow anonymous bindings to the LDAP server. Default: yes.
|
||||
@@ -1,30 +0,0 @@
|
||||
{#
|
||||
@todo: activate
|
||||
{% for dn, entry in (applications | build_ldap_role_entries(users, ldap)).items() %}
|
||||
|
||||
dn: {{ dn }}
|
||||
{% for oc in entry.objectClass %}
|
||||
objectClass: {{ oc }}
|
||||
{% endfor %}
|
||||
{% if entry.ou is defined %}
|
||||
ou: {{ entry.ou }}
|
||||
{% else %}
|
||||
cn: {{ entry.cn }}
|
||||
{% endif %}
|
||||
{% if entry.gidNumber is defined %}
|
||||
gidNumber: {{ entry.gidNumber }}
|
||||
{% endif %}
|
||||
description: {{ entry.description }}
|
||||
{% if entry.memberUid is defined %}
|
||||
{% for uid in entry.memberUid %}
|
||||
memberUid: {{ uid }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if entry.member is defined %}
|
||||
{% for m in entry.member %}
|
||||
member: {{ m }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
#}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% for dn, entry in (applications | build_ldap_role_entries(users, ldap)).items() %}
|
||||
{% for dn, entry in (applications | build_ldap_role_entries(users, LDAP)).items() %}
|
||||
|
||||
dn: {{ dn }}
|
||||
{% for oc in entry.objectClass %}
|
||||
@@ -1,24 +1,27 @@
|
||||
application_id: "svc-db-openldap"
|
||||
|
||||
# LDAP Variables
|
||||
openldap_docker_port_secure: 636
|
||||
openldap_docker_port_open: 389
|
||||
openldap_server_uri: "ldap://127.0.0.1:{{ ports.localhost.ldap[application_id] }}"
|
||||
openldap_bind_dn: "{{ LDAP.DN.ADMINISTRATOR.CONFIGURATION }}"
|
||||
openldap_bind_pw: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password', True) }}"
|
||||
OPENLDAP_DOCKER_PORT_SECURE: 636
|
||||
OPENLDAP_DOCKER_PORT_OPEN: 389
|
||||
OPENLDAP_SERVER_URI: "ldap://127.0.0.1:{{ ports.localhost.ldap[application_id] }}"
|
||||
OPENLDAP_BIND_DN: "{{ LDAP.DN.ADMINISTRATOR.CONFIGURATION }}"
|
||||
OPENLDAP_BIND_PW: "{{ applications | get_app_conf(application_id, 'credentials.administrator_password') }}"
|
||||
|
||||
# LDIF Variables
|
||||
openldap_ldif_host_path: "{{ docker_compose.directories.volumes }}ldif/"
|
||||
openldap_ldif_docker_path: "/tmp/ldif/"
|
||||
openldap_ldif_types:
|
||||
OPENLDAP_LDIF_PATH_HOST: "{{ docker_compose.directories.volumes }}ldif/"
|
||||
OPENLDAP_LDIF_PATH_DOCKER: "/tmp/ldif/"
|
||||
OPENLDAP_LDIF_TYPES:
|
||||
- configuration
|
||||
- groups
|
||||
- schema # Don't know if this is still needed, it's now setup via tasks
|
||||
|
||||
openldap_name: "{{ applications | get_app_conf(application_id, 'docker.services.openldap.name', True) }}"
|
||||
openldap_image: "{{ applications | get_app_conf(application_id, 'docker.services.openldap.image', True) }}"
|
||||
openldap_version: "{{ applications | get_app_conf(application_id, 'docker.services.openldap.version', True) }}"
|
||||
openldap_volume: "{{ applications | get_app_conf(application_id, 'docker.volumes.data', True) }}"
|
||||
openldap_network: "{{ applications | get_app_conf(application_id, 'docker.network', True) }}"
|
||||
# Container
|
||||
OPENLDAP_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.openldap.name') }}"
|
||||
OPENLDAP_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.openldap.image') }}"
|
||||
OPENLDAP_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.openldap.version') }}"
|
||||
OPENLDAP_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
OPENLDAP_NETWORK: "{{ applications | get_app_conf(application_id, 'docker.network') }}"
|
||||
|
||||
openldap_network_expose_local: "{{ applications | get_app_conf(application_id, 'network.public', True) | bool or applications | get_app_conf(application_id, 'network.local') | bool }}"
|
||||
# Network
|
||||
OPENLDAP_NETWORK_SWITCH_PUBLIC: "{{ applications | get_app_conf(application_id, 'network.public') }}"
|
||||
OPENLDAP_NETWORK_SWITCH_LOCAL: "{{ applications | get_app_conf(application_id, 'network.local') }}"
|
||||
OPENLDAP_NETWORK_EXPOSE_LOCAL: "{{ OPENLDAP_NETWORK_SWITCH_PUBLIC | bool or OPENLDAP_NETWORK_SWITCH_LOCAL | bool }}"
|
||||
@@ -2,13 +2,17 @@ docker:
|
||||
services:
|
||||
postgres:
|
||||
# Postgis is necessary for mobilizon
|
||||
image: postgis/postgis
|
||||
name: postgres
|
||||
image: postgis/postgis
|
||||
name: postgres
|
||||
# Please set an version in your inventory file!
|
||||
# Rolling release isn't recommended
|
||||
version: "latest"
|
||||
version: "17-3.5"
|
||||
backup:
|
||||
database_routine: true
|
||||
cpus: "2.0"
|
||||
mem_reservation: "4g"
|
||||
mem_limit: "6g"
|
||||
pids_limit: 1024
|
||||
volumes:
|
||||
data: "postgres_data"
|
||||
network: "postgres"
|
||||
data: "postgres_data"
|
||||
network: "postgres"
|
||||
@@ -5,7 +5,7 @@
|
||||
flush_handlers: true
|
||||
when: run_once_svc_db_postgres is not defined
|
||||
|
||||
- include_tasks: "{{ playbook_dir }}/tasks/utils/load_handlers.yml"
|
||||
- include_tasks: "{{ [ playbook_dir, 'tasks/utils/load_handlers.yml' ] | path_join }}"
|
||||
# Necessary because docker handlers are overwritten by condition
|
||||
vars:
|
||||
handler_role_name: "docker-compose"
|
||||
|
||||
@@ -5,7 +5,7 @@ RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
git \
|
||||
postgresql-server-dev-all \
|
||||
postgresql-server-dev-{{ POSTGRES_VERSION_MAJOR | default('all', true) }} \
|
||||
&& git clone https://github.com/pgvector/pgvector.git /tmp/pgvector \
|
||||
&& cd /tmp/pgvector \
|
||||
&& make \
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
postgres:
|
||||
container_name: "{{ POSTGRES_CONTAINER }}"
|
||||
image: "{{ POSTGRES_CUSTOM_IMAGE_NAME }}"
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
pull_policy: never
|
||||
{{ lookup('template', 'roles/docker-container/templates/build.yml.j2') | indent(4) }}
|
||||
command:
|
||||
- "postgres"
|
||||
- "-c"
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
# General
|
||||
application_id: svc-db-postgres
|
||||
entity_name: "{{ application_id | get_entity_name }}"
|
||||
|
||||
# Docker
|
||||
docker_compose_flush_handlers: true
|
||||
|
||||
# Docker Compose
|
||||
database_type: "{{ application_id | get_entity_name }}"
|
||||
database_type: "{{ entity_name }}"
|
||||
|
||||
## Postgres
|
||||
POSTGRES_VOLUME: "{{ applications | get_app_conf(application_id, 'docker.volumes.data') }}"
|
||||
POSTGRES_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.postgres.name') }}"
|
||||
POSTGRES_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.postgres.image') }}"
|
||||
POSTGRES_SUBNET: "{{ networks.local['svc-db-postgres'].subnet }}"
|
||||
POSTGRES_CONTAINER: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ entity_name ~ '.name') }}"
|
||||
POSTGRES_IMAGE: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ entity_name ~ '.image') }}"
|
||||
POSTGRES_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.' ~ entity_name ~ '.version') }}"
|
||||
POSTGRES_VERSION_MAJOR: "{{ POSTGRES_VERSION | regex_replace('^([0-9]+).*', '\\1') }}"
|
||||
POSTGRES_NETWORK_NAME: "{{ applications | get_app_conf(application_id, 'docker.network') }}"
|
||||
POSTGRES_VERSION: "{{ applications | get_app_conf(application_id, 'docker.services.postgres.version') }}"
|
||||
POSTGRES_SUBNET: "{{ networks.local['svc-db-postgres'].subnet }}"
|
||||
POSTGRES_PASSWORD: "{{ applications | get_app_conf(application_id, 'credentials.POSTGRES_PASSWORD') }}"
|
||||
POSTGRES_PORT: "{{ database_port | default(ports.localhost.database[ application_id ]) }}"
|
||||
POSTGRES_INIT: "{{ database_username is defined and database_password is defined and database_name is defined }}"
|
||||
|
||||
@@ -16,4 +16,12 @@
|
||||
retries: 30
|
||||
networks:
|
||||
- default
|
||||
{% macro include_resource_for(svc, indent=4) -%}
|
||||
{% set service_name = svc -%}
|
||||
{%- set _snippet -%}
|
||||
{% include 'roles/docker-container/templates/resource.yml.j2' %}
|
||||
{%- endset -%}
|
||||
{{ _snippet | indent(indent, true) }}
|
||||
{%- endmacro %}
|
||||
{{ include_resource_for('redis') }}
|
||||
{{ "\n" }}
|
||||
14
roles/svc-opt-swapfile/tasks/01_core.yml
Normal file
14
roles/svc-opt-swapfile/tasks/01_core.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
- name: Install '
|
||||
include_role:
|
||||
name: pkgmgr-install
|
||||
vars:
|
||||
package_name: "{{ SWAPFILE_PKG }}"
|
||||
when: run_once_pkgmgr_install is not defined
|
||||
|
||||
- name: Execute create swapfile script
|
||||
shell: "{{ SWAPFILE_PKG }} '{{ SWAPFILE_SIZE }}'"
|
||||
become: true
|
||||
async: "{{ ASYNC_TIME if ASYNC_ENABLED | bool else omit }}"
|
||||
poll: "{{ ASYNC_POLL if ASYNC_ENABLED | bool else omit }}"
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
@@ -1,17 +1,3 @@
|
||||
- block:
|
||||
- name: Include dependency 'pkgmgr-install'
|
||||
include_role:
|
||||
name: pkgmgr-install
|
||||
when: run_once_pkgmgr_install is not defined
|
||||
- include_tasks: utils/run_once.yml
|
||||
- include_tasks: 01_core.yml
|
||||
when: run_once_svc_opt_swapfile is not defined
|
||||
|
||||
- name: "pkgmgr install"
|
||||
include_role:
|
||||
name: pkgmgr-install
|
||||
vars:
|
||||
package_name: swap-forge
|
||||
|
||||
- name: Execute create swapfile script
|
||||
shell: swap-forge "{{swapfile_size}}"
|
||||
become: true
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
application_id: "svc-opt-swapfile"
|
||||
swapfile_size: "{{ applications | get_app_conf(application_id, 'swapfile_size') }}"
|
||||
|
||||
SWAPFILE_SIZE: "{{ applications | get_app_conf(application_id, 'swapfile_size') }}"
|
||||
SWAPFILE_PKG: "swap-forge"
|
||||
@@ -1,7 +1,10 @@
|
||||
docker:
|
||||
services:
|
||||
openresty:
|
||||
name: "openresty"
|
||||
name: "openresty"
|
||||
cpus: 0.5
|
||||
mem_reservation: 1g
|
||||
mem_limit: 2g
|
||||
volumes:
|
||||
www: "/var/www/"
|
||||
nginx: "/etc/nginx/"
|
||||
@@ -1,6 +1,6 @@
|
||||
- block:
|
||||
- name: "For '{{ application_id }}': Load docker-compose"
|
||||
include_role:
|
||||
include_role:
|
||||
name: docker-compose
|
||||
vars:
|
||||
docker_compose_flush_handlers: true
|
||||
|
||||
@@ -13,7 +13,7 @@ get_backup_types="find /Backups/$hashed_machine_id/ -maxdepth 1 -type d -execdir
|
||||
|
||||
|
||||
# @todo This configuration is not scalable yet. If other backup services then sys-ctl-bkp-docker-2-loc are integrated, this logic needs to be optimized
|
||||
get_version_directories="ls -d /Backups/$hashed_machine_id/sys-ctl-bkp-docker-2-loc/*"
|
||||
get_version_directories="ls -d /Backups/$hashed_machine_id/backup-docker-to-local/*"
|
||||
last_version_directory="$($get_version_directories | tail -1)"
|
||||
rsync_command="sudo rsync --server --sender -blogDtpre.iLsfxCIvu . $last_version_directory/"
|
||||
|
||||
|
||||
@@ -3,30 +3,6 @@
|
||||
name: backup
|
||||
create_home: yes
|
||||
|
||||
- name: create .ssh directory
|
||||
file:
|
||||
path: /home/backup/.ssh
|
||||
state: directory
|
||||
owner: backup
|
||||
group: backup
|
||||
mode: '0700'
|
||||
|
||||
- name: create /home/backup/.ssh/authorized_keys
|
||||
template:
|
||||
src: "authorized_keys.j2"
|
||||
dest: /home/backup/.ssh/authorized_keys
|
||||
owner: backup
|
||||
group: backup
|
||||
mode: '0644'
|
||||
|
||||
- name: create /home/backup/ssh-wrapper.sh
|
||||
copy:
|
||||
src: "ssh-wrapper.sh"
|
||||
dest: /home/backup/ssh-wrapper.sh
|
||||
owner: backup
|
||||
group: backup
|
||||
mode: '0700'
|
||||
|
||||
- name: grant backup sudo rights
|
||||
copy:
|
||||
src: "backup"
|
||||
@@ -35,3 +11,9 @@
|
||||
owner: root
|
||||
group: root
|
||||
notify: sshd restart
|
||||
|
||||
- include_tasks: 02_permissions_ssh.yml
|
||||
|
||||
- include_tasks: 03_permissions_folders.yml
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
23
roles/sys-bkp-provider-user/tasks/02_permissions_ssh.yml
Normal file
23
roles/sys-bkp-provider-user/tasks/02_permissions_ssh.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
- name: create .ssh directory
|
||||
file:
|
||||
path: /home/backup/.ssh
|
||||
state: directory
|
||||
owner: backup
|
||||
group: backup
|
||||
mode: '0700'
|
||||
|
||||
- name: create /home/backup/.ssh/authorized_keys
|
||||
template:
|
||||
src: "authorized_keys.j2"
|
||||
dest: /home/backup/.ssh/authorized_keys
|
||||
owner: backup
|
||||
group: backup
|
||||
mode: '0644'
|
||||
|
||||
- name: create /home/backup/ssh-wrapper.sh
|
||||
copy:
|
||||
src: "ssh-wrapper.sh"
|
||||
dest: /home/backup/ssh-wrapper.sh
|
||||
owner: backup
|
||||
group: backup
|
||||
mode: '0700'
|
||||
66
roles/sys-bkp-provider-user/tasks/03_permissions_folders.yml
Normal file
66
roles/sys-bkp-provider-user/tasks/03_permissions_folders.yml
Normal file
@@ -0,0 +1,66 @@
|
||||
# Ensure the backups root exists and is owned by backup
|
||||
- name: Ensure backups root exists and owned by backup
|
||||
file:
|
||||
path: "{{ BACKUPS_FOLDER_PATH }}"
|
||||
state: directory
|
||||
owner: backup
|
||||
group: backup
|
||||
mode: "0700"
|
||||
|
||||
# Explicit ACL so 'backup' has rwx, others none
|
||||
- name: Grant ACL rwx on backups root to backup user
|
||||
ansible.posix.acl:
|
||||
path: "{{ BACKUPS_FOLDER_PATH }}"
|
||||
entity: backup
|
||||
etype: user
|
||||
permissions: rwx
|
||||
state: present
|
||||
|
||||
# Set default ACLs so new entries inherit rwx for backup and nothing for others
|
||||
- name: Set default ACL (inherit) for backup user under backups root
|
||||
ansible.posix.acl:
|
||||
path: "{{ BACKUPS_FOLDER_PATH }}"
|
||||
entity: backup
|
||||
etype: user
|
||||
permissions: rwx
|
||||
default: true
|
||||
state: present
|
||||
|
||||
# Remove default ACLs for group/others (defensive hardening)
|
||||
# Default ACLs so new entries inherit only backup's rwx
|
||||
- name: Default ACL for backup user (inherit)
|
||||
ansible.posix.acl:
|
||||
path: "{{ BACKUPS_FOLDER_PATH }}"
|
||||
etype: user
|
||||
entity: backup
|
||||
permissions: rwx
|
||||
default: true
|
||||
state: present
|
||||
|
||||
# Explicitly set default group/other to no permissions (instead of absent)
|
||||
- name: Default ACL for group -> none
|
||||
ansible.posix.acl:
|
||||
path: "{{ BACKUPS_FOLDER_PATH }}"
|
||||
etype: group
|
||||
permissions: '---'
|
||||
default: true
|
||||
state: present
|
||||
|
||||
- name: Default ACL for other -> none
|
||||
ansible.posix.acl:
|
||||
path: "{{ BACKUPS_FOLDER_PATH }}"
|
||||
etype: other
|
||||
permissions: '---'
|
||||
default: true
|
||||
state: present
|
||||
|
||||
- name: Fix ownership level 0..2 directories to backup:backup
|
||||
ansible.builtin.shell: >
|
||||
find "{{ BACKUPS_FOLDER_PATH }}" -mindepth 0 -maxdepth 2 -xdev -type d -exec chown backup:backup {} +
|
||||
changed_when: false
|
||||
|
||||
- name: Fix perms level 0..2 directories to 0700
|
||||
ansible.builtin.shell: >
|
||||
find "{{ BACKUPS_FOLDER_PATH }}" -mindepth 0 -maxdepth 2 -xdev -type d -exec chmod 700 {} +
|
||||
changed_when: false
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
- block:
|
||||
- include_tasks: 01_core.yml
|
||||
- include_tasks: utils/run_once.yml
|
||||
- include_tasks: 01_core.yml
|
||||
when: run_once_sys_bkp_provider_user is not defined
|
||||
@@ -5,21 +5,23 @@
|
||||
- sys-ctl-alm-telegram
|
||||
- sys-ctl-alm-email
|
||||
vars:
|
||||
flush_handlers: true
|
||||
system_service_timer_enabled: false
|
||||
system_service_copy_files: true
|
||||
system_service_tpl_exec_start: "{{ system_service_script_exec }} %I"
|
||||
system_service_tpl_on_failure: ""
|
||||
flush_handlers: true
|
||||
system_service_timer_enabled: false
|
||||
system_service_copy_files: true
|
||||
system_service_tpl_exec_start: "{{ system_service_script_exec }} %I"
|
||||
system_service_tpl_on_failure: ""
|
||||
system_service_force_linear_sync: false
|
||||
|
||||
- name: "Include core service for '{{ system_service_id }}'"
|
||||
include_role:
|
||||
name: sys-service
|
||||
vars:
|
||||
flush_handlers: true
|
||||
system_service_timer_enabled: false
|
||||
system_service_copy_files: true
|
||||
system_service_tpl_exec_start: "{{ system_service_script_exec }} %I"
|
||||
system_service_tpl_on_failure: "" # No on failure needed, because it's anyhow the default on failure procedure
|
||||
flush_handlers: true
|
||||
system_service_timer_enabled: false
|
||||
system_service_copy_files: true
|
||||
system_service_tpl_exec_start: "{{ system_service_script_exec }} %I"
|
||||
system_service_tpl_on_failure: "" # No on failure needed, because it's anyhow the default on failure procedure
|
||||
system_service_force_linear_sync: false
|
||||
|
||||
- name: Assert '{{ system_service_id }}'
|
||||
block:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
- name: Include dependencies
|
||||
include_role:
|
||||
name: '{{ item }}'
|
||||
loop:
|
||||
- sys-svc-msmtp
|
||||
name: "sys-svc-msmtp"
|
||||
when: run_once_sys_svc_msmtp is not defined or run_once_sys_svc_msmtp is false
|
||||
|
||||
- include_role:
|
||||
name: sys-service
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
vars:
|
||||
system_service_copy_files: false
|
||||
system_service_timer_enabled: true
|
||||
system_service_force_linear_sync: true
|
||||
system_service_force_flush: "{{ MODE_BACKUP | bool }}"
|
||||
system_service_on_calendar: "{{ SYS_SCHEDULE_BACKUP_DOCKER_TO_LOCAL }}"
|
||||
system_service_tpl_exec_start_pre: '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ SYS_SERVICE_GROUP_MANIPULATION | join(" ") }} --ignore {{ SYS_SERVICE_BACKUP_DOCKER_2_LOC }} --timeout "{{ SYS_TIMEOUT_BACKUP_SERVICES }}"'
|
||||
system_service_tpl_exec_start: "/bin/sh -c '{{ BKP_DOCKER_2_LOC_EXEC }}'"
|
||||
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }} {{ SYS_SERVICE_CLEANUP_BACKUPS_FAILED }}"
|
||||
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
|
||||
# system_service_tpl_exec_start_post: "/usr/bin/systemctl start {{ SYS_SERVICE_CLEANUP_BACKUPS }}" # Not possible to use it because it's a deathlock. Keep this line for documentation purposes
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
@@ -1,9 +1,6 @@
|
||||
- block:
|
||||
- include_tasks: 01_core.yml
|
||||
when:
|
||||
- run_once_sys_ctl_bkp_docker_2_loc is not defined
|
||||
- include_tasks: 01_core.yml
|
||||
when: run_once_sys_ctl_bkp_docker_2_loc is not defined
|
||||
|
||||
- name: "include 04_seed-database-to-backup.yml"
|
||||
include_tasks: 04_seed-database-to-backup.yml
|
||||
when:
|
||||
- BKP_DOCKER_2_LOC_DB_ENABLED | bool
|
||||
when: BKP_DOCKER_2_LOC_DB_ENABLED | bool
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
system_service_tpl_exec_start: dockreap --no-confirmation
|
||||
system_service_tpl_exec_start_pre: "" # Anonymous volumes can allways be removed. It isn't necessary to wait for any service to stop.
|
||||
system_service_copy_files: false
|
||||
system_service_force_linear_sync: false
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
when:
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
name: sys-service
|
||||
vars:
|
||||
system_service_tpl_on_failure: "{{ SYS_SERVICE_ON_FAILURE_COMPOSE }}"
|
||||
system_service_tpl_exec_start: "{{ system_service_script_exec }} --backups-folder-path {{ BACKUPS_FOLDER_PATH }} --maximum-backup-size-percent {{SIZE_PERCENT_MAXIMUM_BACKUP}}"
|
||||
system_service_tpl_exec_start: "{{ system_service_script_exec }} --backups-folder-path {{ BACKUPS_FOLDER_PATH }} --maximum-backup-size-percent {{ SIZE_PERCENT_MAXIMUM_BACKUP }}"
|
||||
system_service_tpl_exec_start_pre: '/usr/bin/python {{ PATH_SYSTEM_LOCK_SCRIPT }} {{ SYS_SERVICE_GROUP_MANIPULATION | join(" ") }} --ignore {{ SYS_SERVICE_GROUP_CLEANUP | join(" ") }} --timeout "{{ SYS_TIMEOUT_BACKUP_SERVICES }}"'
|
||||
system_service_copy_files: true
|
||||
system_service_force_linear_sync: false
|
||||
|
||||
- include_tasks: utils/run_once.yml
|
||||
vars:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user