Compare commits

..

No commits in common. "main" and "unbound-sophie" have entirely different histories.

149 changed files with 2258 additions and 1211 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*
__pycache__
*.swp
.direnv
.envrc.local
.bw_debug_history

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

@ -5,7 +5,6 @@ supported_os = {
10: 'buster',
11: 'bullseye',
12: 'bookworm',
13: 'trixie',
99: 'unstable',
},
}

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': {},
'util-linux': {}, # provides 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

@ -2,6 +2,7 @@
from datetime import datetime
from json import load
from subprocess import check_output
from sys import argv, exit
from time import time
@ -17,17 +18,29 @@ try:
with open(f'/etc/backup-server/config.json', 'r') as f:
server_settings = load(f)
with open(f'/etc/backup-server/backups.json', 'r') as f:
backups = load(f)
# get all existing snapshots for NODE
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!')
exit(2)
delta = NOW - backups[NODE]
last_snap = sorted(snaps)[-1]
delta = NOW - last_snap
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

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)):
_, 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
ts, bucket = snapname.split('-', 1)
buckets.setdefault(bucket, set()).add(int(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': {
'mode': '0755',
},
'/usr/local/share/icinga/plugins/check_backup_for_node-cron': {
'mode': '0755',
},
}
directories['/etc/backup-server/clients'] = {

View file

@ -1,5 +1,3 @@
from bundlewrap.exceptions import BundleError
defaults = {
'backup-server': {
'my_ssh_port': 22,
@ -10,14 +8,6 @@ defaults = {
'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': {
# The whole point of doing backups is to keep them for a long
# time, which eliminates the need for this check.
@ -79,51 +69,25 @@ def zfs_pool(metadata):
return {}
crypt_devices = {}
pool_devices = 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())):
crypt_devices[device] = {
'dm-name': f'backup{number}',
'passphrase': passphrase,
}
pool_devices.add(f'/dev/mapper/backup{number}')
unlock_actions.add(f'action:dm-crypt_open_backup{number}')
# TODO remove this once we have migrated all systems
if isinstance(devices, dict):
pool_devices = set()
pool_opts = {
'devices': pool_devices,
}
for number, (device, passphrase) in enumerate(sorted(devices.items())):
crypt_devices[device] = {
'dm-name': f'backup{number}',
'passphrase': passphrase,
}
pool_devices.add(f'/dev/mapper/backup{number}')
unlock_actions.add(f'action:dm-crypt_open_backup{number}')
pool_config = [{
'devices': pool_devices,
}]
if len(pool_devices) > 2:
pool_config[0]['type'] = 'raidz'
elif len(pool_devices) > 1:
pool_config[0]['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')
if len(pool_devices) > 2:
pool_opts['type'] = 'raidz'
elif len(pool_devices) > 1:
pool_opts['type'] = 'mirror'
return {
'backup-server': {
@ -136,8 +100,9 @@ def zfs_pool(metadata):
'pools': {
'backups': {
'when_creating': {
'config': pool_config,
**metadata.get('backup-server/zpool_create_options', {}),
'config': [
pool_opts,
],
},
'needs': unlock_actions,
# That's a bit hacky. We do it this way to auto-import
@ -191,7 +156,7 @@ def monitoring(metadata):
continue
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,
config['one_backup_every_hours'],
),

View file

@ -24,6 +24,7 @@ files = {
'before': {
'action:',
'pkg_apt:',
'pkg_pacman:',
},
},
}

View file

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

View file

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

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 = {
'/etc/crontab': {
'content_type': 'mako',
@ -17,9 +24,9 @@ directories = {
}
svc_systemd = {
'cron': {
service_name: {
'needs': {
'pkg_apt:cron',
package_name,
},
},
}

View file

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

View file

@ -18,13 +18,7 @@ try:
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
]
containers = loads(f"[{','.join([l for l in docker_ps.decode().splitlines() if l])}]")
if not containers:
print(f'CRITICAL: container {container_name} not found!')

View file

@ -12,20 +12,11 @@ then
exit 1
fi
PUID="$(id -u "${user}")"
PGID="$(id -g "${user}")"
PUID="$(id -u "docker-${name}")"
PGID="$(id -g "docker-${name}")"
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" \
@ -34,28 +25,20 @@ then
% for k, v in sorted(environment.items()):
--env "${k}=${v}" \
% endfor
--network aaarghhh \
--network host \
% for host_port, container_port in sorted(ports.items()):
--publish "127.0.0.1:${host_port}:${container_port}" \
--expose "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}"
docker rm "${name}"
else
echo "Unknown action $ACTION"

View file

@ -28,40 +28,18 @@ 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',
@ -70,17 +48,16 @@ for app, config in node.metadata.get('docker-engine/containers', {}).items():
},
}
users[user] = {
users[f'docker-{app}'] = {
'home': f'/var/opt/docker-engine/{app}',
'groups': {
'docker',
},
'after': {
'action:docker_create_nondefault_network',
'svc_systemd:docker',
# provides docker group
'pkg_apt:docker-ce',
},
}
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',
@ -101,26 +78,22 @@ for app, config in node.metadata.get('docker-engine/containers', {}).items():
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}',
f'user:docker-{app}',
'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,
directories[f'/var/opt/docker-engine/{app}/{volume}'] = {
'owner': f'docker-{app}',
'group': f'docker-{app}',
'needed_by': {
f'svc_systemd:docker-{app}',
},
# don't do anything if the directory exists, docker images
# mangle owners
'unless': f'test -d {volume}',
'unless': f'test -d /var/opt/docker-engine/{app}/{volume}',
}

View file

@ -13,17 +13,16 @@ defaults = {
},
},
},
'nftables': {
'forward': {
'docker-engine': [
'ct state { related, established } accept',
'ip saddr 172.16.0.0/12 accept',
],
'backups': {
'paths': {
'/var/opt/docker-engine',
},
'postrouting': {
'docker-engine': [
'ip saddr 172.16.0.0/12 masquerade',
],
},
'hosts': {
'entries': {
'172.17.0.1': {
'host.docker.internal',
},
},
},
'docker-engine': {
@ -34,7 +33,9 @@ defaults = {
},
'zfs': {
'datasets': {
'tank/docker-data': {},
'tank/docker-data': {
'mountpoint': '/var/opt/docker-engine',
},
},
},
}
@ -65,7 +66,6 @@ def monitoring(metadata):
@metadata_reactor.provides(
'backups/paths',
'zfs/datasets',
)
def zfs(metadata):
@ -73,19 +73,10 @@ def zfs(metadata):
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}',
},
'mountpoint': f'/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,40 +1,32 @@
assert node.has_bundle('docker-engine')
assert node.has_bundle('redis')
assert not node.has_bundle('postgresql') # docker container uses that port
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_HOSTNAME': 'host.docker.internal',
'DB_PASSWORD': repo.vault.password_for(f'{node.name} postgresql immich'),
'DB_USERNAME': 'immich',
'REDIS_HOSTNAME': 'immich-redis',
'REDIS_HOSTNAME': 'host.docker.internal',
},
'volumes': {
'config': '/config',
'libraries': '/libraries',
'photos': '/photos',
},
'ports': {
'8080': '8080',
},
'needs': {
'svc_systemd:docker-immich-postgresql',
'svc_systemd:docker-immich-redis',
'svc_systemd:docker-postgresql14',
},
'requires': {
'docker-immich-postgresql.service',
'docker-immich-redis.service',
'docker-postgresql14.service',
},
},
'immich-postgresql': {
'postgresql14': {
'image': 'tensorchord/pgvecto-rs:pg14-v0.2.0',
'environment': {
'POSTGRES_PASSWORD': repo.vault.password_for(f'{node.name} postgresql immich'),
@ -45,14 +37,8 @@ defaults = {
'database': '/var/lib/postgresql/data',
},
},
'immich-redis': {
'image': 'docker.io/redis:6.2-alpine',
},
},
},
'docker-immich': {
'enable_auto_album_share': False,
},
'nginx': {
'vhosts': {
'immich': {
@ -60,33 +46,19 @@ defaults = {
'/': {
'target': 'http://127.0.0.1:8080/',
'websockets': True,
'max_body_size': '5000m',
'max_body_size': '500m',
},
#'/api/socket.io/': {
# 'target': 'http://127.0.0.1:8081/',
# 'websockets': True,
#},
},
},
},
},
'redis': {
'bind': '0.0.0.0',
},
}
@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

@ -20,7 +20,7 @@ def nodejs(metadata):
if version >= (1, 11, 71):
return {
'nodejs': {
'version': 22,
'version': 20,
},
}
else:

View file

@ -23,25 +23,6 @@ object Host "${rnode.name}" {
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())):
object Dependency "${rnode.name}_depends_on_${depends_on_host}" {
parent_host_name = "${depends_on_host}"

View file

@ -401,6 +401,22 @@ for rnode in sorted(repo.nodes):
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'] = {
'source': 'icinga2/groups.conf',

View file

@ -54,6 +54,7 @@ defaults = {
'setup-token': repo.vault.password_for(f'{node.name} icingaweb2 setup-token'),
},
'php': {
'version': '8.2',
'packages': {
'curl',
'gd',

View file

@ -1,13 +1,10 @@
from datetime import datetime, timedelta, timezone
assert node.has_bundle('redis')
from datetime import datetime, timedelta
defaults = {
'infobeamer-cms': {
'config': {
'MAX_UPLOADS': 5,
'PREFERRED_URL_SCHEME': 'https',
'REDIS_HOST': '127.0.0.1',
'SESSION_COOKIE_NAME': '__Host-sess',
'STATIC_PATH': '/opt/infobeamer-cms/static',
'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',
)
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_end = event_start + timedelta(days=event_duration)

View file

@ -40,10 +40,7 @@ def mqtt_out(message, level="INFO", device=None):
key = "infobeamer"
if device:
key += f"/{device['id']}"
if device["description"]:
message = f"[{device['description']}] {message}"
else:
message = f"[{device['serial']}] {message}"
message = f"[{device['description']}] {message}"
client.publish(
CONFIG["mqtt"]["topic"],
@ -93,7 +90,7 @@ while True:
)
else:
new_state = {}
for device in sorted(ib_state, key=lambda x: x["id"]):
for device in ib_state:
did = str(device["id"])
if did in new_state:

View file

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

View file

@ -12,10 +12,6 @@ actions = {
'needs': {
'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',
},
},
},
},
}

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': {},
},
},
'pacman': {
'packages': {
'lm_sensors': {},
},
},
'telegraf': {
'input_plugins': {
'builtin': {

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

@ -57,3 +57,32 @@ svc_systemd = {
},
},
}
if node.metadata.get('matrix-synapse/sliding_sync/version', None):
files['/usr/local/bin/matrix-sliding-sync'] = {
'content_type': 'download',
'source': 'https://github.com/matrix-org/sliding-sync/releases/download/{}/syncv3_linux_amd64'.format(
node.metadata.get('matrix-synapse/sliding_sync/version'),
),
'content_hash': node.metadata.get('matrix-synapse/sliding_sync/sha1', None),
'mode': '0755',
'triggers': {
'svc_systemd:matrix-sliding-sync:restart',
},
}
files['/usr/local/lib/systemd/system/matrix-sliding-sync.service'] = {
'content_type': 'mako',
'triggers': {
'action:systemd-reload',
'svc_systemd:matrix-sliding-sync:restart',
},
}
svc_systemd['matrix-sliding-sync'] = {
'needs': {
'file:/usr/local/bin/matrix-sliding-sync',
'file:/usr/local/lib/systemd/system/matrix-sliding-sync.service',
'postgres_db:synapse',
},
}

View file

@ -88,6 +88,14 @@ def nginx(metadata):
if not node.has_bundle('nginx'):
raise DoNotRunAgain
wellknown_client_sliding_sync = {}
if metadata.get('matrix-synapse/sliding_sync/version', None):
wellknown_client_sliding_sync = {
'org.matrix.msc3575.proxy': {
'url': 'https://{}'.format(metadata.get('matrix-synapse/baseurl')),
},
}
wellknown = {
'/.well-known/matrix/client': {
'content': dumps({
@ -97,6 +105,7 @@ def nginx(metadata):
'm.identity_server': {
'base_url': metadata.get('matrix-synapse/identity_server', 'https://matrix.org'),
},
**wellknown_client_sliding_sync,
**metadata.get('matrix-synapse/additional_client_config', {}),
}, sort_keys=True),
'return': 200,
@ -125,6 +134,9 @@ def nginx(metadata):
'target': 'http://[::1]:20080',
'max_body_size': '50M',
},
'/_matrix/client/unstable/org.matrix.msc3575/sync': {
'target': 'http://127.0.0.1:20070',
},
'/_synapse': {
'target': 'http://[::1]:20080',
},

View file

@ -45,9 +45,6 @@ defaults = {
'pwd': '/var/www/nextcloud',
'user': 'www-data',
'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():
data['mount'] = mount
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),
'directory:{}'.format(data['mountpoint']),
'svc_systemd:systemd-networkd',
'pkg_apt:nfs-common',
package,
},
}
else:
@ -53,7 +58,7 @@ for mount, data in node.metadata.get('nfs-client/mounts',{}).items():
'file:/etc/systemd/system/{}.mount'.format(unitname),
'directory:{}'.format(data['mountpoint']),
'svc_systemd:systemd-networkd',
'pkg_apt:nfs-common',
package,
},
}

View file

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

View file

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

View file

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

View file

@ -10,6 +10,23 @@ defaults = {
'blocked_v4': repo.libs.firewall.global_ip4_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'):

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};
pid /var/run/nginx.pid;
@ -26,7 +26,7 @@ http {
send_timeout 10;
access_log off;
error_log /dev/null;
error_log off;
client_body_buffer_size 16K;
client_header_buffer_size 4k;

View file

@ -1,5 +1,12 @@
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 = {
'/etc/nginx/sites': {
'purge': True,
@ -17,9 +24,9 @@ directories = {
},
},
'/var/log/nginx-timing': {
'owner': 'www-data',
'owner': username,
'needs': {
'pkg_apt:nginx',
package,
},
},
'/var/www': {},
@ -33,6 +40,7 @@ files = {
'/etc/nginx/nginx.conf': {
'content_type': 'mako',
'context': {
'username': username,
**node.metadata['nginx'],
},
'triggers': {
@ -61,13 +69,21 @@ files = {
'/var/www/error.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',
},
}
svc_systemd = {
'nginx': {
'needs': {
'action:generate-dhparam',
'directory:/var/log/nginx-timing',
'pkg_apt:nginx',
package,
},
},
}
@ -104,7 +120,7 @@ for vhost, config in node.metadata.get('nginx/vhosts', {}).items():
'context': {
'create_logs': config.get('create_logs', False),
'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,
'vhost': vhost,
**config,

View file

@ -33,6 +33,11 @@ defaults = {
'nginx': {
'worker_connections': 768,
},
'pacman': {
'packages': {
'nginx': {},
},
},
}
if node.has_bundle('telegraf'):

View file

@ -27,22 +27,29 @@ files = {
},
}
if node.has_bundle('pacman'):
package = 'pkg_pacman:openssh'
service = 'sshd'
else:
package = 'pkg_apt:openssh-server'
service = 'ssh'
actions = {
'sshd_check_config': {
'command': 'sshd -T -C user=root -C host=localhost -C addr=localhost',
'triggered': True,
'triggers': {
'svc_systemd:ssh:restart',
'svc_systemd:{}:restart'.format(service),
},
},
}
svc_systemd = {
'ssh': {
service: {
'needs': {
'file:/etc/systemd/system/ssh.service.d/bundlewrap.conf',
'file:/etc/ssh/sshd_config',
'pkg_apt:openssh-server',
package,
},
},
}

View file

@ -8,6 +8,11 @@ defaults = {
'openssh-sftp-server': {},
},
},
'pacman': {
'packages': {
'openssh': {},
},
},
}
@metadata_reactor.provides(

View file

@ -0,0 +1,38 @@
#!/bin/bash
statusfile="/var/tmp/unattended_upgrades.status"
if ! [[ -f "$statusfile" ]]
then
echo "Status file not found"
exit 3
fi
mtime=$(stat -c %Y $statusfile)
now=$(date +%s)
if (( $now - $mtime > 60*60*24*8 ))
then
echo "Status file is older than 8 days!"
exit 3
fi
exitcode=$(cat $statusfile)
case "$exitcode" in
abort_ssh)
echo "Upgrades skipped due to active SSH login"
exit 1
;;
0)
if [[ -f /var/run/reboot-required ]]
then
echo "OK, but updates require a reboot"
exit 1
else
echo "OK"
exit 0
fi
;;
*)
echo "Last exitcode was $exitcode"
exit 2
;;
esac

View file

@ -0,0 +1,18 @@
#!/bin/bash
set -xeuo pipefail
pacman -Syu --noconfirm --noprogressbar
% for affected, restarts in sorted(restart_triggers.items()):
up_since=$(systemctl show "${affected}" | sed -n 's/^ActiveEnterTimestamp=//p' || echo 0)
up_since_ts=$(date -d "$up_since" +%s || echo 0)
now=$(date +%s)
if [ $((now - up_since_ts)) -lt 3600 ]
then
% for restart in sorted(restarts):
systemctl restart "${restart}" || true
% endfor
fi
% endfor

View file

@ -0,0 +1,2 @@
# just disable faillock.
deny = 0

View file

@ -0,0 +1,52 @@
[options]
Architecture = auto
CheckSpace
Color
HoldPkg = ${' '.join(sorted(node.metadata.get('pacman/ask_before_removal')))}
ILoveCandy
IgnorePkg = ${' '.join(sorted(node.metadata.get('pacman/ignore_packages', set())))}
LocalFileSigLevel = Optional
NoExtract=${' '.join(sorted(node.metadata.get('pacman/no_extract', set())))}
ParallelDownloads = ${node.metadata.get('pacman/parallel_downloads')}
SigLevel = Required DatabaseOptional
VerbosePkgLists
% for line in sorted(node.metadata.get('pacman/additional_config', set())):
${line}
% endfor
[core]
Server = ${node.metadata.get('pacman/repository')}
Include = /etc/pacman.d/mirrorlist
[extra]
Server = ${node.metadata.get('pacman/repository')}
Include = /etc/pacman.d/mirrorlist
[community]
Server = ${node.metadata.get('pacman/repository')}
Include = /etc/pacman.d/mirrorlist
% if node.metadata.get('pacman/enable_multilib', False):
[multilib]
Server = ${node.metadata.get('pacman/repository')}
Include = /etc/pacman.d/mirrorlist
% endif
% if node.metadata.get('pacman/enable_aurto', True):
[aurto]
Server = https://aurto.kunbox.net/
SigLevel = Optional TrustAll
% endif
% if node.has_bundle('zfs'):
[archzfs]
Server = http://archzfs.com/archzfs/x86_64
% if node.metadata.get('pacman/linux-lts', False):
[zfs-linux-lts]
% else:
[zfs-linux]
% endif
Server = http://kernels.archzfs.com/$repo/
% endif

View file

@ -0,0 +1,49 @@
#!/bin/bash
# With systemd, we can force logging to the journal. This is better than
# spamming the world with cron mails. You can then view these logs using
# "journalctl -rat upgrade-and-reboot".
if which logger >/dev/null 2>&1
then
# Dump stdout and stderr to logger, which will then put everything
# into the journal.
exec 1> >(logger -t upgrade-and-reboot -p user.info)
exec 2> >(logger -t upgrade-and-reboot -p user.error)
fi
. /etc/upgrade-and-reboot.conf
echo "Starting upgrade-and-reboot for node $nodename ..."
statusfile="/var/tmp/unattended_upgrades.status"
# Workaround, because /var/tmp is usually 1777
[[ "$UID" == 0 ]] && chown root:root "$statusfile"
logins=$(ps h -C sshd -o euser | awk '$1 != "root" && $1 != "sshd" && $1 != "sshmon" && $1 != "nobody"')
if [[ -n "$logins" ]]
then
echo "Will abort now, there are active SSH logins: $logins"
echo "abort_ssh" > "$statusfile"
exit 1
fi
softlockdir=/var/lib/bundlewrap/soft-$nodename
mkdir -p "$softlockdir"
printf '{"comment": "UPDATE", "date": %s, "expiry": %s, "id": "UNATTENDED", "items": ["*"], "user": "root@localhost"}\n' \
$(date +%s) \
$(date -d 'now + 30 mins' +%s) \
>"$softlockdir"/UNATTENDED
trap 'rm -f "$softlockdir"/UNATTENDED' EXIT
do-unattended-upgrades
ret=$?
echo "$ret" > "$statusfile"
if (( $ret != 0 ))
then
exit 1
fi
systemctl reboot
echo "upgrade-and-reboot for node $nodename is DONE"

View file

@ -0,0 +1,3 @@
nodename="${node.name}"
reboot_mail_to="${node.metadata.get('apt/unattended-upgrades/reboot_mail_to', '')}"
auto_reboot_enabled="${node.metadata.get('apt/unattended-upgrades/reboot_enabled', True)}"

113
bundles/pacman/items.py Normal file
View file

@ -0,0 +1,113 @@
from bundlewrap.exceptions import BundleError
if not node.os == 'arch':
raise BundleError(f'{node.name}: bundle:pacman requires arch linux')
files = {
'/etc/pacman.conf': {
'content_type': 'mako',
},
'/etc/upgrade-and-reboot.conf': {
'content_type': 'mako',
},
'/etc/security/faillock.conf': {},
'/usr/local/sbin/upgrade-and-reboot': {
'mode': '0700',
},
'/usr/local/sbin/do-unattended-upgrades': {
'content_type': 'mako',
'mode': '0700',
'context': {
'restart_triggers': node.metadata.get('pacman/restart_triggers', {}),
}
},
'/usr/local/share/icinga/plugins/check_unattended_upgrades': {
'mode': '0755',
},
}
svc_systemd['paccache.timer'] = {
'needs': {
'pkg_pacman:pacman-contrib',
},
}
pkg_pacman = {
'at': {},
'autoconf': {},
'automake': {},
'bind': {},
'binutils': {},
'bison': {},
'bzip2': {},
'curl': {},
'dialog': {},
'diffutils': {},
'fakeroot': {},
'file': {},
'findutils': {},
'flex': {},
'fwupd': {},
'gawk': {},
'gcc': {},
'gettext': {},
'git': {},
'gnu-netcat': {},
'grep': {},
'groff': {},
'gzip': {},
'htop': {},
'jq': {},
'ldns': {},
'less': {},
'libtool': {},
'logrotate': {},
'lsof': {},
'm4': {},
'mailutils': {},
'make': {},
'man-db': {},
'man-pages': {},
'moreutils': {},
'mtr': {},
'ncdu': {},
'nmap': {},
'pacman-contrib': {},
'patch': {},
'pkgconf': {},
'python': {},
'python-setuptools': {
'needed_by': {
'pkg_pip:',
},
},
'python-pip': {
'needed_by': {
'pkg_pip:',
},
},
'python-virtualenv': {},
'rsync': {},
'run-parts': {},
'sed': {},
'tar': {},
'texinfo': {},
'tmux': {},
'tree': {},
'unzip': {},
'vim': {},
'wget': {},
'which': {},
'whois': {},
'zip': {},
}
if node.metadata.get('pacman/linux-lts', False):
pkg_pacman['linux-lts'] = {}
pkg_pacman['acpi_call-lts'] = {}
else:
pkg_pacman['linux'] = {}
pkg_pacman['acpi_call'] = {}
for pkg, config in node.metadata.get('pacman/packages', {}).items():
pkg_pacman[pkg] = config

View file

@ -0,0 +1,54 @@
defaults = {
'pacman': {
'ask_before_removal': {
'glibc',
'pacman',
},
'no_extract': {
'etc/cron.d/0hourly',
# don't install systemd-homed pam module. It produces a lot of spam in
# journal about systemd-homed not being active, so just get rid of it.
# Requires reinstall of systemd package, though
'usr/lib/security/pam_systemd_home.so',
},
'parallel_downloads': 4,
'repository': 'http://ftp.uni-kl.de/pub/linux/archlinux/$repo/os/$arch',
'unattended-upgrades': {
'day': 5,
'hour': 21,
},
},
}
@metadata_reactor.provides(
'cron/jobs/upgrade-and-reboot',
'icinga2_api/pacman/services',
)
def patchday(metadata):
if not metadata.get('pacman/unattended-upgrades/is_enabled', False):
return {}
day = metadata.get('pacman/unattended-upgrades/day')
hour = metadata.get('pacman/unattended-upgrades/hour')
return {
'cron': {
'jobs': {
'upgrade-and-reboot': '{minute} {hour} * * {day} root /usr/local/sbin/upgrade-and-reboot'.format(
minute=node.magic_number % 30,
hour=hour,
day=day,
),
},
},
'icinga2_api': {
'pacman': {
'services': {
'UNATTENDED UPGRADES': {
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_unattended_upgrades',
},
},
},
},
}

View file

@ -12,7 +12,7 @@ PAPERLESS_CONSUMPTION_DIR=/mnt/paperless/consume
PAPERLESS_DATA_DIR=/mnt/paperless/data
PAPERLESS_MEDIA_ROOT=/mnt/paperless/media
PAPERLESS_STATICDIR=/opt/paperless/src/paperless-ngx/static
PAPERLESS_FILENAME_FORMAT={{ created_year }}/{{ created_month }}/{{ correspondent }}/{{ asn }}_{{ title }}
PAPERLESS_FILENAME_FORMAT={created_year}/{created_month}/{correspondent}/{asn}_{title}
# Security and hosting

View file

@ -34,7 +34,7 @@ defaults = {
},
},
'nodejs': {
'version': 22,
'version': 18,
},
'postgresql': {
'roles': {

View file

@ -1,4 +1,4 @@
version = node.metadata.get('php/__version')
version = node.metadata.get('php/version')
directories['/var/lib/php/sessions'] = {
'owner': 'www-data',

View file

@ -1,11 +1,12 @@
OS_PHP_VERSION = {
12: '8.2',
13: '8.4',
}
defaults = {
'php': {
'__version': OS_PHP_VERSION[node.os_version[0]],
'apt': {
'repos': {
'php': {
'items': {
'deb https://packages.sury.org/php/ {os_release} main',
},
},
},
},
}
@ -14,7 +15,7 @@ defaults = {
'apt/packages',
)
def php_packages_with_features(metadata):
version = metadata.get('php/__version')
version = metadata.get('php/version')
packages = {
f'php{version}': {},

View file

@ -0,0 +1,6 @@
[Service]
# arch postfix is not set up for chrooting by default
ExecStartPre=-/usr/sbin/mkdir -p /var/spool/postfix/etc
% for file in ['/etc/localtime', '/etc/nsswitch.conf', '/etc/resolv.conf', '/etc/services']:
ExecStartPre=-/usr/sbin/cp -p ${file} /var/spool/postfix${file}
% endfor

View file

@ -21,12 +21,13 @@ for identifier in node.metadata.get('postfix/mynetworks', set()):
netmask = '128'
mynetworks.add(f'[{ip6}]/{netmask}')
my_package = 'pkg_pacman:postfix' if node.os == 'arch' else 'pkg_apt:postfix'
files = {
'/etc/mailname': {
'content': node.metadata.get('postfix/myhostname'),
'before': {
'pkg_apt:postfix',
my_package,
},
'triggers': {
'svc_systemd:postfix:restart',
@ -81,7 +82,7 @@ actions = {
'command': 'newaliases',
'triggered': True,
'needs': {
'pkg_apt:postfix',
my_package,
},
'before': {
'svc_systemd:postfix',
@ -91,7 +92,7 @@ actions = {
'command': 'postmap hash:/etc/postfix/blocked_recipients',
'triggered': True,
'needs': {
'pkg_apt:postfix',
my_package,
},
'before': {
'svc_systemd:postfix',
@ -104,7 +105,17 @@ svc_systemd = {
'needs': {
'file:/etc/postfix/master.cf',
'file:/etc/postfix/main.cf',
'pkg_apt:postfix',
my_package,
},
},
}
if node.os == 'arch':
files['/etc/systemd/system/postfix.service.d/bundlewrap.conf'] = {
'source': 'arch-override.conf',
'content_type': 'mako',
'triggers': {
'action:systemd-reload',
'svc_systemd:postfix:restart',
},
}

View file

@ -14,7 +14,7 @@ defaults = {
'postfix': {
'services': {
'POSTFIX PROCESS': {
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_systemd_unit postfix' + ('' if node.os_version >= (13,) else '@-'),
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_systemd_unit postfix' + ('' if node.os == 'arch' else '@-'),
},
'POSTFIX QUEUE': {
'command_on_monitored_host': 'sudo /usr/local/share/icinga/plugins/check_postfix_queue -w 20 -c 40 -d 50',
@ -22,6 +22,12 @@ defaults = {
},
},
},
'pacman': {
'packages': {
'postfix': {},
's-nail': {},
},
},
}
if node.has_bundle('postfixadmin'):

View file

@ -14,7 +14,7 @@ defaults = {
},
},
'nodejs': {
'version': 22,
'version': 18,
},
'users': {
'powerdnsadmin': {

View file

@ -116,7 +116,7 @@ svc_systemd = {
'pretalx-worker': {
'needs': {
'action:pretalx_install',
'action:pretalx_migrate',
'action:pretalx_migrate',,
'action:pretalx_rebuild',
'file:/etc/systemd/system/pretalx-worker.service',
'file:/opt/pretalx/pretalx.cfg',

View file

@ -27,7 +27,7 @@ defaults = {
},
},
'nodejs': {
'version': 22,
'version': 18,
},
'pretalx': {
'database': {

View file

@ -10,13 +10,11 @@ interface ${interface}
AdvOnLink on;
AdvAutonomous on;
AdvRouterAddr on;
AdvPreferredLifetime 600;
AdvValidLifetime 900;
};
% if config.get('rdnss'):
RDNSS ${' '.join(sorted(config['rdnss']))}
{
AdvRDNSSLifetime 600;
AdvRDNSSLifetime 900;
};
% endif
};

View file

@ -1,5 +1,3 @@
from bundlewrap.metadata import atomic
defaults = {
'apt': {
'packages': {
@ -50,16 +48,3 @@ if node.has_bundle('telegraf'):
},
},
}
@metadata_reactor.provides(
'firewall/port_rules',
)
def firewall(metadata):
return {
'firewall': {
'port_rules': {
'6379/tcp': atomic(metadata.get('redis/restrict-to', set())),
},
},
}

View file

@ -96,7 +96,7 @@ if 'dkim' in node.metadata.get('rspamd', {}):
},
}
dkim_key = repo.libs.faults.ensure_fault_or_none(node.metadata.get('rspamd/dkim'))
dkim_key = repo.libs.faults.ensure_fault_or_none(node.metadata['rspamd']['dkim'])
actions = {
'rspamd_assure_dkim_key_permissions': {

View file

@ -13,13 +13,6 @@ map to guest = bad user
load printers = no
usershare allow guests = yes
allow insecure wide links = yes
min protocol = SMB2
% if timemachine:
vfs objects = fruit
fruit:aapl = yes
fruit:copyfile = yes
fruit:model = MacSamba
% endif
% for name, opts in sorted(node.metadata.get('samba/shares', {}).items()):
[${name}]
@ -44,24 +37,3 @@ follow symlinks = yes
wide links = yes
% endif
% endfor
% for name in sorted(timemachine):
[timemachine-${name}]
comment = Time Machine backup for ${name}
available = yes
browseable = yes
guest ok = no
read only = false
valid users = timemachine-${name}
path = /srv/timemachine/${name}
durable handles = yes
vfs objects = catia fruit streams_xattr
fruit:delete_empty_adfiles = yes
fruit:metadata = stream
fruit:posix_rename = yes
fruit:time machine = yes
fruit:time machine max size = 2000G
fruit:veto_appledouble = no
fruit:wipe_intentionally_left_blank_rfork = yes
% endfor

View file

@ -1,21 +0,0 @@
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h</name>
<service>
<type>_smb._tcp</type>
<port>445</port>
</service>
<service>
<type>_device-info._tcp</type>
<port>0</port>
<txt-record>model=RackMac1,2</txt-record>
</service>
<service>
<type>_adisk._tcp</type>
% for idx, share_name in enumerate(sorted(shares)):
<txt-record>dk${idx}=adVN=timemachine-${share_name},adVF=0x82</txt-record>
% endfor
<txt-record>sys=waMa=0,adVF=0x100</txt-record>
</service>
</service-group>

View file

@ -11,14 +11,9 @@ svc_systemd = {
},
}
timemachine_shares = node.metadata.get('samba/timemachine-shares', set())
files = {
'/etc/samba/smb.conf': {
'content_type': 'mako',
'context': {
'timemachine': timemachine_shares,
},
'triggers': {
'svc_systemd:nmbd:restart',
'svc_systemd:smbd:restart',
@ -62,30 +57,3 @@ for user, uconfig in node.metadata.get('users', {}).items():
last_action = {
f'action:smbpasswd_for_user_{user}',
}
if timemachine_shares:
assert node.has_bundle('avahi-daemon'), f'{node.name}: samba needs avahi-daemon to publish time machine shares'
for share, share_config in node.metadata.get('samba/shares', {}).items():
assert not share_config.get('guest_ok', True), f'{node.name} samba {share}: cannot have time machine shares and "guest ok" shares on the same machine'
files['/etc/avahi/services/timemachine.service'] = {
'content_type': 'mako',
'context': {
'shares': timemachine_shares,
},
}
for share_name in timemachine_shares:
users[f'timemachine-{share_name}'] = {
'home': f'/srv/timemachine/{share_name}',
}
directories[f'/srv/timemachine/{share_name}'] = {
'owner': f'timemachine-{share_name}',
'group': f'timemachine-{share_name}',
'mode': '0700',
'needs': {
f'zfs_dataset:tank/timemachine/{share_name}',
},
}

View file

@ -24,30 +24,3 @@ def firewall(metadata):
},
},
}
@metadata_reactor.provides(
'zfs/datasets',
)
def timemachine_zfs(metadata):
shares = metadata.get('samba/timemachine-shares', set())
if not shares:
return {}
assert node.has_bundle('zfs'), f'{node.name}: time machine backups require zfs'
datasets = {
'tank/timemachine': {},
}
for share_name in shares:
datasets[f'tank/timemachine/{share_name}'] = {
'mountpoint': f'/srv/timemachine/{share_name}',
}
return {
'zfs': {
'datasets': datasets,
},
}

View file

@ -38,10 +38,10 @@ try:
for i in releases:
if i["tag_name"].startswith(tag_prefix):
if not (i["prerelease"] or i["draft"]) and (
if (
newest_release is None
or parse(i["tag_name"]) > parse(newest_release["tag_name"])
):
) and not (i["prerelease"] or i["draft"]):
newest_release = i
assert newest_release is not None, "Could not determine latest release"

View file

@ -37,10 +37,10 @@ try:
for i in releases:
if i["tag_name"].startswith(tag_prefix):
if not (i["prerelease"] or i["draft"]) and (
if (
newest_release is None
or parse(i["tag_name"]) > parse(newest_release["tag_name"])
):
) and not (i["prerelease"] or i["draft"]):
newest_release = i
assert newest_release is not None, "Could not determine latest release"

View file

@ -64,3 +64,12 @@ for check in {
files["/usr/local/share/icinga/plugins/check_{}".format(check)] = {
'mode': "0755",
}
if node.has_bundle('pacman'):
symlinks['/usr/lib/nagios/plugins'] = {
'target': '/usr/lib/monitoring-plugins',
'needs': {
'pkg_pacman:monitoring-plugins',
},
}

View file

@ -36,6 +36,14 @@ defaults = {
'sshmon',
},
},
'pacman': {
'packages': {
'gawk': {},
'perl-libwww': {},
'monitoring-plugins': {},
'python-requests': {},
},
},
}

View file

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

View file

@ -0,0 +1,13 @@
title ${config['title']}
% if 'linux' in config:
linux ${config['linux']}
% for line in config['initrd']:
initrd ${line}
% endfor
% if config.get('options', set()):
options ${' '.join(sorted(config['options']))}
% endif
% else:
efi ${config['efi']}
% endif

View file

@ -0,0 +1,5 @@
auto-entries no
auto-firmware yes
console-mode keep
default ${config['default']}
timeout ${config.get('timeout', 5)}

View file

@ -0,0 +1,9 @@
[Trigger]
Type = Package
Operation = Upgrade
Target = systemd
[Action]
Description = Gracefully upgrading systemd-boot...
When = PostTransaction
Exec = /usr/bin/systemctl restart systemd-boot-update.service

View file

@ -0,0 +1,32 @@
assert node.os == 'arch'
assert node.metadata.get('systemd-boot/default') in node.metadata.get('systemd-boot/entries')
files = {
'/etc/pacman.d/hooks/99-systemd-boot-update': {
'source': 'pacman_hook',
},
'/boot/loader/loader.conf': {
'content_type': 'mako',
'context': {
'config': node.metadata.get('systemd-boot'),
},
'mode': None,
},
}
directories = {
'/boot/loader/entries': {
'purge': True,
},
}
for entry, config in node.metadata.get('systemd-boot/entries').items():
files[f'/boot/loader/entries/{entry}.conf'] = {
'source': 'entry',
'content_type': 'mako',
'context': {
'entry': entry,
'config': config,
},
'mode': None,
}

View file

@ -7,11 +7,8 @@
[Unit]
Description=Service for Timer ${timer}
After=network.target
% if config.get('requires', set()):
Requires=${' '.join(sorted(config['requires']))}
% endif
% if config.get('requisite', set()):
Requisite=${' '.join(sorted(config['requisite']))}
% if config.get('requires', ''):
Requires=${config['requires']}
% endif
[Service]

View file

@ -25,4 +25,14 @@ defaults = {
},
},
},
'pacman': {
'packages': {
'telegraf-bin': {
'needed_by': {
'svc_systemd:telegraf',
'user:telegraf',
},
},
},
},
}

View file

@ -7,6 +7,11 @@ defaults = {
'kitty-terminfo': {},
},
},
'pacman': {
'packages': {
'kitty-terminfo': {},
},
},
'users': {
'root': {
'home': '/root',

View file

@ -24,3 +24,12 @@ if node.has_bundle('nftables') and node.has_bundle('apt'):
'svc_systemd:nftables:reload',
},
}
if node.has_bundle('pacman'):
svc_systemd['libvirtd'] = {
'running': None, # triggered via .socket
}
svc_systemd['virtlogd'] = {
'running': None, # triggered via .socket
'enabled': None, # triggered via .socket
}

View file

@ -21,6 +21,12 @@ defaults = {
},
},
},
'pacman': {
'packages': {
'edk2-ovmf': {},
'libvirt': {},
},
},
}
if node.os == 'debian' and node.os_version[0] < 11:
@ -36,6 +42,9 @@ if node.has_bundle('nftables'):
},
}
if node.has_bundle('arch-with-gui'):
defaults['pacman']['packages']['virt-manager'] = {}
@metadata_reactor.provides(
'users',

View file

@ -0,0 +1,16 @@
[Unit]
Description=CRS runner for ${script}
After=network.target
[Service]
User=voc
Group=voc
EnvironmentFile=/etc/default/crs-worker
ExecStart=/opt/crs-scripts/bin/crs_run ${script}
WorkingDirectory=/opt/crs-scripts
Restart=on-failure
RestartSec=10
SyslogIdentifier=crs-${worker}
[Install]
WantedBy=crs-worker.target

View file

@ -0,0 +1,6 @@
CRS_TRACKER=${url}
CRS_TOKEN=${token}
CRS_SECRET=${secret}
% if use_vaapi:
CRS_USE_VAAPI=yes
% endif

View file

@ -0,0 +1,56 @@
paths = { # subpaths of /video
'capture',
'encoded',
'fuse',
'intros',
'repair',
'tmp',
}
directories = {
'/opt/crs-scripts': {},
}
for path in paths:
directories[f'/video/{path}'] = {
'owner': 'voc',
'group': 'voc',
}
git_deploy = {
'/opt/crs-scripts': {
'repo': 'https://github.com/crs-tools/crs-scripts.git',
'rev': 'master',
},
}
files = {
'/etc/default/crs-worker': {
'content_type': 'mako',
'source': 'environment',
'context': node.metadata.get('voc-tracker-worker'),
},
}
for worker, script in {
'recording-scheduler': 'script-A-recording-scheduler.pl',
'mount4cut': 'script-B-mount4cut.pl',
'cut-postprocessor': 'script-C-cut-postprocessor.pl',
'encoding': 'script-D-encoding.pl',
'postencoding': 'script-E-postencoding-auphonic.pl',
'postprocessing': 'script-F-postprocessing-upload.pl',
}.items():
files[f'/etc/systemd/system/crs-{worker}.service'] = {
'content_type': 'mako',
'source': 'crs-runner.service',
'context': {
'worker': worker,
'script': script,
},
'needs': {
'file:/etc/default/crs-worker',
},
'triggers': {
'action:systemd-reload',
},
}

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