Compare commits

..

No commits in common. "main" and "feature/kunsi-ipv6-only-vlan" have entirely different histories.

291 changed files with 4537 additions and 4649 deletions

3
.envrc
View file

@ -1,3 +0,0 @@
layout python3
source_env_if_exists .envrc.local

3
.gitignore vendored
View file

@ -1,6 +1,3 @@
.secrets.cfg* .secrets.cfg*
__pycache__ __pycache__
*.swp *.swp
.direnv
.envrc.local
.bw_debug_history

View file

@ -30,7 +30,6 @@ Rule of thumb: keep ports below 10000 free for stuff that reserves ports.
| 20010 | mautrix-telegram | Bridge | | 20010 | mautrix-telegram | Bridge |
| 20020 | mautrix-whatsapp | Bridge | | 20020 | mautrix-whatsapp | Bridge |
| 20030 | matrix-dimension | Matrix Integrations Manager| | 20030 | matrix-dimension | Matrix Integrations Manager|
| 20070 | matrix-synapse | sliding-sync |
| 20080 | matrix-synapse | client, federation | | 20080 | matrix-synapse | client, federation |
| 20081 | matrix-synapse | prometheus metrics | | 20081 | matrix-synapse | prometheus metrics |
| 20090 | matrix-media-repo | media_repo | | 20090 | matrix-media-repo | media_repo |

View file

@ -6,9 +6,9 @@ apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y -q -o Dpkg::Options::=--force-confold dist-upgrade DEBIAN_FRONTEND=noninteractive apt-get -y -q -o Dpkg::Options::=--force-confold dist-upgrade
DEBIAN_FRONTEND=noninteractive apt-get -y -q autoremove DEBIAN_FRONTEND=noninteractive apt-get -y -q autoclean
DEBIAN_FRONTEND=noninteractive apt-get -y -q clean DEBIAN_FRONTEND=noninteractive apt-get -y -q autoremove
% if clean_old_kernels: % if clean_old_kernels:
existing=$(dpkg --get-selections | grep -E '^linux-(image|headers)-[0-9]' || true) existing=$(dpkg --get-selections | grep -E '^linux-(image|headers)-[0-9]' || true)

View file

@ -1,3 +0,0 @@
deb http://deb.debian.org/debian/ trixie main non-free contrib non-free-firmware
deb http://security.debian.org/debian-security trixie-security main contrib non-free
deb http://deb.debian.org/debian/ trixie-updates main contrib non-free

View file

@ -0,0 +1 @@
deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi

View file

@ -5,9 +5,11 @@ supported_os = {
10: 'buster', 10: 'buster',
11: 'bullseye', 11: 'bullseye',
12: 'bookworm', 12: 'bookworm',
13: 'trixie',
99: 'unstable', 99: 'unstable',
}, },
'raspbian': {
10: 'buster',
},
} }
try: try:
@ -25,10 +27,6 @@ actions = {
'triggered': True, 'triggered': True,
'cascade_skip': False, 'cascade_skip': False,
}, },
'apt_execute_update_commands': {
'command': ' && '.join(sorted(node.metadata.get('apt/additional_update_commands', {'true'}))),
'triggered': True,
},
} }
files = { files = {

View file

@ -21,9 +21,6 @@ defaults = {
'cron/jobs/upgrade-and-reboot' 'cron/jobs/upgrade-and-reboot'
) )
def patchday(metadata): def patchday(metadata):
if not node.metadata.get('apt/unattended-upgrades/enabled', True):
return {}
day = metadata.get('apt/unattended-upgrades/day') day = metadata.get('apt/unattended-upgrades/day')
hour = metadata.get('apt/unattended-upgrades/hour') hour = metadata.get('apt/unattended-upgrades/hour')

View file

@ -0,0 +1,5 @@
context.exec = [
{ path = "pactl" args = "load-module module-native-protocol-tcp" }
{ path = "pactl" args = "load-module module-zeroconf-discover" }
{ path = "pactl" args = "load-module module-zeroconf-publish" }
]

View file

@ -0,0 +1,3 @@
[Autologin]
User=${user}
Session=i3.desktop

View file

@ -0,0 +1,110 @@
from os import listdir
from os.path import join
actions = {
'fc-cache_flush': {
'command': 'fc-cache -f',
'triggered': True,
'needs': {
'pkg_pacman:fontconfig',
},
},
'i3pystatus_create_virtualenv': {
'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/i3pystatus/venv/',
'unless': 'test -d /opt/i3pystatus/venv/',
'needs': {
'directory:/opt/i3pystatus/src',
'pkg_pacman:python-virtualenv',
},
},
'i3pystatus_install': {
'command': ' && '.join([
'cd /opt/i3pystatus/src',
'/opt/i3pystatus/venv/bin/pip install --upgrade pip colour netifaces basiciw pytz',
'/opt/i3pystatus/venv/bin/pip install --upgrade -e .',
]),
'needs': {
'action:i3pystatus_create_virtualenv',
},
'triggered': True,
},
}
directories = {
'/etc/sddm.conf.d': {
'purge': True,
},
'/opt/i3pystatus/src': {},
'/usr/share/fonts/bundlewrap': {
'purge': True,
'triggers': {
'action:fc-cache_flush',
},
},
}
svc_systemd = {
'avahi-daemon': {
'needs': {
'pkg_pacman:avahi',
},
},
'sddm': {
'needs': {
'pkg_pacman:sddm',
},
},
}
git_deploy = {
'/opt/i3pystatus/src': {
'repo': 'https://github.com/enkore/i3pystatus.git',
'rev': 'current',
'triggers': {
'action:i3pystatus_install',
},
},
}
files['/etc/pipewire/pipewire-pulse.conf.d/50-network.conf'] = {}
for filename in listdir(join(repo.path, 'data', 'arch-with-gui', 'files', 'fonts')):
if filename.startswith('.'):
continue
if filename.endswith('.vault'):
# XXX remove this once we have a new bundlewrap release
# https://github.com/bundlewrap/bundlewrap/commit/2429b153dd1ca6781cf3812e2dec9c2b646a546b
from os import environ
if environ.get('BW_VAULT_DUMMY_MODE', '0') == '1':
continue
font_name = filename[:-6]
attrs = {
'content': repo.vault.decrypt_file_as_base64(join('arch-with-gui', 'files', 'fonts', filename)),
'content_type': 'base64',
}
else:
font_name = filename
attrs = {
'source': join('fonts', filename),
'content_type': 'binary',
}
files[f'/usr/share/fonts/bundlewrap/{font_name}'] = {
'triggers': {
'action:fc-cache_flush',
},
**attrs,
}
if node.metadata.get('arch-with-gui/autologin_as', None):
files['/etc/sddm.conf.d/autologin.conf'] = {
'context': {
'user': node.metadata.get('arch-with-gui/autologin_as'),
},
'content_type': 'mako',
'before': {
'svc_systemd:sddm',
},
}

View file

@ -0,0 +1,124 @@
assert node.os == 'arch'
defaults = {
'backups': {
'paths': {
'/etc/netctl',
},
},
'icinga_options': {
'exclude_from_monitoring': True,
},
'nftables': {
'input': {
'50-avahi': {
'udp dport 5353 accept',
'udp sport 5353 accept',
},
},
},
'pacman': {
'packages': {
# fonts
'fontconfig': {},
'ttf-dejavu': {
'needed_by': {
'pkg_pacman:sddm',
},
},
# login management
'sddm': {},
# networking
'avahi': {},
'netctl': {},
'rfkill': {},
'wpa_supplicant': {},
'wpa_actiond': {},
# shell and other gui stuff
'dunst': {},
'fish': {},
'kitty': {},
'libnotify': {}, # provides notify-send
'light': {},
'redshift': {},
'rofi': {},
# sound
'calf': {},
'easyeffects': {},
'lsp-plugins': {},
'pavucontrol': {},
'pipewire': {},
'pipewire-jack': {},
'pipewire-pulse': {},
'pipewire-zeroconf': {},
'qpwgraph': {},
# window management
'i3-wm': {},
'i3lock': {},
'xss-lock': {},
# i3pystatus dependencies
'iw': {},
'wireless_tools': {},
# Xorg
'xf86-input-libinput': {},
'xf86-input-wacom': {},
'xorg-server': {},
'xorg-setxkbmap': {},
'xorg-xev': {},
'xorg-xinput': {},
'xorg-xset': {},
# all them apps
'browserpass': {},
'browserpass-firefox': {},
'ffmpeg': {},
'firefox': {},
'gimp': {},
'imagemagick': {},
'inkscape': {},
'kdenlive': {},
'maim': {},
'mosh': {},
'mosquitto': {},
'mpv': {},
'pass': {},
'pass-otp': {},
'pdftk': {},
'pwgen': {},
'qpdfview': {},
'samba': {},
'shotcut': {},
'sipcalc': {},
'the_silver_searcher': {},
'tlp': {},
'virt-manager': {},
'xclip': {},
'xdotool': {}, # needed for maim window selection
},
},
}
@metadata_reactor.provides(
'backups/paths',
)
def backup_every_user_home(metadata):
paths = set()
for user, config in metadata.get('users', {}).items():
if config.get('delete', False):
continue
paths.add(config.get('home', f'/home/{user}'))
return {
'backups': {
'paths': paths,
},
}

View file

@ -1,22 +0,0 @@
[server]
host-name=${node.name.split('.')[-1]}
use-ipv4=yes
use-ipv6=${'yes' if node.metadata.get('avahi-daemon/use-ipv6') else 'no'}
allow-interfaces=${','.join(sorted(node.metadata.get('interfaces', {}).keys()))}
ratelimit-interval-usec=1000000
ratelimit-burst=1000
[wide-area]
enable-wide-area=yes
[publish]
disable-publishing=no
disable-user-service-publishing=no
publish-hinfo=yes
publish-workstation=no
publish-aaaa-on-ipv4=no
publish-a-on-ipv6=no
[reflector]
[rlimits]

View file

@ -1,18 +0,0 @@
directories['/etc/avahi/services'] = {
'purge': True,
}
files['/etc/avahi/avahi-daemon.conf'] = {
'content_type': 'mako',
'triggers': {
'svc_systemd:avahi-daemon:restart',
},
}
svc_systemd['avahi-daemon'] = {
'needs': {
'file:/etc/avahi/avahi-daemon.conf',
'pkg_apt:avahi-daemon',
'pkg_apt:libnss-mdns',
},
}

View file

@ -1,11 +0,0 @@
defaults = {
'apt': {
'packages': {
'avahi-daemon': {},
'libnss-mdns': {},
},
},
'avahi-daemon': {
'use-ipv6': True,
}
}

View file

@ -62,13 +62,10 @@ trap "on_exit" EXIT
# redirect stdout and stderr to logfile # redirect stdout and stderr to logfile
prepare_and_cleanup_logdir prepare_and_cleanup_logdir
if [[ -z "$DEBUG" ]] logfile="$logdir/backup--$(date '+%F--%H-%M-%S')--$$.log.gz"
then echo "All log output will go to $logfile" | logger -it backup-client
logfile="$logdir/backup--$(date '+%F--%H-%M-%S')--$$.log.gz" exec > >(gzip >"$logfile")
echo "All log output will go to $logfile" | logger -it backup-client exec 2>&1
exec > >(gzip >"$logfile")
exec 2>&1
fi
# this is where the real work starts # this is where the real work starts
ts_begin=$(date +%s) ts_begin=$(date +%s)

View file

@ -2,6 +2,7 @@
from datetime import datetime from datetime import datetime
from json import load from json import load
from subprocess import check_output
from sys import argv, exit from sys import argv, exit
from time import time from time import time
@ -17,17 +18,29 @@ try:
with open(f'/etc/backup-server/config.json', 'r') as f: with open(f'/etc/backup-server/config.json', 'r') as f:
server_settings = load(f) server_settings = load(f)
with open(f'/etc/backup-server/backups.json', 'r') as f: # get all existing snapshots for NODE
backups = load(f) for line in check_output('LC_ALL=C zfs list -H -t snapshot -o name', shell=True).splitlines():
line = line.decode('UTF-8')
if NODE not in backups: if line.startswith('{}/{}@'.format(server_settings['zfs-base'], NODE)):
_, snapname = line.split('@', 1)
if 'zfs-auto-snap' in snapname:
# migration from auto-snapshots, ignore
continue
ts, bucket = snapname.split('-', 1)
snaps.add(int(ts))
if not snaps:
print('No backups found!') print('No backups found!')
exit(2) exit(2)
delta = NOW - backups[NODE] last_snap = sorted(snaps)[-1]
delta = NOW - last_snap
print('Last backup was on {} UTC'.format( print('Last backup was on {} UTC'.format(
datetime.fromtimestamp(backups[NODE]).strftime('%Y-%m-%d %H:%M:%S'), datetime.fromtimestamp(last_snap).strftime('%Y-%m-%d %H:%M:%S'),
)) ))
# One day without backups is still okay. There may be fluctuations # One day without backups is still okay. There may be fluctuations

View file

@ -1,39 +0,0 @@
#!/usr/bin/env python3
from json import load, dump
from subprocess import check_output
from shutil import move
from os import remove
from collections import defaultdict
with open('/etc/backup-server/config.json', 'r') as f:
server_settings = load(f)
snapshots = defaultdict(set)
for line in check_output('LC_ALL=C zfs list -H -t snapshot -o name', shell=True).splitlines():
line = line.decode('UTF-8')
if line.startswith('{}/'.format(server_settings['zfs-base'])):
dataset, snapname = line.split('@', 1)
dataset = dataset.split('/')[-1]
ts, bucket = snapname.split('-', 1)
if not ts.isdigit():
# garbage, ignore
continue
snapshots[dataset].add(int(ts))
backups = {}
for dataset, snaps in snapshots.items():
backups[dataset] = sorted(snaps)[-1]
with open('/etc/backup-server/backups.tmp.json', 'w') as f:
dump(backups, f)
move(
'/etc/backup-server/backups.tmp.json',
'/etc/backup-server/backups.json',
)

View file

@ -33,11 +33,12 @@ for line in check_output('LC_ALL=C zfs list -H -t snapshot -o name', shell=True)
if line.startswith('{}/{}@'.format(server_settings['zfs-base'], NODE)): if line.startswith('{}/{}@'.format(server_settings['zfs-base'], NODE)):
_, snapname = line.split('@', 1) _, snapname = line.split('@', 1)
ts, bucket = snapname.split('-', 1)
if not ts.isdigit(): if 'zfs-auto-snap' in snapname:
# migration from auto-snapshots, ignore
continue continue
ts, bucket = snapname.split('-', 1)
buckets.setdefault(bucket, set()).add(int(ts)) buckets.setdefault(bucket, set()).add(int(ts))
syslog(f'classified {line} as {bucket} from {ts}') syslog(f'classified {line} as {bucket} from {ts}')

View file

@ -18,9 +18,6 @@ files = {
'/usr/local/share/icinga/plugins/check_backup_for_node': { '/usr/local/share/icinga/plugins/check_backup_for_node': {
'mode': '0755', 'mode': '0755',
}, },
'/usr/local/share/icinga/plugins/check_backup_for_node-cron': {
'mode': '0755',
},
} }
directories['/etc/backup-server/clients'] = { directories['/etc/backup-server/clients'] = {

View file

@ -1,5 +1,3 @@
from bundlewrap.exceptions import BundleError
defaults = { defaults = {
'backup-server': { 'backup-server': {
'my_ssh_port': 22, 'my_ssh_port': 22,
@ -10,14 +8,6 @@ defaults = {
'c-*', 'c-*',
}, },
}, },
'systemd-timers': {
'timers': {
'check_backup_for_node-cron': {
'command': '/usr/local/share/icinga/plugins/check_backup_for_node-cron',
'when': '*-*-* *:00/5:00', # every five minutes
}
},
},
'zfs': { 'zfs': {
# The whole point of doing backups is to keep them for a long # The whole point of doing backups is to keep them for a long
# time, which eliminates the need for this check. # time, which eliminates the need for this check.
@ -79,15 +69,10 @@ def zfs_pool(metadata):
return {} return {}
crypt_devices = {} crypt_devices = {}
pool_devices = set()
unlock_actions = set() unlock_actions = set()
devices = metadata.get('backup-server/encrypted-devices') for number, (device, passphrase) in enumerate(sorted(metadata.get('backup-server/encrypted-devices', {}).items())):
# TODO remove this once we have migrated all systems
if isinstance(devices, dict):
pool_devices = set()
for number, (device, passphrase) in enumerate(sorted(devices.items())):
crypt_devices[device] = { crypt_devices[device] = {
'dm-name': f'backup{number}', 'dm-name': f'backup{number}',
'passphrase': passphrase, 'passphrase': passphrase,
@ -95,35 +80,14 @@ def zfs_pool(metadata):
pool_devices.add(f'/dev/mapper/backup{number}') pool_devices.add(f'/dev/mapper/backup{number}')
unlock_actions.add(f'action:dm-crypt_open_backup{number}') unlock_actions.add(f'action:dm-crypt_open_backup{number}')
pool_config = [{ pool_opts = {
'devices': pool_devices, 'devices': pool_devices,
}] }
if len(pool_devices) > 2: if len(pool_devices) > 2:
pool_config[0]['type'] = 'raidz' pool_opts['type'] = 'raidz'
elif len(pool_devices) > 1: elif len(pool_devices) > 1:
pool_config[0]['type'] = 'mirror' pool_opts['type'] = 'mirror'
elif isinstance(devices, list):
pool_config = []
for idx, intended_pool in enumerate(devices):
pool_devices = set()
for number, (device, passphrase) in enumerate(sorted(intended_pool.items())):
crypt_devices[device] = {
'dm-name': f'backup{idx}-{number}',
'passphrase': passphrase,
}
pool_devices.add(f'/dev/mapper/backup{idx}-{number}')
unlock_actions.add(f'action:dm-crypt_open_backup{idx}-{number}')
pool_config.append({
'devices': pool_devices,
'type': 'raidz',
})
else:
raise BundleError(f'{node.name}: unsupported configuration for backup-server/encrypted-devices')
return { return {
'backup-server': { 'backup-server': {
@ -136,8 +100,9 @@ def zfs_pool(metadata):
'pools': { 'pools': {
'backups': { 'backups': {
'when_creating': { 'when_creating': {
'config': pool_config, 'config': [
**metadata.get('backup-server/zpool_create_options', {}), pool_opts,
],
}, },
'needs': unlock_actions, 'needs': unlock_actions,
# That's a bit hacky. We do it this way to auto-import # That's a bit hacky. We do it this way to auto-import
@ -191,11 +156,11 @@ def monitoring(metadata):
continue continue
services[f'BACKUPS FOR NODE {client}'] = { services[f'BACKUPS FOR NODE {client}'] = {
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_backup_for_node {} {}'.format( 'command_on_monitored_host': 'sudo /usr/local/share/icinga/plugins/check_backup_for_node {} {}'.format(
client, client,
config['one_backup_every_hours'], config['one_backup_every_hours'],
), ),
'vars.sshmon_timeout': 40, 'vars.sshmon_timeout': 20,
} }
return { return {

View file

@ -24,21 +24,11 @@ files = {
'before': { 'before': {
'action:', 'action:',
'pkg_apt:', 'pkg_apt:',
'pkg_pacman:',
}, },
}, },
} }
if node.has_any_bundle([
'dovecot',
'nginx',
'postfix',
]):
actions['generate-dhparam'] = {
'command': 'openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048',
'unless': 'test -f /etc/ssl/certs/dhparam.pem',
}
locale_needs = set() locale_needs = set()
for locale in sorted(node.metadata.get('locale/installed')): for locale in sorted(node.metadata.get('locale/installed')):
actions[f'ensure_locale_{locale}_is_enabled'] = { actions[f'ensure_locale_{locale}_is_enabled'] = {
@ -51,9 +41,11 @@ for locale in sorted(node.metadata.get('locale/installed')):
} }
locale_needs = {f'action:ensure_locale_{locale}_is_enabled'} locale_needs = {f'action:ensure_locale_{locale}_is_enabled'}
actions['locale-gen'] = { actions = {
'locale-gen': {
'triggered': True, 'triggered': True,
'command': 'locale-gen', 'command': 'locale-gen',
},
} }
description = [] description = []

View file

@ -1,5 +1,10 @@
if node.os == 'arch':
filename = '/etc/bird.conf'
else:
filename = '/etc/bird/bird.conf'
files = { files = {
'/etc/bird/bird.conf': { filename: {
'content_type': 'mako', 'content_type': 'mako',
'triggers': { 'triggers': {
'svc_systemd:bird:reload', 'svc_systemd:bird:reload',
@ -10,7 +15,7 @@ files = {
svc_systemd = { svc_systemd = {
'bird': { 'bird': {
'needs': { 'needs': {
f'file:/etc/bird/bird.conf', f'file:{filename}',
}, },
}, },
} }

View file

@ -13,9 +13,18 @@ defaults = {
}, },
}, },
}, },
'pacman': {
'packages': {
'bird': {
'needed_by': {
'svc_systemd:bird',
},
},
},
},
'sysctl': { 'sysctl': {
'options': { 'options': {
'net.ipv4.conf.all.forwarding': '1', 'net.ipv4.ip_forward': '1',
'net.ipv6.conf.all.forwarding': '1', 'net.ipv6.conf.all.forwarding': '1',
}, },
}, },

View file

@ -7,6 +7,9 @@ supported_os = {
12: 'bookworm', 12: 'bookworm',
99: 'unstable', 99: 'unstable',
}, },
'raspbian': {
10: 'buster',
},
} }
try: try:
@ -79,10 +82,6 @@ actions = {
'triggered': True, 'triggered': True,
'cascade_skip': False, 'cascade_skip': False,
}, },
'apt_execute_update_commands': {
'command': ' && '.join(sorted(node.metadata.get('apt/additional_update_commands', {'true'}))),
'triggered': True,
},
} }
directories = { directories = {

View file

@ -1,3 +1,10 @@
if node.os == 'arch':
service_name = 'cronie'
package_name = 'pkg_pacman:cronie'
else:
service_name = 'cron'
package_name = 'pkg_apt:cron'
files = { files = {
'/etc/crontab': { '/etc/crontab': {
'content_type': 'mako', 'content_type': 'mako',
@ -17,9 +24,9 @@ directories = {
} }
svc_systemd = { svc_systemd = {
'cron': { service_name: {
'needs': { 'needs': {
'pkg_apt:cron', package_name,
}, },
}, },
} }

View file

@ -4,4 +4,9 @@ defaults = {
'cron': {}, 'cron': {},
}, },
}, },
'pacman': {
'packages': {
'cronie': {},
},
},
} }

View file

@ -1,45 +0,0 @@
#!/usr/bin/env python3
from json import loads
from subprocess import check_output
from sys import argv
try:
container_name = argv[1]
docker_ps = check_output([
'docker',
'container',
'ls',
'--all',
'--format',
'json',
'--filter',
f'name={container_name}'
])
docker_json = loads(f"[{','.join([l for l in docker_ps.decode().splitlines() if l])}]")
containers = [
container
for container in docker_json
if container['Names'] == container_name
]
if not containers:
print(f'CRITICAL: container {container_name} not found!')
exit(2)
if len(containers) > 1:
print(f'Found more than one container matching {container_name}!')
print(docker_ps)
exit(3)
if containers[0]['State'] != 'running':
print(f'WARNING: container {container_name} not "running"')
exit(2)
print(f"OK: {containers[0]['Status']}")
except Exception as e:
print(repr(e))
exit(2)

View file

@ -1,67 +0,0 @@
#!/bin/bash
[[ -n "$DEBUG" ]] && set -x
ACTION="$1"
set -euo pipefail
if [[ -z "$ACTION" ]]
then
echo "Usage: $0 start|stop"
exit 1
fi
PUID="$(id -u "${user}")"
PGID="$(id -g "${user}")"
if [ "$ACTION" == "start" ]
then
# just exit if the container is actually running already.
set +e
/usr/local/share/icinga/plugins/check_docker_container "${name}" && exit 0
set -e
docker rm "${name}" || true
docker pull "${image}"
docker run -d \
--name "${name}" \
--env "PUID=$PUID" \
--env "PGID=$PGID" \
--env "TZ=${timezone}" \
% for k, v in sorted(environment.items()):
--env "${k}=${v}" \
% endfor
--network aaarghhh \
% for host_port, container_port in sorted(ports.items()):
--publish "127.0.0.1:${host_port}:${container_port}" \
% endfor
% for host_path, container_path in sorted(volumes.items()):
% if host_path.startswith('/'):
--volume "${host_path}:${container_path}" \
% else:
--volume "/var/opt/docker-engine/${name}/${host_path}:${container_path}" \
% endif
% endfor
--restart unless-stopped \
% if command:
"${image}" \
"${command}"
% else:
"${image}"
% endif
elif [ "$ACTION" == "stop" ]
then
docker stop "${name}"
else
echo "Unknown action $ACTION"
exit 1
fi
% if node.has_bundle('nftables'):
systemctl reload nftables
% endif

View file

@ -1,14 +0,0 @@
[Unit]
Description=docker-engine app ${name}
After=network.target
Requires=${' '.join(sorted(requires))}
[Service]
WorkingDirectory=/var/opt/docker-engine/${name}/
ExecStart=/opt/docker-engine/${name} start
ExecStop=/opt/docker-engine/${name} stop
Type=simple
RemainAfterExit=true
[Install]
WantedBy=multi-user.target

View file

@ -1,126 +0,0 @@
from bundlewrap.metadata import metadata_to_json
deps = {
'pkg_apt:docker-ce',
'pkg_apt:docker-ce-cli',
}
directories['/opt/docker-engine'] = {
'purge': True,
}
directories['/var/opt/docker-engine'] = {}
files['/etc/docker/daemon.json'] = {
'content': metadata_to_json(node.metadata.get('docker-engine/config')),
'triggers': {
'svc_systemd:docker:restart',
},
# install config before installing packages to ensure the config is
# applied to the first start as well
'before': deps,
}
svc_systemd['docker'] = {
'needs': deps,
}
files['/usr/local/share/icinga/plugins/check_docker_container'] = {
'mode': '0755',
}
actions['docker_create_nondefault_network'] = {
# <https://docs.docker.com/engine/network/#dns-services>
# By default, containers inherit the DNS settings as defined in the
# /etc/resolv.conf configuration file. Containers that attach to the
# default bridge network receive a copy of this file. Containers that
# attach to a custom network use Docker's embedded DNS server. The embedded
# DNS server forwards external DNS lookups to the DNS servers configured on
# the host.
'command': 'docker network create aaarghhh',
'unless': 'docker network ls | grep -q -F aaarghhh',
'needs': {
'svc_systemd:docker',
},
}
for app, config in node.metadata.get('docker-engine/containers', {}).items():
volumes = config.get('volumes', {})
user = config.get('user', f'docker-{app}')
directories[f'/var/opt/docker-engine/{app}'] = {
'owner': user,
'group': user,
}
files[f'/opt/docker-engine/{app}'] = {
'source': 'docker-wrapper',
'content_type': 'mako',
'context': {
'command': config.get('command'),
'environment': config.get('environment', {}),
'image': config['image'],
'name': app,
'ports': config.get('ports', {}),
'timezone': node.metadata.get('timezone'),
'user': user,
'volumes': volumes,
},
'mode': '0755',
'triggers': {
f'svc_systemd:docker-{app}:restart',
},
}
users[user] = {
'groups': {
'docker',
},
'after': {
'action:docker_create_nondefault_network',
'svc_systemd:docker',
},
}
if user == f'docker-{app}':
users[user]['home'] = f'/var/opt/docker-engine/{app}'
files[f'/usr/local/lib/systemd/system/docker-{app}.service'] = {
'source': 'docker-wrapper.service',
'content_type': 'mako',
'context': {
'name': app,
'requires': {
*set(config.get('requires', set())),
'docker.service',
}
},
'triggers': {
'action:systemd-reload',
f'svc_systemd:docker-{app}:restart',
},
}
svc_systemd[f'docker-{app}'] = {
'needs': {
*deps,
f'directory:/var/opt/docker-engine/{app}',
f'file:/opt/docker-engine/{app}',
f'file:/usr/local/lib/systemd/system/docker-{app}.service',
f'user:{user}',
'svc_systemd:docker',
*set(config.get('needs', set())),
},
}
for volume in volumes:
if not volume.startswith('/'):
volume = f'/var/opt/docker-engine/{app}/{volume}'
directories[volume] = {
'owner': user,
'group': user,
'needed_by': {
f'svc_systemd:docker-{app}',
},
# don't do anything if the directory exists, docker images
# mangle owners
'unless': f'test -d {volume}',
}

View file

@ -1,92 +0,0 @@
defaults = {
'apt': {
'packages': {
'docker-ce': {},
'docker-ce-cli': {},
'docker-compose-plugin': {},
},
'repos': {
'docker': {
'items': {
'deb https://download.docker.com/linux/debian {os_release} stable',
},
},
},
},
'nftables': {
'forward': {
'docker-engine': [
'ct state { related, established } accept',
'ip saddr 172.16.0.0/12 accept',
],
},
'postrouting': {
'docker-engine': [
'ip saddr 172.16.0.0/12 masquerade',
],
},
},
'docker-engine': {
'config': {
'iptables': False,
'no-new-privileges': True,
},
},
'zfs': {
'datasets': {
'tank/docker-data': {},
},
},
}
@metadata_reactor.provides(
'icinga2_api/docker-engine/services',
)
def monitoring(metadata):
services = {
'DOCKER PROCESS': {
'command_on_monitored_host': '/usr/lib/nagios/plugins/check_procs -C dockerd -c 1:',
},
}
for app in metadata.get('docker-engine/containers', {}):
services[f'DOCKER CONTAINER {app}'] = {
'command_on_monitored_host': f'sudo /usr/local/share/icinga/plugins/check_docker_container {app}'
}
return {
'icinga2_api': {
'docker-engine': {
'services': services,
},
},
}
@metadata_reactor.provides(
'backups/paths',
'zfs/datasets',
)
def zfs(metadata):
datasets = {}
for app in metadata.get('docker-engine/containers', {}):
datasets[f'tank/docker-data/{app}'] = {
'mountpoint': f'/var/opt/docker-engine/{app}',
'needed_by': {
f'directory:/var/opt/docker-engine/{app}',
},
}
return {
'backups': {
'paths': {
v['mountpoint']
for v in datasets.values()
},
},
'zfs': {
'datasets': datasets,
},
}

View file

@ -1,89 +0,0 @@
assert node.has_bundle('docker-engine')
defaults = {
'docker-engine': {
'containers': {
'goauthentik-server': {
'image': 'ghcr.io/goauthentik/server:latest',
'command': 'server',
'environment': {
'AUTHENTIK_POSTGRESQL__HOST': 'goauthentik-postgresql',
'AUTHENTIK_POSTGRESQL__NAME': 'goauthentik',
'AUTHENTIK_POSTGRESQL__PASSWORD': repo.vault.password_for(f'{node.name} postgresql goauthentik'),
'AUTHENTIK_POSTGRESQL__USER': 'goauthentik',
'AUTHENTIK_REDIS__HOST': 'goauthentik-redis',
'AUTHENTIK_SECRET_KEY': repo.vault.password_for(f'{node.name} goauthentik secret key'),
},
'volumes': {
'media': '/media',
'templates': '/templates',
},
'ports': {
'9000': '9000',
'9443': '9443',
},
'needs': {
'svc_systemd:docker-goauthentik-postgresql',
'svc_systemd:docker-goauthentik-redis',
},
'requires': {
'docker-goauthentik-postgresql.service',
'docker-goauthentik-redis.service',
},
},
'goauthentik-worker': {
'image': 'ghcr.io/goauthentik/server:latest',
'command': 'worker',
'user': 'docker-goauthentik-server',
'environment': {
'AUTHENTIK_POSTGRESQL__HOST': 'goauthentik-postgresql',
'AUTHENTIK_POSTGRESQL__NAME': 'goauthentik',
'AUTHENTIK_POSTGRESQL__PASSWORD': repo.vault.password_for(f'{node.name} postgresql goauthentik'),
'AUTHENTIK_POSTGRESQL__USER': 'goauthentik',
'AUTHENTIK_REDIS__HOST': 'goauthentik-redis',
'AUTHENTIK_SECRET_KEY': repo.vault.password_for(f'{node.name} goauthentik secret key'),
},
'volumes': {
'/var/opt/docker-engine/goauthentik-server/media': '/media',
'/var/opt/docker-engine/goauthentik-server/certs': '/certs',
'/var/opt/docker-engine/goauthentik-server/templates': '/templates',
},
'needs': {
'svc_systemd:docker-goauthentik-postgresql',
'svc_systemd:docker-goauthentik-redis',
},
'requires': {
'docker-goauthentik-postgresql.service',
'docker-goauthentik-redis.service',
},
},
'goauthentik-postgresql': {
'image': 'docker.io/library/postgres:16-alpine',
'environment': {
'POSTGRES_PASSWORD': repo.vault.password_for(f'{node.name} postgresql goauthentik'),
'POSTGRES_USER': 'goauthentik',
'POSTGRES_DB': 'goauthentik',
},
'volumes': {
'database': '/var/lib/postgresql/data',
},
},
'goauthentik-redis': {
'image': 'docker.io/library/redis:alpine',
},
},
},
'nginx': {
'vhosts': {
'goauthentik': {
'locations': {
'/': {
'target': 'http://127.0.0.1:9000/',
'websockets': True,
'max_body_size': '5000m',
},
},
},
},
},
}

View file

@ -1,80 +0,0 @@
#!/usr/bin/env python3
from json import loads
from os import environ
from subprocess import check_output
from sys import exit
import psycopg2
PSQL_HOST = environ['DB_HOSTNAME']
PSQL_USER = environ['DB_USERNAME']
PSQL_PASS = environ['DB_PASSWORD']
PSQL_DB = environ['DB_DATABASE_NAME']
docker_networks = loads(check_output(['docker', 'network', 'inspect', 'aaarghhh']))
container_ip = None
# why the fuck is this a list of networks, even though we have to provide
# a network name to inspect ...
for network in docker_networks:
if network['Name'] != 'aaarghhh':
continue
for _, container in network['Containers'].items():
if container['Name'] == PSQL_HOST:
container_ip = container['IPv4Address'].split('/')[0]
if not container_ip:
print(f'could not find ip address for container {PSQL_HOST=} in json')
print(docker_networks)
exit(1)
print(f'{PSQL_HOST=} {container_ip=}')
conn = psycopg2.connect(
dbname=PSQL_DB,
host=container_ip,
password=PSQL_PASS,
user=PSQL_USER,
)
with conn:
with conn.cursor() as cur:
cur.execute('SELECT "id","ownerId","albumName" FROM albums;')
albums = {
i[0]: {
'owner': i[1],
'name': i[2],
}
for i in cur.fetchall()
}
with conn.cursor() as cur:
cur.execute('SELECT "id","name" FROM users;')
users = {
i[0]: i[1]
for i in cur.fetchall()
}
for album_id, album in albums.items():
print(f'----- working on album: {album["name"]}')
with conn:
with conn.cursor() as cur:
cur.execute('SELECT "usersId" FROM albums_shared_users_users WHERE "albumsId" = %s;', (album_id,))
album_shares = [i[0] for i in cur.fetchall()]
print(f' album is shared with {len(album_shares)} users: {album_shares}')
for user_id, user_name in users.items():
if user_id == album['owner'] or user_id in album_shares:
continue
print(f' sharing album with user {user_name} ... ', end='')
with conn.cursor() as cur:
cur.execute(
'INSERT INTO albums_shared_users_users ("albumsId","usersId","role") VALUES (%s, %s, %s);',
(album_id, user_id, 'viewer'),
)
print('done')
print()
conn.close()

View file

@ -1,3 +0,0 @@
files['/usr/local/bin/immich-auto-album-share.py'] = {
'mode': '0755',
}

View file

@ -1,92 +0,0 @@
assert node.has_bundle('docker-engine')
defaults = {
'apt': {
'packages': {
'python3-psycopg2': {},
},
},
'docker-engine': {
'containers': {
'immich': {
'image': 'ghcr.io/imagegenius/immich:latest',
'environment': {
'DB_DATABASE_NAME': 'immich',
'DB_HOSTNAME': 'immich-postgresql',
'DB_PASSWORD': repo.vault.password_for(f'{node.name} postgresql immich'),
'DB_USERNAME': 'immich',
'REDIS_HOSTNAME': 'immich-redis',
},
'volumes': {
'config': '/config',
'libraries': '/libraries',
'photos': '/photos',
},
'ports': {
'8080': '8080',
},
'needs': {
'svc_systemd:docker-immich-postgresql',
'svc_systemd:docker-immich-redis',
},
'requires': {
'docker-immich-postgresql.service',
'docker-immich-redis.service',
},
},
'immich-postgresql': {
'image': 'tensorchord/pgvecto-rs:pg14-v0.2.0',
'environment': {
'POSTGRES_PASSWORD': repo.vault.password_for(f'{node.name} postgresql immich'),
'POSTGRES_USER': 'immich',
'POSTGRES_DB': 'immich',
},
'volumes': {
'database': '/var/lib/postgresql/data',
},
},
'immich-redis': {
'image': 'docker.io/redis:6.2-alpine',
},
},
},
'docker-immich': {
'enable_auto_album_share': False,
},
'nginx': {
'vhosts': {
'immich': {
'locations': {
'/': {
'target': 'http://127.0.0.1:8080/',
'websockets': True,
'max_body_size': '5000m',
},
},
},
},
},
}
@metadata_reactor.provides(
'systemd-timers/timers/immich-auto-album-share',
)
def auto_album_share(metadata):
if not metadata.get('docker-immich/enable_auto_album_share'):
return {}
return {
'systemd-timers': {
'timers': {
'immich-auto-album-share': {
'command': '/usr/local/bin/immich-auto-album-share.py',
'environment': metadata.get('docker-engine/containers/immich/environment'),
'when': 'minutely',
'requisite': {
'docker-immich-postgresql.service',
},
},
},
},
}

View file

@ -3,4 +3,3 @@ driver = pgsql
default_pass_scheme = MD5-CRYPT default_pass_scheme = MD5-CRYPT
password_query = SELECT username as user, password FROM mailbox WHERE username = '%u' AND active = true password_query = SELECT username as user, password FROM mailbox WHERE username = '%u' AND active = true
user_query = SELECT '/var/mail/vmail/' || maildir as home, 65534 as uid, 65534 as gid FROM mailbox WHERE username = '%u' AND active = true user_query = SELECT '/var/mail/vmail/' || maildir as home, 65534 as uid, 65534 as gid FROM mailbox WHERE username = '%u' AND active = true
iterate_query = SELECT username as user FROM mailbox WHERE active = true

View file

@ -28,19 +28,19 @@ namespace inbox {
mail_location = maildir:/var/mail/vmail/%d/%n mail_location = maildir:/var/mail/vmail/%d/%n
protocols = imap lmtp sieve protocols = imap lmtp sieve
ssl = required ssl = yes
ssl_cert = </var/lib/dehydrated/certs/${node.metadata.get('postfix/myhostname')}/fullchain.pem ssl_cert = </var/lib/dehydrated/certs/${node.metadata.get('postfix/myhostname', node.metadata['hostname'])}/fullchain.pem
ssl_key = </var/lib/dehydrated/certs/${node.metadata.get('postfix/myhostname')}/privkey.pem ssl_key = </var/lib/dehydrated/certs/${node.metadata.get('postfix/myhostname', node.metadata['hostname'])}/privkey.pem
ssl_dh = </etc/ssl/certs/dhparam.pem ssl_dh = </etc/dovecot/ssl/dhparam.pem
ssl_min_protocol = TLSv1.2 ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 ssl_cipher_list = EECDH+AESGCM:EDH+AESGCM
ssl_prefer_server_ciphers = no ssl_prefer_server_ciphers = yes
login_greeting = IMAPd ready login_greeting = IMAPd ready
auth_mechanisms = plain login auth_mechanisms = plain login
first_valid_uid = 65534 first_valid_uid = 65534
disable_plaintext_auth = yes disable_plaintext_auth = yes
mail_plugins = $mail_plugins zlib old_stats fts fts_xapian mail_plugins = $mail_plugins zlib old_stats
plugin { plugin {
zlib_save_level = 6 zlib_save_level = 6
@ -56,15 +56,6 @@ plugin {
old_stats_refresh = 30 secs old_stats_refresh = 30 secs
old_stats_track_cmds = yes old_stats_track_cmds = yes
fts = xapian
fts_xapian = partial=3 full=20
fts_autoindex = yes
fts_enforced = yes
# Index attachements
fts_decoder = decode2text
% if node.has_bundle('rspamd'): % if node.has_bundle('rspamd'):
sieve_before = /var/mail/vmail/sieve/global/spam-global.sieve sieve_before = /var/mail/vmail/sieve/global/spam-global.sieve
@ -95,19 +86,14 @@ service auth {
} }
} }
service decode2text { service lmtp {
executable = script /usr/lib/dovecot/decode2text.sh unix_listener /var/spool/postfix/private/dovecot-lmtp {
user = dovecot group = postfix
unix_listener decode2text { mode = 0600
mode = 0666 user = postfix
} }
} }
service indexer-worker {
vsz_limit = 0
process_limit = 0
}
service imap { service imap {
executable = imap executable = imap
} }
@ -118,14 +104,6 @@ service imap-login {
vsz_limit = 64M vsz_limit = 64M
} }
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
group = postfix
mode = 0600
user = postfix
}
}
service managesieve-login { service managesieve-login {
inet_listener sieve { inet_listener sieve {
port = 4190 port = 4190

View file

@ -2,6 +2,10 @@
# by this bundle # by this bundle
repo.libs.tools.require_bundle(node, 'postfix') repo.libs.tools.require_bundle(node, 'postfix')
directories = {
'/etc/dovecot/ssl': {},
}
files = { files = {
'/etc/dovecot/dovecot.conf': { '/etc/dovecot/dovecot.conf': {
'content_type': 'mako', 'content_type': 'mako',
@ -45,17 +49,25 @@ files = {
}, },
} }
symlinks['/usr/lib/dovecot/decode2text.sh'] = { actions = {
'target': '/usr/share/doc/dovecot-core/examples/decode2text.sh', 'dovecot_generate_dhparam': {
'before': { 'command': 'openssl dhparam -out /etc/dovecot/ssl/dhparam.pem 2048',
'svc_systemd:dovecot', 'unless': 'test -f /etc/dovecot/ssl/dhparam.pem',
'cascade_skip': False,
'needs': {
'directory:/etc/dovecot/ssl',
'pkg_apt:'
},
'triggers': {
'svc_systemd:dovecot:restart',
},
}, },
} }
svc_systemd = { svc_systemd = {
'dovecot': { 'dovecot': {
'needs': { 'needs': {
'action:generate-dhparam', 'action:dovecot_generate_dhparam',
'file:/etc/dovecot/dovecot.conf', 'file:/etc/dovecot/dovecot.conf',
'file:/etc/dovecot/dovecot-sql.conf', 'file:/etc/dovecot/dovecot-sql.conf',
}, },

View file

@ -3,7 +3,6 @@ from bundlewrap.metadata import atomic
defaults = { defaults = {
'apt': { 'apt': {
'packages': { 'packages': {
'dovecot-fts-xapian': {},
'dovecot-imapd': {}, 'dovecot-imapd': {},
'dovecot-lmtpd': {}, 'dovecot-lmtpd': {},
'dovecot-managesieved': {}, 'dovecot-managesieved': {},
@ -36,16 +35,6 @@ defaults = {
'dovecot', 'dovecot',
}, },
}, },
'systemd-timers': {
'timers': {
'dovecot_fts_optimize': {
'command': [
'/usr/bin/doveadm fts optimize -A',
],
'when': '02:{}:00'.format(node.magic_number % 60),
},
},
},
} }
if node.has_bundle('postfixadmin'): if node.has_bundle('postfixadmin'):

View file

@ -33,7 +33,7 @@ actions = {
'yarn build', 'yarn build',
]), ]),
'needs': { 'needs': {
'action:apt_execute_update_commands', 'action:nodejs_install_yarn',
'pkg_apt:nodejs', 'pkg_apt:nodejs',
}, },
'triggered': True, 'triggered': True,

View file

@ -11,26 +11,6 @@ defaults = {
}, },
} }
@metadata_reactor.provides(
'nodejs/version',
)
def nodejs(metadata):
version = tuple([int(i) for i in metadata.get('element-web/version')[1:].split('.')])
if version >= (1, 11, 71):
return {
'nodejs': {
'version': 22,
},
}
else:
return {
'nodejs': {
'version': 18,
},
}
@metadata_reactor.provides( @metadata_reactor.provides(
'nginx/vhosts/element-web', 'nginx/vhosts/element-web',
) )

View file

@ -100,7 +100,7 @@ def nginx(metadata):
}, },
}, },
'website_check_path': '/user/login', 'website_check_path': '/user/login',
'website_check_string': 'Sign in', 'website_check_string': 'Sign In',
}, },
}, },
}, },

View file

@ -43,7 +43,6 @@ def nginx(metadata):
'locations': { 'locations': {
'/': { '/': {
'target': 'http://127.0.0.1:21010', 'target': 'http://127.0.0.1:21010',
'websockets': True,
}, },
'/api/ds/query': { '/api/ds/query': {
'target': 'http://127.0.0.1:21010', 'target': 'http://127.0.0.1:21010',

View file

@ -72,6 +72,7 @@ actions = {
'yarn build', 'yarn build',
]), ]),
'needs': { 'needs': {
'action:nodejs_install_yarn',
'file:/opt/hedgedoc/config.json', 'file:/opt/hedgedoc/config.json',
'git_deploy:/opt/hedgedoc', 'git_deploy:/opt/hedgedoc',
'pkg_apt:nodejs', 'pkg_apt:nodejs',

View file

@ -2,42 +2,48 @@
from sys import exit from sys import exit
from packaging.version import parse import requests
from requests import get from packaging import version
API_TOKEN = "${token}" bearer = "${bearer}"
DOMAIN = "${domain}" domain = "${domain}"
OK = 0
WARN = 1
CRITICAL = 2
UNKNOWN = 3
status = 3
message = "Unknown Update Status"
domain = "hass.home.kunbox.net"
s = requests.Session()
s.headers.update({"Content-Type": "application/json"})
try: try:
r = get("https://version.home-assistant.io/stable.json") stable_version = version.parse(
r.raise_for_status() s.get("https://version.home-assistant.io/stable.json").json()["homeassistant"][
stable_version = parse(r.json()["homeassistant"]["generic-x86-64"]) "generic-x86-64"
except Exception as e: ]
print(f"Could not get stable version information from home-assistant.io: {e!r}")
exit(3)
try:
r = get(
f"https://{DOMAIN}/api/config",
headers={"Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json"},
) )
r.raise_for_status() s.headers.update(
running_version = parse(r.json()["version"]) {"Authorization": f"Bearer {bearer}", "Content-Type": "application/json"}
except Exception as e:
print(f"Could not get running version information from homeassistant: {e!r}")
exit(3)
try:
if stable_version > running_version:
print(
f"There is a newer version available: {stable_version} (currently installed: {running_version})"
) )
exit(2) running_version = version.parse(
s.get(f"https://{domain}/api/config").json()["version"]
)
if running_version == stable_version:
status = 0
message = f"OK - running version {running_version} equals stable version {stable_version}"
elif running_version > stable_version:
status = 1
message = f"WARNING - stable version {stable_version} is lower than running version {running_version}, check if downgrade is necessary."
else: else:
print( status = 2
f"Currently running version {running_version} matches newest release on home-assistant.io" message = f"CRITICAL - update necessary, running version {running_version} is lower than stable version {stable_version}"
)
exit(0)
except Exception as e: except Exception as e:
print(repr(e)) message = f"{message}: {repr(e)}"
exit(3)
print(message)
exit(status)

View file

@ -5,8 +5,6 @@ After=network-online.target
[Service] [Service]
Type=simple Type=simple
User=homeassistant User=homeassistant
Environment="VIRTUAL_ENV=/opt/homeassistant/venv"
Environment="PATH=/opt/homeassistant/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
WorkingDirectory=/var/opt/homeassistant WorkingDirectory=/var/opt/homeassistant
ExecStart=/opt/homeassistant/venv/bin/hass -c "/var/opt/homeassistant" ExecStart=/opt/homeassistant/venv/bin/hass -c "/var/opt/homeassistant"
RestartForceExitStatus=100 RestartForceExitStatus=100

View file

@ -1,13 +1,6 @@
if node.has_bundle('pyenv'):
python_version = sorted(node.metadata.get('pyenv/python_versions'))[-1]
python_path = f'/opt/pyenv/versions/{python_version}/bin/python'
else:
python_path = '/usr/bin/python3'
users = { users = {
'homeassistant': { 'homeassistant': {
'home': '/var/opt/homeassistant', 'home': '/var/opt/homeassistant',
"groups": ["dialout"],
}, },
} }
@ -30,7 +23,7 @@ files = {
'/usr/local/share/icinga/plugins/check_homeassistant_update': { '/usr/local/share/icinga/plugins/check_homeassistant_update': {
'content_type': 'mako', 'content_type': 'mako',
'context': { 'context': {
'token': node.metadata.get('homeassistant/api_secret'), 'bearer': repo.vault.decrypt(node.metadata.get('homeassistant/api_secret')),
'domain': node.metadata.get('homeassistant/domain'), 'domain': node.metadata.get('homeassistant/domain'),
}, },
'mode': '0755', 'mode': '0755',
@ -39,7 +32,7 @@ files = {
actions = { actions = {
'homeassistant_create_virtualenv': { 'homeassistant_create_virtualenv': {
'command': f'sudo -u homeassistant virtualenv -p {python_path} /opt/homeassistant/venv/', 'command': 'sudo -u homeassistant /usr/bin/python3 -m virtualenv -p python3 /opt/homeassistant/venv/',
'unless': 'test -d /opt/homeassistant/venv/', 'unless': 'test -d /opt/homeassistant/venv/',
'needs': { 'needs': {
'directory:/opt/homeassistant', 'directory:/opt/homeassistant',

View file

@ -1,132 +0,0 @@
#!/usr/bin/env python3
import re
from hashlib import md5
from sys import argv, exit
# Supress SSL certificate warnings for ssl_verify=False
import urllib3
from lxml import html
from requests import Session
USERNAME_FIELD = "g2"
PASSWORD_FIELD = "g3"
CRSF_FIELD = "password"
STATUS_OK = 0
STATUS_WARNING = 1
STATUS_CRITICAL = 2
STATUS_UNKNOWN = 3
class OMMCrawler:
def __init__(self, hostname, username, password):
self.session = Session()
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
self.session.verify = False
self.url = f"https://{hostname}"
self.login_data = {
USERNAME_FIELD: username,
PASSWORD_FIELD: password,
CRSF_FIELD: md5(password.encode()).hexdigest(),
}
self.logged_in = False
def login(self):
# if we have multiple dect masters, find out which one is the current master
current_master_url = self.session.get(self.url, verify=False).url
self.hostname = re.search(r"^(.*[\\\/])", current_master_url).group(0)[:-1]
response = self.session.post(f"{self.url}/login_set.html", data=self.login_data)
response.raise_for_status()
# set cookie
pass_value = re.search(r"(?<=pass=)\d+(?=;)", response.text).group(0)
self.session.cookies.set("pass", pass_value)
self.logged_in = True
def get_station_status(self):
if not self.logged_in:
self.login()
data = {}
response = self.session.get(f"{self.url}/fp_pnp_status.html")
response.raise_for_status()
tree = html.fromstring(response.text)
xpath_results = tree.xpath('//tr[@class="l0" or @class="l1"]')
for result in xpath_results:
bubble_is_in_inactive_cluster = False
bubble_is_connected = False
bubble_is_active = False
bubble_name = result.xpath("td[4]/text()")[0]
try:
bubble_is_connected = result.xpath("td[11]/img/@alt")[0] == "yes"
if bubble_is_connected:
try:
bubble_is_active = result.xpath("td[12]/img/@alt")[0] == "yes"
except IndexError:
# If an IndexError occurs, there is no image in the
# 12th td. This means this bubble is in the not inside
# an active DECT cluster, but is a backup bubble.
# This is probably fine.
bubble_is_active = False
bubble_is_in_inactive_cluster = True
else:
bubble_is_active = False
except:
# There is no Image in the 11th td. This usually means there
# is a warning message in the 10th td. We do not care about
# that, currently.
pass
data[bubble_name] = {
"is_connected": bubble_is_connected,
"is_active": bubble_is_active,
"is_in_inactive_cluster": bubble_is_in_inactive_cluster,
}
return data
def handle_station_data(self):
try:
data = self.get_station_status()
except Exception as e:
print(f"Something went wrong. You should take a look at {self.url}")
print(repr(e))
exit(STATUS_UNKNOWN)
critical = False
for name, status in data.items():
if not status["is_active"] and not status["is_connected"]:
print(
f"Base station {name} is not active or connected! Check manually!"
)
critical = True
elif not status["is_active"] and not status["is_in_inactive_cluster"]:
# Bubble is part of an active DECT cluster, but not active.
# This shouldn't happen.
print(
f"Base station {name} is not active but connected! Check manually!"
)
critical = True
elif not status["is_connected"]:
# This should never happen. Seeing this state means OMM
# itself is broken.
print(
f"Base station {name} is not connected but active! Check manually!"
)
critical = True
if critical:
exit(STATUS_CRITICAL)
else:
print(f"OK - {len(data)} base stations connected")
exit(STATUS_OK)
if __name__ == "__main__":
omm = OMMCrawler(argv[1], argv[2], argv[3])
omm.handle_station_data()

View file

@ -50,13 +50,17 @@ def check_list(ip_list, blocklist, warn_ips):
]).decode().splitlines() ]).decode().splitlines()
for item in result: for item in result:
if item.startswith(';;'): if item.startswith(';;'):
continue msgs.append('{} - {}'.format(
blocklist,
item,
))
else:
msgs.append('{} listed in {} as {}'.format( msgs.append('{} listed in {} as {}'.format(
ip, ip,
blocklist, blocklist,
item, item,
)) ))
if item in warn_ips and returncode < 2: if (item in warn_ips or item.startswith(';;')) and returncode < 2:
returncode = 1 returncode = 1
else: else:
returncode = 2 returncode = 2

View file

@ -23,25 +23,6 @@ object Host "${rnode.name}" {
vars.notification.mail = true vars.notification.mail = true
} }
% if rnode.ipmi_hostname:
object Host "${rnode.name} IPMI" {
import "generic-host"
address = "${rnode.ipmi_hostname}"
vars.location = "${rnode.metadata.get('location', 'unknown')}"
vars.os = "ipmi"
vars.pretty_name = "${rnode.metadata.get('icinga_options/pretty_name', rnode.metadata.get('hostname'))} IPMI"
vars.show_on_statuspage = false
vars.period = "${rnode.metadata.get('icinga_options/period', '24x7')}"
vars.notification.sms = ${str(rnode.metadata.get('icinga_options/vars.notification.sms', True)).lower()}
vars.notification.mail = true
}
% endif
% for depends_on_host in sorted(rnode.metadata.get('icinga_options/also_affected_by', set())): % for depends_on_host in sorted(rnode.metadata.get('icinga_options/also_affected_by', set())):
object Dependency "${rnode.name}_depends_on_${depends_on_host}" { object Dependency "${rnode.name}_depends_on_${depends_on_host}" {
parent_host_name = "${depends_on_host}" parent_host_name = "${depends_on_host}"

View file

@ -9,11 +9,6 @@ app = Flask(__name__)
@app.route('/status') @app.route('/status')
def statuspage(): def statuspage():
everything_fine = True everything_fine = True
try:
check_output(['/usr/local/share/icinga/plugins/check_mounts'])
except:
everything_fine = False
try: try:
check_output(['/usr/lib/nagios/plugins/check_procs', '-C', 'icinga2', '-c', '1:']) check_output(['/usr/lib/nagios/plugins/check_procs', '-C', 'icinga2', '-c', '1:'])
except: except:

View file

@ -3,6 +3,8 @@ Description=Icinga2 Statusmonitor
After=network.target After=network.target
[Service] [Service]
User=nagios
Group=nagios
Environment="FLASK_APP=/etc/icinga2/icinga_statusmonitor.py" Environment="FLASK_APP=/etc/icinga2/icinga_statusmonitor.py"
ExecStart=/usr/bin/python3 -m flask run ExecStart=/usr/bin/python3 -m flask run
WorkingDirectory=/tmp WorkingDirectory=/tmp

View file

@ -113,14 +113,9 @@ def notify_per_ntfy():
else: else:
subject = '[ICINGA] {}'.format(args.host_name) subject = '[ICINGA] {}'.format(args.host_name)
if args.notification_type.lower() == 'recovery':
priority = 'default'
else:
priority = 'urgent'
headers = { headers = {
'Title': subject, 'Title': subject,
'Priority': priority, 'Priority': 'urgent',
} }
try: try:
@ -129,14 +124,11 @@ def notify_per_ntfy():
data=message_text, data=message_text,
headers=headers, headers=headers,
auth=(CONFIG['ntfy']['user'], CONFIG['ntfy']['password']), auth=(CONFIG['ntfy']['user'], CONFIG['ntfy']['password']),
timeout=10,
) )
r.raise_for_status() r.raise_for_status()
except Exception as e: except Exception as e:
log_to_syslog('Sending a Notification failed: {}'.format(repr(e))) log_to_syslog('Sending a Notification failed: {}'.format(repr(e)))
return False
return True
def notify_per_mail(): def notify_per_mail():
@ -202,8 +194,7 @@ if __name__ == '__main__':
notify_per_mail() notify_per_mail()
if args.sms: if args.sms:
ntfy_worked = False if args.service_name:
if CONFIG['ntfy']['user']:
ntfy_worked = notify_per_ntfy()
if not args.service_name or not ntfy_worked:
notify_per_sms() notify_per_sms()
if CONFIG['ntfy']['user']:
notify_per_ntfy()

View file

@ -275,27 +275,6 @@ files = {
'mode': '0660', 'mode': '0660',
'group': 'icingaweb2', 'group': 'icingaweb2',
}, },
# monitoring
'/etc/icinga2/icinga_statusmonitor.py': {
'triggers': {
'svc_systemd:icinga_statusmonitor:restart',
},
},
'/usr/local/lib/systemd/system/icinga_statusmonitor.service': {
'triggers': {
'action:systemd-reload',
'svc_systemd:icinga_statusmonitor:restart',
},
},
}
svc_systemd['icinga_statusmonitor'] = {
'needs': {
'file:/etc/icinga2/icinga_statusmonitor.py',
'file:/usr/local/lib/systemd/system/icinga_statusmonitor.service',
'pkg_apt:python3-flask',
},
} }
actions = { actions = {
@ -337,12 +316,15 @@ for name in files:
for name in symlinks: for name in symlinks:
icinga_run_deps.add(f'symlink:{name}') icinga_run_deps.add(f'symlink:{name}')
svc_systemd['icinga2'] = { svc_systemd = {
'icinga2': {
'needs': icinga_run_deps, 'needs': icinga_run_deps,
},
} }
# The actual hosts and services management starts here # The actual hosts and services management starts here
bundles = set() bundles = set()
downtimes = [] downtimes = []
@ -401,6 +383,22 @@ for rnode in sorted(repo.nodes):
DAYS_TO_STRING[day%7]: f'{hour}:{minute}-{hour}:{minute+15}', DAYS_TO_STRING[day%7]: f'{hour}:{minute}-{hour}:{minute+15}',
}, },
}) })
elif (
rnode.has_bundle('pacman')
and rnode.metadata.get('pacman/unattended-upgrades/is_enabled', False)
):
day = rnode.metadata.get('pacman/unattended-upgrades/day')
hour = rnode.metadata.get('pacman/unattended-upgrades/hour')
minute = rnode.magic_number%30
downtimes.append({
'name': 'unattended-upgrades',
'host': rnode.name,
'comment': f'Downtime for upgrade-and-reboot of node {rnode.name}',
'times': {
DAYS_TO_STRING[day%7]: f'{hour}:{minute}-{hour}:{minute+15}',
},
})
files['/etc/icinga2/conf.d/groups.conf'] = { files['/etc/icinga2/conf.d/groups.conf'] = {
'source': 'icinga2/groups.conf', 'source': 'icinga2/groups.conf',

View file

@ -17,8 +17,8 @@ defaults = {
'icinga2': {}, 'icinga2': {},
'icinga2-ido-pgsql': {}, 'icinga2-ido-pgsql': {},
'icingaweb2': {}, 'icingaweb2': {},
'icingaweb2-module-monitoring': {},
'python3-easysnmp': {}, 'python3-easysnmp': {},
'python3-flask': {},
'snmp': {}, 'snmp': {},
} }
}, },
@ -54,6 +54,7 @@ defaults = {
'setup-token': repo.vault.password_for(f'{node.name} icingaweb2 setup-token'), 'setup-token': repo.vault.password_for(f'{node.name} icingaweb2 setup-token'),
}, },
'php': { 'php': {
'version': '8.2',
'packages': { 'packages': {
'curl', 'curl',
'gd', 'gd',
@ -130,9 +131,6 @@ def nginx(metadata):
'/api/': { '/api/': {
'target': 'https://127.0.0.1:5665/', 'target': 'https://127.0.0.1:5665/',
}, },
'/statusmonitor/': {
'target': 'http://127.0.0.1:5000/',
},
}, },
'extras': True, 'extras': True,
}, },

View file

@ -0,0 +1,4 @@
<%
from tomlkit import dumps as toml_dumps
from bundlewrap.utils.text import toml_clean
%>${toml_clean(toml_dumps(repo.libs.faults.resolve_faults(config), sort_keys=True))}

View file

@ -23,7 +23,7 @@ actions = {
git_deploy = { git_deploy = {
'/opt/infobeamer-cms/src': { '/opt/infobeamer-cms/src': {
'rev': 'master', 'rev': 'master',
'repo': 'https://github.com/voc/infobeamer-cms.git', 'repo': 'https://github.com/sophieschi/36c3-cms.git',
'needs': { 'needs': {
'directory:/opt/infobeamer-cms/src', 'directory:/opt/infobeamer-cms/src',
}, },
@ -68,7 +68,10 @@ for room, device_id in sorted(node.metadata.get('infobeamer-cms/rooms', {}).item
files = { files = {
'/opt/infobeamer-cms/settings.toml': { '/opt/infobeamer-cms/settings.toml': {
'content': repo.libs.faults.dict_as_toml(config), 'content_type': 'mako',
'context': {
'config': config,
},
'triggers': { 'triggers': {
'svc_systemd:infobeamer-cms:restart', 'svc_systemd:infobeamer-cms:restart',
}, },
@ -96,6 +99,14 @@ files = {
}, },
} }
pkg_pip = {
'github-flask': {
'needed_by': {
'svc_systemd:infobeamer-cms',
},
},
}
svc_systemd = { svc_systemd = {
'infobeamer-cms': { 'infobeamer-cms': {
'needs': { 'needs': {

View file

@ -1,13 +1,10 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
assert node.has_bundle('redis')
defaults = { defaults = {
'infobeamer-cms': { 'infobeamer-cms': {
'config': { 'config': {
'MAX_UPLOADS': 5, 'MAX_UPLOADS': 5,
'PREFERRED_URL_SCHEME': 'https', 'PREFERRED_URL_SCHEME': 'https',
'REDIS_HOST': '127.0.0.1',
'SESSION_COOKIE_NAME': '__Host-sess', 'SESSION_COOKIE_NAME': '__Host-sess',
'STATIC_PATH': '/opt/infobeamer-cms/static', 'STATIC_PATH': '/opt/infobeamer-cms/static',
'URL_KEY': repo.vault.password_for(f'{node.name} infobeamer-cms url key'), 'URL_KEY': repo.vault.password_for(f'{node.name} infobeamer-cms url key'),
@ -52,7 +49,7 @@ def nginx(metadata):
'infobeamer-cms/config/TIME_MIN', 'infobeamer-cms/config/TIME_MIN',
) )
def event_times(metadata): def event_times(metadata):
event_start = datetime.strptime(metadata.get('infobeamer-cms/event_start_date'), '%Y-%m-%d').replace(tzinfo=timezone.utc) event_start = datetime.strptime(metadata.get('infobeamer-cms/event_start_date'), '%Y-%m-%d')
event_duration = metadata.get('infobeamer-cms/event_duration_days', 4) event_duration = metadata.get('infobeamer-cms/event_duration_days', 4)
event_end = event_start + timedelta(days=event_duration) event_end = event_start + timedelta(days=event_duration)

View file

@ -0,0 +1,4 @@
<%
from tomlkit import dumps as toml_dumps
from bundlewrap.utils.text import toml_clean
%>${toml_clean(toml_dumps(repo.libs.faults.resolve_faults(config), sort_keys=True))}

View file

@ -1,10 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import logging import logging
from datetime import datetime from datetime import datetime, timezone
from json import dumps from json import dumps
from time import sleep from time import sleep
from zoneinfo import ZoneInfo
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from requests import RequestException, get from requests import RequestException, get
@ -25,8 +24,7 @@ logging.basicConfig(
) )
LOG = logging.getLogger("main") LOG = logging.getLogger("main")
TZ = ZoneInfo("Europe/Berlin") MLOG = logging.getLogger("mqtt")
DUMP_TIME = "0900"
state = None state = None
@ -40,10 +38,7 @@ def mqtt_out(message, level="INFO", device=None):
key = "infobeamer" key = "infobeamer"
if device: if device:
key += f"/{device['id']}" key += f"/{device['id']}"
if device["description"]:
message = f"[{device['description']}] {message}" message = f"[{device['description']}] {message}"
else:
message = f"[{device['serial']}] {message}"
client.publish( client.publish(
CONFIG["mqtt"]["topic"], CONFIG["mqtt"]["topic"],
@ -60,20 +55,17 @@ def mqtt_out(message, level="INFO", device=None):
def mqtt_dump_state(device): def mqtt_dump_state(device):
if not device["is_online"]: if not device["is_online"]:
return return
out = []
if device["location"]:
out.append("Location: {}".format(device["location"]))
out.append("Setup: {} ({})".format(device["setup"]["name"], device["setup"]["id"]))
out.append("Resolution: {}".format(device["run"].get("resolution", "unknown")))
mqtt_out( mqtt_out(
" - ".join(out), "Sync status: {} - Location: {} - Running Setup: {} ({}) - Resolution: {}".format(
"yes" if device["is_synced"] else "syncing",
device["location"],
device["setup"]["name"],
device["setup"]["id"],
device["run"].get("resolution", "unknown"),
),
device=device, device=device,
) )
def is_dump_time():
return datetime.now(TZ).strftime("%H%M") == DUMP_TIME
mqtt_out("Monitor starting up") mqtt_out("Monitor starting up")
while True: while True:
@ -86,14 +78,15 @@ while True:
r.raise_for_status() r.raise_for_status()
ib_state = r.json()["devices"] ib_state = r.json()["devices"]
except RequestException as e: except RequestException as e:
LOG.exception("Could not get device data from info-beamer") LOG.exception("Could not get data from info-beamer")
mqtt_out( mqtt_out(
f"Could not get device data from info-beamer: {e!r}", f"Could not get data from info-beamer: {e!r}",
level="WARN", level="WARN",
) )
else: else:
new_state = {} new_state = {}
for device in sorted(ib_state, key=lambda x: x["id"]): online_devices = set()
for device in ib_state:
did = str(device["id"]) did = str(device["id"])
if did in new_state: if did in new_state:
@ -101,8 +94,7 @@ while True:
continue continue
new_state[did] = device new_state[did] = device
# force information output for every online device at 09:00 CE(S)T must_dump_state = False
must_dump_state = is_dump_time()
if state is not None: if state is not None:
if did not in state: if did not in state:
@ -145,15 +137,17 @@ while True:
if device["is_online"]: if device["is_online"]:
if device["maintenance"]: if device["maintenance"]:
mqtt_out( mqtt_out(
"maintenance required: {}".format( "maintenance required: {}".join(
" ".join(sorted(device["maintenance"])) sorted(device["maintenance"])
), ),
level="WARN", level="WARN",
device=device, device=device,
) )
must_dump_state = True
if ( if (
device["location"] != state[did]["location"] device["is_synced"] != state[did]["is_synced"]
or device["location"] != state[did]["location"]
or device["setup"]["id"] != state[did]["setup"]["id"] or device["setup"]["id"] != state[did]["setup"]["id"]
or device["run"].get("resolution") or device["run"].get("resolution")
!= state[did]["run"].get("resolution") != state[did]["run"].get("resolution")
@ -165,52 +159,23 @@ while True:
else: else:
LOG.info("adding device {} to empty state".format(device["id"])) LOG.info("adding device {} to empty state".format(device["id"]))
if device["is_online"]:
online_devices.add(
"{} ({})".format(
device["id"],
device["description"],
)
)
state = new_state state = new_state
try: if (
r = get( datetime.now(timezone.utc).strftime("%H%M") == "1312"
"https://info-beamer.com/api/v1/account", and online_devices
auth=("", CONFIG["api_key"]), and int(datetime.now(timezone.utc).strftime("%S")) < 30
) ):
r.raise_for_status() mqtt_out("Online Devices: {}".format(", ".join(sorted(online_devices))))
ib_account = r.json() sleep(30)
except RequestException as e:
LOG.exception("Could not get account data from info-beamer")
mqtt_out(
f"Could not get account data from info-beamer: {e!r}",
level="WARN",
)
else:
available_credits = ib_account["balance"]
if is_dump_time():
mqtt_out(f"Available Credits: {available_credits}")
if available_credits < 50:
mqtt_out(
f"balance has dropped below 50 credits! (available: {available_credits})",
level="ERROR",
)
elif available_credits < 100:
mqtt_out(
f"balance has dropped below 100 credits! (available: {available_credits})",
level="WARN",
)
for quota_name, quota_config in sorted(ib_account["quotas"].items()):
value = quota_config["count"]["value"]
limit = quota_config["count"]["limit"]
if value > limit * 0.9:
mqtt_out(
f"quota {quota_name} is over 90% (limit {limit}, value {value})",
level="ERROR",
)
elif value > limit * 0.8:
mqtt_out(
f"quota {quota_name} is over 80% (limit {limit}, value {value})",
level="WARN",
)
sleep(60)
except KeyboardInterrupt: except KeyboardInterrupt:
break break

View file

@ -1,7 +1,10 @@
assert node.has_bundle('infobeamer-cms') # uses same venv assert node.has_bundle('infobeamer-cms') # uses same venv
files['/opt/infobeamer-monitor/config.toml'] = { files['/opt/infobeamer-monitor/config.toml'] = {
'content': repo.libs.faults.dict_as_toml(node.metadata.get('infobeamer-monitor')), 'content_type': 'mako',
'context': {
'config': node.metadata.get('infobeamer-monitor'),
},
'triggers': { 'triggers': {
'svc_systemd:infobeamer-monitor:restart', 'svc_systemd:infobeamer-monitor:restart',
}, },

View file

@ -19,4 +19,9 @@ defaults = {
'/usr/bin/ipmitool *', '/usr/bin/ipmitool *',
}, },
}, },
'pacman': {
'packages': {
'ipmitool': {},
},
},
} }

View file

@ -63,7 +63,7 @@ def firewall(metadata):
return { return {
'firewall': { 'firewall': {
'port_rules': { 'port_rules': {
'8096/tcp': atomic(metadata.get('jellyfin/restrict-to', set())), '8096/tcp': atomic(metadata.get('jellyfin/restrict-to', {'*'})),
}, },
}, },
} }

View file

@ -1,15 +0,0 @@
actions['modprobe_jool'] = {
'command': 'modprobe jool',
'unless': 'lsmod | grep -F jool',
}
actions['jool_add_nat64_instance'] = {
'command': 'jool instance add "nat64" --netfilter --pool6 64:ff9b::/96',
'unless': 'jool instance display --no-headers --csv | grep -E ",nat64,netfilter$"',
'needs': {
'action:modprobe_jool',
'pkg_apt:jool-dkms',
'pkg_apt:jool-tools',
'pkg_apt:linux-headers-amd64',
},
}

View file

@ -1,14 +0,0 @@
defaults = {
'apt': {
'packages': {
'jool-dkms': {},
'jool-tools': {},
'linux-headers-amd64': {},
},
},
'modules': {
'jool': [
'jool',
],
},
}

View file

@ -0,0 +1,4 @@
<%
from tomlkit import dumps as toml_dumps
from bundlewrap.utils.text import toml_clean
%>${toml_clean(toml_dumps(repo.libs.faults.resolve_faults(node.metadata.get('jugendhackt_tools')), sort_keys=True))}

View file

@ -47,7 +47,7 @@ actions['jugendhackt_tools_migrate'] = {
} }
files['/opt/jugendhackt_tools/config.toml'] = { files['/opt/jugendhackt_tools/config.toml'] = {
'content': repo.libs.faults.dict_as_toml(node.metadata.get('jugendhackt_tools')), 'content_type': 'mako',
'triggers': { 'triggers': {
'svc_systemd:jugendhackt_tools:restart', 'svc_systemd:jugendhackt_tools:restart',
}, },

View file

@ -1,8 +0,0 @@
# This file is managed using bundlewrap
% for identifier, modules in sorted(node.metadata.get('modules', {}).items()):
# ${identifier}
% for module in modules:
${module}
% endfor
% endfor

View file

@ -1,3 +0,0 @@
files['/etc/modules'] = {
'content_type': 'mako',
}

View file

@ -12,10 +12,6 @@ actions = {
'needs': { 'needs': {
'svc_systemd:nginx', 'svc_systemd:nginx',
}, },
'after': {
'svc_systemd:nginx:reload',
'svc_systemd:nginx:restart',
},
}, },
} }

View file

@ -13,6 +13,15 @@ defaults = {
}, },
}, },
}, },
'pacman': {
'packages': {
'dehydrated': {
'needed_by': {
'action:letsencrypt_update_certificates',
},
},
},
},
} }
@ -30,7 +39,6 @@ def cron(metadata):
'/usr/bin/dehydrated --cleanup', '/usr/bin/dehydrated --cleanup',
], ],
'when': '04:{}:00'.format(node.magic_number % 60), 'when': '04:{}:00'.format(node.magic_number % 60),
'exclude_from_monitoring': True,
}, },
}, },
}, },

View file

@ -10,4 +10,15 @@ defaults = {
}, },
}, },
}, },
'pacman': {
'packages': {
'lldpd': {
'needed_by': {
'directory:/etc/lldpd.d',
'file:/etc/lldpd.conf',
'svc_systemd:lldpd',
},
},
},
},
} }

View file

@ -4,6 +4,11 @@ defaults = {
'lm-sensors': {}, 'lm-sensors': {},
}, },
}, },
'pacman': {
'packages': {
'lm_sensors': {},
},
},
'telegraf': { 'telegraf': {
'input_plugins': { 'input_plugins': {
'builtin': { 'builtin': {

View file

@ -3,9 +3,6 @@ repo:
bindAddress: '${node.metadata.get('matrix-media-repo/listen-addr', '127.0.0.1')}' bindAddress: '${node.metadata.get('matrix-media-repo/listen-addr', '127.0.0.1')}'
port: ${node.metadata.get('matrix-media-repo/port', 20090)} port: ${node.metadata.get('matrix-media-repo/port', 20090)}
logDirectory: '-' logDirectory: '-'
logColors: false
jsonLogs: false
logLevel: 'info'
trustAnyForwardedAddress: false trustAnyForwardedAddress: false
useForwardedHost: true useForwardedHost: true
@ -25,13 +22,10 @@ homeservers:
csApi: "${config['domain']}" csApi: "${config['domain']}"
backoffAt: ${config.get('backoff_at', 10)} backoffAt: ${config.get('backoff_at', 10)}
adminApiKind: "${config.get('api', 'matrix')}" adminApiKind: "${config.get('api', 'matrix')}"
% if config.get('signing_key_path'):
signingKeyPath: "${config['signing_key_path']}"
% endif
% endfor % endfor
accessTokens: accessTokens:
maxCacheTimeSeconds: 10 maxCacheTimeSeconds: 0
useLocalAppserviceConfig: false useLocalAppserviceConfig: false
admins: admins:
@ -59,9 +53,7 @@ archiving:
uploads: uploads:
maxBytes: ${node.metadata.get('matrix-media-repo/upload_max_mb')*1024*1024} maxBytes: ${node.metadata.get('matrix-media-repo/upload_max_mb')*1024*1024}
minBytes: 100 minBytes: 100
#reportedMaxBytes: 0 reportedMaxBytes: 0
maxPending: 5
maxAgeSeconds: 1800
quotas: quotas:
enabled: false enabled: false
@ -69,6 +61,14 @@ downloads:
maxBytes: ${node.metadata.get('matrix-media-repo/download_max_mb')*1024*1024} maxBytes: ${node.metadata.get('matrix-media-repo/download_max_mb')*1024*1024}
numWorkers: ${node.metadata.get('matrix-media-repo/workers')} numWorkers: ${node.metadata.get('matrix-media-repo/workers')}
failureCacheMinutes: 5 failureCacheMinutes: 5
cache:
enabled: true
maxSizeBytes: ${node.metadata.get('matrix-media-repo/download_max_mb')*10*1024*1024}
maxFileSizeBytes: ${node.metadata.get('matrix-media-repo/download_max_mb')*1024*1024}
trackedMinutes: 30
minDownloads: 5
minCacheTimeSeconds: 300
minEvictedTimeSeconds: 60
expireAfterDays: 0 expireAfterDays: 0
urlPreviews: urlPreviews:
@ -137,8 +137,8 @@ thumbnails:
rateLimit: rateLimit:
enabled: true enabled: true
requestsPerSecond: 100 requestsPerSecond: 10
burst: 5000 burst: 50
identicons: identicons:
enabled: true enabled: true

View file

@ -19,6 +19,9 @@ files = {
'/opt/matrix-media-repo/config.yaml': { '/opt/matrix-media-repo/config.yaml': {
'owner': 'matrix-media-repo', 'owner': 'matrix-media-repo',
'content_type': 'mako', 'content_type': 'mako',
'triggers': {
'svc_systemd:matrix-media-repo:restart',
},
}, },
'/etc/systemd/system/matrix-media-repo.service': { '/etc/systemd/system/matrix-media-repo.service': {
'triggers': { 'triggers': {

View file

@ -0,0 +1,40 @@
server_location: 'http://[::1]:20080'
server_name: '${server_name}'
registration_shared_secret: '${reg_secret}'
admin_api_shared_secret: '${admin_secret}'
base_url: '${base_url}'
client_redirect: '${client_redirect}'
client_logo: 'static/images/element-logo.png' # use '{cwd}' for current working directory
#db: 'sqlite:///opt/matrix-registration/data/db.sqlite3'
db: 'postgresql://${database['user']}:${database['password']}@localhost/${database['database']}'
host: 'localhost'
port: 20100
rate_limit: ["100 per day", "10 per minute"]
allow_cors: false
ip_logging: false
logging:
disable_existing_loggers: false
version: 1
root:
level: DEBUG
handlers: [console]
formatters:
brief:
format: '%(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: INFO
formatter: brief
stream: ext://sys.stdout
# password requirements
password:
min_length: 8
# username requirements
username:
validation_regex: [] #list of regexes that the selected username must match. Example: '[a-zA-Z]\.[a-zA-Z]'
invalidation_regex: #list of regexes that the selected username must NOT match. Example: '(admin|support)'
- '^abuse'
- 'admin'
- 'support'
- 'help'

View file

@ -0,0 +1,14 @@
[Unit]
Description=matrix-registration
After=network.target
[Service]
User=matrix-registration
Group=matrix-registration
WorkingDirectory=/opt/matrix-registration/src
ExecStart=/opt/matrix-registration/venv/bin/matrix-registration --config-path /opt/matrix-registration/config.yaml serve
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,65 @@
actions['matrix-registration_create_virtualenv'] = {
'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/matrix-registration/venv/',
'unless': 'test -d /opt/matrix-registration/venv/',
'needs': {
# actually /opt/matrix-registration, but we don't create that
'directory:/opt/matrix-registration/src',
},
}
actions['matrix-registration_install'] = {
'command': ' && '.join([
'cd /opt/matrix-registration/src',
'/opt/matrix-registration/venv/bin/pip install psycopg2-binary',
'/opt/matrix-registration/venv/bin/pip install -e .',
]),
'needs': {
'action:matrix-registration_create_virtualenv',
},
'triggered': True,
}
users['matrix-registration'] = {
'home': '/opt/matrix-registration',
}
directories['/opt/matrix-registration/src'] = {}
git_deploy['/opt/matrix-registration/src'] = {
'repo': 'https://github.com/zeratax/matrix-registration.git',
'rev': 'master',
'triggers': {
'action:matrix-registration_install',
'svc_systemd:matrix-registration:restart',
},
}
files['/opt/matrix-registration/config.yaml'] = {
'content_type': 'mako',
'context': {
'admin_secret': node.metadata.get('matrix-registration/admin_secret'),
'base_url': node.metadata.get('matrix-registration/base_path', ''),
'client_redirect': node.metadata.get('matrix-registration/client_redirect'),
'database': node.metadata.get('matrix-registration/database'),
'reg_secret': node.metadata.get('matrix-synapse/registration_shared_secret'),
'server_name': node.metadata.get('matrix-synapse/server_name'),
},
'triggers': {
'svc_systemd:matrix-registration:restart',
},
}
files['/usr/local/lib/systemd/system/matrix-registration.service'] = {
'triggers': {
'action:systemd-reload',
'svc_systemd:matrix-registration:restart',
},
}
svc_systemd['matrix-registration'] = {
'needs': {
'action:matrix-registration_install',
'file:/opt/matrix-registration/config.yaml',
'file:/usr/local/lib/systemd/system/matrix-registration.service',
},
}

View file

@ -0,0 +1,25 @@
defaults = {
'bash_aliases': {
'matrix-registration': '/opt/matrix-registration/venv/bin/matrix-registration --config-path /opt/matrix-registration/config.yaml',
},
'matrix-registration': {
'admin_secret': repo.vault.password_for(f'{node.name} matrix-registration admin secret'),
'database': {
'user': 'matrix-registration',
'password': repo.vault.password_for(f'{node.name} postgresql matrix-registration'),
'database': 'matrix-registration',
},
},
'postgresql': {
'roles': {
'matrix-registration': {
'password': repo.vault.password_for(f'{node.name} postgresql matrix-registration'),
},
},
'databases': {
'matrix-registration': {
'owner': 'matrix-registration',
},
},
},
}

View file

@ -1,27 +0,0 @@
<%
database = node.metadata.get('matrix-synapse/database')
db_string = 'postgresql://{}:{}@{}/{}?sslmode=disable'.format(
database['user'],
database['password'],
database.get('host', 'localhost'),
database['database'],
)
%>\
[Unit]
Description=matrix-org sliding-sync proxy
After=network.target
Requires=postgresql.service
[Service]
User=matrix-synapse
Group=matrix-synapse
Environment=SYNCV3_SERVER=https://${node.metadata.get('matrix-synapse/baseurl')}
Environment=SYNCV3_DB=${db_string}
Environment=SYNCV3_SECRET=${node.metadata.get('matrix-synapse/sliding_sync/secret')}
Environment=SYNCV3_BINDADDR=127.0.0.1:20070
ExecStart=/usr/local/bin/matrix-sliding-sync
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target

View file

@ -118,9 +118,6 @@ def nginx(metadata):
} }
locations = { locations = {
'/_client/': {
'target': 'http://127.0.0.1:20070',
},
'/_matrix': { '/_matrix': {
'target': 'http://[::1]:20080', 'target': 'http://[::1]:20080',
'max_body_size': '50M', 'max_body_size': '50M',
@ -132,8 +129,7 @@ def nginx(metadata):
} }
if node.has_bundle('matrix-media-repo'): if node.has_bundle('matrix-media-repo'):
for path in ('/_matrix/media', '/_matrix/client/v1/media', '/_matrix/federation/v1/media'): locations['/_matrix/media'] = {
locations[path] = {
'target': 'http://localhost:20090', 'target': 'http://localhost:20090',
'max_body_size': '{}M'.format(metadata.get('matrix-media-repo/upload_max_mb')), 'max_body_size': '{}M'.format(metadata.get('matrix-media-repo/upload_max_mb')),
# matrix-media-repo needs this to be the # matrix-media-repo needs this to be the

View file

@ -1,15 +1,11 @@
#!/bin/bash #!/bin/bash
OPTS="--netrc" OPTS=""
OPTS="$OPTS --netrc-location /opt/mixcloud-downloader/netrc"
OPTS="$OPTS --retry-sleep linear=1::2"
OPTS="$OPTS --retry-sleep fragment:exp=1:60"
OPTS="$OPTS --extractor-retries 5"
if [[ -n "$DEBUG" ]] if [[ -n "$DEBUG" ]]
then then
set -x set -x
else else
OPTS="$OPTS -q" OPTS="-q"
fi fi
set -euo pipefail set -euo pipefail

View file

@ -1,3 +0,0 @@
% for domain, data in sorted(node.metadata.get('mixcloud-downloader/netrc', {}).items()):
machine ${domain} login ${data['username']} password ${data['password']}
% endfor

View file

@ -6,9 +6,3 @@ files['/opt/mixcloud-downloader/download.sh'] = {
directories['/opt/mixcloud-downloader'] = { directories['/opt/mixcloud-downloader'] = {
'owner': 'kunsi', 'owner': 'kunsi',
} }
files['/opt/mixcloud-downloader/netrc'] = {
'content_type': 'mako',
'mode': '0400',
'owner': 'kunsi',
}

View file

@ -5,6 +5,12 @@ files = {
'svc_systemd:mosquitto:restart', 'svc_systemd:mosquitto:restart',
}, },
}, },
'/usr/local/bin/tasmota-telegraf-plugin': {
'mode': '0755',
'needs': {
'pkg_apt:python3-paho-mqtt',
},
},
} }
svc_systemd = { svc_systemd = {
@ -17,12 +23,6 @@ svc_systemd = {
} }
if node.has_bundle('telegraf'): if node.has_bundle('telegraf'):
files['/usr/local/bin/tasmota-telegraf-plugin'] = { files['/usr/local/bin/tasmota-telegraf-plugin']['triggers'] = {
'mode': '0755',
'needs': {
'pkg_apt:python3-paho-mqtt',
},
'triggers': {
'svc_systemd:telegraf:restart', 'svc_systemd:telegraf:restart',
},
} }

View file

@ -5,6 +5,7 @@ defaults = {
'packages': { 'packages': {
'mosquitto': {}, 'mosquitto': {},
'mosquitto-clients': {}, 'mosquitto-clients': {},
'python3-paho-mqtt': {}, # for telegraf plugin
}, },
}, },
'icinga2_api': { 'icinga2_api': {
@ -23,9 +24,6 @@ defaults = {
}, },
} }
if node.has_bundle('telegraf'):
defaults['apt']['packages']['python3-paho-mqtt'] = {}
@metadata_reactor.provides( @metadata_reactor.provides(
'firewall/port_rules', 'firewall/port_rules',

View file

@ -1,40 +1,42 @@
users['netbox'] = { users = {
'netbox': {
'home': '/opt/netbox', 'home': '/opt/netbox',
},
} }
directories['/opt/netbox/src'] = {} directories = {
'/opt/netbox/src': {},
directories['/opt/netbox/media'] = { '/opt/netbox/media': {
'owner': 'netbox', 'owner': 'netbox',
} },
'/opt/netbox/scripts': {
directories['/opt/netbox/scripts'] = {
'owner': 'netbox', 'owner': 'netbox',
},
} }
git_deploy['/opt/netbox/src'] = { git_deploy = {
'/opt/netbox/src': {
'repo': 'https://github.com/netbox-community/netbox.git', 'repo': 'https://github.com/netbox-community/netbox.git',
'rev': node.metadata.get('netbox/version'), 'rev': node.metadata.get('netbox/version'),
'triggers': { 'triggers': {
'action:netbox_install', 'action:netbox_install',
'action:netbox_upgrade',
'svc_systemd:netbox-web:restart', 'svc_systemd:netbox-web:restart',
'svc_systemd:netbox-worker:restart', 'svc_systemd:netbox-worker:restart',
}, },
'tags': {
'netbox-install',
}, },
} }
# This is a recreation of https://github.com/netbox-community/netbox/blob/develop/upgrade.sh # This is a recreation of https://github.com/netbox-community/netbox/blob/develop/upgrade.sh
actions['netbox_create_virtualenv'] = { actions = {
'netbox_create_virtualenv': {
'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/netbox/venv', 'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/netbox/venv',
'unless': 'test -d /opt/netbox/venv/', 'unless': 'test -d /opt/netbox/venv/',
'needed_by': { 'needed_by': {
'action:netbox_install', 'action:netbox_install',
}, },
} },
'netbox_install': {
actions['netbox_install'] = {
'triggered': True, 'triggered': True,
'command': ' && '.join([ 'command': ' && '.join([
'cd /opt/netbox/src', 'cd /opt/netbox/src',
@ -53,51 +55,37 @@ actions['netbox_install'] = {
'pkg_apt:libxslt1-dev', 'pkg_apt:libxslt1-dev',
'pkg_apt:python3-dev', 'pkg_apt:python3-dev',
'pkg_apt:zlib1g-dev', 'pkg_apt:zlib1g-dev',
}
},
'netbox_upgrade': {
'triggered': True,
'command': ' && '.join([
'/opt/netbox/venv/bin/python /opt/netbox/src/netbox/manage.py migrate',
'/opt/netbox/venv/bin/python /opt/netbox/src/netbox/manage.py collectstatic --no-input',
'/opt/netbox/venv/bin/python /opt/netbox/src/netbox/manage.py remove_stale_contenttypes --no-input',
'/opt/netbox/venv/bin/python /opt/netbox/src/netbox/manage.py clearsessions',
]),
'needs': {
'action:netbox_install',
'file:/opt/netbox/src/netbox/netbox/configuration.py',
}, },
'tags': {
'netbox-install',
}, },
} }
last_action = 'netbox_install' files = {
for upgrade_command in ( '/usr/local/lib/systemd/system/netbox-web.service': {
'migrate',
'trace_paths --no-input',
'collectstatic --no-input',
'remove_stale_contenttypes --no-input',
'reindex --lazy',
'clearsessions',
):
actions[f'netbox_upgrade_{upgrade_command.split()[0]}'] = {
'triggered': True,
'command': f'/opt/netbox/venv/bin/python /opt/netbox/src/netbox/manage.py {upgrade_command}',
'needs': {
f'action:{last_action}',
},
'tags': {
'netbox-upgrade',
},
'triggered_by': {
'tag:netbox-install',
},
}
last_action = f'netbox_upgrade_{upgrade_command.split()[0]}'
files['/usr/local/lib/systemd/system/netbox-web.service'] = {
'triggers': { 'triggers': {
'action:systemd-reload', 'action:systemd-reload',
'svc_systemd:netbox-web:restart', 'svc_systemd:netbox-web:restart',
}, },
} },
'/usr/local/lib/systemd/system/netbox-worker.service': {
files['/usr/local/lib/systemd/system/netbox-worker.service'] = {
'triggers': { 'triggers': {
'action:systemd-reload', 'action:systemd-reload',
'svc_systemd:netbox-worker:restart', 'svc_systemd:netbox-worker:restart',
}, },
} },
'/opt/netbox/src/netbox/netbox/configuration.py': {
files['/opt/netbox/src/netbox/netbox/configuration.py'] = {
'content_type': 'mako', 'content_type': 'mako',
'triggers': { 'triggers': {
'svc_systemd:netbox-web:restart', 'svc_systemd:netbox-web:restart',
@ -106,33 +94,31 @@ files['/opt/netbox/src/netbox/netbox/configuration.py'] = {
'needs': { 'needs': {
'git_deploy:/opt/netbox/src', 'git_deploy:/opt/netbox/src',
}, },
'tags': {
'netbox-install',
}, },
} '/opt/netbox/gunicorn_config.py': {
files['/opt/netbox/gunicorn_config.py'] = {
'content_type': 'mako', 'content_type': 'mako',
'triggers': { 'triggers': {
'svc_systemd:netbox-web:restart', 'svc_systemd:netbox-web:restart',
}, },
},
} }
svc_systemd['netbox-web'] = { svc_systemd = {
'netbox-web': {
'needs': { 'needs': {
'action:netbox_install',
'action:netbox_upgrade',
'file:/usr/local/lib/systemd/system/netbox-web.service', 'file:/usr/local/lib/systemd/system/netbox-web.service',
'file:/opt/netbox/gunicorn_config.py', 'file:/opt/netbox/gunicorn_config.py',
'file:/opt/netbox/src/netbox/netbox/configuration.py', 'file:/opt/netbox/src/netbox/netbox/configuration.py',
'tag:netbox-install',
'tag:netbox-upgrade',
}, },
} },
'netbox-worker': {
svc_systemd['netbox-worker'] = {
'needs': { 'needs': {
'action:netbox_install',
'action:netbox_upgrade',
'file:/usr/local/lib/systemd/system/netbox-worker.service', 'file:/usr/local/lib/systemd/system/netbox-worker.service',
'file:/opt/netbox/src/netbox/netbox/configuration.py', 'file:/opt/netbox/src/netbox/netbox/configuration.py',
'tag:netbox-install', },
'tag:netbox-upgrade',
}, },
} }

View file

@ -45,9 +45,6 @@ defaults = {
'pwd': '/var/www/nextcloud', 'pwd': '/var/www/nextcloud',
'user': 'www-data', 'user': 'www-data',
'when': '*:00/5', 'when': '*:00/5',
'requisite': {
'postgresql.service',
},
}, },
}, },
}, },

View file

@ -1,3 +1,8 @@
if node.has_bundle('pacman'):
package = 'pkg_pacman:nfs-utils'
else:
package = 'pkg_apt:nfs-common'
for mount, data in node.metadata.get('nfs-client/mounts',{}).items(): for mount, data in node.metadata.get('nfs-client/mounts',{}).items():
data['mount'] = mount data['mount'] = mount
data['mount_options'] = set(data.get('mount_options', set())) data['mount_options'] = set(data.get('mount_options', set()))
@ -37,7 +42,7 @@ for mount, data in node.metadata.get('nfs-client/mounts',{}).items():
'file:/etc/systemd/system/{}.automount'.format(unitname), 'file:/etc/systemd/system/{}.automount'.format(unitname),
'directory:{}'.format(data['mountpoint']), 'directory:{}'.format(data['mountpoint']),
'svc_systemd:systemd-networkd', 'svc_systemd:systemd-networkd',
'pkg_apt:nfs-common', package,
}, },
} }
else: else:
@ -53,7 +58,7 @@ for mount, data in node.metadata.get('nfs-client/mounts',{}).items():
'file:/etc/systemd/system/{}.mount'.format(unitname), 'file:/etc/systemd/system/{}.mount'.format(unitname),
'directory:{}'.format(data['mountpoint']), 'directory:{}'.format(data['mountpoint']),
'svc_systemd:systemd-networkd', 'svc_systemd:systemd-networkd',
'pkg_apt:nfs-common', package,
}, },
} }

View file

@ -4,6 +4,11 @@ defaults = {
'nfs-common': {}, 'nfs-common': {},
}, },
}, },
'pacman': {
'packages': {
'nfs-utils': {},
},
},
} }
if node.has_bundle('telegraf'): if node.has_bundle('telegraf'):

View file

@ -33,10 +33,7 @@ def firewall(metadata):
ips.add(share_target) ips.add(share_target)
rules = {} rules = {}
ports = ('111', '2049', '1110', '4045', '35295') for port in ('111', '2049', '1110', '4045', '35295'):
if metadata.get('nfs-server/version', 3) == 4:
ports = ('111', '2049')
for port in ports:
for proto in ('/tcp', '/udp'): for proto in ('/tcp', '/udp'):
rules[port + proto] = atomic(ips) rules[port + proto] = atomic(ips)

View file

@ -23,8 +23,9 @@ table inet filter {
icmp type timestamp-request drop icmp type timestamp-request drop
icmp type timestamp-reply drop icmp type timestamp-reply drop
meta l4proto {icmp, ipv6-icmp} accept ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
% for ruleset, rules in sorted(input.items()): % for ruleset, rules in sorted(input.items()):
# ${ruleset} # ${ruleset}

View file

@ -1,3 +1,8 @@
if node.has_bundle('pacman'):
package = 'pkg_pacman:nftables'
else:
package = 'pkg_apt:nftables'
directories = { directories = {
# used by other bundles # used by other bundles
'/etc/nftables-rules.d': { '/etc/nftables-rules.d': {
@ -37,7 +42,7 @@ svc_systemd = {
'nftables': { 'nftables': {
'needs': { 'needs': {
'file:/etc/nftables.conf', 'file:/etc/nftables.conf',
'pkg_apt:nftables', package,
}, },
}, },
} }

View file

@ -10,9 +10,26 @@ defaults = {
'blocked_v4': repo.libs.firewall.global_ip4_blocklist, 'blocked_v4': repo.libs.firewall.global_ip4_blocklist,
'blocked_v6': repo.libs.firewall.global_ip6_blocklist, 'blocked_v6': repo.libs.firewall.global_ip6_blocklist,
}, },
'pacman': {
'packages': {
'nftables': {},
# https://github.com/bundlewrap/bundlewrap/issues/688
# 'iptables': {
# 'installed': False,
# 'needed_by': {
# 'pkg_pacman:iptables-nft',
# },
# },
'iptables-nft': {
'needed_by': {
'pkg_pacman:nftables',
},
},
},
},
} }
if not node.has_bundle('vmhost') and not node.has_bundle('docker-engine'): if not node.has_bundle('vmhost'):
# see comment in bundles/vmhost/items.py # see comment in bundles/vmhost/items.py
defaults['apt']['packages']['iptables'] = { defaults['apt']['packages']['iptables'] = {
'installed': False, 'installed': False,

View file

@ -0,0 +1,9 @@
[Service]
ExecStart=
ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf
ExecReload=
ExecReload=/bin/sh -c "/bin/kill -s HUP $(/bin/cat /var/run/nginx.pid)"
ExecStop=
ExecStop=/bin/sh -c "/bin/kill -s TERM $(/bin/cat /var/run/nginx.pid)"

View file

@ -1,4 +1,4 @@
user www-data; user ${username};
worker_processes ${worker_processes}; worker_processes ${worker_processes};
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
@ -10,9 +10,6 @@ events {
http { http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
types {
application/javascript mjs;
}
default_type application/octet-stream; default_type application/octet-stream;
charset UTF-8; charset UTF-8;
override_charset on; override_charset on;
@ -26,7 +23,7 @@ http {
send_timeout 10; send_timeout 10;
access_log off; access_log off;
error_log /dev/null; error_log off;
client_body_buffer_size 16K; client_body_buffer_size 16K;
client_header_buffer_size 4k; client_header_buffer_size 4k;

View file

@ -29,9 +29,8 @@ server {
root ${webroot if webroot else '/var/www/{}/'.format(vhost)}; root ${webroot if webroot else '/var/www/{}/'.format(vhost)};
index ${' '.join(index)}; index ${' '.join(index)};
listen 443 ssl; listen 443 ssl http2;
listen [::]:443 ssl; listen [::]:443 ssl http2;
http2 on;
% if ssl == 'letsencrypt': % if ssl == 'letsencrypt':
ssl_certificate /var/lib/dehydrated/certs/${domain}/fullchain.pem; ssl_certificate /var/lib/dehydrated/certs/${domain}/fullchain.pem;
@ -71,9 +70,8 @@ server {
root ${webroot if webroot else '/var/www/{}/'.format(vhost)}; root ${webroot if webroot else '/var/www/{}/'.format(vhost)};
index ${' '.join(index)}; index ${' '.join(index)};
listen 443 ssl; listen 443 ssl http2;
listen [::]:443 ssl; listen [::]:443 ssl http2;
http2 on;
% if ssl == 'letsencrypt': % if ssl == 'letsencrypt':
ssl_certificate /var/lib/dehydrated/certs/${domain}/fullchain.pem; ssl_certificate /var/lib/dehydrated/certs/${domain}/fullchain.pem;
@ -82,13 +80,12 @@ server {
ssl_certificate /etc/nginx/ssl/${vhost}.crt; ssl_certificate /etc/nginx/ssl/${vhost}.crt;
ssl_certificate_key /etc/nginx/ssl/${vhost}.key; ssl_certificate_key /etc/nginx/ssl/${vhost}.key;
% endif % endif
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_dhparam /etc/ssl/certs/dhparam.pem; ssl_dhparam /etc/ssl/certs/dhparam.pem;
ssl_prefer_server_ciphers off;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; ssl_session_tickets off;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
% endif % endif
@ -149,18 +146,18 @@ server {
% if 'target' in options: % if 'target' in options:
proxy_pass ${options['target']}; proxy_pass ${options['target']};
proxy_http_version ${options.get('http_version', '1.1')}; proxy_http_version ${options.get('http_version', '1.1')};
proxy_set_header Host ${options.get('proxy_pass_host', domain)}; proxy_set_header Host ${domain};
% if options.get('websockets', False): % if options.get('websockets', False):
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
% endif % endif
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host ${options.get('x_forwarded_host', options.get('proxy_pass_host', domain))}; proxy_set_header X-Forwarded-Host ${options.get('x_forwarded_host', domain)};
% for option, value in options.get('proxy_set_header', {}).items(): % for option, value in options.get('proxy_set_header', {}).items():
proxy_set_header ${option} ${value}; proxy_set_header ${option} ${value};
% endfor % endfor
% if location != '/' and location != '= /': % if location != '/':
proxy_set_header X-Script-Name ${location}; proxy_set_header X-Script-Name ${location};
% endif % endif
proxy_buffering off; proxy_buffering off;
@ -201,8 +198,6 @@ server {
fastcgi_hide_header X-XSS-Protection; fastcgi_hide_header X-XSS-Protection;
% endif % endif
fastcgi_hide_header Permissions-Policy; fastcgi_hide_header Permissions-Policy;
fastcgi_request_buffering off;
proxy_buffering off;
} }
% if not max_body_size: % if not max_body_size:
client_max_body_size 5M; client_max_body_size 5M;

View file

@ -1,5 +1,12 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
if node.has_bundle('pacman'):
package = 'pkg_pacman:nginx'
username = 'http'
else:
package = 'pkg_apt:nginx'
username = 'www-data'
directories = { directories = {
'/etc/nginx/sites': { '/etc/nginx/sites': {
'purge': True, 'purge': True,
@ -17,9 +24,9 @@ directories = {
}, },
}, },
'/var/log/nginx-timing': { '/var/log/nginx-timing': {
'owner': 'www-data', 'owner': username,
'needs': { 'needs': {
'pkg_apt:nginx', package,
}, },
}, },
'/var/www': {}, '/var/www': {},
@ -33,6 +40,7 @@ files = {
'/etc/nginx/nginx.conf': { '/etc/nginx/nginx.conf': {
'content_type': 'mako', 'content_type': 'mako',
'context': { 'context': {
'username': username,
**node.metadata['nginx'], **node.metadata['nginx'],
}, },
'triggers': { 'triggers': {
@ -61,13 +69,28 @@ files = {
'/var/www/error.html': {}, '/var/www/error.html': {},
'/var/www/not_found.html': {}, '/var/www/not_found.html': {},
} }
if node.has_bundle('pacman'):
files['/etc/systemd/system/nginx.service.d/bundlewrap.conf'] = {
'source': 'arch-override.conf',
'triggers': {
'action:systemd-reload',
'svc_systemd:nginx:restart',
},
}
actions = {
'nginx-generate-dhparam': {
'command': 'openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048',
'unless': 'test -f /etc/ssl/certs/dhparam.pem',
},
}
svc_systemd = { svc_systemd = {
'nginx': { 'nginx': {
'needs': { 'needs': {
'action:generate-dhparam', 'action:nginx-generate-dhparam',
'directory:/var/log/nginx-timing', 'directory:/var/log/nginx-timing',
'pkg_apt:nginx', package,
}, },
}, },
} }
@ -104,7 +127,7 @@ for vhost, config in node.metadata.get('nginx/vhosts', {}).items():
'context': { 'context': {
'create_logs': config.get('create_logs', False), 'create_logs': config.get('create_logs', False),
'create_timing_log': config.get('timing_log', True), 'create_timing_log': config.get('timing_log', True),
'php_version': node.metadata.get('php/__version', ''), 'php_version': node.metadata.get('php/version', ''),
'security_txt': security_txt_enabled, 'security_txt': security_txt_enabled,
'vhost': vhost, 'vhost': vhost,
**config, **config,

View file

@ -33,6 +33,11 @@ defaults = {
'nginx': { 'nginx': {
'worker_connections': 768, 'worker_connections': 768,
}, },
'pacman': {
'packages': {
'nginx': {},
},
},
} }
if node.has_bundle('telegraf'): if node.has_bundle('telegraf'):
@ -195,8 +200,8 @@ def telegraf_anon_timing(metadata):
result[f'nginx-{vname}'] = { result[f'nginx-{vname}'] = {
'files': [f'/var/log/nginx-timing/{vname}.log'], 'files': [f'/var/log/nginx-timing/{vname}.log'],
'from_beginning': False, 'from_beginning': False,
'grok_patterns': [r'%{LOGPATTERN}'], 'grok_patterns': ['%{LOGPATTERN}'],
'grok_custom_patterns': r'LOGPATTERN \[%{HTTPDATE:ts:ts-httpd}\] %{NUMBER:request_time:float} (?:%{NUMBER:upstream_response_time:float}|-) "%{WORD:verb:tag} %{NOTSPACE:request} HTTP/%{NUMBER:http_version:float}" %{NUMBER:resp_code:tag}', 'grok_custom_patterns': 'LOGPATTERN \[%{HTTPDATE:ts:ts-httpd}\] %{NUMBER:request_time:float} (?:%{NUMBER:upstream_response_time:float}|-) "%{WORD:verb:tag} %{NOTSPACE:request} HTTP/%{NUMBER:http_version:float}" %{NUMBER:resp_code:tag}',
'data_format': 'grok', 'data_format': 'grok',
'name_override': 'nginx_timing', 'name_override': 'nginx_timing',
} }

Some files were not shown because too many files have changed in this diff Show more