Compare commits

..

51 Commits

Author SHA1 Message Date
e5c1e19e5e Backups: fix host name
Some checks failed
Linting / YAML Lint (push) Failing after 8s
Linting / Ansible Lint (push) Successful in 16s
2025-12-21 14:18:05 +03:00
8439ab3693 Outline: migrate assets to local storage 2025-12-21 14:17:47 +03:00
dcb09e349b Secrets: update outline secrets before migration to local storage 2025-12-21 14:17:30 +03:00
a6781b4f64 Gramps: upgrade to 25.12.0
Some checks failed
Linting / YAML Lint (push) Failing after 8s
Linting / Ansible Lint (push) Successful in 16s
2025-12-21 12:24:11 +03:00
a31a07bd16 Backup: remove old config
Some checks failed
Linting / YAML Lint (push) Failing after 8s
Linting / Ansible Lint (push) Successful in 16s
2025-12-21 10:13:05 +03:00
54a951b96a Backup: refactor notifications 2025-12-21 10:10:12 +03:00
e1379bc480 Backup: roots parameter 2025-12-20 21:33:21 +03:00
037e0cab9b Backup: restic backup refactoring 2025-12-20 21:31:39 +03:00
2655869814 Backup: support for multiple storages
Some checks failed
Linting / YAML Lint (push) Failing after 9s
Linting / Ansible Lint (push) Successful in 19s
2025-12-20 21:19:06 +03:00
0e96b5030d Backup: refactoring 2025-12-20 21:04:54 +03:00
a217c79e7d Backup: extract restic storage into separate class 2025-12-20 21:03:32 +03:00
6a16ebf084 Backup: parse config to dataclasses
Some checks failed
Linting / YAML Lint (push) Failing after 9s
Linting / Ansible Lint (push) Successful in 16s
2025-12-20 17:44:02 +03:00
2617aa2bd2 Backup: support multiple roots 2025-12-20 17:27:29 +03:00
b686e4da4d Backup: change config format to toml
With support of multiple config values
2025-12-20 17:13:35 +03:00
439c239ac8 Lefthook: fix python format hook 2025-12-20 11:55:13 +03:00
acf599f905 Lefthook: check py files with mypy
Some checks failed
Linting / YAML Lint (push) Failing after 8s
Linting / Ansible Lint (push) Successful in 18s
2025-12-20 11:38:14 +03:00
eae4f5e27b Lefthook: format py files on commit 2025-12-20 11:35:54 +03:00
4fbe9bd5de Backups: skip system dir lost+found
Some checks failed
Linting / YAML Lint (push) Failing after 8s
Linting / Ansible Lint (push) Successful in 15s
2025-12-20 11:22:24 +03:00
dcc4970b20 Add owner and group to backup-targets files 2025-12-20 11:18:37 +03:00
2eac1362b5 Wanderer: backup all data with restic 2025-12-20 11:18:11 +03:00
e3d8479397 Memos: exclude media files from gobackup
Backup media files with backup-targets
2025-12-20 11:06:56 +03:00
91c5eab236 Gramps: exclude media files from gobackup
Backup media files with backup-targets
2025-12-20 11:04:50 +03:00
ca7f089fe6 Backups: use dataclass Application for app info 2025-12-20 10:48:40 +03:00
479e256b1e Backups: use constants for file names 2025-12-20 10:36:19 +03:00
11e5b5752e Backups: add backup-targets file support 2025-12-20 10:32:00 +03:00
392938d0fb Gitea: upgrade to 1.25.3
Some checks failed
Linting / YAML Lint (push) Failing after 10s
Linting / Ansible Lint (push) Successful in 19s
2025-12-19 20:35:53 +03:00
2cc059104e Netdata: upgrade to 2.8.4
Some checks failed
Linting / YAML Lint (push) Failing after 10s
Linting / Ansible Lint (push) Successful in 20s
2025-12-17 20:18:12 +03:00
d09a26b73a Gramps: update vars
Some checks failed
Linting / YAML Lint (push) Failing after 9s
Linting / Ansible Lint (push) Successful in 17s
2025-12-16 20:36:51 +03:00
097676f569 Gramps: move assets to local storage 2025-12-16 20:34:25 +03:00
e878661cb3 Specify secret files to ignore 2025-12-16 19:43:40 +03:00
cb50c1c515 Docker: prune images every night
Some checks failed
Linting / YAML Lint (push) Failing after 10s
Linting / Ansible Lint (push) Successful in 18s
2025-12-16 19:34:31 +03:00
33de71a087 Add agents.md file for ai agents 2025-12-16 19:29:52 +03:00
fbd5fa5faa Memos: upgrade to 0.25.3
Some checks failed
Linting / YAML Lint (push) Failing after 10s
Linting / Ansible Lint (push) Successful in 18s
2025-12-16 11:51:21 +03:00
faf7d58f78 Netdata: update config
Some checks failed
Linting / YAML Lint (push) Failing after 9s
Linting / Ansible Lint (push) Successful in 17s
map /etc/hostname config into container
2025-12-14 21:22:00 +03:00
0a75378bbc Remove old ports config
Some checks failed
Linting / YAML Lint (push) Failing after 9s
Linting / Ansible Lint (push) Successful in 16s
2025-12-14 19:25:33 +03:00
bdd74bdf2e Authelia: add backup for storage database
Some checks failed
Linting / YAML Lint (push) Failing after 9s
Linting / Ansible Lint (push) Successful in 19s
2025-12-14 18:10:59 +03:00
78bee84061 Outline: add postgres health check 2025-12-14 17:58:04 +03:00
7b81858af6 Miniflux: change file names 2025-12-14 17:51:06 +03:00
08fda17561 Gramps: move cache to separate dir 2025-12-13 15:40:56 +03:00
841bd38807 Update valkey to 9.0 2025-12-13 15:35:43 +03:00
fb1fd711c2 Dozzle: upgrade to 8.14.11
All checks were successful
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Successful in 17s
2025-12-13 14:57:41 +03:00
ecf714eda7 Gramps: reduce celery workers to 1
And update valkey to 9
2025-12-13 14:57:23 +03:00
81f693938e Netdata: upgrade to 2.8.2
All checks were successful
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Successful in 17s
Tune config, setup update every 10s instead of 1s
2025-12-13 14:46:15 +03:00
10d67861a0 Netdata: revert to 2.7.3
All checks were successful
Linting / YAML Lint (push) Successful in 10s
Linting / Ansible Lint (push) Successful in 17s
High cpu usage for dockerd and containerd
2025-12-13 13:29:40 +03:00
3f5befb44d Netdata: upgrade to 2.8.2
All checks were successful
Linting / YAML Lint (push) Successful in 9s
Linting / Ansible Lint (push) Successful in 17s
2025-12-13 09:52:08 +03:00
1b75ddaef2 Disable python docker package 2025-12-13 09:51:49 +03:00
7d6ef77e64 Authelia: fix run-app behavior 2025-12-13 09:51:35 +03:00
ae7c20a7aa Add mount configuration 2025-12-13 09:03:29 +03:00
67df03efca Add combined application playbook
All checks were successful
Linting / YAML Lint (push) Successful in 11s
Linting / Ansible Lint (push) Successful in 21s
2025-12-13 08:46:34 +03:00
48bb8c9d33 Add combined system playbook 2025-12-13 08:38:12 +03:00
5b53cb30ac Add tag 'run-app' for application run
Useful skip run when configure app
2025-12-13 08:22:11 +03:00
52 changed files with 1028 additions and 498 deletions

4
.crushignore Normal file
View File

@@ -0,0 +1,4 @@
ansible-vault-password-file
*secrets.yml
*secrets.toml

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@
/ansible-vault-password-file /ansible-vault-password-file
/temp /temp
*.retry *.retry
__pycache__

69
AGENTS.md Normal file
View File

@@ -0,0 +1,69 @@
# AGENTS GUIDE
## Overview
Ansible-based server automation for personal services. Playbooks provision Dockerized apps (e.g., gitea, authelia, homepage, miniflux, wakapi, memos) via per-app users, Caddy proxy, and Yandex Docker Registry. Secrets are managed with Ansible Vault.
## Project Layout
- Playbooks: `playbook-*.yml` (per service), `playbook-all-*.yml` for grouped actions.
- Inventory: `production.yml` (ungrouped host `server`).
- Variables: `vars/*.yml` (app configs, images), secrets in `vars/secrets.yml` (vault-encrypted).
- Roles: custom roles under `roles/` (e.g., `eget`, `owner`, `secrets`) plus galaxy roles fetched to `galaxy.roles/`.
- Files/templates: service docker-compose and backup templates under `files/`, shared templates under `templates/`.
- Scripts: helper Python scripts in `scripts/` (SMTP utilities) and `files/backups/backup-all.py`.
- CI: `.gitea/workflows/lint.yml` runs yamllint and ansible-lint.
- Hooks: `lefthook.yml` references local hooks in `/home/av/projects/private/git-hooks` (gitleaks, vault check).
- Formatting: `.editorconfig` enforces LF, trailing newline, 4-space indent; YAML/Jinja use 2-space indent.
## Setup
- Copy vault password sample: `cp ansible-vault-password-file.dist ansible-vault-password-file` (needed for ansible and CI).
- Install galaxy roles: `ansible-galaxy role install --role-file requirements.yml --force` (or `task install-roles`).
- Ensure `yq`, `task`, `ansible` installed per README requirements.
## Tasks (taskfile)
- `task install-roles` — install galaxy roles into `galaxy.roles/`.
- `task ssh` — SSH to target using inventory (`production.yml`).
- `task btop` — run `btop` on remote.
- `task encrypt|decrypt -- <files>` — ansible-vault helpers.
- Authelia helpers:
- `task authelia-cli -- <args>` — run authelia CLI in docker.
- `task authelia-validate-config` — render `files/authelia/configuration.template.yml` with secrets and validate via authelia docker image.
- `task authelia-gen-random-string LEN=64` — generate random string.
- `task authelia-gen-secret-and-hash LEN=72` — generate hashed secret.
- `task format-py-files` — run Black via docker (pyfound/black).
## Ansible Usage
- Inventory: `production.yml` with `server` host. `ansible.cfg` points to `./ansible-vault-password-file` and `./galaxy.roles` for roles path.
- Typical deploy example (from README): `ansible-playbook -i production.yml --diff playbook-gitea.yml`.
- Per-app playbooks: `playbook-<app>.yml`; grouped runs: `playbook-all-setup.yml`, `playbook-all-applications.yml`, `playbook-upgrade.yml`, etc.
- Secrets: encrypted `vars/secrets.yml`; additional `files/<app>/secrets.yml` used for templating (e.g., Authelia). Respect `.crushignore` ignoring vault files.
- Templates: many `docker-compose.template.yml` and `*.template.sh` files under `files/*` plus shared `templates/env.j2`. Use `vars/*.yml` to supply values.
- Custom roles:
- `roles/eget`: installs `eget` tool; see defaults/vars for version/source.
- `roles/owner`: manages user/group and env template.
- `roles/secrets`: manages vault-related items.
## Linting & CI
- Local lint configs: `.yamllint.yml`, `.ansible-lint.yml` (excludes `.ansible/`, `.gitea/`, `galaxy.roles/`, `Taskfile.yml`).
- CI (.gitea/workflows/lint.yml) installs `yamllint` and `ansible-lint` and runs `yamllint .` then `ansible-lint .`; creates dummy vault file if missing.
- Pre-commit via lefthook (local hooks path): runs `gitleaks git --staged` and secret-file vault check script.
## Coding/Templating Conventions
- Indentation: 2 spaces for YAML/Jinja (`.editorconfig`), 4 spaces default elsewhere.
- End-of-line: LF; ensure final newline.
- Template suffixes `.template.yml`, `.yml.j2`, `.template.sh` are rendered via Ansible `template` module.
- Avoid committing real secrets; `.crushignore` excludes `ansible-vault-password-file` and `*secrets.yml`.
- Service directories under `files/` hold docker-compose and backup templates; ensure per-app users and registry settings align with `vars/*.yml`.
## Testing/Validation
- YAML lint: `yamllint .` (CI default).
- Ansible lint: `ansible-lint .` (CI default).
- Authelia config validation: `task authelia-validate-config` (renders with secrets then validates via docker).
- Black formatting for Python helpers: `task format-py-files`.
- Python types validation with mypy: `mypy <file.py>`.
## Operational Notes
- Deployments rely on `production.yml` inventory and per-app playbooks; run with `--diff` for visibility.
- Yandex Docker Registry auth helper: `files/yandex-docker-registry-auth.sh`.
- Backups: templates and scripts under `files/backups/` per service; `backup-all.py` orchestrates.
- Home network/DNS reference in README (Yandex domains).
- Ensure `ansible-vault-password-file` present for vault operations and CI.

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -eu
set -o pipefail
echo "{{ app_name }}: backup data with gobackups"
(cd "{{ base_dir }}" && gobackup perform --config "{{ gobackup_config }}")
echo "{{ app_name }}: done."

View File

@@ -1026,7 +1026,7 @@ storage:
## ##
local: local:
## Path to the SQLite3 Database. ## Path to the SQLite3 Database.
path: '/config/authelia_storage.sqlite3' path: '/data/authelia_storage.sqlite3'
## ##
## MySQL / MariaDB (Storage Provider) ## MySQL / MariaDB (Storage Provider)

View File

@@ -10,9 +10,10 @@ services:
- "monitoring_network" - "monitoring_network"
volumes: volumes:
- "{{ config_dir }}:/config" - "{{ config_dir }}:/config"
- "{{ data_dir }}:/data"
authelia_redis: authelia_redis:
image: valkey/valkey:9-alpine image: valkey/valkey:9.0-alpine
container_name: authelia_redis container_name: authelia_redis
restart: unless-stopped restart: unless-stopped
networks: networks:

View File

@@ -0,0 +1,16 @@
# https://gobackup.github.io/configuration
models:
authelia:
compress_with:
type: 'tgz'
storages:
local:
type: 'local'
path: '{{ backups_dir }}'
keep: 3
databases:
users:
type: sqlite
path: "{{ (data_dir, 'authelia_storage.sqlite3') | path_join }}"

View File

@@ -4,18 +4,28 @@ Backup script for all applications
Automatically discovers and runs backup scripts for all users, Automatically discovers and runs backup scripts for all users,
then creates restic backups and sends notifications. then creates restic backups and sends notifications.
""" """
import itertools
import os import os
import sys import sys
import subprocess import subprocess
import logging import logging
import pwd import pwd
from abc import ABC
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List, Tuple, Optional from typing import Dict, List, Optional, Any
import requests import requests
import configparser import tomllib
import itertools
# Default config path
CONFIG_PATH = Path("/etc/backup/config.toml")
# File name to store directories and files to back up
BACKUP_TARGETS_FILE = "backup-targets"
# Default directory fo backups (relative to app dir)
# Used when backup-targets file not exists
BACKUP_DEFAULT_DIR = "backups"
# Configure logging # Configure logging
logging.basicConfig( logging.basicConfig(
@@ -28,44 +38,193 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
config = configparser.ConfigParser()
config.read("/etc/backup/config.ini")
RESTIC_REPOSITORY = config.get("restic", "RESTIC_REPOSITORY") @dataclass
RESTIC_PASSWORD = config.get("restic", "RESTIC_PASSWORD") class Config:
AWS_ACCESS_KEY_ID = config.get("restic", "AWS_ACCESS_KEY_ID") host_name: str
AWS_SECRET_ACCESS_KEY = config.get("restic", "AWS_SECRET_ACCESS_KEY") roots: List[Path]
AWS_DEFAULT_REGION = config.get("restic", "AWS_DEFAULT_REGION")
TELEGRAM_BOT_TOKEN = config.get("telegram", "TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = config.get("telegram", "TELEGRAM_CHAT_ID") @dataclass
NOTIFICATIONS_NAME = config.get("telegram", "NOTIFICATIONS_NAME") class Application:
path: Path
owner: str
class Storage(ABC):
def backup(self, backup_dirs: List[str]) -> bool:
"""Backup directories"""
raise NotImplementedError()
class ResticStorage(Storage):
TYPE_NAME = "restic"
def __init__(self, name: str, params: Dict[str, Any]):
self.name = name
self.restic_repository = str(params.get("restic_repository", ""))
self.restic_password = str(params.get("restic_password", ""))
self.aws_access_key_id = str(params.get("aws_access_key_id", ""))
self.aws_secret_access_key = str(params.get("aws_secret_access_key", ""))
self.aws_default_region = str(params.get("aws_default_region", ""))
if not all(
[
self.restic_repository,
self.restic_password,
self.aws_access_key_id,
self.aws_secret_access_key,
self.aws_default_region,
]
):
raise ValueError(
f"Missing storage configuration values for backend ResticStorage: '{self.name}'"
)
def backup(self, backup_dirs: List[str]) -> bool:
if not backup_dirs:
logger.warning("No backup directories found")
return True
try:
return self.__backup_internal(backup_dirs)
except Exception as exc: # noqa: BLE001
logger.error("Restic backup process failed: %s", exc)
return False
def __backup_internal(self, backup_dirs: List[str]) -> bool:
logger.info("Starting restic backup")
logger.info("Destination: %s", self.restic_repository)
env = os.environ.copy()
env.update(
{
"RESTIC_REPOSITORY": self.restic_repository,
"RESTIC_PASSWORD": self.restic_password,
"AWS_ACCESS_KEY_ID": self.aws_access_key_id,
"AWS_SECRET_ACCESS_KEY": self.aws_secret_access_key,
"AWS_DEFAULT_REGION": self.aws_default_region,
}
)
backup_cmd = ["restic", "backup", "--verbose"] + backup_dirs
result = subprocess.run(backup_cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
logger.error("Restic backup failed: %s", result.stderr)
return False
logger.info("Restic backup completed successfully")
check_cmd = ["restic", "check"]
result = subprocess.run(check_cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
logger.error("Restic check failed: %s", result.stderr)
return False
logger.info("Restic check completed successfully")
forget_cmd = [
"restic",
"forget",
"--compact",
"--prune",
"--keep-daily",
"90",
"--keep-monthly",
"36",
]
result = subprocess.run(forget_cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
logger.error("Restic forget/prune failed: %s", result.stderr)
return False
logger.info("Restic forget/prune completed successfully")
result = subprocess.run(check_cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
logger.error("Final restic check failed: %s", result.stderr)
return False
logger.info("Final restic check completed successfully")
return True
class Notifier(ABC):
def send(self, html_message: str):
raise NotImplementedError()
class TelegramNotifier(Notifier):
TYPE_NAME = "telegram"
def __init__(self, name: str, params: Dict[str, Any]):
self.name = name
self.telegram_bot_token = str(params.get("telegram_bot_token", ""))
self.telegram_chat_id = str(params.get("telegram_chat_id", ""))
if not all(
[
self.telegram_bot_token,
self.telegram_chat_id,
]
):
raise ValueError(
f"Missing notification configuration values for backend {name}"
)
def send(self, html_message: str):
url = f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage"
data = {
"chat_id": self.telegram_chat_id,
"parse_mode": "HTML",
"text": html_message,
}
response = requests.post(url, data=data, timeout=30)
if response.status_code == 200:
logger.info("Telegram notification sent successfully")
else:
logger.error(
f"Failed to send Telegram notification: {response.status_code} - {response.text}"
)
class BackupManager: class BackupManager:
def __init__(self): def __init__(
self.errors = [] self,
self.warnings = [] config: Config,
self.successful_backups = [] roots: List[Path],
storages: List[Storage],
notifiers: List[Notifier],
):
self.errors: List[str] = []
self.warnings: List[str] = []
self.successful_backups: List[str] = []
self.config = config
self.roots: List[Path] = roots
self.storages = storages
self.notifiers = notifiers
def get_application_directories(self) -> List[Tuple[str, str]]: def find_applications(self) -> List[Application]:
"""Get all home directories and their owners""" """Get all application directories and their owners."""
app_dirs = [] applications: List[Application] = []
applications_path = Path("/mnt/applications") source_dirs = itertools.chain(*(root.iterdir() for root in self.roots))
source_dirs = applications_path.iterdir()
for app_dir in source_dirs: for app_dir in source_dirs:
if app_dir == "lost+found": if "lost+found" in str(app_dir):
continue continue
if app_dir.is_dir(): if app_dir.is_dir():
try: try:
# Get the owner of the directory
stat_info = app_dir.stat() stat_info = app_dir.stat()
owner = pwd.getpwuid(stat_info.st_uid).pw_name owner = pwd.getpwuid(stat_info.st_uid).pw_name
app_dirs.append((str(app_dir), owner)) applications.append(Application(path=app_dir, owner=owner))
except (KeyError, OSError) as e: except (KeyError, OSError) as e:
logger.warning(f"Could not get owner for {app_dir}: {e}") logger.warning(f"Could not get owner for {app_dir}: {e}")
return app_dirs return applications
def find_backup_script(self, app_dir: str) -> Optional[str]: def find_backup_script(self, app_dir: str) -> Optional[str]:
"""Find backup script in user's home directory""" """Find backup script in user's home directory"""
@@ -126,150 +285,102 @@ class BackupManager:
return False return False
def get_backup_directories(self) -> List[str]: def get_backup_directories(self) -> List[str]:
"""Get all backup directories that exist""" """Collect backup targets according to backup-targets rules"""
backup_dirs = [] backup_dirs: List[str] = []
app_dirs = self.get_application_directories() applications = self.find_applications()
for app_dir, _ in app_dirs: def parse_targets_file(targets_file: Path) -> List[str]:
backup_path = os.path.join(app_dir, "backups") """Parse backup-targets file, skipping comments and empty lines."""
if os.path.exists(backup_path) and os.path.isdir(backup_path): targets: List[str] = []
backup_dirs.append(backup_path) try:
for raw_line in targets_file.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
targets.append(line)
except OSError as e:
warning_msg = f"Could not read backup targets file {targets_file}: {e}"
logger.warning(warning_msg)
self.warnings.append(warning_msg)
return targets
for app in applications:
app_dir = app.path
targets_file = app_dir / BACKUP_TARGETS_FILE
resolved_targets: List[Path] = []
if targets_file.exists():
# Read custom targets defined by the application.
for target_line in parse_targets_file(targets_file):
target_path = Path(target_line)
if not target_path.is_absolute():
target_path = (app_dir / target_path).resolve()
else:
target_path = target_path.resolve()
if target_path.exists():
resolved_targets.append(target_path)
else:
warning_msg = (
f"Backup target does not exist for {app_dir}: {target_path}"
)
logger.warning(warning_msg)
self.warnings.append(warning_msg)
else:
# Fallback to default backups directory when no list is provided.
default_target = (app_dir / BACKUP_DEFAULT_DIR).resolve()
if default_target.exists():
resolved_targets.append(default_target)
else:
warning_msg = f"Default backup path does not exist for {app_dir}: {default_target}"
logger.warning(warning_msg)
self.warnings.append(warning_msg)
for target in resolved_targets:
target_str = str(target)
if target_str not in backup_dirs:
backup_dirs.append(target_str)
return backup_dirs return backup_dirs
def run_restic_backup(self, backup_dirs: List[str]) -> bool: def send_notification(self, success: bool) -> None:
"""Run restic backup for all backup directories""" """Send notification to Notifiers"""
if not backup_dirs:
logger.warning("No backup directories found")
return True
try: if success and not self.errors:
logger.info("Starting restic backup") message = f"<b>{self.config.host_name}</b>: бекап успешно завершен!"
logger.info("Destination: %s", RESTIC_REPOSITORY) if self.successful_backups:
message += f"\n\nУспешные бекапы: {', '.join(self.successful_backups)}"
else:
message = f"<b>{self.config.host_name}</b>: бекап завершен с ошибками!"
# Set environment variables for restic if self.successful_backups:
env = os.environ.copy() message += (
env.update( f"\n\n✅ Успешные бекапы: {', '.join(self.successful_backups)}"
{
"RESTIC_REPOSITORY": RESTIC_REPOSITORY,
"RESTIC_PASSWORD": RESTIC_PASSWORD,
"AWS_ACCESS_KEY_ID": AWS_ACCESS_KEY_ID,
"AWS_SECRET_ACCESS_KEY": AWS_SECRET_ACCESS_KEY,
"AWS_DEFAULT_REGION": AWS_DEFAULT_REGION,
}
)
# Run backup
backup_cmd = ["restic", "backup", "--verbose"] + backup_dirs
result = subprocess.run(backup_cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
error_msg = f"Restic backup failed: {result.stderr}"
logger.error(error_msg)
self.errors.append(f"Restic backup: {error_msg}")
return False
logger.info("Restic backup completed successfully")
# Run check
check_cmd = ["restic", "check"]
result = subprocess.run(check_cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
error_msg = f"Restic check failed: {result.stderr}"
logger.error(error_msg)
self.errors.append(f"Restic check: {error_msg}")
return False
logger.info("Restic check completed successfully")
# Run forget and prune
forget_cmd = [
"restic",
"forget",
"--compact",
"--prune",
"--keep-daily",
"90",
"--keep-monthly",
"36",
]
result = subprocess.run(forget_cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
error_msg = f"Restic forget/prune failed: {result.stderr}"
logger.error(error_msg)
self.errors.append(f"Restic forget/prune: {error_msg}")
return False
logger.info("Restic forget/prune completed successfully")
# Final check
result = subprocess.run(check_cmd, env=env, capture_output=True, text=True)
if result.returncode != 0:
error_msg = f"Final restic check failed: {result.stderr}"
logger.error(error_msg)
self.errors.append(f"Final restic check: {error_msg}")
return False
logger.info("Final restic check completed successfully")
return True
except Exception as e:
error_msg = f"Restic backup process failed: {str(e)}"
logger.error(error_msg)
self.errors.append(f"Restic: {error_msg}")
return False
def send_telegram_notification(self, success: bool) -> None:
"""Send notification to Telegram"""
try:
if success and not self.errors:
message = f"<b>{NOTIFICATIONS_NAME}</b>: бекап успешно завершен!"
if self.successful_backups:
message += (
f"\n\nУспешные бекапы: {', '.join(self.successful_backups)}"
)
else:
message = f"<b>{NOTIFICATIONS_NAME}</b>: бекап завершен с ошибками!"
if self.successful_backups:
message += (
f"\n\n✅ Успешные бекапы: {', '.join(self.successful_backups)}"
)
if self.warnings:
message += f"\n\n⚠️ Предупреждения:\n" + "\n".join(self.warnings)
if self.errors:
message += f"\n\n❌ Ошибки:\n" + "\n".join(self.errors)
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
data = {"chat_id": TELEGRAM_CHAT_ID, "parse_mode": "HTML", "text": message}
response = requests.post(url, data=data, timeout=30)
if response.status_code == 200:
logger.info("Telegram notification sent successfully")
else:
logger.error(
f"Failed to send Telegram notification: {response.status_code} - {response.text}"
) )
except Exception as e: if self.warnings:
logger.error(f"Failed to send Telegram notification: {str(e)}") message += f"\n\n⚠️ Предупреждения:\n" + "\n".join(self.warnings)
if self.errors:
message += f"\n\n❌ Ошибки:\n" + "\n".join(self.errors)
for notificator in self.notifiers:
try:
notificator.send(message)
except Exception as e:
logger.error(f"Failed to send notification: {str(e)}")
def run_backup_process(self) -> bool: def run_backup_process(self) -> bool:
"""Main backup process""" """Main backup process"""
logger.info("Starting backup process") logger.info("Starting backup process")
# Get all home directories # Get all home directories
app_dirs = self.get_application_directories() applications = self.find_applications()
logger.info(f"Found {len(app_dirs)} application directories") logger.info(f"Found {len(applications)} application directories")
# Process each user's backup # Process each user's backup
for app_dir, username in app_dirs: for app in applications:
app_dir = str(app.path)
username = app.owner
logger.info(f"Processing backup for app: {app_dir} (user {username})") logger.info(f"Processing backup for app: {app_dir} (user {username})")
# Find backup script # Find backup script
@@ -289,14 +400,18 @@ class BackupManager:
backup_dirs = self.get_backup_directories() backup_dirs = self.get_backup_directories()
logger.info(f"Found backup directories: {backup_dirs}") logger.info(f"Found backup directories: {backup_dirs}")
# Run restic backup overall_success = True
restic_success = self.run_restic_backup(backup_dirs)
# Determine overall success for storage in self.storages:
overall_success = restic_success and len(self.errors) == 0 backup_result = storage.backup(backup_dirs)
if not backup_result:
self.errors.append("Restic backup failed")
# Determine overall success
overall_success = overall_success and backup_result
# Send notification # Send notification
self.send_telegram_notification(overall_success) self.send_notification(overall_success)
logger.info("Backup process completed") logger.info("Backup process completed")
@@ -311,9 +426,53 @@ class BackupManager:
return True return True
def initialize(config_path: Path) -> BackupManager:
try:
with config_path.open("rb") as config_file:
raw_config = tomllib.load(config_file)
except OSError as e:
logger.error(f"Failed to read config file {config_path}: {e}")
raise
host_name = str(raw_config.get("host_name", "unknown"))
roots_raw = raw_config.get("roots") or []
if not isinstance(roots_raw, list) or not roots_raw:
raise ValueError("roots must be a non-empty list of paths in config.toml")
roots = [Path(root) for root in roots_raw]
storage_raw = raw_config.get("storage") or {}
storages: List[Storage] = []
for name, params in storage_raw.items():
if not isinstance(params, dict):
raise ValueError(f"Storage config for {name} must be a table")
storage_type = params.get("type", "")
if storage_type == ResticStorage.TYPE_NAME:
storages.append(ResticStorage(name, params))
if not storages:
raise ValueError("At least one storage backend must be configured")
notifications_raw = raw_config.get("notifier") or {}
notifiers: List[Notifier] = []
for name, params in notifications_raw.items():
if not isinstance(params, dict):
raise ValueError(f"Notificator config for {name} must be a table")
notifier_type = params.get("type", "")
if notifier_type == TelegramNotifier.TYPE_NAME:
notifiers.append(TelegramNotifier(name, params))
if not notifiers:
raise ValueError("At least one notification backend must be configured")
config = Config(host_name=host_name, roots=roots)
return BackupManager(
config=config, roots=roots, storages=storages, notifiers=notifiers
)
def main(): def main():
try: try:
backup_manager = BackupManager() backup_manager = initialize(CONFIG_PATH)
success = backup_manager.run_backup_process() success = backup_manager.run_backup_process()
if not success: if not success:
sys.exit(1) sys.exit(1)

View File

@@ -1,11 +0,0 @@
[restic]
RESTIC_REPOSITORY={{ restic_repository }}
RESTIC_PASSWORD={{ restic_password }}
AWS_ACCESS_KEY_ID={{ restic_s3_access_key }}
AWS_SECRET_ACCESS_KEY={{ restic_s3_access_secret }}
AWS_DEFAULT_REGION={{ restic_s3_region }}
[telegram]
TELEGRAM_BOT_TOKEN={{ notifications_tg_bot_token }}
TELEGRAM_CHAT_ID={{ notifications_tg_chat_id }}
NOTIFICATIONS_NAME={{ notifications_name }}

View File

@@ -0,0 +1,18 @@
host_name = "{{ host_name }}"
roots = [
"{{ application_dir }}"
]
[storage.yandex_cloud_s3]
type = "restic"
restic_repository = "{{ restic_repository }}"
restic_password = "{{ restic_password }}"
aws_access_key_id = "{{ restic_s3_access_key }}"
aws_secret_access_key = "{{ restic_s3_access_secret }}"
aws_default_region = "{{ restic_s3_region }}"
[notifier.server_notifications_channel]
type = "telegram"
telegram_bot_token = "{{ notifications_tg_bot_token }}"
telegram_chat_id = "{{ notifications_tg_chat_id }}"

View File

@@ -1,7 +1,7 @@
services: services:
dozzle_app: dozzle_app:
image: amir20/dozzle:v8.14.8 image: amir20/dozzle:v8.14.11
container_name: dozzle_app container_name: dozzle_app
restart: unless-stopped restart: unless-stopped
volumes: volumes:

View File

@@ -1,7 +1,7 @@
services: services:
gitea_app: gitea_app:
image: gitea/gitea:1.25.2 image: gitea/gitea:1.25.3
restart: unless-stopped restart: unless-stopped
container_name: gitea_app container_name: gitea_app
ports: ports:

View File

@@ -3,7 +3,7 @@
services: services:
gramps_app: &gramps_app gramps_app: &gramps_app
image: ghcr.io/gramps-project/grampsweb:25.11.2 image: ghcr.io/gramps-project/grampsweb:25.12.0
container_name: gramps_app container_name: gramps_app
depends_on: depends_on:
- gramps_redis - gramps_redis
@@ -15,10 +15,10 @@ services:
- "{{ (data_dir, 'gramps_db') | path_join }}:/root/.gramps/grampsdb" # persist Gramps database - "{{ (data_dir, 'gramps_db') | path_join }}:/root/.gramps/grampsdb" # persist Gramps database
- "{{ (data_dir, 'gramps_users') | path_join }}:/app/users" # persist user database - "{{ (data_dir, 'gramps_users') | path_join }}:/app/users" # persist user database
- "{{ (data_dir, 'gramps_index') | path_join }}:/app/indexdir" # persist search index - "{{ (data_dir, 'gramps_index') | path_join }}:/app/indexdir" # persist search index
- "{{ (data_dir, 'gramps_thumb_cache') | path_join }}:/app/thumbnail_cache" # persist thumbnails
- "{{ (data_dir, 'gramps_cache') | path_join }}:/app/cache" # persist export and report caches
- "{{ (data_dir, 'gramps_secret') | path_join }}:/app/secret" # persist flask secret - "{{ (data_dir, 'gramps_secret') | path_join }}:/app/secret" # persist flask secret
- "{{ (data_dir, 'gramps_media') | path_join }}:/app/media" # persist media files - "{{ (cache_dir, 'gramps_thumb_cache') | path_join }}:/app/thumbnail_cache" # persist thumbnails
- "{{ (cache_dir, 'gramps_cache') | path_join }}:/app/cache" # persist export and report caches
- "{{ media_dir }}:/app/media" # persist media files
environment: environment:
GRAMPSWEB_TREE: "Gramps" # will create a new tree if not exists GRAMPSWEB_TREE: "Gramps" # will create a new tree if not exists
GRAMPSWEB_SECRET_KEY: "{{ gramps_secret_key }}" GRAMPSWEB_SECRET_KEY: "{{ gramps_secret_key }}"
@@ -37,12 +37,8 @@ services:
GRAMPSWEB_EMAIL_USE_TLS: "false" GRAMPSWEB_EMAIL_USE_TLS: "false"
GRAMPSWEB_DEFAULT_FROM_EMAIL: "gramps@vakhrushev.me" GRAMPSWEB_DEFAULT_FROM_EMAIL: "gramps@vakhrushev.me"
# media storage at s3 # media storage
GRAMPSWEB_MEDIA_BASE_DIR: "s3://av-gramps-media-storage" GRAMPSWEB_MEDIA_BASE_DIR: "/app/media"
AWS_ENDPOINT_URL: "{{ gramps_s3_endpoint }}"
AWS_ACCESS_KEY_ID: "{{ gramps_s3_access_key_id }}"
AWS_SECRET_ACCESS_KEY: "{{ gramps_s3_secret_access_key }}"
AWS_DEFAULT_REGION: "{{ gramps_s3_region }}"
gramps_celery: gramps_celery:
<<: *gramps_app # YAML merge key copying the entire grampsweb service config <<: *gramps_app # YAML merge key copying the entire grampsweb service config
@@ -53,10 +49,10 @@ services:
ports: [] ports: []
networks: networks:
- "gramps_network" - "gramps_network"
command: celery -A gramps_webapi.celery worker --loglevel=INFO --concurrency=2 command: celery -A gramps_webapi.celery worker --loglevel=INFO --concurrency=1
gramps_redis: gramps_redis:
image: valkey/valkey:8.1.1-alpine image: valkey/valkey:9.0-alpine
container_name: gramps_redis container_name: gramps_redis
restart: unless-stopped restart: unless-stopped
networks: networks:

View File

@@ -23,10 +23,3 @@ models:
undo: undo:
type: sqlite type: sqlite
path: "{{ (data_dir, 'gramps_db/59a0f3d6-1c3d-4410-8c1d-1c9c6689659f/undo.db') | path_join }}" path: "{{ (data_dir, 'gramps_db/59a0f3d6-1c3d-4410-8c1d-1c9c6689659f/undo.db') | path_join }}"
archive:
includes:
- "{{ data_dir }}"
excludes:
- "{{ (data_dir, 'gramps_cache') | path_join }}"
- "{{ (data_dir, 'gramps_thumb_cache') | path_join }}"
- "{{ (data_dir, 'gramps_tmp') | path_join }}"

65
files/gramps/gramps_rename.py Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3.12
import argparse
import sys
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Rename Gramps document files by appending extensions from a list."
)
parser.add_argument("directory", type=Path, help="Directory containing hashed files")
parser.add_argument("names_file", type=Path, help="Text file with target names")
return parser.parse_args()
def read_names(path: Path) -> list[str]:
if not path.is_file():
raise FileNotFoundError(f"Names file not found: {path}")
names = []
for line in path.read_text(encoding="utf-8").splitlines():
name = line.strip()
if name:
names.append(name)
return names
def rename_files(directory: Path, names: list[str]) -> None:
if not directory.is_dir():
raise NotADirectoryError(f"Directory not found: {directory}")
for name in names:
hash_part, dot, _ = name.partition(".")
if not dot:
print(f"Skipping invalid entry (missing extension): {name}", file=sys.stderr)
continue
source = directory / hash_part
target = directory / name
if target.exists():
print(f"Target already exists, skipping: {target}", file=sys.stderr)
continue
if not source.exists():
print(f"Source not found, skipping: {source}", file=sys.stderr)
continue
source.rename(target)
print(f"Renamed {source.name} -> {target.name}")
def main() -> None:
args = parse_args()
try:
names = read_names(args.names_file)
rename_files(args.directory, names)
except Exception as exc: # noqa: BLE001
print(str(exc), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -3,7 +3,7 @@
services: services:
memos_app: memos_app:
image: neosmemo/memos:0.25.2 image: neosmemo/memos:0.25.3
container_name: memos_app container_name: memos_app
restart: unless-stopped restart: unless-stopped
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}" user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"

View File

@@ -2,7 +2,7 @@
models: models:
gramps: memos:
compress_with: compress_with:
type: 'tgz' type: 'tgz'
storages: storages:
@@ -14,8 +14,3 @@ models:
users: users:
type: sqlite type: sqlite
path: "{{ (data_dir, 'memos_prod.db') | path_join }}" path: "{{ (data_dir, 'memos_prod.db') | path_join }}"
archive:
includes:
- "{{ data_dir }}"
excludes:
- "{{ (data_dir, '.thumbnail_cache') | path_join }}"

View File

@@ -1,7 +1,7 @@
services: services:
netdata: netdata:
image: netdata/netdata:v2.7.3 image: netdata/netdata:v2.8.4
container_name: netdata container_name: netdata
restart: unless-stopped restart: unless-stopped
cap_add: cap_add:
@@ -16,12 +16,16 @@ services:
- "{{ config_dir }}:/etc/netdata" - "{{ config_dir }}:/etc/netdata"
- "{{ (data_dir, 'lib') | path_join }}:/var/lib/netdata" - "{{ (data_dir, 'lib') | path_join }}:/var/lib/netdata"
- "{{ (data_dir, 'cache') | path_join }}:/var/cache/netdata" - "{{ (data_dir, 'cache') | path_join }}:/var/cache/netdata"
# Netdata system volumes # Netdata system volumes
- "/:/host/root:ro,rslave" - "/:/host/root:ro,rslave"
- "/etc/group:/host/etc/group:ro" - "/etc/group:/host/etc/group:ro"
- "/etc/hostname:/host/etc/hostname:ro"
- "/etc/localtime:/etc/localtime:ro" - "/etc/localtime:/etc/localtime:ro"
- "/etc/os-release:/host/etc/os-release:ro" - "/etc/os-release:/host/etc/os-release:ro"
- "/etc/passwd:/host/etc/passwd:ro" - "/etc/passwd:/host/etc/passwd:ro"
- "/proc:/host/proc:ro" - "/proc:/host/proc:ro"
- "/run/dbus:/run/dbus:ro" - "/run/dbus:/run/dbus:ro"
- "/sys:/host/sys:ro" - "/sys:/host/sys:ro"

View File

@@ -1,3 +1,3 @@
jobs: jobs:
- name: fail2ban - name: fail2ban
update_every: 15 # Collect Fail2Ban jails statistics every 15 seconds update_every: 60 # Collect Fail2Ban jails statistics every N seconds

View File

@@ -1,4 +1,4 @@
update_every: 15 update_every: 60
jobs: jobs:

View File

@@ -19,7 +19,7 @@
# cpu cores = 2 # cpu cores = 2
# libuv worker threads = 16 # libuv worker threads = 16
# profile = standalone # profile = standalone
hostname = {{ host_name }} # hostname = rivendell-v2
# glibc malloc arena max for plugins = 1 # glibc malloc arena max for plugins = 1
# glibc malloc arena max for netdata = 1 # glibc malloc arena max for netdata = 1
# crash reports = all # crash reports = all
@@ -30,12 +30,15 @@
# has unstable connection = no # has unstable connection = no
[db] [db]
#| >>> [db].update every <<<
#| datatype: duration (seconds), default value: 1s
update every = 10s
# enable replication = yes # enable replication = yes
# replication period = 1d # replication period = 1d
# replication step = 1h # replication step = 1h
# replication threads = 1 # replication threads = 1
# replication prefetch = 10 # replication prefetch = 10
# update every = 1s
# db = dbengine # db = dbengine
# memory deduplication (ksm) = auto # memory deduplication (ksm) = auto
# cleanup orphan hosts after = 1h # cleanup orphan hosts after = 1h
@@ -47,7 +50,7 @@
# dbengine extent cache size = off # dbengine extent cache size = off
# dbengine enable journal integrity check = no # dbengine enable journal integrity check = no
# dbengine use all ram for caches = no # dbengine use all ram for caches = no
# dbengine out of memory protection = 391.99MiB # dbengine out of memory protection = 391.49MiB
# dbengine use direct io = yes # dbengine use direct io = yes
# dbengine journal v2 unmount time = 2m # dbengine journal v2 unmount time = 2m
# dbengine pages per extent = 109 # dbengine pages per extent = 109
@@ -93,12 +96,9 @@
[environment variables] [environment variables]
# PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin # PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin
# PYTHONPATH = # PYTHONPATH =
# TZ = :/etc/localtime # TZ = :/etc/localtime
[host labels]
# name = value
[cloud] [cloud]
# conversation log = no # conversation log = no
# scope = full # scope = full
@@ -107,15 +107,15 @@
[ml] [ml]
# enabled = auto # enabled = auto
# maximum num samples to train = 21600 # training window = 6h
# minimum num samples to train = 900 # min training window = 15m
# max training vectors = 1440
# max samples to smooth = 3
# train every = 3h # train every = 3h
# number of models per dimension = 18 # number of models per dimension = 18
# delete models older than = 7d # delete models older than = 7d
# num samples to diff = 1 # num samples to diff = 1
# num samples to smooth = 3
# num samples to lag = 5 # num samples to lag = 5
# random sampling ratio = 0.20000
# maximum number of k-means iterations = 1000 # maximum number of k-means iterations = 1000
# dimension anomaly score threshold = 0.99000 # dimension anomaly score threshold = 0.99000
# host anomaly rate threshold = 1.00000 # host anomaly rate threshold = 1.00000
@@ -163,7 +163,7 @@
# timeout for first request = 1m # timeout for first request = 1m
# accept a streaming request every = off # accept a streaming request every = off
# respect do not track policy = no # respect do not track policy = no
# x-frame-options response header = # x-frame-options response header =
# allow connections from = localhost * # allow connections from = localhost *
# allow connections by dns = heuristic # allow connections by dns = heuristic
# allow dashboard from = localhost * # allow dashboard from = localhost *
@@ -181,7 +181,7 @@
# gzip compression level = 3 # gzip compression level = 3
# ssl skip certificate verification = no # ssl skip certificate verification = no
# web server threads = 6 # web server threads = 6
# web server max sockets = 262144 # web server max sockets = 131072
[registry] [registry]
# enabled = no # enabled = no
@@ -189,9 +189,9 @@
# registry log file = /var/lib/netdata/registry/registry-log.db # registry log file = /var/lib/netdata/registry/registry-log.db
# registry save db every new entries = 1000000 # registry save db every new entries = 1000000
# registry expire idle persons = 1y # registry expire idle persons = 1y
# registry domain = # registry domain =
# registry to announce = https://registry.my-netdata.io # registry to announce = https://registry.my-netdata.io
# registry hostname = 7171b7f9fc69 # registry hostname = rivendell-v2
# verify browser cookies support = yes # verify browser cookies support = yes
# enable cookies SameSite and Secure = yes # enable cookies SameSite and Secure = yes
# max URL length = 1024 # max URL length = 1024
@@ -202,9 +202,29 @@
[pulse] [pulse]
# extended = no # extended = no
# update every = 1s # update every = 10s
[plugins] [plugins]
#| >>> [plugins].perf <<<
#| datatype: yes or no, default value: yes
perf = no
#| >>> [plugins].python.d <<<
#| datatype: yes or no, default value: yes
python.d = no
#| >>> [plugins].charts.d <<<
#| datatype: yes or no, default value: yes
charts.d = no
#| >>> [plugins].otel <<<
#| datatype: yes or no, default value: yes
otel = no
#| >>> [plugins].statsd <<<
#| datatype: yes or no, default value: yes
statsd = no
# idlejitter = yes # idlejitter = yes
# netdata pulse = yes # netdata pulse = yes
# profile = no # profile = no
@@ -213,23 +233,20 @@
# proc = yes # proc = yes
# cgroups = yes # cgroups = yes
# timex = yes # timex = yes
# statsd = yes
# enable running new plugins = yes # enable running new plugins = yes
# check for new plugins every = 1m # check for new plugins every = 1m
# slabinfo = no # slabinfo = no
# freeipmi = no # freeipmi = no
# python.d = yes
# go.d = yes
# apps = yes
# systemd-journal = yes
# network-viewer = yes
# charts.d = yes
# debugfs = yes # debugfs = yes
# perf = yes
# ioping = yes # ioping = yes
# network-viewer = yes
# apps = yes
# go.d = yes
# systemd-units = yes
# systemd-journal = yes
[statsd] [statsd]
# update every (flushInterval) = 1s # update every (flushInterval) = 10s
# udp messages to process at once = 10 # udp messages to process at once = 10
# create private charts for metrics matching = * # create private charts for metrics matching = *
# max private charts hard limit = 1000 # max private charts hard limit = 1000
@@ -247,10 +264,7 @@
# gaps on histograms (deleteHistograms) = no # gaps on histograms (deleteHistograms) = no
# gaps on timers (deleteTimers) = no # gaps on timers (deleteTimers) = no
# gaps on dictionaries (deleteDictionaries) = no # gaps on dictionaries (deleteDictionaries) = no
# statsd server max TCP sockets = 262144 # statsd server max TCP sockets = 131072
# listen backlog = 4096
# default port = 8125
# bind to = udp:localhost tcp:localhost
[plugin:idlejitter] [plugin:idlejitter]
# loop time = 20ms # loop time = 20ms
@@ -300,22 +314,31 @@
# /sys/class/drm = yes # /sys/class/drm = yes
[plugin:cgroups] [plugin:cgroups]
# update every = 1s #| >>> [plugin:cgroups].update every <<<
# check for new cgroups every = 10s #| datatype: duration (seconds), default value: 10s
update every = 20s
#| >>> [plugin:cgroups].check for new cgroups every <<<
#| datatype: duration (seconds), default value: 10s
check for new cgroups every = 20s
# use unified cgroups = auto # use unified cgroups = auto
# max cgroups to allow = 1000 # max cgroups to allow = 1000
# max cgroups depth to monitor = 0 # max cgroups depth to monitor = 0
# enable by default cgroups matching = !*/init.scope !/system.slice/run-*.scope *user.slice/docker-* !*user.slice* *.scope !/machine.slice/*/.control !/machine.slice/*/payload* !/machine.slice/*/supervisor /machine.slice/*.service */kubepods/pod*/* */kubepods/*/pod*/* */*-kubepods-pod*/* */*-kubepods-*-pod*/* !*kubepods* !*kubelet* !*/vcpu* !*/emulator !*.mount !*.partition !*.service !*.service/udev !*.socket !*.slice !*.swap !*.user !/ !/docker !*/libvirt !/lxc !/lxc/*/* !/lxc.monitor* !/lxc.pivot !/lxc.payload !*lxcfs.service/.control !/machine !/qemu !/system !/systemd !/user * # enable by default cgroups matching = !*/init.scope !/system.slice/run-*.scope *user.slice/docker-* !*user.slice* *.scope !/machine.slice/*/.control !/machine.slice/*/payload* !/machine.slice/*/supervisor /machine.slice/*.service */kubepods/pod*/* */kubepods/*/pod*/* */*-kubepods-pod*/* */*-kubepods-*-pod*/* !*kubepods* !*kubelet* !*/vcpu* !*/emulator !*.mount !*.partition !*.service !*.service/udev !*.socket !*.slice !*.swap !*.user !/ !/docker !*/libvirt !/lxc !/lxc/*/* !/lxc.monitor* !/lxc.pivot !/lxc.payload !*lxcfs.service/.control !/machine !/qemu !/system !/systemd !/user *
# enable by default cgroups names matching = * # enable by default cgroups names matching = *
# search for cgroups in subpaths matching = !*/init.scope !*-qemu !*.libvirt-qemu !/init.scope !/system !/systemd !/user !/lxc/*/* !/lxc.monitor !/lxc.payload/*/* !/lxc.payload.* * # search for cgroups in subpaths matching = !*/init.scope !*-qemu !*.libvirt-qemu !/init.scope !/system !/systemd !/user !/lxc/*/* !/lxc.monitor !/lxc.payload/*/* !/lxc.payload.* *
# script to get cgroup names = /usr/libexec/netdata/plugins.d/cgroup-name.sh # script to get cgroup names = /usr/libexec/netdata/plugins.d/cgroup-name.sh
# script to get cgroup network interfaces = /usr/libexec/netdata/plugins.d/cgroup-network # script to get cgroup network interfaces = /usr/libexec/netdata/plugins.d/cgroup-network
# run script to rename cgroups matching = !/ !*.mount !*.socket !*.partition /machine.slice/*.service !*.service !*.slice !*.swap !*.user !init.scope !*.scope/vcpu* !*.scope/emulator *.scope *docker* *lxc* *qemu* */kubepods/pod*/* */kubepods/*/pod*/* */*-kubepods-pod*/* */*-kubepods-*-pod*/* !*kubepods* !*kubelet* *.libvirt-qemu * # run script to rename cgroups matching = !/ !*.mount !*.socket !*.partition /machine.slice/*.service !*.service !*.slice !*.swap !*.user !init.scope !*.scope/vcpu* !*.scope/emulator *.scope *docker* *lxc* *qemu* */kubepods/pod*/* */kubepods/*/pod*/* */*-kubepods-pod*/* */*-kubepods-*-pod*/* !*kubepods* !*kubelet* *.libvirt-qemu *
# cgroups to match as systemd services = !/system.slice/*/*.service /system.slice/*.service # cgroups to match as systemd services = !/system.slice/*/*.service /system.slice/*.service
[plugin:proc:diskspace] [plugin:proc:diskspace]
#| >>> [plugin:proc:diskspace].update every <<<
#| datatype: duration (seconds), default value: 10s
update every = 1m
# remove charts of unmounted disks = yes # remove charts of unmounted disks = yes
# update every = 1s
# check for new mount points every = 15s # check for new mount points every = 15s
# exclude space metrics on paths = /dev /dev/shm /proc/* /sys/* /var/run/user/* /run/lock /run/user/* /snap/* /var/lib/docker/* /var/lib/containers/storage/* /run/credentials/* /run/containerd/* /rpool /rpool/* # exclude space metrics on paths = /dev /dev/shm /proc/* /sys/* /var/run/user/* /run/lock /run/user/* /snap/* /var/lib/docker/* /var/lib/containers/storage/* /run/credentials/* /run/containerd/* /rpool /rpool/*
# exclude space metrics on filesystems = *gvfs *gluster* *s3fs *ipfs *davfs2 *httpfs *sshfs *gdfs *moosefs fusectl autofs cgroup cgroup2 hugetlbfs devtmpfs fuse.lxcfs # exclude space metrics on filesystems = *gvfs *gluster* *s3fs *ipfs *davfs2 *httpfs *sshfs *gdfs *moosefs fusectl autofs cgroup cgroup2 hugetlbfs devtmpfs fuse.lxcfs
@@ -326,41 +349,29 @@
[plugin:tc] [plugin:tc]
# script to run to get tc values = /usr/libexec/netdata/plugins.d/tc-qos-helper.sh # script to run to get tc values = /usr/libexec/netdata/plugins.d/tc-qos-helper.sh
[plugin:python.d]
# update every = 1s
# command options =
[plugin:go.d] [plugin:go.d]
# update every = 1s # update every = 10s
# command options = # command options =
[plugin:apps] [plugin:apps]
# update every = 1s # update every = 10s
# command options = # command options =
[plugin:systemd-journal] [plugin:systemd-journal]
# update every = 1s # update every = 10s
# command options = # command options =
[plugin:network-viewer] [plugin:network-viewer]
# update every = 1s # update every = 10s
# command options = # command options =
[plugin:charts.d]
# update every = 1s
# command options =
[plugin:debugfs] [plugin:debugfs]
# update every = 1s # update every = 10s
# command options = # command options =
[plugin:perf]
# update every = 1s
# command options =
[plugin:ioping] [plugin:ioping]
# update every = 1s # update every = 10s
# command options = # command options =
[plugin:proc:/proc/net/dev] [plugin:proc:/proc/net/dev]
# compressed packets for all interfaces = no # compressed packets for all interfaces = no
@@ -580,7 +591,7 @@
# hardware packets counters = auto # hardware packets counters = auto
# hardware errors counters = auto # hardware errors counters = auto
# monitor only active ports = auto # monitor only active ports = auto
# disable by default interfaces matching = # disable by default interfaces matching =
# refresh ports state every = 30s # refresh ports state every = 30s
[plugin:proc:/proc/net/stat/nf_conntrack] [plugin:proc:/proc/net/stat/nf_conntrack]
@@ -635,7 +646,7 @@
# preferred disk ids = * # preferred disk ids = *
# exclude disks = loop* ram* # exclude disks = loop* ram*
# filename to monitor = /host/proc/diskstats # filename to monitor = /host/proc/diskstats
# performance metrics for disks with major 252 = yes # performance metrics for disks with major 253 = yes
[plugin:proc:/proc/mdstat] [plugin:proc:/proc/mdstat]
# faulty devices = yes # faulty devices = yes
@@ -685,3 +696,7 @@
[plugin:proc:/sys/class/drm] [plugin:proc:/sys/class/drm]
# directory to monitor = /host/sys/class/drm # directory to monitor = /host/sys/class/drm
[plugin:systemd-units]
# update every = 10s
# command options =

View File

@@ -5,15 +5,16 @@ services:
outline_app: outline_app:
image: outlinewiki/outline:1.1.0 image: outlinewiki/outline:1.1.0
container_name: outline_app container_name: outline_app
user: "{{ user_create_result.uid }}:{{ user_create_result.group }}"
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- outline_postgres - outline_postgres
- outline_redis - outline_redis
ports:
- "127.0.0.1:{{ outline_port }}:3000"
networks: networks:
- "outline_network" - "outline_network"
- "web_proxy_network" - "web_proxy_network"
volumes:
- "{{ media_dir }}:/var/lib/outline/data"
environment: environment:
NODE_ENV: 'production' NODE_ENV: 'production'
URL: 'https://outline.vakhrushev.me' URL: 'https://outline.vakhrushev.me'
@@ -24,16 +25,8 @@ services:
PGSSLMODE: 'disable' PGSSLMODE: 'disable'
REDIS_URL: 'redis://outline_redis:6379' REDIS_URL: 'redis://outline_redis:6379'
FILE_STORAGE: 's3' FILE_STORAGE: 'local'
FILE_STORAGE_UPLOAD_MAX_SIZE: '262144000' FILE_STORAGE_UPLOAD_MAX_SIZE: '262144000' # 250 MB
AWS_ACCESS_KEY_ID: '{{ outline_s3_access_key }}'
AWS_SECRET_ACCESS_KEY: '{{ outline_s3_secret_key }}'
AWS_REGION: '{{ outline_s3_region }}'
AWS_S3_ACCELERATE_URL: ''
AWS_S3_UPLOAD_BUCKET_URL: '{{ outline_s3_url }}'
AWS_S3_UPLOAD_BUCKET_NAME: '{{ outline_s3_bucket }}'
AWS_S3_FORCE_PATH_STYLE: 'true'
AWS_S3_ACL: 'private'
OIDC_CLIENT_ID: '{{ outline_oidc_client_id | replace("$", "$$") }}' OIDC_CLIENT_ID: '{{ outline_oidc_client_id | replace("$", "$$") }}'
OIDC_CLIENT_SECRET: '{{ outline_oidc_client_secret | replace("$", "$$") }}' OIDC_CLIENT_SECRET: '{{ outline_oidc_client_secret | replace("$", "$$") }}'
@@ -54,7 +47,7 @@ services:
SMTP_SECURE: 'false' SMTP_SECURE: 'false'
outline_redis: outline_redis:
image: valkey/valkey:8.1.1-alpine image: valkey/valkey:9.0-alpine
container_name: outline_redis container_name: outline_redis
restart: unless-stopped restart: unless-stopped
networks: networks:
@@ -76,6 +69,10 @@ services:
networks: networks:
- "outline_network" - "outline_network"
- "monitoring_network" - "monitoring_network"
healthcheck:
test: ["CMD", "pg_isready", "--username={{ outline_postgres_user }}", "--dbname={{ outline_postgres_database }}"]
interval: 10s
start_period: 30s
networks: networks:
outline_network: outline_network:

View File

@@ -1,6 +1,8 @@
# Refer for explanation to following link: # Refer for explanation to following link:
# https://lefthook.dev/configuration/ # https://lefthook.dev/configuration/
glob_matcher: doublestar
templates: templates:
av-hooks-dir: "/home/av/projects/private/git-hooks" av-hooks-dir: "/home/av/projects/private/git-hooks"
@@ -12,3 +14,12 @@ pre-commit:
- name: "check secret files" - name: "check secret files"
run: "python3 {av-hooks-dir}/pre-commit/check-secrets-encrypted-with-ansible-vault.py" run: "python3 {av-hooks-dir}/pre-commit/check-secrets-encrypted-with-ansible-vault.py"
- name: "format python"
glob: "**/*.py"
run: "black --quiet {staged_files}"
stage_fixed: true
- name: "mypy"
glob: "**/*.py"
run: "mypy {staged_files}"

View File

@@ -0,0 +1,48 @@
---
- name: 'Configure netdata'
ansible.builtin.import_playbook: playbook-netdata.yml
#
- name: 'Configure dozzle'
ansible.builtin.import_playbook: playbook-dozzle.yml
- name: 'Configure gitea'
ansible.builtin.import_playbook: playbook-gitea.yml
- name: 'Configure gramps'
ansible.builtin.import_playbook: playbook-gramps.yml
- name: 'Configure memos'
ansible.builtin.import_playbook: playbook-memos.yml
- name: 'Configure miniflux'
ansible.builtin.import_playbook: playbook-miniflux.yml
- name: 'Configure outline'
ansible.builtin.import_playbook: playbook-outline.yml
- name: 'Configure rssbridge'
ansible.builtin.import_playbook: playbook-rssbridge.yml
- name: 'Configure wakapi'
ansible.builtin.import_playbook: playbook-wakapi.yml
- name: 'Configure wanderer'
ansible.builtin.import_playbook: playbook-wanderer.yml
#
- name: 'Configure homepage'
ansible.builtin.import_playbook: playbook-homepage.yml
- name: 'Configure transcriber'
ansible.builtin.import_playbook: playbook-transcriber.yml
#
- name: 'Configure authelia'
ansible.builtin.import_playbook: playbook-authelia.yml
- name: 'Configure caddy proxy'
ansible.builtin.import_playbook: playbook-caddyproxy.yml

12
playbook-all-setup.yml Normal file
View File

@@ -0,0 +1,12 @@
---
- name: 'Configure system'
ansible.builtin.import_playbook: playbook-system.yml
- name: 'Configure docker'
ansible.builtin.import_playbook: playbook-docker.yml
- name: 'Configure eget applications'
ansible.builtin.import_playbook: playbook-eget.yml
- name: 'Configure backups'
ansible.builtin.import_playbook: playbook-backups.yml

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
- files/authelia/secrets.yml - files/authelia/secrets.yml
@@ -13,7 +12,10 @@
app_owner_uid: 1011 app_owner_uid: 1011
app_owner_gid: 1012 app_owner_gid: 1012
base_dir: "{{ (application_dir, app_name) | path_join }}" base_dir: "{{ (application_dir, app_name) | path_join }}"
data_dir: "{{ (base_dir, 'data') | path_join }}"
config_dir: "{{ (base_dir, 'config') | path_join }}" config_dir: "{{ (base_dir, 'config') | path_join }}"
backups_dir: "{{ (base_dir, 'backups') | path_join }}"
gobackup_config: "{{ (base_dir, 'gobackup.yml') | path_join }}"
tasks: tasks:
- name: "Create user and environment" - name: "Create user and environment"
@@ -34,7 +36,9 @@
mode: "0700" mode: "0700"
loop: loop:
- "{{ base_dir }}" - "{{ base_dir }}"
- "{{ data_dir }}"
- "{{ config_dir }}" - "{{ config_dir }}"
- "{{ backups_dir }}"
- name: "Copy users file" - name: "Copy users file"
ansible.builtin.copy: ansible.builtin.copy:
@@ -44,7 +48,7 @@
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0600" mode: "0600"
- name: "Copy configuration files (templates)" - name: "Copy configuration file"
ansible.builtin.template: ansible.builtin.template:
src: "files/{{ app_name }}/configuration.template.yml" src: "files/{{ app_name }}/configuration.template.yml"
dest: "{{ (config_dir, 'configuration.yml') | path_join }}" dest: "{{ (config_dir, 'configuration.yml') | path_join }}"
@@ -52,6 +56,22 @@
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0600" mode: "0600"
- name: "Copy gobackup config"
ansible.builtin.template:
src: "files/{{ app_name }}/gobackup.template.yml"
dest: "{{ gobackup_config }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0640"
- name: "Copy backup script"
ansible.builtin.template:
src: "files/{{ app_name }}/backup.template.sh"
dest: "{{ (base_dir, 'backup.sh') | path_join }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml" src: "./files/{{ app_name }}/docker-compose.template.yml"
@@ -65,8 +85,12 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app
- name: "Restart application with docker compose" - name: "Restart application with docker compose"
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "restarted" state: "restarted"
tags:
- run-app

View File

@@ -7,7 +7,7 @@
vars: vars:
backup_config_dir: "/etc/backup" backup_config_dir: "/etc/backup"
backup_config_file: "{{ (backup_config_dir, 'config.ini') | path_join }}" backup_config_file: "{{ (backup_config_dir, 'config.toml') | path_join }}"
restic_shell_script: "{{ (bin_prefix, 'restic-shell.sh') | path_join }}" restic_shell_script: "{{ (bin_prefix, 'restic-shell.sh') | path_join }}"
backup_all_script: "{{ (bin_prefix, 'backup-all.py') | path_join }}" backup_all_script: "{{ (bin_prefix, 'backup-all.py') | path_join }}"
@@ -23,7 +23,7 @@
- name: "Create backup config file" - name: "Create backup config file"
ansible.builtin.template: ansible.builtin.template:
src: "files/backups/config.template.ini" src: "files/backups/config.template.toml"
dest: "{{ backup_config_file }}" dest: "{{ backup_config_file }}"
owner: root owner: root
group: root group: root

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -63,14 +62,20 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app
# - name: "Reload caddy" # - name: "Reload caddy"
# community.docker.docker_compose_v2_exec: # community.docker.docker_compose_v2_exec:
# project_src: '{{ base_dir }}' # project_src: '{{ base_dir }}'
# service: "{{ service_name }}" # service: "{{ service_name }}"
# command: caddy reload --config /etc/caddy/Caddyfile # command: caddy reload --config /etc/caddy/Caddyfile
# tags:
# - run-app
- name: "Restart application with docker compose" - name: "Restart application with docker compose"
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "restarted" state: "restarted"
tags:
- run-app

View File

@@ -3,13 +3,12 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
tasks: tasks:
- name: "Install python docker lib from pip" # - name: "Install python docker lib from pip"
ansible.builtin.pip: # ansible.builtin.pip:
name: docker # name: docker
- name: "Install docker" - name: "Install docker"
ansible.builtin.import_role: ansible.builtin.import_role:
@@ -32,3 +31,10 @@
community.docker.docker_network: community.docker.docker_network:
name: "monitoring_network" name: "monitoring_network"
driver: "bridge" driver: "bridge"
- name: "Schedule docker image prune"
ansible.builtin.cron:
name: "docker image prune"
minute: "0"
hour: "3"
job: "/usr/bin/docker image prune -af"

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -35,7 +34,7 @@
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.yml.j2" src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml" dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -46,3 +45,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
# See: https://github.com/zyedidia/eget/releases # See: https://github.com/zyedidia/eget/releases

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -58,3 +57,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -13,6 +12,8 @@
app_owner_gid: 1010 app_owner_gid: 1010
base_dir: "{{ (application_dir, app_name) | path_join }}" base_dir: "{{ (application_dir, app_name) | path_join }}"
data_dir: "{{ (base_dir, 'data') | path_join }}" data_dir: "{{ (base_dir, 'data') | path_join }}"
media_dir: "{{ (base_dir, 'media') | path_join }}"
cache_dir: "{{ (base_dir, 'cache') | path_join }}"
backups_dir: "{{ (base_dir, 'backups') | path_join }}" backups_dir: "{{ (base_dir, 'backups') | path_join }}"
gobackup_config: "{{ (base_dir, 'gobackup.yml') | path_join }}" gobackup_config: "{{ (base_dir, 'gobackup.yml') | path_join }}"
@@ -36,6 +37,8 @@
loop: loop:
- "{{ base_dir }}" - "{{ base_dir }}"
- "{{ data_dir }}" - "{{ data_dir }}"
- "{{ media_dir }}"
- "{{ cache_dir }}"
- "{{ backups_dir }}" - "{{ backups_dir }}"
- name: "Copy gobackup config" - name: "Copy gobackup config"
@@ -54,6 +57,27 @@
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0750" mode: "0750"
- name: "Create backup targets file"
ansible.builtin.lineinfile:
path: "{{ base_dir }}/backup-targets"
line: "{{ item }}"
create: true
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ data_dir }}"
- "{{ media_dir }}"
- "{{ backups_dir }}"
- name: "Copy rename script"
ansible.builtin.copy:
src: "files/{{ app_name }}/gramps_rename.py"
dest: "{{ base_dir }}/gramps_rename.py"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml" src: "./files/{{ app_name }}/docker-compose.template.yml"
@@ -67,3 +91,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -4,7 +4,6 @@
gather_facts: false gather_facts: false
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
- vars/homepage.yml - vars/homepage.yml
- vars/homepage.images.yml - vars/homepage.images.yml

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
- vars/homepage.yml - vars/homepage.yml
- vars/homepage.images.yml - vars/homepage.images.yml
@@ -45,3 +44,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -54,6 +53,18 @@
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0750" mode: "0750"
- name: "Create backup targets file"
ansible.builtin.lineinfile:
path: "{{ base_dir }}/backup-targets"
line: "{{ item }}"
create: true
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ data_dir }}"
- "{{ backups_dir }}"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml" src: "./files/{{ app_name }}/docker-compose.template.yml"
@@ -67,3 +78,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -58,7 +57,7 @@
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.yml.j2" src: "./files/{{ app_name }}/docker-compose.template.yml"
dest: "{{ base_dir }}/docker-compose.yml" dest: "{{ base_dir }}/docker-compose.yml"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -66,7 +65,7 @@
- name: "Copy backup script" - name: "Copy backup script"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/backup.sh.j2" src: "./files/{{ app_name }}/backup.template.sh"
dest: "{{ base_dir }}/backup.sh" dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -78,3 +77,5 @@
state: "present" state: "present"
recreate: "always" recreate: "always"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -41,7 +40,7 @@
- name: "Copy netdata config file" - name: "Copy netdata config file"
ansible.builtin.template: ansible.builtin.template:
src: "files/{{ app_name }}/netdata.conf.j2" src: "files/{{ app_name }}/netdata.template.conf"
dest: "{{ config_dir }}/netdata.conf" dest: "{{ config_dir }}/netdata.conf"
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
@@ -98,8 +97,12 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app
- name: "Restart application with docker compose" - name: "Restart application with docker compose"
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "restarted" state: "restarted"
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -11,10 +10,15 @@
app_user: "{{ app_name }}" app_user: "{{ app_name }}"
app_owner_uid: 1007 app_owner_uid: 1007
app_owner_gid: 1008 app_owner_gid: 1008
base_dir: "{{ (application_dir, app_name) | path_join }}" base_dir: "{{ (application_dir, app_name) | path_join }}"
data_dir: "{{ (base_dir, 'data') | path_join }}" data_dir: "{{ (base_dir, 'data') | path_join }}"
postgres_data_dir: "{{ (base_dir, 'data', 'postgres') | path_join }}" media_dir: "{{ (base_dir, 'media') | path_join }}"
postgres_backups_dir: "{{ (base_dir, 'backups', 'postgres') | path_join }}" backups_dir: "{{ (base_dir, 'backups') | path_join }}"
postgres_data_dir: "{{ (data_dir, 'postgres') | path_join }}"
postgres_backups_dir: "{{ (backups_dir, 'postgres') | path_join }}"
tasks: tasks:
- name: "Create user and environment" - name: "Create user and environment"
@@ -36,9 +40,31 @@
loop: loop:
- "{{ base_dir }}" - "{{ base_dir }}"
- "{{ data_dir }}" - "{{ data_dir }}"
- "{{ media_dir }}"
- "{{ backups_dir }}"
- "{{ postgres_data_dir }}" - "{{ postgres_data_dir }}"
- "{{ postgres_backups_dir }}" - "{{ postgres_backups_dir }}"
- name: "Copy backup script"
ansible.builtin.template:
src: "./files/{{ app_name }}/backup.template.sh"
dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
- name: "Create backup targets file"
ansible.builtin.lineinfile:
path: "{{ base_dir }}/backup-targets"
line: "{{ item }}"
create: true
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
loop:
- "{{ media_dir }}"
- "{{ backups_dir }}"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
src: "./files/{{ app_name }}/docker-compose.template.yml" src: "./files/{{ app_name }}/docker-compose.template.yml"
@@ -47,16 +73,10 @@
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0640" mode: "0640"
- name: "Copy backup script"
ansible.builtin.template:
src: "./files/{{ app_name }}/backup.sh.j2"
dest: "{{ base_dir }}/backup.sh"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: "0750"
- name: "Run application with docker compose" - name: "Run application with docker compose"
community.docker.docker_compose_v2: community.docker.docker_compose_v2:
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -46,3 +45,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -40,3 +39,20 @@
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
- name: 'Create directory for mount'
ansible.builtin.file:
path: '/mnt/applications'
state: 'directory'
mode: '0755'
tags:
- mount-storage
- name: 'Mount external storages'
ansible.posix.mount:
path: '/mnt/applications'
src: 'UUID=3942bffd-8328-4536-8e88-07926fb17d17'
fstype: ext4
state: mounted
tags:
- mount-storage

View File

@@ -4,7 +4,6 @@
gather_facts: false gather_facts: false
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
- vars/transcriber.yml - vars/transcriber.yml
- vars/transcriber.images.yml - vars/transcriber.images.yml

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
- vars/transcriber.yml - vars/transcriber.yml
- vars/transcriber.images.yml - vars/transcriber.images.yml
@@ -56,3 +55,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
tasks: tasks:

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -67,3 +66,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -3,7 +3,6 @@
hosts: all hosts: all
vars_files: vars_files:
- vars/ports.yml
- vars/secrets.yml - vars/secrets.yml
vars: vars:
@@ -52,13 +51,29 @@
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0640" mode: "0640"
- name: "Copy backup script" # - name: "Copy backup script"
ansible.builtin.template: # ansible.builtin.template:
src: "files/{{ app_name }}/backup.template.sh" # src: "files/{{ app_name }}/backup.template.sh"
# dest: "{{ base_dir }}/backup.sh"
# owner: "{{ app_user }}"
# group: "{{ app_user }}"
# mode: "0750"
- name: "Disable backup script"
ansible.builtin.file:
dest: "{{ base_dir }}/backup.sh" dest: "{{ base_dir }}/backup.sh"
state: absent
- name: "Create backup targets file"
ansible.builtin.lineinfile:
path: "{{ base_dir }}/backup-targets"
line: "{{ item }}"
create: true
owner: "{{ app_user }}" owner: "{{ app_user }}"
group: "{{ app_user }}" group: "{{ app_user }}"
mode: "0750" mode: "0750"
loop:
- "{{ data_dir }}"
- name: "Copy docker compose file" - name: "Copy docker compose file"
ansible.builtin.template: ansible.builtin.template:
@@ -73,3 +88,5 @@
project_src: "{{ base_dir }}" project_src: "{{ base_dir }}"
state: "present" state: "present"
remove_orphans: true remove_orphans: true
tags:
- run-app

View File

@@ -1,7 +0,0 @@
---
base_port: 41080
homepage_port: "{{ base_port + 3 }}"
netdata_port: "{{ base_port + 4 }}"
gitea_port: "{{ base_port + 8 }}"
outline_port: "{{ base_port + 10 }}"
gramps_port: "{{ base_port + 12 }}"

View File

@@ -1,161 +1,162 @@
$ANSIBLE_VAULT;1.1;AES256 $ANSIBLE_VAULT;1.1;AES256
62373337383866623263326333653134323532613565633430346266396463376439633833656138 35643431306561396237633439336230333563316364353462393534646565653163396362393566
3162643934363462303436653264323862353161633963360a633861646264336133613134336336 6636306532353662343437646631363766376166333934340a363062323332386261643832323762
39333431643666323730356163626236333430313136626134333539333330646237333339306238 33303235646434636636366532626331653865666465643561633863663938313535623162306334
3936353539653730340a613564656330663336336132356431386264666662333864663064396332 3332616236326436650a383636393434303165363964383966633138326361373134306563653363
64393163346538396366366333623431333533666563346163613633393635646531383739323435 37376465643762383761653132636336663762656136303730616463386134346436383539653661
36636533383563383265383439323539396531623466323435303164643038653734623133343739 35623933346565633030373730316331633236623561313166376335636464333439643637623338
34636430643030323031303434346636623764663239653535323035396634613036386230323032 32393433653339656135343766393565366530636132346339306339353438396337303534336266
62623131643639393836346664393734653931346630666539656661626566353730333635393261 34303635316363343031363263626434386139633762353038666266643338343631303530313661
35386339363732386137643937373730303765643234326433336366376135656637376637323862 61373239663739666535636466393937333437316334306631336334303961396134323062666339
35323230333736366238333734386136383534393661383037346235643561386336393438633965 62303437623131626130363838353736386463663734356530333061353031643861636437323431
34646535363835383134353232313839663763303037653766346138363531613162616539353531 62633036316639306366643633333366386132343337313832343133313235626162323266306165
65663832633239613236626366623733323365303534616463333435393763656162343764323138 39326434326235373536633464313234623464373465653061383834363266663435303461633834
37613530636331316665376634623233653935653933613930333130313563313061333435353639 36663031323634326436303138623262396332363934646337353166373163326132626437636138
30636361393562653764613065316563666134336333343433336335353138656239326166636533 38623864666434363939663234623732623962323836323532393031396638343737373764303239
31386539646636333438643366336666623764656335343066616666343737356266356335373733 31316639653066373166326432653930666630393061366461663337333936326239376165633064
31653234643234313462333138353639323362363834353162663064363938353134313232623533 32356530333338303636366234336135353130646134643432613965366430326164393563383135
37363732343935623132303664653436313730316534623630326362376233393233373531316162 63653237646237323832613363643732383436646539396239333632353566386135633961316132
63326537316266613339376239376561613136633830633837393035363337343034643230303630 62313630313065376465346338663661343466366364363761616363376535373031383736616136
61343039333839313164616536306465646262626332383665666139626534613938303739643566 37343634626366356261363030616330396263623965306166386238363039663461396431303733
63343731333835666466626136633761366536366230623364356632653837633934303739623466 61626164353466323762666236376536356364653565333266363730653539626531663639333864
39396535313963643535353236373338383431356331393232303336623962646366316132343263 32366333336234376635616361626635616265376361316439653230646537303031343564653631
34613162323861643632393736383836376161643036323165393837623734386663383436393062 63663237353766643766343130613739386636663534336536353936353561616538316535333064
32383130613063643036656365646332363865646662346431393062323961343039343935643965 62343835643636633831366265313939313433326239633130303863303136363732653766303663
62343033646632333337613164353263353834623430336334356666323761353964306133393439 33643231363761336463623339323831363034636532346237373236656266303063633365393636
61323438346662623435356537623064373832346338613733613061623164623466363561623466 36663066336233643236393837323762393435303665303661356237666237646464326536623731
34393430313838623435663935333638323231663138346539653139663437643066663465666231 64396431323561353832333834336232396366663439643161626338653930633665346566303435
31303232363863383839623037636333626166326135393439316662306435306130353639386563 66636466313936316530353537643637323833653561616339353465323466333332363436613461
65316638376534326363306562353037653765333834663361353864383265643466396265336264 38653837373637343532343662366435616366313831623239343363366131633062303036333135
32353865633131373061323637623736353162366131633161643963623432623133336237366164 36356136376236383431613266663164616135343062666136353430626663653631313064326131
63393465656464646635376438363563663461626438336339386564316463343230323964633430 63353066313433653838613934323833373237356230376363376132306663656461653264383866
37343563656462333638303638356235326635376138626333613636623830363366343435373464 37333863633536346538366665346666346364633733366434393135353664336330623865343132
34623465646437663735333036366566303962356266666363366132643764383638386230643635 31396534643633333134363465323063313435333236646630613066353232613039393235366630
30353838633863313037616165666265393930326333613761366566633230373761313162363135 36646664366339613535616437376437326561636130333063626637316431626330386137306162
64386461343263646331333565396662636133356361616563396563626232313962303862353338 65393533306439613032623061373331333762643336633863333061393734313335386633666338
36383238633134383865663434343062636465623631363461303439666565373339616136613336 31316637643031353462363363626662373462633065653738613733633933623432343535316438
32663732333263353666323766333338396532643134316362613930396134363430313363616663 31356530306263303566636535363465356562633439386365316461323339393339643133323032
34313766303433333936343936386138313939373734316463376531623562633834626561643134 32616662633565346564643737386266623836623830663265323439313762636437336436346430
64356530643637313066656363653165396562616363373637373261316633643331653833623137 65353266373766363166633363636231623031363337626631626466616637616630383632333565
33626162366636633932343135353961366631633866343564613834353237326132643963353466 35303737316531306237613832366565376364313435376266306263623139663964336635633030
65633033616238313532666366313566373261643531333830306232373439323737313330303662 39306662616233646136336237646139326239653435653436323066333332626266646564616631
36613164633264366133623566653034363739633435646634383832343138303931626636373037 31653433663539333633663433643261363065663030373265646533323832323366376230346266
36326464353336383964643439393864666238623939393134326134613938303466343536613933 65616633363938333037656663393334363564626136333162383162663437653565663532646366
63623961656435656363363365633137383563373265636564613861666331623739623532666632 31343063306434663231396464646232353335636138393038633737323339303033373539363739
62646432643432393565396135613364653263386562386262396262333430656335313337646539 64306163343534363131646135396234346166313039323833656636343235656238623333626534
62353335303836376433616639613531623432626434306162666134656637616562356635363931 30653264656166326562343534313063326332336330623530646563313262396233386335316538
31303063313634336335336233353265346563646530376639663032326239393363626136346466 64316363613865313236633437646139353433386234326537633961356133353361333833636339
38616630666432363866316137653730623066373263656331313665643439646636653362393130 65646232336137626130326631626436623933353333383438306237326132303838353833376432
36666130616435626135306639303766656430316536376431303337316631316462623564666237 31653936363131623535386331353066323462613238636130376562376361643166363332343537
34303262663231303437323266643262363434313232346432363030306661333536353732643761 63346366306161373666306536396164323437613066363537366562666130653638356564383235
37333962393436323463633762393135326264323239373838613032353938366238393138393332 66303838616266623335363338376535303665643363326662376166643865383039306236393730
34643162303335666330613263386438653835396135626166636435306238643463623061646432 34303130386631636636393736616237633933346136303530373033383739653531326437656463
30393564643665303764356334363966623939323138336332646437343265343662333836326566 39326239616163313463636232343036626435366261643030386530393763356133663863373264
64663464313730353162306534663731376266373062343534633230663635623535626533613636 62316164386464366663373635393365363161336530313734343633346563396666616262396563
65313961326665333234336663303236393232373334623532333863313766613162616666393732 37383638613930366635623962366662343232303635636330383534653861336264396132653538
33313937326164396639663835653461333033616463373034643830356361656234393330393233 65306539613333306339643063303063333165653734323333353461356536663661613463633632
64313934653133636630323331353262646335363666366339303630303761393262363761663530 61613530653734643761616636386238633535643961633765313863323961313362323265373132
66373638336336386432613939343835636463303838383239333361616538643538633738333834 65346530653962613534343062333039663837623130666261636135343663643036356261643333
66356234383437336234663964613531306165616636616235386363623738616631646164313538 64306430343136616438636231623236316264623533313439643434343333623030376338663365
30313034346264373033633431636331336562316538666639653462313438383038373034663036 36393932613161316137386136343939383035326231323561636337343833613635643261633165
30643361323230313761633439326236663531316339303765663032653835643735386536333062 38316233323862666439633833646235623031336638353834323633333562346463333436316437
63623338613364343137663136666234376264626663323565343762343834633330306463353135 36303237353138353763346166626465663137323062643633323762313137343661323839353231
35643330373139653839313739376134323831383538666532633561323365363534633063653136 38643137383739393866616364386262363733316133313934366165353761643164303439643534
32303661303831373633306164393533303336373766643463383635313035376261363461363464 37633636323535373964323939346339633138386239333466363239393430613139623466643531
62313539646333633838303635326335326330383261326330333533396364393931643138366562 66323662363834653166363835343363303066306262623432353165313239323130616235316264
38343961316432396230616337313164626631616330343861306438396230383466356133643161 65376532623530353935346238346261613062633439333630643962343233326234316130616538
36353364303739326265306430653036633134343166383430663264643466653430663936353462 64353932613038353866383263333561623632663863373231636132653365613836363730636263
32386330626330393734343938623664386430323438333965346536396639613365663461616162 65626666653962653266343134343638666437373532333464633163333861386632356562346637
66633534363737366334633239613663356437366234306437363733376237343132353133326661 62303433383464373165316561646633636130636434633430623031633364643336643835613030
63633362356361653264363435303030313962323462313137656532306537616330613532663665 64653937656262623536353764343661326632303964393862343032343330653534353530326238
64376131343937636138333332353464633831323361623263323766333034386535313136613831 34613236336339653364333565313862613531643763633166363734316363643534323838333531
62363662616661643563326465653664393032666162303364643234303362333164653738616636 36353137653535393862393534303366366464613035376439383134336534366263393661646564
34613766316565333136663831396166643163333334366461616333376365383530396638313931 39616233323638653337333461636465353935613236646430623163656334376463323135323761
35356264623739663935316165376363633338313164613766663137323261303935613230316661 36353064646532323738363837663238653766366634363232323933363033656332626436363166
66376231333539386139353537356163396639663965666334643739333434393134313230356131 36376366336236626265373530646664633335623035646662663261353165313634646463316535
65393331393836343962343235356138366634633563613838333139666565363831346166396630 38633862623433613937666162343936393337336537623663306439373830326434356366323664
30663630376231646665386137326361336132313535336263653338343430613830346363303239 61646337396562373535353834393665643037626539303932663831306535623733626239323939
36623539326161666564633435303439313763356536643737386432633363636162623232656137 61616165643439653235346566346631323035306437323339326665363232333338616133666366
66346665623433333131323339636538663363303035633866656139393566313037613439366630 61396136353432376263333961353738343463663937313136336536623539383835663361353232
32653465656664343130653937373436336236626566386638393136393536356331356637646461 38343737383361613930326265346562356136313539393464623935626566343137346430356261
64336265326438616439336432333432656336393866666666323132323165383039323761313230 62633230396665316533313831346236303265303232363935313138323537373933633232343732
33386162376563653638656563616232323132643534626537323161623661303937386138343034 35346363663938306562663433396138393861343332373934303864303963623739316239303264
31316230646166316263323262623638393336333739326638316264313835653734373232633934 62393634346231393534633861386364336639366663353065333930616335323438383537616666
64376330363035343830353337386632373863346438326332643962303061306138383539363061 63373836363839336634316338643935346634626437326431363132636335326238353930663662
62386636366430643835336131663139306138653030383963383034326466393263663230356634 62383464366263336430633338646263626565393030646230306230353837313965623430636138
63376437306135396137383633376636633464663365303165643131316339313131303836306136 38393334623661363532663464363163383365323736386134393634333332353263353763393636
66633163623162636362636138326530373364316234306565323635303166636132383330316662 35353535303030356665303262333334353630613832323733393638613035343833356662353565
65353839333434646166346662366565623061396330666166636263393962373031623632623036 64373334626565653631613863323866336261393533333639326266343435376666646131313535
30316537616362636634316166396265633165383232383136643235666330313931326138633135 39343734326162623831343730303734643830626338653232633834373565643133393562653336
66376662376230646336343432353634383737623835323038613235323761383830663339363432 35373432316161623061373238333932366161663137616337613632366263373830663464396537
64613734343662306230396165636366356139303539313537356631343837323864653736336539 37303363386336663663303161343963633834623564643661653332623434626466376637323631
64346135376633326634623839643565623339313361626532313566666263333232316562363537 64653265623861363837346462613164373264313766633034353531666561623732353838643639
63633732313336336637313931303037626433646233353361633332343333623639386131663962 37646262646362386331663334666131336335313833313463373463313439353934313733303763
33363731656538353134363436383066333564336138633462386331626130363937643166303161 65626339363732326565366464633230303933386438373539633338363439393630316164376565
62303266666238626562666432383934393435643836333235626531663334343331613766303237 63333132613739663461346361613761396161323863616263333638376437613139353733613963
62613936616136663064353762363666303464633562353061393364303563313831636432326237 32663062393435323061353738316132363438393762316666626231386138366639313566646461
30356464653063663535333037393364363164636334653136323065663162323835336462396433 36306664343433653836316436303736343436343762366133373066313232333834363536343462
39626532383433363835303862613534303535386131343361363664653831656463316437333035 30303866646131663530313463343261633261663932323562636135346664323761343765333730
62333636303236336163363935643361663864623739316461353737383034306261353637363233 39663231636465336361393062666135316534656637306236323137653939383565643637623866
32616533333933633730356365666531336135376430613462343662343930663332613961393433 33633536366163346466303864313732303036306665326664353064366134326539333831663632
65633865346366396134633962303665633766313432383165363636616335323465303162623639 64343533323166623661326536393437623432336435366435396165333431366532653936396636
30356232636165303264366239323932633338613065383537383235373030633736663637383135 30653263306461656230663731336266363832616539373333306238363834333838393063383432
31393365336362326366393861313032303430663231303030653539643263353035316564393130 61343238373861303932653162663330333964623834336235336430376562313963613134353834
31643031363537343936313538393837323634613630326239616435313732396330646562376332 63666537343837336531633536633038666232343165336663356639636537306333303434383231
33643765326538376537376237366263396465323064393830383262386336363838333433373530 64306633383338646362633238386263336632393562623330343532373839663238383965643739
34656466333862313336633361343034663238336139356663653835313838363835613034633830 39313363383331363339616431653434366131303666623263326662636161666635643162663730
62356535613135663435653566353233623635646235313264353731666432313763666638653739 62356136643364666164383932376536623266373535613230336165396163383830346265393738
31393865326632303937373036646432626665356435363762386566653937626237386333656237 30636139353336343630316132366365306234313064636537646262666463646437376336366562
37633664326330323665616633666462636139663565633036643935363534626263376137386137 31663731666664313339353331626534626462663431633530653365613762363666373532383762
63666162346663366366633565346531623162656534303366373965363131303638343134366438 34323866356239383332373632616464666230373339386235393838376333333464613038373337
32373364316565303039386536363833643738623465373566356134386363323066393931646163 33373231386263623664353231626331366565356662623934316632653763363739653862356265
31316235653965343936346366393838313836396563346165623630363835313738663565393032 39653539616232356333326463373666656133303161356630393839336638316338646238623138
30316233313235353165373561316534333065376466356239373964663963376138643930353263 61343833613130376562656465343438653861653431313762313062656563643665356664646465
30326363353434373230383765653933313135373363613439643432343338616564336264326439 33343430663066616536393935366132336666616164356463323663373930653266653238663336
39373939373765613531306533363362656164313731383530313462366537633866373238623032 35616263613461356333633430653661376239663363653434346437656161623961333761633663
30313438306466343337326631346466346535623139363961626239343861306138343861326566 35643031323063666163623639313864313937386339376235653033366136373333653239653238
62323931353637646231303361633162636537373838663235653966663235653236613535623135 36376636653766366632613262326336333430353534326464653931663366383039643466303739
32353038616532376636633064353739633064343639333530643562663436633431616237333062 66393065323965383962633266623033643330333437383262373461363539626333653433356436
39663061623533393932303537343562663962623734336631376532613731373864373531623937 64383738653735386630346264643462353336313830376139353166666330303638386639623039
64636162613066613734313433383330323764393265336239303266356533656166303238623732 34303137623936663636303864623932343766323235653934303263363164326534653137613863
33313531393934383639303163633766323961316532386161343665336431333138316438613362 39353739633466376136343938303634643565613537646166326536303739316536376162353838
32323634633062373535656566303130643366366265623063346532666566386263666436633837 61326235373733623465313530393831623438343530616339326561386239646562343130376336
31343361383735343236316337613762336338353461313965663438616538396530366234313135 63373430313332613561623636393338353933613161656264646366633665373336343530303635
61393337626430313537663738303462356133336564353865663532656534333538306131343930 35336561336334626333343030366264356264343062623531363331313466666136666332376265
61313961363164653566663930346332353737333432333239343264653134386361623761663331 62363862623635376631336235353531343931663131333861616566356438396664633962646236
32646265336533613234326231623466373130376139643763373833316637613136616631333538 61343063383238376261656465386635623263386430656132393631623330326462326166643631
39393934343131323535363136393566353632383935366239323661623434323431656435373137 34333238663531366232393764626665376465386336653133643335656161343261616532386135
36323139303335656139666437316238383932633965663861363363316337383938633262303830 63333931646435373630323931396564636131616234346130393566663131353835323833666338
62626339626638333430653531653039316235393436623237633764383038306235326637313530 63653332383130666164376534656430366664356339323463393338353066663537313336653864
31656132306333343830396531363936656537306233616564633733663762663731333362316165 66323361383737323930663732376261323238653537623335636638363966343264346537383264
62636331356636386632376338396463396164396334633831313733396136656637303031373330 64663334366538326365663266613662356132646230363461643862643337633531333233363134
37663432666263666461386437323536636439366664323730633065396332326464363634386361 66313565313733326363623935363535336166623761346162663964396266613932356462376664
62666564343562643433363437633431333062333330346666666432333335643763323361333832 63313339333035333234353765383735303830653162346565303465363762623465326431353431
30636134343362656562636663363864616439643364333135386438643132616436653739393161 65616335313733353863626561613236616339393038383562313263376633363765306262396230
39303966393261626536646137633935653932613339393764633632313136383366303863333832 38373963306637316534613964376538666536383235393663616236653338373034363765636135
61343062336263323436333633343833383864376464626262363933663664643733626632623234 65666239356563363538666162313038633861353732386630633930616439323838666536383165
61313936633335363361663930613436613233653962646135303961616161333561313434383736 34353761613738616361613862656236623633356364383436383435383632303635343233356435
37323030623832323935313334376362376439646238393030333963376536326531363832353161 35353466636461376436643965653837633461643430386565353530323333313964376233666665
61666263376465356564333462376131393434623036653634343030326538373564303637396466 63353838323363666435393634643237626663356331653461303936366533343435373732613861
35326630616435306361366136363864636437316366333463386534366366663838653734653633 33393231636235613234383737633430343461616261373265313163363730653237346563393835
66646132323165396335316231613932316333366564653538313834383461323561313231643533 38646566313730633866636162393434316365396663333938656235613635613161363938653531
35316433383837643561653433656239613764366337323064663262663333306166383935316135 65316233643561623837633362383232313664313737643231363838616434666635343466653435
64326131656239666562376538613735353235663336346633333864333637636230646237376435 62653765303832353761346563363338656431313439346234333432363535626461336163616330
38666534303038656237386563336136666333383764633564313330353634626336313435323233 33306633313764616533666263633634306633633261663637633133356430313231323331353535
33373632633265396537623065333461656130313063643063366236336363633564613366353932 31316663373063376665373732366365386464393032313335376463353435313133376161336263
32623832653639323966616634616139653837626165626437636538363937396234373933383366 33363835363062646536343833383232386133666137313539663534653138333236626438333866
64386630636235666464396631316434386138313864343230313462616236626134313738383933 39316433626133666636376532366332346134646233383737386337356464363263343334363663
64626161636662323133326466636334666339353565613263656334643838373530353263646539 32363538343566666263396265636261643764383736643663386333623266363535333766353037
33353565323939336164363131346438376637653835343839333833373461353030383734643863 33333864636539636165373433343532326431316138666164353262643236623237636539646636
39376565643437376434666539613364363836343238353365313564613164356664353363623534 30356361663863646366303238643335626434353832306338346231383764616566626163313931
62383066643762363134343765643862353038646639303562626239653630653963633432306635 64663663613363396661396564383531653231663735653535393437356339353466353737366266
35303561306134653566626434323336386239363439623731333761616366313032663664656432 37353739646632393837353032636331316338393564643261383536383536323036333238633237
66363932656564343565346436316438383534613239373337323766346131343364666331393461 38643735663339316263633964336137303939303531626633646236656430373132316432343436
38616266663239623763626364653639353435643133353034336632343235626235313565303931 66383135663130323462373934656666653837336332626137303931303263613038646235623631
34653438383365353335633061616331633539343636313439303738636239633934393362353739 39383936393665316561373637333935643565656433316462333832323034323533316232656164
63613761636465393564363039363235663830373936376236623330313265306630643065396639 63656630356363336231326364656531623839316236616266363037303138306537376131616134
61393535616532396232613562316436303066303732386262396664346334393439643738396537 31323132613533663664376136376437623837303835613331623339623531653563386464306339
32613834306466623066653765333165356231633065376462343236323730343936396562393263 62346465396362303262356239326636666435343131333566653661613463363461633631383030
62663434316466666532313036316539393237653835623432663237376332326138643038643065 63303738613735313262656362383432356236613339646462393836633861303562663262333561
34326565653561353465373939616564653833333161303430623339323062333136616664666236 62323562656461663764336462353230653537383038323931353831643731343837323234643565
30366330666330333362 36386531613931623036636332663561663438333364616232626461333639326564313335376134
3935