Compare commits

..

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

444 changed files with 6272 additions and 8379 deletions

9
Jenkinsfile vendored
View file

@ -25,6 +25,15 @@ pipeline {
""" """
} }
} }
stage('syntax checking using isort') {
steps {
sh """
. venv/bin/activate
isort --check .
"""
}
}
stage('config and metadata determinism') { stage('config and metadata determinism') {
steps { steps {
sh """ sh """

View file

@ -30,13 +30,13 @@ 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 |
| 20090 | matrix-media-repo | prometheus metrics | | 20090 | matrix-media-repo | prometheus metrics |
| 21000 | pleroma | pleroma |
| 21010 | grafana | grafana | | 21010 | grafana | grafana |
| 22000 | forgejo | forgejo | | 22000 | gitea | forgejo |
| 22010 | jenkins-ci | Jenkins CI | | 22010 | jenkins-ci | Jenkins CI |
| 22020 | travelynx | Travelynx Web | | 22020 | travelynx | Travelynx Web |
| 22030 | octoprint | OctoPrint Web Interface | | 22030 | octoprint | OctoPrint Web Interface |
@ -45,9 +45,6 @@ Rule of thumb: keep ports below 10000 free for stuff that reserves ports.
| 22060 | pretalx | gunicorn | | 22060 | pretalx | gunicorn |
| 22070 | paperless-ng | gunicorn | | 22070 | paperless-ng | gunicorn |
| 22080 | netbox | gunicorn | | 22080 | netbox | gunicorn |
| 22090 | jugendhackt_tools | gunicorn |
| 22100 | powerdnsadmin | gunicorn |
| 22110 | icinga2-statuspage | gunicorn |
| 22999 | nginx | stub_status | | 22999 | nginx | stub_status |
| 22100 | ntfy | http | | 22100 | ntfy | http |

View file

@ -7,16 +7,3 @@ onto shared webhosting.
`bw test` runs according to Jenkinsfile after every commit. `bw test` runs according to Jenkinsfile after every commit.
[![Build Status](https://jenkins.franzi.business/buildStatus/icon?job=kunsi%2Fbundlewrap%2Fmain)](https://jenkins.franzi.business/job/kunsi/job/bundlewrap/job/main/) [![Build Status](https://jenkins.franzi.business/buildStatus/icon?job=kunsi%2Fbundlewrap%2Fmain)](https://jenkins.franzi.business/job/kunsi/job/bundlewrap/job/main/)
## automatix
Ensure you set `bundlewrap: true` in your `~/.automatix.cfg.yaml`.
## system naming
All systems should be named after their location and use.
For example, influxdb hosted at hetzner cloud will be `htz-cloud.influxdb`.
The only exception to this are name servers, they are named after [demons
in fiction](https://en.wikipedia.org/wiki/List_of_demons_in_fiction).

View file

@ -1,45 +0,0 @@
name: Upgrade to debian bullseye
systems:
node: foonode
always:
- has_zfs=python: NODES.node.has_bundle('zfs')
pipeline:
- manual: "set icinga2 downtime: https://icinga.franzi.business/monitoring/host/schedule-downtime?host={SYSTEMS.node}"
# apply first so we only see the upgrade changes later
- local: bw apply {SYSTEMS.node}
- manual: update debian version in node groups
- local: "bw apply -o bundle:apt -s symlink:/usr/bin/python pkg_apt: -- {SYSTEMS.node}"
# double time!
- remote@node: DEBIAN_FRONTEND=noninteractive apt-get -y -q -o Dpkg::Options::=--force-confold dist-upgrade
- remote@node: DEBIAN_FRONTEND=noninteractive apt-get -y -q -o Dpkg::Options::=--force-confold dist-upgrade
# reboot into bullseye
- remote@node: systemctl reboot
- local: |
exit=1
while [[ $exit -ne 0 ]];
do
sleep 1
ssh {SYSTEMS.node} true
exit=$?
done
# fix zfs and reboot again
- has_zfs?remote@node: zpool import tank -f
- has_zfs?remote@node: zpool upgrade -a
- has_zfs?remote@node: systemctl reboot
- has_zfs?local: |
exit=1
while [[ $exit -ne 0 ]];
do
sleep 1
ssh {SYSTEMS.node} true
exit=$?
done
# final apply
- local: bw apply {SYSTEMS.node}

View file

@ -1,9 +0,0 @@
% for uri in sorted(uris):
Types: ${' '.join(sorted(data.get('types', {'deb'})))}
URIs: ${uri}
Suites: ${os_release}
Components: ${' '.join(sorted(data.get('components', {'main'})))}
Architectures: ${' '.join(sorted(data.get('architectures', {'amd64'})))}
Signed-By: /etc/apt/trusted.gpg.d/${name}.list.asc
% endfor

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/ bookworm main non-free contrib non-free-firmware
deb http://security.debian.org/debian-security bookworm-security main contrib non-free
deb http://deb.debian.org/debian/ bookworm-updates main contrib non-free

View file

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

View file

@ -46,6 +46,10 @@ fi
if [[ -f /var/run/reboot-required ]] && [[ "$auto_reboot_enabled" == "True" ]] if [[ -f /var/run/reboot-required ]] && [[ "$auto_reboot_enabled" == "True" ]]
then then
if [[ -n "$reboot_mail_to" ]]
then
date | mail -s "SYSREBOOTNOW $nodename" "$reboot_mail_to"
fi
systemctl reboot systemctl reboot
fi fi

View file

@ -1,2 +1,3 @@
nodename="${node.name}" 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)}" auto_reboot_enabled="${node.metadata.get('apt/unattended-upgrades/reboot_enabled', True)}"

View file

@ -4,9 +4,11 @@ supported_os = {
'debian': { 'debian': {
10: 'buster', 10: 'buster',
11: 'bullseye', 11: 'bullseye',
12: 'bookworm',
99: 'unstable', 99: 'unstable',
}, },
'raspbian': {
10: 'buster',
},
} }
try: try:
@ -24,10 +26,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 = {
@ -115,7 +113,7 @@ pkg_apt = {
'mtr': {}, 'mtr': {},
'ncdu': {}, 'ncdu': {},
'ncurses-term': {}, 'ncurses-term': {},
'netcat-openbsd': {}, 'netcat': {},
'nmap': {}, 'nmap': {},
'python3': {}, 'python3': {},
'python3-dev': {}, 'python3-dev': {},
@ -154,9 +152,6 @@ pkg_apt = {
'popularity-contest': { 'popularity-contest': {
'installed': False, 'installed': False,
}, },
'python3-packaging': {
'installed': False,
},
'unattended-upgrades': { 'unattended-upgrades': {
'installed': False, 'installed': False,
}, },
@ -173,44 +168,21 @@ if node.os_version[0] >= 11:
} }
for name, data in node.metadata.get('apt/repos', {}).items(): for name, data in node.metadata.get('apt/repos', {}).items():
if 'items' in data: files['/etc/apt/sources.list.d/{}.list'.format(name)] = {
files['/etc/apt/sources.list.d/{}.list'.format(name)] = { 'content_type': 'mako',
'content_type': 'mako', 'content': ("\n".join(sorted(data['items']))).format(
'content': ("\n".join(sorted(data['items']))).format( os=node.os,
os=node.os, os_release=supported_os[node.os][node.os_version[0]],
os_release=supported_os[node.os][node.os_version[0]], ),
), 'triggers': {
'triggers': { 'action:apt_update',
'action:apt_update', },
}, }
}
elif 'uris' in data:
uris = {
x.format(
os=node.os,
os_release=supported_os[node.os][node.os_version[0]],
) for x in data['uris']
}
files['/etc/apt/sources.list.d/{}.sources'.format(name)] = {
'source': 'deb822-sources',
'content_type': 'mako',
'context': {
'data': data,
'name': name,
'os_release': supported_os[node.os][node.os_version[0]],
'uris': uris,
},
'triggers': {
'action:apt_update',
},
}
if data.get('install_gpg_key', True): if data.get('install_gpg_key', True):
if 'items' in data: files['/etc/apt/sources.list.d/{}.list'.format(name)]['needs'] = {
files['/etc/apt/sources.list.d/{}.list'.format(name)]['needs'] = { 'file:/etc/apt/trusted.gpg.d/{}.list.asc'.format(name),
'file:/etc/apt/trusted.gpg.d/{}.list.asc'.format(name), }
}
files['/etc/apt/trusted.gpg.d/{}.list.asc'.format(name)] = { files['/etc/apt/trusted.gpg.d/{}.list.asc'.format(name)] = {
'source': 'gpg-keys/{}.asc'.format(name), 'source': 'gpg-keys/{}.asc'.format(name),

View file

@ -21,24 +21,16 @@ 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')
spread = metadata.get('apt/unattended-upgrades/spread_in_group', None)
if spread is not None:
spread_nodes = sorted(repo.nodes_in_group(spread))
day += spread_nodes.index(node)
return { return {
'cron': { 'cron': {
'jobs': { 'jobs': {
'upgrade-and-reboot': '{minute} {hour} * * {day} root /usr/local/sbin/upgrade-and-reboot'.format( 'upgrade-and-reboot': '{minute} {hour} * * {day} root /usr/local/sbin/upgrade-and-reboot'.format(
minute=node.magic_number % 30, minute=node.magic_number % 30,
hour=hour, hour=hour,
day=day%7, day=day,
), ),
}, },
}, },

View file

@ -1,5 +0,0 @@
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

@ -44,11 +44,6 @@ directories = {
} }
svc_systemd = { svc_systemd = {
'avahi-daemon': {
'needs': {
'pkg_pacman:avahi',
},
},
'sddm': { 'sddm': {
'needs': { 'needs': {
'pkg_pacman:sddm', 'pkg_pacman:sddm',
@ -66,8 +61,6 @@ git_deploy = {
}, },
} }
files['/etc/pipewire/pipewire-pulse.conf.d/50-network.conf'] = {}
for filename in listdir(join(repo.path, 'data', 'arch-with-gui', 'files', 'fonts')): for filename in listdir(join(repo.path, 'data', 'arch-with-gui', 'files', 'fonts')):
if filename.startswith('.'): if filename.startswith('.'):
continue continue

View file

@ -9,14 +9,6 @@ defaults = {
'icinga_options': { 'icinga_options': {
'exclude_from_monitoring': True, 'exclude_from_monitoring': True,
}, },
'nftables': {
'input': {
'50-avahi': {
'udp dport 5353 accept',
'udp sport 5353 accept',
},
},
},
'pacman': { 'pacman': {
'packages': { 'packages': {
# fonts # fonts
@ -31,9 +23,8 @@ defaults = {
'sddm': {}, 'sddm': {},
# networking # networking
'avahi': {},
'netctl': {}, 'netctl': {},
'util-linux': {}, # provides rfkill 'rfkill': {},
'wpa_supplicant': {}, 'wpa_supplicant': {},
'wpa_actiond': {}, 'wpa_actiond': {},
@ -54,7 +45,6 @@ defaults = {
'pipewire': {}, 'pipewire': {},
'pipewire-jack': {}, 'pipewire-jack': {},
'pipewire-pulse': {}, 'pipewire-pulse': {},
'pipewire-zeroconf': {},
'qpwgraph': {}, 'qpwgraph': {},
# window management # window management

View file

@ -1,22 +0,0 @@
[server]
host-name=${node.name.split('.')[-1]}
use-ipv4=yes
use-ipv6=yes
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=yes
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,8 +0,0 @@
defaults = {
'apt': {
'packages': {
'avahi-daemon': {},
'libnss-mdns': {},
},
},
}

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

@ -35,15 +35,8 @@ def get_my_clients(metadata):
continue continue
my_clients[rnode.name] = { my_clients[rnode.name] = {
'exclude_from_monitoring': rnode.metadata.get(
'backup-client/exclude_from_monitoring',
rnode.metadata.get(
'icinga_options/exclude_from_monitoring',
False,
),
),
'one_backup_every_hours': rnode.metadata.get('backup-client/one_backup_every_hours', 24),
'user': rnode.metadata.get('backup-client/user-name'), 'user': rnode.metadata.get('backup-client/user-name'),
'one_backup_every_hours': rnode.metadata.get('backup-client/one_backup_every_hours', 24),
'retain': { 'retain': {
'daily': rnode.metadata.get('backups/retain/daily', retain_defaults['daily']), 'daily': rnode.metadata.get('backups/retain/daily', retain_defaults['daily']),
'weekly': rnode.metadata.get('backups/retain/weekly', retain_defaults['weekly']), 'weekly': rnode.metadata.get('backups/retain/weekly', retain_defaults['weekly']),
@ -160,7 +153,7 @@ def monitoring(metadata):
client, client,
config['one_backup_every_hours'], config['one_backup_every_hours'],
), ),
'vars.sshmon_timeout': 40, 'vars.sshmon_timeout': 20,
} }
return { return {

View file

@ -32,8 +32,8 @@ account_guest_in_cpu_meter=0
color_scheme=0 color_scheme=0
enable_mouse=0 enable_mouse=0
delay=10 delay=10
left_meters=Tasks LoadAverage Uptime Memory CPU LeftCPUs2 CPU left_meters=Tasks LoadAverage Uptime Memory CPU LeftCPUs CPU
left_meter_modes=2 2 2 1 1 1 2 left_meter_modes=2 2 2 1 1 1 2
right_meters=Hostname CPU RightCPUs2 right_meters=Hostname CPU RightCPUs
right_meter_modes=2 3 1 right_meter_modes=2 3 1
hide_function_bar=0 hide_function_bar=0

View file

@ -29,19 +29,8 @@ files = {
}, },
} }
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['locale']['installed']):
actions[f'ensure_locale_{locale}_is_enabled'] = { actions[f'ensure_locale_{locale}_is_enabled'] = {
'command': f"sed -i '/{locale}/s/^# *//g' /etc/locale.gen", 'command': f"sed -i '/{locale}/s/^# *//g' /etc/locale.gen",
'unless': f"grep -e '^{locale}' /etc/locale.gen", 'unless': f"grep -e '^{locale}' /etc/locale.gen",
@ -52,15 +41,17 @@ 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 = {
'triggered': True, 'locale-gen': {
'command': 'locale-gen', 'triggered': True,
'command': 'locale-gen',
},
} }
description = [] description = []
if not node.metadata.get('icinga_options/exclude_from_monitoring', False): if not node.metadata.get('icinga_options/exclude_from_monitoring', False):
description.append('icingaweb2: https://icinga.franzi.business/monitoring/host/show?host={}'.format(node.name)) description.append('icingaweb2: https://icinga.kunsmann.eu/monitoring/host/show?host={}'.format(node.name))
if node.has_bundle('telegraf'): if node.has_bundle('telegraf'):
description.append('Grafana: https://grafana.kunsmann.eu/d/{}'.format(UUID(int=node.magic_number).hex[:10])) description.append('Grafana: https://grafana.kunsmann.eu/d/{}'.format(UUID(int=node.magic_number).hex[:10]))

View file

@ -19,9 +19,7 @@ protocol static {
ipv4; ipv4;
% for route in sorted(node.metadata.get('bird/static_routes', set())): % for route in sorted(node.metadata.get('bird/static_routes', set())):
% for name, config in sorted(node.metadata.get('bird/bgp_neighbors', {}).items()): route ${route} via ${node.metadata.get('bird/my_ip')};
route ${route} via ${config['local_ip']};
% endfor
% endfor % endfor
} }
% endif % endif

View file

@ -24,7 +24,7 @@ defaults = {
}, },
'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',
}, },
}, },
@ -43,9 +43,6 @@ def neighbor_info_from_wireguard(metadata):
except NoSuchNode: except NoSuchNode:
continue continue
if not rnode.has_bundle('bird'):
continue
neighbors[name] = { neighbors[name] = {
'local_ip': config['my_ip'], 'local_ip': config['my_ip'],
'local_as': my_as, 'local_as': my_as,
@ -65,10 +62,7 @@ def neighbor_info_from_wireguard(metadata):
) )
def my_ip(metadata): def my_ip(metadata):
if node.has_bundle('wireguard'): if node.has_bundle('wireguard'):
wg_ifaces = sorted({iface for iface in metadata.get('interfaces').keys() if iface.startswith('wg_')}) my_ip = sorted(metadata.get('interfaces/wg0/ips'))[0].split('/')[0]
if not wg_ifaces:
return {}
my_ip = sorted(metadata.get(f'interfaces/{wg_ifaces[0]}/ips'))[0].split('/')[0]
else: else:
my_ip = str(sorted(repo.libs.tools.resolve_identifier(repo, node.name))[0]) my_ip = str(sorted(repo.libs.tools.resolve_identifier(repo, node.name))[0])
@ -90,7 +84,7 @@ def firewall(metadata):
return { return {
'firewall': { 'firewall': {
'port_rules': { 'port_rules': {
'179/tcp': atomic(sources), '179': atomic(sources),
}, },
}, },
} }

View file

@ -1,19 +1,5 @@
from bundlewrap.exceptions import BundleError from bundlewrap.exceptions import BundleError
supported_os = {
'debian': {
10: 'buster',
11: 'bullseye',
12: 'bookworm',
99: 'unstable',
},
}
try:
supported_os[node.os][node.os_version[0]]
except (KeyError, IndexError):
raise BundleError(f'{node.name}: OS {node.os} {node.os_version} is not supported by bundle:apt')
CONFLICTING_BUNDLES = { CONFLICTING_BUNDLES = {
'apt', 'apt',
'nginx', 'nginx',
@ -71,18 +57,6 @@ actions = {
'svc_systemd:', 'svc_systemd:',
}, },
}, },
'apt_update': {
'command': 'apt-get update',
'needed_by': {
'pkg_apt:',
},
'triggered': True,
'cascade_skip': False,
},
'apt_execute_update_commands': {
'command': ' && '.join(sorted(node.metadata.get('apt/additional_update_commands', {'true'}))),
'triggered': True,
},
} }
directories = { directories = {
@ -118,30 +92,6 @@ files = {
}, },
} }
for name, data in node.metadata.get('apt/repos', {}).items():
files['/etc/apt/sources.list.d/{}.list'.format(name)] = {
'content_type': 'mako',
'content': ("\n".join(sorted(data['items']))).format(
os=node.os,
os_release=supported_os[node.os][node.os_version[0]],
),
'triggers': {
'action:apt_update',
},
}
if data.get('install_gpg_key', True):
files['/etc/apt/sources.list.d/{}.list'.format(name)]['needs'] = {
'file:/etc/apt/trusted.gpg.d/{}.list.asc'.format(name),
}
files['/etc/apt/trusted.gpg.d/{}.list.asc'.format(name)] = {
'source': 'gpg-keys/{}.asc'.format(name),
'triggers': {
'action:apt_update',
},
}
for crontab, content in node.metadata.get('cron/jobs', {}).items(): for crontab, content in node.metadata.get('cron/jobs', {}).items():
files['/etc/cron.d/{}'.format(crontab)] = { files['/etc/cron.d/{}'.format(crontab)] = {
'source': 'cron_template', 'source': 'cron_template',

View file

@ -17,7 +17,7 @@ files = {
directories = { directories = {
'/etc/cron.d': { '/etc/cron.d': {
'purge': True, 'purge': True,
'after': { 'needs': {
'pkg_apt:', 'pkg_apt:',
}, },
}, },

View file

@ -0,0 +1,36 @@
<%
import re
from ipaddress import ip_network
%>
ddns-update-style none;
authoritative;
% for interface, subnet in sorted(dhcp_config.get('subnets', {}).items()):
<%
network = ip_network(subnet['subnet'])
%>
# interface ${interface} provides ${subnet['subnet']}
subnet ${network.network_address} netmask ${network.netmask} {
% if subnet.get('range_lower', None) and subnet.get('range_higher', None):
range ${subnet['range_lower']} ${subnet['range_higher']};
% endif
interface "${interface}";
default-lease-time ${subnet.get('default-lease-time', 600)};
max-lease-time ${subnet.get('max-lease-time', 3600)};
% for option, value in sorted(subnet.get('options', {}).items()):
% if re.match('([^0-9\.,\ ])', value):
option ${option} "${value}";
% else:
option ${option} ${value};
% endif
% endfor
}
% endfor
% for identifier, allocation in dhcp_config.get('fixed_allocations', {}).items():
host ${identifier} {
hardware ethernet ${allocation['mac']};
fixed-address ${allocation['ipv4']};
}
% endfor

View file

@ -0,0 +1,18 @@
# Defaults for isc-dhcp-server (sourced by /etc/init.d/isc-dhcp-server)
# Path to dhcpd's config file (default: /etc/dhcp/dhcpd.conf).
#DHCPDv4_CONF=/etc/dhcp/dhcpd.conf
#DHCPDv6_CONF=/etc/dhcp/dhcpd6.conf
# Path to dhcpd's PID file (default: /var/run/dhcpd.pid).
#DHCPDv4_PID=/var/run/dhcpd.pid
#DHCPDv6_PID=/var/run/dhcpd6.pid
# Additional options to start dhcpd with.
# Don't use options -cf or -pf here; use DHCPD_CONF/ DHCPD_PID instead
#OPTIONS=""
# On what interfaces should the DHCP server (dhcpd) serve DHCP requests?
# Separate multiple interfaces with spaces, e.g. "eth0 eth1".
INTERFACESv4="${' '.join(sorted(node.metadata.get('dhcpd/subnets', {})))}"
INTERFACESv6=""

41
bundles/dhcpd/items.py Normal file
View file

@ -0,0 +1,41 @@
files = {
'/etc/dhcp/dhcpd.conf': {
'content_type': 'mako',
'context': {
'dhcp_config': node.metadata['dhcpd'],
},
'needs': {
'pkg_apt:isc-dhcp-server'
},
'triggers': {
'svc_systemd:isc-dhcp-server:restart',
},
},
'/etc/default/isc-dhcp-server': {
'content_type': 'mako',
'needs': {
'pkg_apt:isc-dhcp-server'
},
'triggers': {
'svc_systemd:isc-dhcp-server:restart',
},
},
}
actions = {
# needed for dhcp-lease-list
'dhcpd_download_oui.txt': {
'command': 'wget http://standards-oui.ieee.org/oui.txt -O /usr/local/etc/oui.txt',
'unless': 'test -f /usr/local/etc/oui.txt',
},
}
svc_systemd = {
'isc-dhcp-server': {
'needs': {
'pkg_apt:isc-dhcp-server',
'file:/etc/dhcp/dhcpd.conf',
'file:/etc/default/isc-dhcp-server',
},
},
}

54
bundles/dhcpd/metadata.py Normal file
View file

@ -0,0 +1,54 @@
defaults = {
'apt': {
'packages': {
'isc-dhcp-server': {},
},
},
'bash_aliases': {
'leases': 'sudo dhcp-lease-list | tail -n +4 | sort -k 2,2',
},
}
@metadata_reactor.provides(
'dhcpd/fixed_allocations',
)
def get_static_allocations(metadata):
allocations = {}
for rnode in repo.nodes:
if rnode.metadata.get('location', '') != metadata.get('location', ''):
continue
for iface_name, iface_config in rnode.metadata.get('interfaces', {}).items():
if iface_config.get('dhcp', False):
try:
allocations[f'{rnode.name}_{iface_name}'] = {
'ipv4': sorted(iface_config['ips'])[0],
'mac': iface_config['mac'],
}
except KeyError:
pass
return {
'dhcpd': {
'fixed_allocations': allocations,
}
}
@metadata_reactor.provides(
'nftables/rules/10-dhcpd',
)
def nftables(metadata):
rules = set()
for iface in node.metadata.get('dhcpd/subnets', {}):
rules.add(f'inet filter input udp dport {{ 67, 68 }} iif {iface} accept')
return {
'nftables': {
'rules': {
# can't use port_rules here, because we're generating interface based rules.
'10-dhcpd': sorted(rules),
},
}
}

View file

@ -1,39 +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}'
])
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!')
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,50 +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 "docker-${name}")"
PGID="$(id -g "docker-${name}")"
if [ "$ACTION" == "start" ]
then
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 host \
% for host_port, container_port in sorted(ports.items()):
--expose "127.0.0.1:${host_port}:${container_port}" \
% endfor
% for host_path, container_path in sorted(volumes.items()):
--volume "/var/opt/docker-engine/${name}/${host_path}:${container_path}" \
% endfor
--restart unless-stopped \
"${image}"
elif [ "$ACTION" == "stop" ]
then
docker stop "${name}"
docker rm "${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,99 +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',
}
for app, config in node.metadata.get('docker-engine/containers', {}).items():
volumes = config.get('volumes', {})
files[f'/opt/docker-engine/{app}'] = {
'source': 'docker-wrapper',
'content_type': 'mako',
'context': {
'environment': config.get('environment', {}),
'image': config['image'],
'name': app,
'ports': config.get('ports', {}),
'timezone': node.metadata.get('timezone'),
'volumes': volumes,
},
'mode': '0755',
'triggers': {
f'svc_systemd:docker-{app}:restart',
},
}
users[f'docker-{app}'] = {
'home': f'/var/opt/docker-engine/{app}',
'groups': {
'docker',
},
'after': {
# provides docker group
'pkg_apt:docker-ce',
},
}
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'file:/opt/docker-engine/{app}',
f'file:/usr/local/lib/systemd/system/docker-{app}.service',
f'user:docker-{app}',
'svc_systemd:docker',
*set(config.get('needs', set())),
},
}
for volume in volumes:
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 /var/opt/docker-engine/{app}/{volume}',
}

View file

@ -1,83 +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',
},
},
},
},
'backups': {
'paths': {
'/var/opt/docker-engine',
},
},
'hosts': {
'entries': {
'172.17.0.1': {
'host.docker.internal',
},
},
},
'docker-engine': {
'config': {
'iptables': False,
'no-new-privileges': True,
},
},
'zfs': {
'datasets': {
'tank/docker-data': {
'mountpoint': '/var/opt/docker-engine',
},
},
},
}
@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(
'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}'
}
return {
'zfs': {
'datasets': datasets,
},
}

View file

@ -1,64 +0,0 @@
assert node.has_bundle('docker-engine')
assert node.has_bundle('redis')
assert not node.has_bundle('postgresql') # docker container uses that port
defaults = {
'docker-engine': {
'containers': {
'immich': {
'image': 'ghcr.io/imagegenius/immich:latest',
'environment': {
'DB_DATABASE_NAME': 'immich',
'DB_HOSTNAME': 'host.docker.internal',
'DB_PASSWORD': repo.vault.password_for(f'{node.name} postgresql immich'),
'DB_USERNAME': 'immich',
'REDIS_HOSTNAME': 'host.docker.internal',
},
'volumes': {
'config': '/config',
'libraries': '/libraries',
'photos': '/photos',
},
'needs': {
'svc_systemd:docker-postgresql14',
},
'requires': {
'docker-postgresql14.service',
},
},
'postgresql14': {
'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',
},
},
},
},
'nginx': {
'vhosts': {
'immich': {
'locations': {
'/': {
'target': 'http://127.0.0.1:8080/',
'websockets': True,
'max_body_size': '500m',
},
#'/api/socket.io/': {
# 'target': 'http://127.0.0.1:8081/',
# 'websockets': True,
#},
},
},
},
},
'redis': {
'bind': '0.0.0.0',
},
}

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'):
@ -87,19 +76,19 @@ def import_database_settings_from_postfixadmin(metadata):
@metadata_reactor.provides( @metadata_reactor.provides(
'firewall/port_rules', 'firewall/port_rules/143',
'firewall/port_rules', 'firewall/port_rules/993',
'firewall/port_rules', 'firewall/port_rules/4190',
) )
def firewall(metadata): def firewall(metadata):
return { return {
'firewall': { 'firewall': {
'port_rules': { 'port_rules': {
# imap(s) # imap(s)
'143/tcp': atomic(metadata.get('dovecot/restrict-to', {'*'})), '143': atomic(metadata.get('dovecot/restrict-to', {'*'})),
'993/tcp': atomic(metadata.get('dovecot/restrict-to', {'*'})), '993': atomic(metadata.get('dovecot/restrict-to', {'*'})),
# managesieve # managesieve
'4190/tcp': atomic(metadata.get('dovecot/restrict-to', {'*'})), '4190': atomic(metadata.get('dovecot/restrict-to', {'*'})),
}, },
}, },
} }

View file

@ -8,7 +8,7 @@ directories = {
git_deploy = { git_deploy = {
'/opt/element-web': { '/opt/element-web': {
'rev': node.metadata.get('element-web/version'), 'rev': node.metadata['element-web']['version'],
'repo': 'https://github.com/vector-im/element-web.git', 'repo': 'https://github.com/vector-im/element-web.git',
'triggers': { 'triggers': {
'action:element-web_yarn', 'action:element-web_yarn',
@ -18,22 +18,28 @@ git_deploy = {
files = { files = {
'/opt/element-web/webapp/config.json': { '/opt/element-web/webapp/config.json': {
'content': metadata_to_json(node.metadata.get('element-web/config')), 'content': metadata_to_json(node.metadata['element-web']['config']),
'needs': { 'needs': {
'action:element-web_yarn', 'action:element-web_yarn',
}, },
}, },
} }
extra_install_cmds = []
if node.metadata.get('nodejs/version') >= 17:
# TODO verify this is still needed when upgrading to 1.12
extra_install_cmds.append('export NODE_OPTIONS=--openssl-legacy-provider')
actions = { actions = {
'element-web_yarn': { 'element-web_yarn': {
'command': ' && '.join([ 'command': ' && '.join([
*extra_install_cmds,
'cd /opt/element-web', 'cd /opt/element-web',
'yarn install --pure-lockfile --ignore-scripts', 'yarn install --pure-lockfile --ignore-scripts',
'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

@ -1,65 +0,0 @@
users = {
'git': {
'home': '/var/lib/forgejo',
},
}
directories = {
'/var/lib/forgejo/.ssh': {
'mode': '0700',
'owner': 'git',
'group': 'git',
},
'/var/lib/forgejo': {
'owner': 'git',
'mode': '0700',
'triggers': {
'svc_systemd:forgejo:restart',
},
},
}
files = {
'/usr/local/lib/systemd/system/forgejo.service': {
'content_type': 'mako',
'context': node.metadata.get('forgejo'),
'triggers': {
'action:systemd-reload',
'svc_systemd:forgejo:restart',
},
},
'/etc/forgejo/app.ini': {
'content_type': 'mako',
'context': node.metadata.get('forgejo'),
'triggers': {
'svc_systemd:forgejo:restart',
},
},
'/usr/local/bin/forgejo': {
'content_type': 'download',
'source': 'https://codeberg.org/forgejo/forgejo/releases/download/v{0}/forgejo-{0}-linux-amd64'.format(node.metadata.get('forgejo/version')),
'content_hash': node.metadata.get('forgejo/sha1', None),
'mode': '0755',
'triggers': {
'svc_systemd:forgejo:restart',
},
},
}
if node.metadata.get('forgejo/install_ssh_key', False):
files['/var/lib/forgejo/.ssh/id_ed25519'] = {
'content': repo.vault.decrypt_file(f'forgejo/files/ssh-keys/{node.name}.key.vault'),
'mode': '0600',
'owner': 'git',
'group': 'git',
}
svc_systemd = {
'forgejo': {
'needs': {
'file:/etc/forgejo/app.ini',
'file:/usr/local/bin/forgejo',
'file:/usr/local/lib/systemd/system/forgejo.service',
},
},
}

View file

@ -0,0 +1,33 @@
svc_systemd = {}
pkg_apt = {}
for i in {
'gce-disk-expand',
'google-cloud-packages-archive-keyring',
'google-cloud-sdk',
'google-compute-engine',
'google-compute-engine-oslogin',
'google-guest-agent',
'google-osconfig-agent',
}:
pkg_apt[i] = {
'installed': False,
}
for i in {
'google-accounts-daemon.service',
'google-accounts-manager.service',
'google-clock-skew-daemon.service',
'google-clock-sync-manager.service',
'google-guest-agent.service',
'google-osconfig-agent.service',
'google-shutdown-scripts.service',
'google-startup-scripts.service',
'sshguard.service',
'google-oslogin-cache.timer',
}:
svc_systemd[i] = {
'enabled': False,
'running': False,
}

View file

@ -1,10 +1,9 @@
APP_NAME = ${app_name} APP_NAME = ${app_name}
RUN_USER = git RUN_USER = git
RUN_MODE = prod RUN_MODE = prod
WORK_PATH = /var/lib/forgejo
[repository] [repository]
ROOT = /var/lib/forgejo/repositories ROOT = /home/git/gitea-repositories
MAX_CREATION_LIMIT = 0 MAX_CREATION_LIMIT = 0
DEFAULT_BRANCH = main DEFAULT_BRANCH = main

View file

@ -5,13 +5,14 @@ After=network.target
Requires=postgresql.service Requires=postgresql.service
[Service] [Service]
RestartSec=10 RestartSec=2s
Type=simple Type=simple
User=git User=git
Group=git Group=git
WorkingDirectory=/var/lib/forgejo WorkingDirectory=/var/lib/gitea/
ExecStart=/usr/local/bin/forgejo web -c /etc/forgejo/app.ini ExecStart=/usr/local/bin/gitea web -c /etc/gitea/app.ini
Restart=always Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

68
bundles/gitea/items.py Normal file
View file

@ -0,0 +1,68 @@
users = {
'git': {},
}
directories = {
'/home/git': {
'mode': '0755',
'owner': 'git',
'group': 'git',
},
'/home/git/.ssh': {
'mode': '0755',
'owner': 'git',
'group': 'git',
},
'/var/lib/gitea': {
'owner': 'git',
'mode': '0700',
'triggers': {
'svc_systemd:gitea:restart',
},
},
}
files = {
'/etc/systemd/system/gitea.service': {
'content_type': 'mako',
'context': node.metadata.get('gitea'),
'triggers': {
'action:systemd-reload',
'svc_systemd:gitea:restart',
},
},
'/etc/gitea/app.ini': {
'content_type': 'mako',
'context': node.metadata.get('gitea'),
'triggers': {
'svc_systemd:gitea:restart',
},
},
'/usr/local/bin/gitea': {
'content_type': 'download',
'source': node.metadata.get('gitea/url'),
'content_hash': node.metadata.get('gitea/sha1', None),
'mode': '0755',
'triggers': {
'svc_systemd:gitea:restart',
},
},
}
if node.metadata['gitea'].get('install_ssh_key', False):
files['/home/git/.ssh/id_ed25519'] = {
'content': repo.vault.decrypt_file(f'gitea/files/ssh-keys/{node.name}.key.vault'),
'mode': '0600',
'owner': 'git',
'group': 'git',
}
svc_systemd = {
'gitea': {
'needs': {
'file:/etc/gitea/app.ini',
'file:/etc/systemd/system/gitea.service',
'file:/usr/local/bin/gitea',
},
},
}

View file

@ -1,31 +1,33 @@
defaults = { defaults = {
'backups': { 'backups': {
'paths': { 'paths': {
'/var/lib/forgejo', '/home/git',
'/var/lib/gitea',
}, },
}, },
'forgejo': { 'gitea': {
'app_name': 'Forgejo', 'app_name': 'Forgejo',
'database': { 'database': {
'username': 'forgejo', 'username': 'gitea',
'password': repo.vault.password_for('{} postgresql forgejo'.format(node.name)), 'password': repo.vault.password_for('{} postgresql gitea'.format(node.name)),
'database': 'forgejo', 'database': 'gitea',
}, },
'disable_registration': True, 'disable_registration': True,
'email_domain_blocklist': set(), 'email_domain_blocklist': set(),
'enable_git_hooks': False, 'enable_git_hooks': False,
'internal_token': repo.vault.password_for('{} forgejo internal_token'.format(node.name)), 'internal_token': repo.vault.password_for('{} gitea internal_token'.format(node.name)),
'lfs_secret_key': repo.vault.password_for('{} forgejo lfs_secret_key'.format(node.name)), 'lfs_secret_key': repo.vault.password_for('{} gitea lfs_secret_key'.format(node.name)),
'oauth_secret_key': repo.vault.password_for('{} forgejo oauth_secret_key'.format(node.name)), 'oauth_secret_key': repo.vault.password_for('{} gitea oauth_secret_key'.format(node.name)),
'security_secret_key': repo.vault.password_for('{} forgejo security_secret_key'.format(node.name)), 'security_secret_key': repo.vault.password_for('{} gitea security_secret_key'.format(node.name)),
}, },
'icinga2_api': { 'icinga2_api': {
'forgejo': { 'gitea': {
'services': { 'services': {
'FORGEJO PROCESS': { 'FORGEJO PROCESS': {
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_systemd_unit forgejo', 'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_systemd_unit gitea',
}, },
'FORGEJO UPDATE': { 'FORGEJO UPDATE': {
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_forgejo_for_new_release codeberg.org forgejo/forgejo v$(gitea --version | cut -d" " -f3)',
'vars.notification.mail': True, 'vars.notification.mail': True,
'check_interval': '60m', 'check_interval': '60m',
}, },
@ -39,22 +41,29 @@ defaults = {
}, },
'postgresql': { 'postgresql': {
'roles': { 'roles': {
'forgejo': { 'gitea': {
'password': repo.vault.password_for('{} postgresql forgejo'.format(node.name)), 'password': repo.vault.password_for('{} postgresql gitea'.format(node.name)),
}, },
}, },
'databases': { 'databases': {
'forgejo': { 'gitea': {
'owner': 'forgejo', 'owner': 'gitea',
}, },
}, },
}, },
'zfs': { 'zfs': {
'datasets': { 'datasets': {
'tank/forgejo': { 'tank/gitea': {},
'mountpoint': '/var/lib/forgejo', 'tank/gitea/home': {
'mountpoint': '/home/git',
'needed_by': { 'needed_by': {
'directory:/var/lib/forgejo', 'directory:/home/git',
},
},
'tank/gitea/var': {
'mountpoint': '/var/lib/gitea',
'needed_by': {
'directory:/var/lib/gitea',
}, },
}, },
}, },
@ -62,23 +71,6 @@ defaults = {
} }
@metadata_reactor.provides(
'icinga2_api/forgejo',
)
def update_monitoring(metadata):
return {
'icinga2_api': {
'forgejo': {
'services': {
'FORGEJO UPDATE': {
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_forgejo_for_new_release codeberg.org forgejo/forgejo v{}'.format(metadata.get('forgejo/version')),
},
},
},
},
}
@metadata_reactor.provides( @metadata_reactor.provides(
'nginx/vhosts/forgejo', 'nginx/vhosts/forgejo',
) )
@ -90,7 +82,7 @@ def nginx(metadata):
'nginx': { 'nginx': {
'vhosts': { 'vhosts': {
'forgejo': { 'forgejo': {
'domain': metadata.get('forgejo/domain'), 'domain': metadata.get('gitea/domain'),
'locations': { 'locations': {
'/': { '/': {
'target': 'http://127.0.0.1:22000', 'target': 'http://127.0.0.1:22000',
@ -100,8 +92,16 @@ def nginx(metadata):
}, },
}, },
'website_check_path': '/user/login', 'website_check_path': '/user/login',
'website_check_string': 'Sign in', 'website_check_string': 'Sign In',
}, },
}, },
}, },
} }
@metadata_reactor.provides(
'icinga2_api/gitea/services',
)
def icinga_check_for_new_release(metadata):
return {
}

View file

@ -47,7 +47,7 @@ def dashboard_row_smartd(panel_id, node):
'renderer': 'flot', 'renderer': 'flot',
'seriesOverrides': [], 'seriesOverrides': [],
'spaceLength': 10, 'spaceLength': 10,
'span': 12, 'span': 8,
'stack': False, 'stack': False,
'steppedLine': False, 'steppedLine': False,
'targets': [ 'targets': [
@ -114,5 +114,115 @@ def dashboard_row_smartd(panel_id, node):
'alignLevel': None 'alignLevel': None
} }
}, },
{
'aliasColors': {},
'bars': False,
'dashLength': 10,
'dashes': False,
'datasource': None,
'fieldConfig': {
'defaults': {
'displayName': '${__field.labels.device}'
},
'overrides': []
},
'fill': 0,
'fillGradient': 0,
'hiddenSeries': False,
'id': next(panel_id),
'legend': {
'alignAsTable': False,
'avg': False,
'current': False,
'hideEmpty': True,
'hideZero': True,
'max': False,
'min': False,
'rightSide': False,
'show': True,
'total': False,
'values': False
},
'lines': True,
'linewidth': 1,
'NonePointMode': 'None',
'options': {
'alertThreshold': True
},
'percentage': False,
'pluginVersion': '7.5.5',
'pointradius': 2,
'points': False,
'renderer': 'flot',
'seriesOverrides': [],
'spaceLength': 10,
'span': 4,
'stack': False,
'steppedLine': False,
'targets': [
{
'groupBy': [
{'type': 'time', 'params': ['$__interval']},
{'type': 'fill', 'params': ['linear']},
],
'orderByTime': "ASC",
'policy': "default",
'query': f"""from(bucket: "telegraf")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) =>
r["_measurement"] == "smartd_stats" and
r["_field"] == "power_on_hours" and
r["host"] == "{node.name}"
)
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|> yield(name: "fan")""",
'resultFormat': 'time_series',
'select': [[
{'type': 'field', 'params': ['value']},
{'type': 'mean', 'params': []},
]],
"tags": []
},
],
'thresholds': [],
'timeRegions': [],
'title': 'fans',
'tooltip': {
'shared': True,
'sort': 0,
'value_type': 'individual'
},
'type': 'graph',
'xaxis': {
'buckets': None,
'mode': 'time',
'name': None,
'show': True,
'values': []
},
'yaxes': [
{
'format': 'hours',
'label': None,
'logBase': 1,
'max': None,
'min': None,
'show': True,
'decimals': 0,
},
{
'format': 'short',
'label': None,
'logBase': 1,
'max': None,
'min': None,
'show': False,
}
],
'yaxis': {
'align': False,
'alignLevel': None
}
},
], ],
} }

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

@ -33,11 +33,7 @@ ProtectSystem=strict
ProtectHome=true ProtectHome=true
PrivateTmp=true PrivateTmp=true
SystemCallArchitectures=native SystemCallArchitectures=native
# FIXME SystemCallFilter=@system-service
# causes problems on bookworm
# see https://github.com/hedgedoc/hedgedoc/issues/4686
# cmmented out for now ...
#SystemCallFilter=@system-service
# You may have to adjust these settings # You may have to adjust these settings
User=hedgedoc User=hedgedoc

View file

@ -1,5 +1,3 @@
from semver import compare
repo.libs.tools.require_bundle(node, 'nodejs') repo.libs.tools.require_bundle(node, 'nodejs')
git_deploy = { git_deploy = {
@ -49,29 +47,16 @@ directories = {
}, },
} }
if compare(node.metadata.get('hedgedoc/version'), '1.9.7') <= 0:
command = ' && '.join([
'cd /opt/hedgedoc',
'yarn workspaces focus --production',
'yarn install --ignore-scripts',
'yarn build',
])
elif compare(node.metadata.get('hedgedoc/version'), '1.9.9') >= 0:
command = ' && '.join([
'cd /opt/hedgedoc',
'bin/setup',
'yarn install --immutable',
'yarn build',
])
actions = { actions = {
'hedgedoc_yarn': { 'hedgedoc_yarn': {
'command': ' && '.join([ 'command': ' && '.join([
'cd /opt/hedgedoc', 'cd /opt/hedgedoc',
'yarn install --immutable', 'yarn install --production=true --pure-lockfile --ignore-scripts',
'yarn install --ignore-scripts',
'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}") running_version = version.parse(
exit(3) s.get(f"https://{domain}/api/config").json()["version"]
)
try: if running_version == stable_version:
if stable_version > running_version: status = 0
print( message = f"OK - running version {running_version} equals stable version {stable_version}"
f"There is a newer version available: {stable_version} (currently installed: {running_version})" elif running_version > stable_version:
) status = 1
exit(2) 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,13 +5,9 @@ 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
Restart=on-failure
RestartSec=2
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

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,18 +32,11 @@ 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',
'user:homeassistant', 'user:homeassistant',
},
},
'homeassistant_install': {
'command': 'sudo -u homeassistant /opt/homeassistant/venv/bin/pip install homeassistant',
'unless': 'test -f /opt/homeassistant/venv/bin/hass',
'needs': {
'action:homeassistant_create_virtualenv',
'pkg_apt:bluez', 'pkg_apt:bluez',
'pkg_apt:libffi-dev', 'pkg_apt:libffi-dev',
'pkg_apt:libssl-dev', 'pkg_apt:libssl-dev',
@ -59,10 +45,17 @@ actions = {
'pkg_apt:autoconf', 'pkg_apt:autoconf',
'pkg_apt:build-essential', 'pkg_apt:build-essential',
'pkg_apt:libopenjp2-7', 'pkg_apt:libopenjp2-7',
'pkg_apt:libtiff6', 'pkg_apt:libtiff5',
'pkg_apt:libturbojpeg0-dev', 'pkg_apt:libturbojpeg0-dev',
'pkg_apt:tzdata', 'pkg_apt:tzdata',
}, },
},
'homeassistant_install': {
'command': 'sudo -u homeassistant /opt/homeassistant/venv/bin/pip install homeassistant',
'unless': 'test -f /opt/homeassistant/venv/bin/hass',
'needs': {
'action:homeassistant_create_virtualenv',
},
'triggers': { 'triggers': {
'svc_systemd:homeassistant:restart', 'svc_systemd:homeassistant:restart',
}, },

View file

@ -4,12 +4,11 @@ defaults = {
'autoconf': {}, 'autoconf': {},
'bluez': {}, 'bluez': {},
'build-essential': {}, 'build-essential': {},
'ffmpeg': {},
'libffi-dev': {}, 'libffi-dev': {},
'libjpeg-dev': {}, 'libjpeg-dev': {},
'libopenjp2-7': {}, 'libopenjp2-7': {},
'libssl-dev': {}, 'libssl-dev': {},
'libtiff6': {}, 'libtiff5': {},
'libturbojpeg0-dev': {}, 'libturbojpeg0-dev': {},
'python3-packaging': {}, 'python3-packaging': {},
'tzdata': {}, 'tzdata': {},
@ -23,8 +22,6 @@ defaults = {
}, },
}, },
} }
@metadata_reactor.provides( @metadata_reactor.provides(
'icinga2_api/homeassistant/services', 'icinga2_api/homeassistant/services',
) )
@ -34,17 +31,15 @@ def icinga_check_for_new_release(metadata):
'homeassistant': { 'homeassistant': {
'services': { 'services': {
'HOMEASSISTANT UPDATE': { 'HOMEASSISTANT UPDATE': {
'check_interval': '60m',
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_homeassistant_update', 'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_homeassistant_update',
'vars.notification.mail': True, 'vars.notification.mail': True,
'vars.sshmon_timeout': 20, 'check_interval': '60m',
}, },
}, },
}, },
}, },
} }
@metadata_reactor.provides( @metadata_reactor.provides(
'nginx/vhosts/homeassistant', 'nginx/vhosts/homeassistant',
) )

View file

@ -1,16 +0,0 @@
[Unit]
Description=icinga2-statuspage
After=network.target
Requires=postgresql.service
[Service]
User=www-data
Group=www-data
Environment=APP_CONFIG=/opt/icinga2-statuspage/config.json
WorkingDirectory=/opt/icinga2-statuspage/src
ExecStart=/usr/bin/gunicorn statuspage:app --workers 4 --max-requests 1200 --max-requests-jitter 50 --log-level=info --bind=127.0.0.1:22110
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

View file

@ -1,34 +0,0 @@
directories['/opt/icinga2-statuspage/src'] = {}
git_deploy['/opt/icinga2-statuspage/src'] = {
'repo': 'https://git.franzi.business/kunsi/icinga-dynamic-statuspage.git',
'rev': 'main',
'triggers': {
'svc_systemd:icinga2-statuspage:restart',
},
}
files['/opt/icinga2-statuspage/config.json'] = {
'content': repo.libs.faults.dict_as_json(node.metadata.get('icinga2-statuspage')),
'triggers': {
'svc_systemd:icinga2-statuspage:restart',
},
}
files['/usr/local/lib/systemd/system/icinga2-statuspage.service'] = {
'triggers': {
'action:systemd-reload',
'svc_systemd:icinga2-statuspage:restart',
},
}
svc_systemd['icinga2-statuspage'] = {
'needs': {
'file:/opt/icinga2-statuspage/config.json',
'git_deploy:/opt/icinga2-statuspage/src',
'pkg_apt:gunicorn',
'pkg_apt:python3-flask',
'pkg_apt:python3-psycopg2',
},
}

View file

@ -1,47 +0,0 @@
defaults = {
'apt': {
'packages': {
'gunicorn': {},
'python3-flask': {},
'python3-psycopg2': {},
},
},
}
@metadata_reactor.provides(
'icinga2-statuspage',
)
def import_db_settings_from_icinga(metadata):
return {
'icinga2-statuspage': {
'DB_USER': 'icinga2',
'DB_PASS': metadata.get('postgresql/roles/icinga2/password'),
'DB_NAME': 'icinga2',
},
}
@metadata_reactor.provides(
'nginx/vhosts/icinga2-statuspage',
)
def nginx(metadata):
if not node.has_bundle('nginx'):
raise DoNotRunAgain
return {
'nginx': {
'vhosts': {
'icinga2-statuspage': {
'domain': metadata.get('icinga2-statuspage/DOMAIN'),
'locations': {
'/': {
'target': 'http://127.0.0.1:22110',
},
},
'website_check_path': '/',
'website_check_string': 'status page',
},
},
},
}

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

@ -1,17 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from json import load
from sys import exit from sys import exit
from requests import get from requests import get
with open('/etc/icinga2/notification_config.json') as f: SIPGATE_USER = '${node.metadata['icinga2']['sipgate_user']}'
CONFIG = load(f) SIPGATE_PASS = '${node.metadata['icinga2']['sipgate_pass']}'
try: try:
r = get( r = get(
'https://api.sipgate.com/v2/balance', 'https://api.sipgate.com/v2/balance',
auth=(CONFIG['sipgate']['user'], CONFIG['sipgate']['password']), auth=(SIPGATE_USER, SIPGATE_PASS),
headers={'Accept': 'application/json'}, headers={'Accept': 'application/json'},
) )

View file

@ -5,33 +5,30 @@ from ipaddress import IPv6Address, ip_address
from subprocess import check_output from subprocess import check_output
from sys import argv, exit from sys import argv, exit
BLOCKLISTS = { BLOCKLISTS = [
'0spam.fusionzero.com': set(), '0spam.fusionzero.com',
'bl.mailspike.org': set(), 'bl.mailspike.org',
'bl.spamcop.net': set(), 'bl.spamcop.net',
'blackholes.brainerd.net': set(), 'blackholes.brainerd.net',
'dnsbl-1.uceprotect.net': set(), 'dnsbl-1.uceprotect.net',
'l2.spews.dnsbl.sorbs.net': set(), 'dnsbl-2.uceprotect.net',
'list.dsbl.org': set(), 'l2.spews.dnsbl.sorbs.net',
'multihop.dsbl.org': set(), 'list.dsbl.org',
'ns1.unsubscore.com': set(), 'map.spam-rbl.com',
'opm.blitzed.org': set(), 'multihop.dsbl.org',
'psbl.surriel.com': set(), 'ns1.unsubscore.com',
'rbl.efnet.org': set(), 'opm.blitzed.org',
'rbl.schulte.org': set(), 'psbl.surriel.com',
'spamguard.leadmon.net': set(), 'rbl.efnet.org',
'ubl.unsubscore.com': set(), 'rbl.schulte.org',
'unconfirmed.dsbl.org': set(), 'spamguard.leadmon.net',
'virbl.dnsbl.bit.nl': set(), 'ubl.unsubscore.com',
'zen.spamhaus.org': { 'unconfirmed.dsbl.org',
# https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now. 'virbl.dnsbl.bit.nl',
'127.255.255.252', # Typing Error 'zen.spamhaus.org',
'127.255.255.254', # public resolver / generic rdns ]
'127.255.255.255', # rate limited
},
}
def check_list(ip_list, blocklist, warn_ips): def check_list(ip_list, blocklist):
dns_name = '{}.{}'.format( dns_name = '{}.{}'.format(
'.'.join(ip_list), '.'.join(ip_list),
blocklist, blocklist,
@ -44,22 +41,17 @@ def check_list(ip_list, blocklist, warn_ips):
result = check_output([ result = check_output([
'dig', 'dig',
'+tries=2', '+tries=2',
'+time=10', '+time=5',
'+short', '+short',
dns_name dns_name
]).decode().splitlines() ]).decode().splitlines()
for item in result: for item in result:
if item.startswith(';;'):
continue
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: returncode = 2
returncode = 1
else:
returncode = 2
except Exception as e: except Exception as e:
if e.returncode == 9: if e.returncode == 9:
# no reply from server # no reply from server
@ -86,8 +78,8 @@ exitcode = 0
with ThreadPoolExecutor(max_workers=len(BLOCKLISTS)) as executor: with ThreadPoolExecutor(max_workers=len(BLOCKLISTS)) as executor:
futures = set() futures = set()
for blocklist, warn_ips in BLOCKLISTS.items(): for blocklist in BLOCKLISTS:
futures.add(executor.submit(check_list, ip_list, blocklist, warn_ips)) futures.add(executor.submit(check_list, ip_list, blocklist))
for future in as_completed(futures): for future in as_completed(futures):
msgs, this_exitcode = future.result() msgs, this_exitcode = future.result()

View file

@ -1,18 +1,31 @@
% for dt in downtimes: % for monitored_node in sorted(repo.nodes):
object ScheduledDowntime "${dt['name']}" { <%
host_name = "${dt['host']}" auto_updates_enabled = (
monitored_node.has_any_bundle(['apt', 'c3voc-addons'])
or (
monitored_node.has_bundle('pacman')
and monitored_node.metadata.get('pacman/unattended-upgrades/is_enabled', False)
)
) and not monitored_node.metadata.get('icinga_options/exclude_from_monitoring', False)
%>\
% if auto_updates_enabled:
object ScheduledDowntime "unattended_upgrades" {
host_name = "${monitored_node.name}"
author = "${dt['name']}" author = "unattended-upgrades"
comment = "${dt['comment']}" comment = "Downtime for upgrade-and-reboot of node ${monitored_node.name}"
fixed = true fixed = true
ranges = { ranges = {
% for d,t in dt['times'].items(): % if monitored_node.has_bundle('pacman'):
"${d}" = "${t}" "${days[monitored_node.metadata.get('pacman/unattended-upgrades/day')]}" = "${monitored_node.metadata.get('pacman/unattended-upgrades/hour')}:${monitored_node.magic_number%30}-${monitored_node.metadata.get('pacman/unattended-upgrades/hour')}:${(monitored_node.magic_number%30)+30}"
% endfor % else:
"${days[monitored_node.metadata.get('apt/unattended-upgrades/day')]}" = "${monitored_node.metadata.get('apt/unattended-upgrades/hour')}:${monitored_node.magic_number%30}-${monitored_node.metadata.get('apt/unattended-upgrades/hour')}:${(monitored_node.magic_number%30)+30}"
% endif
} }
child_options = "DowntimeTriggeredChildren" child_options = "DowntimeTriggeredChildren"
} }
% endif
% endfor % endfor

View file

@ -33,11 +33,3 @@ object ServiceGroup "checks_with_sms" {
assign where service.vars.notification.sms == true assign where service.vars.notification.sms == true
ignore where host.vars.notification.sms == false ignore where host.vars.notification.sms == false
} }
object ServiceGroup "statuspage" {
display_name = "Checks which are show on the public status page"
assign where service.vars.notification.sms == true
ignore where host.vars.notification.sms == false
ignore where host.vars.show_on_statuspage == false
}

View file

@ -14,8 +14,7 @@ object Host "${rnode.name}" {
vars.os = "${rnode.os}" vars.os = "${rnode.os}"
# used for status page # used for status page
vars.pretty_name = "${rnode.metadata.get('icinga_options/pretty_name', rnode.metadata.get('hostname'))}" vars.pretty_name = "${rnode.metadata.get('icinga_options/pretty_name', rnode.name)}"
vars.show_on_statuspage = ${str(rnode.metadata.get('icinga_options/show_on_statuspage', True)).lower()}
vars.period = "${rnode.metadata.get('icinga_options/period', '24x7')}" vars.period = "${rnode.metadata.get('icinga_options/period', '24x7')}"

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

@ -1,5 +0,0 @@
[settings]
acknowledge_sticky = 1
hostdowntime_all_services = 1
hostdowntime_end_fixed = P1W
servicedowntime_end_fixed = P2D

View file

@ -3,14 +3,22 @@
import email.mime.text import email.mime.text
import smtplib import smtplib
from argparse import ArgumentParser from argparse import ArgumentParser
from json import dumps, load from json import dumps
from subprocess import run from subprocess import run
from sys import argv from sys import argv
from requests import post from requests import post
with open('/etc/icinga2/notification_config.json') as f: SIPGATE_USER='${node.metadata['icinga2']['sipgate_user']}'
CONFIG = load(f) SIPGATE_PASS='${node.metadata['icinga2']['sipgate_pass']}'
STATUS_TO_EMOJI = {
'critical': '🔥',
'down': '🚨🚨🚨',
'ok': '🆗',
'up': '👌',
'warning': '⚡',
}
parser = ArgumentParser( parser = ArgumentParser(
prog='icinga_notification_wrapper', prog='icinga_notification_wrapper',
@ -65,31 +73,36 @@ def notify_per_sms():
output_text = '' output_text = ''
else: else:
output_text = '\n\n{}'.format(args.output) output_text = '\n\n{}'.format(args.output)
if args.state.lower() in STATUS_TO_EMOJI:
message_text = 'ICINGA: {host}{service} is {state}{output}'.format( message_text = '{emoji} {host}{service} {emoji}{output}'.format(
host=args.host_name, emoji=STATUS_TO_EMOJI[args.state.lower()],
service=('/'+args.service_name if args.service_name else ''), host=args.host_name,
state=args.state.upper(), service=('/'+args.service_name if args.service_name else ''),
output=output_text, state=args.state.upper(),
) output=output_text,
)
else:
message_text = 'ICINGA: {host}{service} is {state}{output}'.format(
host=args.host_name,
service=('/'+args.service_name if args.service_name else ''),
state=args.state.upper(),
output=output_text,
)
message = { message = {
'message': message_text, 'message': message_text,
'smsId': 's0', # XXX what does this mean? Documentation is unclear 'smsId': 's0', # XXX what does this mean? Documentation is unclear
'recipient': args.sms 'recipient': args.sms
} }
headers = { headers = {
'Content-type': 'application/json', 'Content-type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json'
} }
try: try:
r = post( r = post(
'https://api.sipgate.com/v2/sessions/sms', 'https://api.sipgate.com/v2/sessions/sms',
json=message, json=message,
headers=headers, headers=headers,
auth=(CONFIG['sipgate']['user'], CONFIG['sipgate']['password']), auth=(SIPGATE_USER, SIPGATE_PASS),
) )
if r.status_code == 204: if r.status_code == 204:
@ -100,45 +113,6 @@ def notify_per_sms():
log_to_syslog('Sending a SMS to "{}" failed: {}'.format(args.sms, repr(e))) log_to_syslog('Sending a SMS to "{}" failed: {}'.format(args.sms, repr(e)))
def notify_per_ntfy():
message_text = 'ICINGA: {host}{service} is {state}\n\n{output}'.format(
host=args.host_name,
service=('/'+args.service_name if args.service_name else ''),
state=args.state.upper(),
output=args.output,
)
if args.service_name:
subject = '[ICINGA] {}/{}'.format(args.host_name, args.service_name)
else:
subject = '[ICINGA] {}'.format(args.host_name)
if args.notification_type.lower() == 'recovery':
priority = 'default'
else:
priority = 'urgent'
headers = {
'Title': subject,
'Priority': priority,
}
try:
r = post(
CONFIG['ntfy']['url'],
data=message_text,
headers=headers,
auth=(CONFIG['ntfy']['user'], CONFIG['ntfy']['password']),
timeout=10,
)
r.raise_for_status()
except Exception as e:
log_to_syslog('Sending a Notification failed: {}'.format(repr(e)))
return False
return True
def notify_per_mail(): def notify_per_mail():
if args.notification_type.lower() == 'recovery': if args.notification_type.lower() == 'recovery':
# Do not send recovery emails. # Do not send recovery emails.
@ -202,8 +176,4 @@ if __name__ == '__main__':
notify_per_mail() notify_per_mail()
if args.sms: if args.sms:
ntfy_worked = False notify_per_sms()
if CONFIG['ntfy']['user']:
ntfy_worked = notify_per_ntfy()
if not args.service_name or not ntfy_worked:
notify_per_sms()

View file

@ -76,6 +76,8 @@ files = {
}, },
'/usr/local/share/icinga/plugins/check_sipgate_account_balance': { '/usr/local/share/icinga/plugins/check_sipgate_account_balance': {
'mode': '0755', 'mode': '0755',
'content_type': 'mako',
'cascade_skip': False, # contains faults
}, },
'/usr/local/share/icinga/plugins/check_freifunk_node': { '/usr/local/share/icinga/plugins/check_freifunk_node': {
'mode': '0755', 'mode': '0755',
@ -112,22 +114,11 @@ files = {
'svc_systemd:icinga2:restart', 'svc_systemd:icinga2:restart',
}, },
}, },
'/etc/icinga2/notification_config.json': {
'content': repo.libs.faults.dict_as_json({
'sipgate': {
'user': node.metadata.get('icinga2/sipgate/user'),
'password': node.metadata.get('icinga2/sipgate/pass'),
},
'ntfy': {
'url': node.metadata.get('icinga2/ntfy/url'),
'user': node.metadata.get('icinga2/ntfy/user'),
'password': node.metadata.get('icinga2/ntfy/pass'),
},
}),
},
'/etc/icinga2/scripts/icinga_notification_wrapper': { '/etc/icinga2/scripts/icinga_notification_wrapper': {
'source': 'scripts/icinga_notification_wrapper', 'source': 'scripts/icinga_notification_wrapper',
'content_type': 'mako',
'mode': '0755', 'mode': '0755',
'cascade_skip': False, # contains faults
}, },
'/etc/icinga2/features-available/ido-pgsql.conf': { '/etc/icinga2/features-available/ido-pgsql.conf': {
'source': 'icinga2/ido-pgsql.conf', 'source': 'icinga2/ido-pgsql.conf',
@ -254,11 +245,6 @@ files = {
'mode': '0660', 'mode': '0660',
'group': 'icingaweb2', 'group': 'icingaweb2',
}, },
'/etc/icingaweb2/modules/monitoring/config.ini': {
'source': 'icingaweb2/monitoring_config.ini',
'mode': '0660',
'group': 'icingaweb2',
},
'/etc/icingaweb2/groups.ini': { '/etc/icingaweb2/groups.ini': {
'source': 'icingaweb2/groups.ini', 'source': 'icingaweb2/groups.ini',
'mode': '0660', 'mode': '0660',
@ -276,13 +262,13 @@ files = {
'group': 'icingaweb2', 'group': 'icingaweb2',
}, },
# monitoring # Statusmonitor
'/etc/icinga2/icinga_statusmonitor.py': { '/etc/icinga2/icinga_statusmonitor.py': {
'triggers': { 'triggers': {
'svc_systemd:icinga_statusmonitor:restart', 'svc_systemd:icinga_statusmonitor:restart',
}, },
}, },
'/usr/local/lib/systemd/system/icinga_statusmonitor.service': { '/etc/systemd/system/icinga_statusmonitor.service': {
'triggers': { 'triggers': {
'action:systemd-reload', 'action:systemd-reload',
'svc_systemd:icinga_statusmonitor:restart', 'svc_systemd:icinga_statusmonitor:restart',
@ -290,12 +276,8 @@ files = {
}, },
} }
svc_systemd['icinga_statusmonitor'] = { pkg_pip = {
'needs': { 'easysnmp': {}, # for check_usv_snmp
'file:/etc/icinga2/icinga_statusmonitor.py',
'file:/usr/local/lib/systemd/system/icinga_statusmonitor.service',
'pkg_apt:python3-flask',
},
} }
actions = { actions = {
@ -337,30 +319,44 @@ 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 = {
'needs': icinga_run_deps, 'icinga2': {
'needs': icinga_run_deps,
},
'icinga_statusmonitor': {
'needs': {
'file:/etc/icinga2/icinga_statusmonitor.py',
'file:/etc/systemd/system/icinga_statusmonitor.service',
'pkg_apt:python3-flask',
},
},
} }
# The actual hosts and services management starts here # The actual hosts and services management starts here
bundles = set() bundles = set()
downtimes = [] for rnode in repo.nodes:
for rnode in sorted(repo.nodes):
if rnode.metadata.get('icinga_options/exclude_from_monitoring', False): if rnode.metadata.get('icinga_options/exclude_from_monitoring', False):
continue continue
host_ips = repo.libs.tools.resolve_identifier(repo, rnode.name, only_physical=True) host_ips = repo.libs.tools.resolve_identifier(repo, rnode.name)
icinga_ips = {} icinga_ips = {}
for ip_type in ('ipv4', 'ipv6'): # XXX for the love of god, PLEASE remove this once DNS is no longer
for ip in sorted(host_ips[ip_type]): # hosted at GCE
if ip.is_private and not ip.is_link_local: if rnode.in_group('gce'):
icinga_ips[ip_type] = str(ip) icinga_ips['ipv4'] = rnode.metadata.get('external_ipv4')
break else:
else: for ip_type in ('ipv4', 'ipv6'):
if host_ips[ip_type]: for ip in sorted(host_ips[ip_type]):
icinga_ips[ip_type] = sorted(host_ips[ip_type])[0] if ip.is_private and not ip.is_link_local:
icinga_ips[ip_type] = str(ip)
break
else:
if host_ips[ip_type]:
icinga_ips[ip_type] = sorted(host_ips[ip_type])[0]
if not icinga_ips: if not icinga_ips:
raise ValueError(f'{rnode.name} requests monitoring, but has neither IPv4 nor IPv6 addresses!') raise ValueError(f'{rnode.name} requests monitoring, but has neither IPv4 nor IPv6 addresses!')
@ -383,41 +379,6 @@ for rnode in sorted(repo.nodes):
bundles |= set(rnode.metadata.get('icinga2_api', {}).keys()) bundles |= set(rnode.metadata.get('icinga2_api', {}).keys())
if rnode.has_any_bundle(['apt', 'c3voc-addons']):
day = rnode.metadata.get('apt/unattended-upgrades/day')
hour = rnode.metadata.get('apt/unattended-upgrades/hour')
minute = rnode.magic_number%30
spread = rnode.metadata.get('apt/unattended-upgrades/spread_in_group', None)
if spread is not None:
spread_nodes = sorted(repo.nodes_in_group(spread))
day += spread_nodes.index(rnode)
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}',
},
})
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',
'content_type': 'mako', 'content_type': 'mako',
@ -438,7 +399,7 @@ files['/etc/icinga2/conf.d/downtimes.conf'] = {
'source': 'icinga2/downtimes.conf', 'source': 'icinga2/downtimes.conf',
'content_type': 'mako', 'content_type': 'mako',
'context': { 'context': {
'downtimes': downtimes, 'days': DAYS_TO_STRING,
}, },
'owner': 'nagios', 'owner': 'nagios',
'group': 'nagios', 'group': 'nagios',

View file

@ -17,9 +17,12 @@ defaults = {
'icinga2': {}, 'icinga2': {},
'icinga2-ido-pgsql': {}, 'icinga2-ido-pgsql': {},
'icingaweb2': {}, 'icingaweb2': {},
'python3-easysnmp': {},
# apparently no longer needed
#'icingaweb2-module-monitoring': {},
# neeeded for statusmonitor
'python3-flask': {}, 'python3-flask': {},
'snmp': {},
} }
}, },
'icinga2': { 'icinga2': {
@ -40,6 +43,9 @@ defaults = {
'check_interval': '30m', 'check_interval': '30m',
'vars.notification.mail': True, 'vars.notification.mail': True,
}, },
'ICINGA STATUSMONITOR': {
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_systemd_unit icinga_statusmonitor',
},
'IDO-PGSQL': { 'IDO-PGSQL': {
'check_command': 'ido', 'check_command': 'ido',
'vars.ido_type': 'IdoPgsqlConnection', 'vars.ido_type': 'IdoPgsqlConnection',
@ -53,21 +59,6 @@ defaults = {
'icingaweb2': { 'icingaweb2': {
'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': {
'version': '8.2',
'packages': {
'curl',
'gd',
'intl',
'imagick',
'ldap',
'mysql',
'opcache',
'pgsql',
'readline',
'xml',
},
},
'postgresql': { 'postgresql': {
'roles': { 'roles': {
'icinga2': { 'icinga2': {
@ -114,29 +105,13 @@ def add_users_from_json(metadata):
@metadata_reactor.provides( @metadata_reactor.provides(
'nginx/vhosts/icingaweb2', 'firewall/port_rules/5665',
'nginx/vhosts/icinga_statusmonitor',
) )
def nginx(metadata): def firewall(metadata):
if not node.has_bundle('nginx'):
raise DoNotRunAgain
return { return {
'nginx': { 'firewall': {
'vhosts': { 'port_rules': {
'icingaweb2': { '5665': atomic(metadata.get('icinga2/restrict-to', set())),
'domain': metadata.get('icinga2/web_domain'),
'webroot': '/usr/share/icingaweb2/public',
'locations': {
'/api/': {
'target': 'https://127.0.0.1:5665/',
},
'/statusmonitor/': {
'target': 'http://127.0.0.1:5000/',
},
},
'extras': True,
},
}, },
}, },
} }

View file

@ -10,7 +10,7 @@ defaults = {
'repos': { 'repos': {
'influxdb': { 'influxdb': {
'items': { 'items': {
'deb https://repos.influxdata.com/{os} stable main', 'deb https://repos.influxdata.com/{os} {os_release} stable',
}, },
}, },
}, },

View file

@ -4,8 +4,7 @@ After=network.target
Requires=infobeamer-cms.service Requires=infobeamer-cms.service
[Service] [Service]
Environment=SETTINGS=/opt/infobeamer-cms/settings.toml
WorkingDirectory=/opt/infobeamer-cms/src
User=infobeamer-cms User=infobeamer-cms
Group=infobeamer-cms Group=infobeamer-cms
ExecStart=/opt/infobeamer-cms/venv/bin/python syncer.py WorkingDirectory=/opt/infobeamer-cms
ExecStart=curl -s -H "Host: ${domain}" http://127.0.0.1:8000/sync

View file

@ -2,7 +2,7 @@
Description=Run infobeamer-cms sync Description=Run infobeamer-cms sync
[Timer] [Timer]
OnCalendar=minutely OnCalendar=*:0/5
Persistent=true Persistent=true
[Install] [Install]

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,4 +1,8 @@
actions = { actions = {
'infobeamer-cms_set_directory_permissions': {
'triggered': True,
'command': 'chown -R infobeamer-cms:infobeamer-cms /opt/infobeamer-cms/src/static/'
},
'infobeamer-cms_create_virtualenv': { 'infobeamer-cms_create_virtualenv': {
'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/infobeamer-cms/venv/', 'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/infobeamer-cms/venv/',
'unless': 'test -d /opt/infobeamer-cms/venv/', 'unless': 'test -d /opt/infobeamer-cms/venv/',
@ -8,11 +12,7 @@ actions = {
}, },
}, },
'infobeamer-cms_install_requirements': { 'infobeamer-cms_install_requirements': {
'command': ' && '.join([ 'command': 'cd /opt/infobeamer-cms/src && /opt/infobeamer-cms/venv/bin/pip install --upgrade pip gunicorn -r requirements.txt',
'cd /opt/infobeamer-cms/src',
'/opt/infobeamer-cms/venv/bin/pip install --upgrade pip gunicorn -r requirements.txt',
'rsync /opt/infobeamer-cms/src/static/* /opt/infobeamer-cms/static/',
]),
'needs': { 'needs': {
'action:infobeamer-cms_create_virtualenv', 'action:infobeamer-cms_create_virtualenv',
}, },
@ -23,12 +23,13 @@ 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',
}, },
'triggers': { 'triggers': {
'svc_systemd:infobeamer-cms:restart', 'svc_systemd:infobeamer-cms:restart',
'action:infobeamer-cms_set_directory_permissions',
'action:infobeamer-cms_install_requirements', 'action:infobeamer-cms_install_requirements',
}, },
}, },
@ -36,9 +37,6 @@ git_deploy = {
directories = { directories = {
'/opt/infobeamer-cms/src': {}, '/opt/infobeamer-cms/src': {},
'/opt/infobeamer-cms/static': {
'owner': 'infobeamer-cms',
},
} }
config = node.metadata.get('infobeamer-cms/config', {}) config = node.metadata.get('infobeamer-cms/config', {})
@ -68,7 +66,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,11 +97,19 @@ files = {
}, },
} }
pkg_pip = {
'github-flask': {
'needed_by': {
'svc_systemd:infobeamer-cms',
},
},
}
svc_systemd = { svc_systemd = {
'infobeamer-cms': { 'infobeamer-cms': {
'needs': { 'needs': {
'action:infobeamer-cms_install_requirements', 'action:infobeamer-cms_install_requirements',
'directory:/opt/infobeamer-cms/static', 'action:infobeamer-cms_set_directory_permissions',
'file:/etc/systemd/system/infobeamer-cms.service', 'file:/etc/systemd/system/infobeamer-cms.service',
'file:/opt/infobeamer-cms/settings.toml', 'file:/opt/infobeamer-cms/settings.toml',
'git_deploy:/opt/infobeamer-cms/src', 'git_deploy:/opt/infobeamer-cms/src',
@ -108,12 +117,8 @@ svc_systemd = {
}, },
'infobeamer-cms-runperiodic.timer': { 'infobeamer-cms-runperiodic.timer': {
'needs': { 'needs': {
'action:infobeamer-cms_install_requirements',
'directory:/opt/infobeamer-cms/static',
'file:/etc/systemd/system/infobeamer-cms-runperiodic.service',
'file:/etc/systemd/system/infobeamer-cms-runperiodic.timer', 'file:/etc/systemd/system/infobeamer-cms-runperiodic.timer',
'file:/opt/infobeamer-cms/settings.toml', 'file:/etc/systemd/system/infobeamer-cms-runperiodic.service',
'git_deploy:/opt/infobeamer-cms/src',
}, },
}, },
} }

View file

@ -1,15 +1,11 @@
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',
'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'),
'VERSION': 1, 'VERSION': 1,
}, },
@ -33,13 +29,15 @@ def nginx(metadata):
'/': { '/': {
'target': 'http://127.0.0.1:8000', 'target': 'http://127.0.0.1:8000',
}, },
'/sync': {
'return': 403,
},
'/static': { '/static': {
'alias': '/opt/infobeamer-cms/static', 'alias': '/opt/infobeamer-cms/src/static',
}, },
}, },
'website_check_path': '/', 'website_check_path': '/',
'website_check_string': 'Share your projects', 'website_check_string': 'Share your projects',
'do_not_set_content_security_headers': True,
}, },
}, },
}, },
@ -47,12 +45,11 @@ def nginx(metadata):
@metadata_reactor.provides( @metadata_reactor.provides(
'infobeamer-cms/config/DOMAIN',
'infobeamer-cms/config/TIME_MAX', 'infobeamer-cms/config/TIME_MAX',
'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)
@ -60,7 +57,6 @@ def event_times(metadata):
return { return {
'infobeamer-cms': { 'infobeamer-cms': {
'config': { 'config': {
'DOMAIN': metadata.get('infobeamer-cms/domain'),
'TIME_MAX': int(event_end.timestamp()), 'TIME_MAX': int(event_end.timestamp()),
'TIME_MIN': int(event_start.timestamp()), 'TIME_MIN': int(event_start.timestamp()),
}, },

View file

@ -1,15 +0,0 @@
[Unit]
Description=infobeamer-monitor
After=network.target
[Service]
Type=exec
Restart=always
RestartSec=5s
ExecStart=/opt/infobeamer-cms/venv/bin/python monitor.py
User=infobeamer-cms
Group=infobeamer-cms
WorkingDirectory=/opt/infobeamer-monitor/
[Install]
WantedBy=multi-user.target

View file

@ -1,217 +0,0 @@
#!/usr/bin/env python3
import logging
from datetime import datetime
from json import dumps
from time import sleep
from zoneinfo import ZoneInfo
import paho.mqtt.client as mqtt
from requests import RequestException, get
try:
# python 3.11
from tomllib import loads as toml_load
except ImportError:
from rtoml import load as toml_load
with open("config.toml") as f:
CONFIG = toml_load(f.read())
logging.basicConfig(
format="[%(levelname)s %(name)s] %(message)s",
level=logging.INFO,
)
LOG = logging.getLogger("main")
TZ = ZoneInfo("Europe/Berlin")
DUMP_TIME = "0900"
state = None
client = mqtt.Client()
client.username_pw_set(CONFIG["mqtt"]["user"], CONFIG["mqtt"]["password"])
client.connect(CONFIG["mqtt"]["host"], 1883, 60)
client.loop_start()
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}"
client.publish(
CONFIG["mqtt"]["topic"],
dumps(
{
"level": level,
"component": key,
"msg": message,
}
),
)
def mqtt_dump_state(device):
if not device["is_online"]:
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(
" - ".join(out),
device=device,
)
def is_dump_time():
return datetime.now(TZ).strftime("%H%M") == DUMP_TIME
mqtt_out("Monitor starting up")
while True:
try:
try:
r = get(
"https://info-beamer.com/api/v1/device/list",
auth=("", CONFIG["api_key"]),
)
r.raise_for_status()
ib_state = r.json()["devices"]
except RequestException as e:
LOG.exception("Could not get device data from info-beamer")
mqtt_out(
f"Could not get device data from info-beamer: {e!r}",
level="WARN",
)
else:
new_state = {}
for device in sorted(ib_state, key=lambda x: x["id"]):
did = str(device["id"])
if did in new_state:
mqtt_out("DUPLICATE DETECTED!", level="ERROR", device=device)
continue
new_state[did] = device
# force information output for every online device at 09:00 CE(S)T
must_dump_state = is_dump_time()
if state is not None:
if did not in state:
LOG.info(
"new device found: {} [{}]".format(
did,
device["description"],
)
)
mqtt_out(
"new device found!",
device=device,
)
must_dump_state = True
else:
if device["is_online"] != state[did]["is_online"]:
online_status = (
"online from {}".format(device["run"]["public_addr"])
if device["is_online"]
else "offline"
)
LOG.info("device {} is now {}".format(did, online_status))
mqtt_out(
f"status changed to {online_status}",
level="INFO" if device["is_online"] else "WARN",
device=device,
)
must_dump_state = True
if device["description"] != state[did]["description"]:
LOG.info(
"device {} changed name to {}".format(
did, device["description"]
)
)
must_dump_state = True
if device["is_online"]:
if device["maintenance"]:
mqtt_out(
"maintenance required: {}".format(
" ".join(sorted(device["maintenance"]))
),
level="WARN",
device=device,
)
if (
device["location"] != state[did]["location"]
or device["setup"]["id"] != state[did]["setup"]["id"]
or device["run"].get("resolution")
!= state[did]["run"].get("resolution")
):
must_dump_state = True
if must_dump_state:
mqtt_dump_state(device)
else:
LOG.info("adding device {} to empty state".format(device["id"]))
state = new_state
try:
r = get(
"https://info-beamer.com/api/v1/account",
auth=("", CONFIG["api_key"]),
)
r.raise_for_status()
ib_account = r.json()
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:
break
mqtt_out("Monitor exiting")

View file

@ -1,30 +0,0 @@
assert node.has_bundle('infobeamer-cms') # uses same venv
files['/opt/infobeamer-monitor/config.toml'] = {
'content': repo.libs.faults.dict_as_toml(node.metadata.get('infobeamer-monitor')),
'triggers': {
'svc_systemd:infobeamer-monitor:restart',
},
}
files['/opt/infobeamer-monitor/monitor.py'] = {
'mode': '0755',
'triggers': {
'svc_systemd:infobeamer-monitor:restart',
},
}
files['/usr/local/lib/systemd/system/infobeamer-monitor.service'] = {
'triggers': {
'action:systemd-reload',
'svc_systemd:infobeamer-monitor:restart',
},
}
svc_systemd['infobeamer-monitor'] = {
'needs': {
'file:/opt/infobeamer-monitor/config.toml',
'file:/opt/infobeamer-monitor/monitor.py',
'file:/usr/local/lib/systemd/system/infobeamer-monitor.service',
},
}

View file

@ -1,7 +0,0 @@
Cmnd_Alias RESTARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl restart jellyfin
Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemd-run systemctl start jellyfin
Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemd-run systemctl stop jellyfin
jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSTEMD

View file

@ -1,5 +0,0 @@
files['/etc/sudoers.d/jellyfin-sudoers'] = {
'after': {
'pkg_apt:jellyfin',
},
}

View file

@ -1,69 +0,0 @@
from bundlewrap.metadata import atomic
defaults = {
'apt': {
'packages': {
'jellyfin': {},
},
'repos': {
'jellyfin': {
'uris': {
'https://repo.jellyfin.org/{os}'
},
},
},
},
'backups': {
'paths': {
f'/var/lib/jellyfin/{x}' for x in ('data', 'metadata', 'plugins', 'root')
},
},
'icinga2_api': {
'transmission': {
'services': {
'JELLYFIN PROCESS': {
'command_on_monitored_host': '/usr/lib/nagios/plugins/check_procs -C jellyfin -c 1:',
},
},
},
},
}
@metadata_reactor.provides(
'nginx/vhosts/jellyfin',
)
def nginx(metadata):
if not node.has_bundle('nginx'):
raise DoNotRunAgain
if 'jellyfin' not in metadata.get('nginx/vhosts', {}):
return {}
return {
'nginx': {
'vhosts': {
'jellyfin': {
'do_not_add_content_security_headers': True,
'locations': {
'/': {
'target': 'http://127.0.0.1:8096',
'websockets': True,
},
},
},
},
},
}
@metadata_reactor.provides(
'firewall/port_rules',
)
def firewall(metadata):
return {
'firewall': {
'port_rules': {
'8096/tcp': atomic(metadata.get('jellyfin/restrict-to', set())),
},
},
}

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

@ -1,16 +0,0 @@
[Unit]
Description=jugendhackt_tools web service
After=network.target
Requires=postgresql.service
[Service]
User=jugendhackt_tools
Group=jugendhackt_tools
Environment=CONFIG_PATH=/opt/jugendhackt_tools/config.toml
WorkingDirectory=/opt/jugendhackt_tools/src
ExecStart=/opt/jugendhackt_tools/venv/bin/gunicorn jugendhackt_tools.wsgi --name jugendhackt_tools --workers 4 --max-requests 1200 --max-requests-jitter 50 --log-level=info --bind=127.0.0.1:22090
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View file

@ -1,75 +0,0 @@
directories['/opt/jugendhackt_tools/src'] = {}
git_deploy['/opt/jugendhackt_tools/src'] = {
'repo': 'https://github.com/kunsi/jugendhackt_schedule.git',
'rev': 'main',
'triggers': {
'action:jugendhackt_tools_install',
'action:jugendhackt_tools_migrate',
'svc_systemd:jugendhackt_tools:restart',
},
}
actions['jugendhackt_tools_create_virtualenv'] = {
'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/jugendhackt_tools/venv/',
'unless': 'test -d /opt/jugendhackt_tools/venv/',
'needs': {
# actually /opt/jugendhackt_tools, but we don't create that
'directory:/opt/jugendhackt_tools/src',
},
}
actions['jugendhackt_tools_install'] = {
'command': ' && '.join([
'cd /opt/jugendhackt_tools/src',
'/opt/jugendhackt_tools/venv/bin/pip install --upgrade pip wheel gunicorn psycopg2-binary',
'/opt/jugendhackt_tools/venv/bin/pip install --upgrade -r requirements.txt',
]),
'needs': {
'action:jugendhackt_tools_create_virtualenv',
},
'triggered': True,
}
actions['jugendhackt_tools_migrate'] = {
'command': ' && '.join([
'cd /opt/jugendhackt_tools/src',
'CONFIG_PATH=/opt/jugendhackt_tools/config.toml /opt/jugendhackt_tools/venv/bin/python manage.py migrate',
'CONFIG_PATH=/opt/jugendhackt_tools/config.toml /opt/jugendhackt_tools/venv/bin/python manage.py collectstatic --noinput',
]),
'needs': {
'action:jugendhackt_tools_install',
'file:/opt/jugendhackt_tools/config.toml',
'postgres_db:jugendhackt_tools',
'postgres_role:jugendhackt_tools',
},
'triggered': True,
}
files['/opt/jugendhackt_tools/config.toml'] = {
'content': repo.libs.faults.dict_as_toml(node.metadata.get('jugendhackt_tools')),
'triggers': {
'svc_systemd:jugendhackt_tools:restart',
},
}
files['/usr/local/lib/systemd/system/jugendhackt_tools.service'] = {
'triggers': {
'action:systemd-reload',
'svc_systemd:jugendhackt_tools:restart',
},
}
svc_systemd['jugendhackt_tools'] = {
'needs': {
'action:jugendhackt_tools_migrate',
'file:/opt/jugendhackt_tools/config.toml',
'file:/usr/local/lib/systemd/system/jugendhackt_tools.service',
'git_deploy:/opt/jugendhackt_tools/src',
'user:jugendhackt_tools',
},
}
users['jugendhackt_tools'] = {
'home': '/opt/jugendhackt_tools/src',
}

View file

@ -1,28 +0,0 @@
defaults = {
'jugendhackt_tools': {
'django_secret': repo.vault.random_bytes_as_base64_for(f'{node.name} jugendhackt_tools django_secret'),
'django_debug': False,
'static_root': '/opt/jugendhackt_tools/src/static/',
'database': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'jugendhackt_tools',
'USER': 'jugendhackt_tools',
'PASSWORD': repo.vault.password_for(f'{node.name} postgresql jugendhackt_tools'),
'HOST': 'localhost',
'PORT': '5432'
},
},
'postgresql': {
'roles': {
'jugendhackt_tools': {
'password': repo.vault.password_for(f'{node.name} postgresql jugendhackt_tools'),
},
},
'databases': {
'jugendhackt_tools': {
'owner': 'jugendhackt_tools',
},
},
},
}

View file

@ -1,37 +0,0 @@
#!/usr/bin/env python3
from csv import DictReader
from datetime import datetime, timezone
from os import scandir
from os.path import join
def parse():
NOW = datetime.now()
active_leases = {}
for file in scandir("/var/lib/kea/"):
with open(file.path) as f:
for row in DictReader(f):
expires = datetime.fromtimestamp(int(row["expire"]))
if expires >= NOW:
if (
row["address"] not in active_leases
or active_leases[row["address"]]["expires_dt"] < expires
):
row["expires_dt"] = expires
active_leases[row["address"]] = row
return active_leases.values()
def print_table(leases):
print(""" address | MAC | expires | hostname
-----------------+-------------------+---------+----------""")
for lease in sorted(leases, key=lambda r: r["address"]):
print(
f' {lease["address"]:<15} | {lease["hwaddr"].lower()} | {lease["expires_dt"]:%H:%M} | {lease["hostname"]}'
)
if __name__ == "__main__":
print_table(parse())

View file

@ -1,56 +0,0 @@
kea_config = {
'Dhcp4': {
**node.metadata.get('kea-dhcp-server/config'),
'interfaces-config': {
'interfaces': sorted(node.metadata.get('kea-dhcp-server/subnets', {}).keys()),
},
'subnet4': [],
'loggers': [{
'name': 'kea-dhcp4',
'output_options': [{
# -> journal
'output': 'stdout',
}],
'severity': 'WARN',
}],
},
}
for iface, config in sorted(node.metadata.get('kea-dhcp-server/subnets', {}).items()):
kea_config['Dhcp4']['subnet4'].append({
'subnet': config['subnet'],
'pools': [{
'pool': f'{config["lower"]} - {config["higher"]}',
}],
'option-data': [
{
'name': k,
'data': v,
} for k, v in sorted(config.get('options', {}).items())
],
'reservations': [
{
'ip-address': v['ip'],
'hw-address': v['mac'],
'hostname': k,
} for k, v in sorted(node.metadata.get(f'kea-dhcp-server/fixed_allocations/{iface}', {}).items())
]
})
files['/etc/kea/kea-dhcp4.conf'] = {
'content': repo.libs.faults.dict_as_json(kea_config),
'triggers': {
'svc_systemd:kea-dhcp4-server:restart',
},
}
files['/usr/local/bin/kea-lease-list'] = {
'mode': '0500',
}
svc_systemd['kea-dhcp4-server'] = {
'needs': {
'file:/etc/kea/kea-dhcp4.conf',
'pkg_apt:kea-dhcp4-server',
},
}

View file

@ -1,83 +0,0 @@
from ipaddress import ip_address, ip_network
defaults = {
'apt': {
'packages': {
'kea-dhcp4-server': {},
},
},
'kea-dhcp-server': {
'config': {
'authoritative': True,
'rebind-timer': 450,
'renew-timer': 300,
'valid-lifetime': 600,
'expired-leases-processing': {
'max-reclaim-leases': 0,
'max-reclaim-time': 0,
},
'lease-database': {
'lfc-interval': 3600,
'name': '/var/lib/kea/kea-leases4.csv',
'persist': True,
'type': 'memfile',
},
},
},
}
@metadata_reactor.provides(
'kea-dhcp-server/fixed_allocations',
)
def get_static_allocations(metadata):
result = {}
mapping = {}
for iface, config in metadata.get('kea-dhcp-server/subnets', {}).items():
result[iface] = {}
mapping[iface] = ip_network(config['subnet'])
for rnode in repo.nodes:
if (
rnode.metadata.get('location', '') != metadata.get('location', '')
or rnode == node
):
continue
for iface_name, iface_config in rnode.metadata.get('interfaces', {}).items():
if iface_config.get('dhcp', False) and iface_config.get('mac'):
for ip in iface_config.get('ips', set()):
ipaddr = ip_address(ip)
for kea_iface, kea_subnet in mapping.items():
if ipaddr in kea_subnet:
result[kea_iface][f'{rnode.name}_{iface_name}'] = {
'ip': ip,
'mac': iface_config['mac'],
}
break
return {
'kea-dhcp-server': {
'fixed_allocations': result,
}
}
@metadata_reactor.provides(
'nftables/input/10-kea-dhcp-server',
)
def nftables(metadata):
rules = set()
for iface in node.metadata.get('kea-dhcp-server/subnets', {}):
rules.add(f'udp dport {{ 67, 68 }} iifname {iface} accept')
return {
'nftables': {
'input': {
# can't use port_rules here, because we're generating interface based rules.
'10-kea-dhcp-server': sorted(rules),
},
}
}

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

@ -43,15 +43,15 @@ defaults = {
@metadata_reactor.provides( @metadata_reactor.provides(
'firewall/port_rules', 'firewall/port_rules/8080',
'firewall/port_rules', 'firewall/port_rules/9090',
) )
def firewall(metadata): def firewall(metadata):
return { return {
'firewall': { 'firewall': {
'port_rules': { 'port_rules': {
'8080/tcp': atomic(metadata.get('kodi/restrict-to', {'*'})), '8080': atomic(metadata.get('kodi/restrict-to', {'*'})),
'9090/tcp': atomic(metadata.get('kodi/restrict-to', {'*'})), '9090': atomic(metadata.get('kodi/restrict-to', {'*'})),
}, },
}, },
} }

View file

@ -39,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

@ -0,0 +1,14 @@
[Unit]
Description=Matrix Dimension
After=network.target
[Service]
User=matrix-dimension
Group=matrix-dimension
Environment="NODE_ENV=production"
ExecStart=/usr/bin/node ${config['install_dir']}/build/app/index.js
WorkingDirectory=${config['install_dir']}
Restart=on-failure
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,93 @@
# The web settings for the service (API and UI).
# It is best to have this run on localhost and use a reverse proxy to access Dimension.
web:
port: 20030
address: '127.0.0.1'
# Homeserver configuration
homeserver:
# The domain name of the homeserver. This is used in many places, such as with go-neb
# setups, to identify the homeserver.
name: "${config['homeserver']['name']}"
# The URL that Dimension, go-neb, and other services provisioned by Dimension should
# use to access the homeserver with.
clientServerUrl: "${config['homeserver']['clientServerUrl']}"
# The URL that Dimension should use when trying to communicate with federated APIs on
# the homeserver. If not supplied or left empty Dimension will try to resolve the address
# through the normal federation process.
#federationUrl: "https://t2bot.io:8448"
# The URL that Dimension will redirect media requests to for downloading media such as
# stickers. If not supplied or left empty Dimension will use the clientServerUrl.
#mediaUrl: "https://t2bot.io"
# The access token Dimension should use for miscellaneous access to the homeserver, and
# for tracking custom sticker pack updates. This should be a user configured on the homeserver
# and be dedicated to Dimension (create a user named "dimension" on your homeserver). For
# information on how to acquire an access token, visit https://t2bot.io/docs/access_tokens
accessToken: "${config['homeserver']['accessToken']}"
# These users can modify the integrations this Dimension supports.
# To access the admin interface, open Dimension in Riot and click the settings icon.
admins:
% for i in config['admins']:
- "${i}"
% endfor
# IPs and CIDR ranges listed here will be blocked from being widgets.
# Note: Widgets may still be embedded with restricted content, although not through Dimension directly.
widgetBlacklist:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8
database:
# Where the database for Dimension is
uri: "postgres://${node.metadata['matrix-dimension']['database']['user']}:${node.metadata['matrix-dimension']['database']['password']}@${node.metadata['matrix-dimension']['database'].get('host', 'localhost')}/${node.metadata['matrix-dimension']['database']['database']}"
# Where to store misc information for the utility bot account.
botData: "${config['data_dir']}/dimension.bot.json"
# Display settings that apply to self-hosted go-neb instances
goneb:
# The avatars to set for each bot. Usually these don't need to be changed, however if your homeserver
# is not able to reach t2bot.io then you should specify your own here. To not use an avatar for a bot,
# make the bot's avatar an empty string.
avatars:
giphy: "mxc://t2bot.io/c5eaab3ef0133c1a61d3c849026deb27"
imgur: "mxc://t2bot.io/6749eaf2b302bb2188ae931b2eeb1513"
github: "mxc://t2bot.io/905b64b3cd8e2347f91a60c5eb0832e1"
wikipedia: "mxc://t2bot.io/7edfb54e9ad9e13fec0df22636feedf1"
travisci: "mxc://t2bot.io/7f4703126906fab8bb27df34a17707a8"
rss: "mxc://t2bot.io/aace4fcbd045f30afc1b4e5f0928f2f3"
google: "mxc://t2bot.io/636ad10742b66c4729bf89881a505142"
guggy: "mxc://t2bot.io/e7ef0ed0ba651aaf907655704f9a7526"
echo: "mxc://t2bot.io/3407ff2db96b4e954fcbf2c6c0415a13"
circleci: "mxc://t2bot.io/cf7d875845a82a6b21f5f66de78f6bee"
jira: "mxc://t2bot.io/f4a38ebcc4280ba5b950163ca3e7c329"
# Settings for interacting with Telegram. Currently only applies for importing
# sticker packs from Telegram.
telegram:
# Talk to @BotFather on Telegram to get a token
botToken: "${config['telegram']['botToken']}"
# Custom sticker pack options.
# Largely based on https://github.com/turt2live/matrix-sticker-manager
stickers:
# Whether or not to allow people to add custom sticker packs
enabled: true
# The sticker manager bot to promote
stickerBot: "@stickers:t2bot.io"
# The sticker manager URL to promote
managerUrl: "https://stickers.t2bot.io"
# Settings for controlling how logging works
logging:
console: true
consoleLevel: info

View file

@ -0,0 +1,78 @@
repo.libs.tools.require_bundle(node, 'nodejs')
directories = {
node.metadata['matrix-dimension']['install_dir']: {
'owner': 'matrix-dimension',
'group': 'matrix-dimension',
},
}
git_deploy = {
node.metadata['matrix-dimension']['install_dir']: {
'rev': node.metadata.get('matrix-dimension/version', 'master'), # doesn't have releases yet
'repo': 'https://github.com/turt2live/matrix-dimension.git',
'triggers': {
'action:matrix_dimension_build',
},
'needs': {
'directory:{}'.format(node.metadata.get('matrix-dimension/install_dir')),
'directory:{}'.format(node.metadata.get('matrix-dimension/data_dir')),
},
},
}
files = {
'{}/config/production.yaml'.format(node.metadata.get('matrix-dimension/install_dir')): {
'owner': 'matrix-dimension',
'group': 'matrix-dimension',
'content_type': 'mako',
'context': {
'config': node.metadata.get('matrix-dimension', {}),
},
'needs': {
'git_deploy:{}'.format(node.metadata.get('matrix-dimension/install_dir')),
},
'triggers': {
'svc_systemd:matrix-dimension:restart',
},
},
'/etc/systemd/system/matrix-dimension.service': {
'content_type': 'mako',
'context': {
'config': node.metadata.get('matrix-dimension', {}),
},
'triggers': {
'action:systemd-reload',
'svc_systemd:matrix-dimension:restart',
},
},
}
actions = {
'matrix_dimension_build': {
'command': ' && '.join([
'cd ' + node.metadata.get('matrix-dimension/install_dir'),
'sudo -u matrix-dimension npm install --legacy-peer-deps',
'sudo -u matrix-dimension NODE_OPTIONS=--openssl-legacy-provider npm run build',
]),
'needs': {
'pkg_apt:nodejs',
},
'triggered': True,
'triggers': {
'svc_systemd:matrix-dimension:restart',
},
},
}
svc_systemd = {
'matrix-dimension': {
'needs': {
'action:matrix_dimension_build',
'file:{}/config/production.yaml'.format(node.metadata.get('matrix-dimension/install_dir')),
'postgres_db:matrix-dimension',
'postgres_role:matrix-dimension',
},
},
}

View file

@ -0,0 +1,110 @@
defaults = {
'backups': {
'paths': {
'/opt/matrix-dimension',
'/var/opt/matrix-dimension',
},
},
'icinga2_api': {
'matrix-dimension': {
'services': {
'MATRIX-DIMENSION PROCESS': {
'command_on_monitored_host': '/usr/lib/nagios/plugins/check_procs -a matrix-dimension -c 1:',
},
},
},
},
'matrix-dimension': {
'install_dir': '/opt/matrix-dimension',
'data_dir': '/var/opt/matrix-dimension',
'database': {
'user': 'matrix-dimension',
'password': repo.vault.password_for('{} postgresql matrix-dimension'.format(node.name)),
'database': 'matrix-dimension',
},
},
'postgresql': {
'roles': {
'matrix-dimension': {
'password': repo.vault.password_for('{} postgresql matrix-dimension'.format(node.name)),
},
},
'databases': {
'matrix-dimension': {
'owner': 'matrix-dimension',
},
},
},
'users': {
'matrix-dimension': {
'home': '/var/opt/matrix-dimension',
},
},
}
@metadata_reactor.provides(
'nginx/vhosts/matrix-dimension',
)
def nginx_config(metadata):
return {
'nginx': {
'vhosts': {
'matrix-dimension': {
'domain': metadata.get('matrix-dimension/url'),
'do_not_set_content_security_headers': True,
'max_body_size': '50M',
'locations': {
'/': {
'target': 'http://127.0.0.1:20030',
},
},
},
},
},
}
@metadata_reactor.provides(
'zfs/datasets',
)
def zfs(metadata):
return {
'zfs': {
'datasets': {
'tank/matrix-dimension': {},
'tank/matrix-dimension/install': {
'mountpoint': metadata.get('matrix-dimension/install_dir'),
'needed_by': {
'directory:{}'.format(metadata.get('matrix-dimension/install_dir')),
},
},
'tank/matrix-dimension/var': {
'mountpoint': metadata.get('matrix-dimension/data_dir'),
'needed_by': {
'directory:{}'.format(metadata.get('matrix-dimension/data_dir')),
},
},
},
},
}
# XXX enable this once there are releases for matrix-dimension
#@metadata_reactor.provides(
# 'icinga2_api/matrix-dimension/services',
#)
#def icinga_check_for_new_release(metadata):
# return {
# 'icinga2_api': {
# 'matrix-dimension': {
# 'services': {
# 'MATRIX-DIMENSION UPDATE': {
# 'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_github_for_new_release turt2live/matrix-dimension {}'.format(metadata.get('matrix-dimension/version')),
# 'vars.notification.mail': True,
# 'check_interval': '60m',
# },
# },
# },
# },
# }

View file

@ -1,11 +1,8 @@
# General repo configuration # General repo configuration
repo: repo:
bindAddress: '${node.metadata.get('matrix-media-repo/listen-addr', '127.0.0.1')}' bindAddress: '${node.metadata['matrix-media-repo'].get('listen-addr', '127.0.0.1')}'
port: ${node.metadata.get('matrix-media-repo/port', 20090)} port: ${node.metadata['matrix-media-repo'].get('port', 20090)}
logDirectory: '-' logDirectory: '-'
logColors: false
jsonLogs: false
logLevel: 'info'
trustAnyForwardedAddress: false trustAnyForwardedAddress: false
useForwardedHost: true useForwardedHost: true
@ -13,67 +10,73 @@ federation:
backoffAt: 20 backoffAt: 20
database: database:
postgres: "postgres://${node.metadata.get('matrix-media-repo/database/user')}:${node.metadata.get('matrix-media-repo/database/password')}@${node.metadata.get('matrix-media-repo/database/host', 'localhost')}/${node.metadata.get('matrix-media-repo/database/database')}?sslmode=disable" postgres: "postgres://${node.metadata['matrix-media-repo']['database']['user']}:${node.metadata['matrix-media-repo']['database']['password']}@${node.metadata['matrix-media-repo']['database'].get('host', 'localhost')}/${node.metadata['matrix-media-repo']['database']['database']}?sslmode=disable"
pool: pool:
maxConnections: 25 maxConnections: 25
maxIdleConnections: 5 maxIdleConnections: 5
homeservers: homeservers:
% for homeserver, config in node.metadata.get('matrix-media-repo/homeservers').items(): % for homeserver, config in node.metadata['matrix-media-repo'].get('homeservers', {}).items():
- name: ${homeserver} - name: ${homeserver}
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:
% for user in sorted(node.metadata.get('matrix-media-repo/admins')): % for user in sorted(node.metadata['matrix-media-repo']['admins']):
- "${user}" - "${user}"
% endfor % endfor
sharedSecretAuth: sharedSecretAuth:
enabled: false enabled: false
token: "${node.metadata.get('matrix-media-repo/shared-secret-token')}" token: "${node.metadata['matrix-media-repo']['shared-secret-token']}"
datastores: datastores:
- type: file - type: file
id: "${node.metadata.get('matrix-media-repo/datastore_id')}"
enabled: true enabled: true
forKinds: ['all'] forKinds:
- 'thumbnails'
- 'remote_media'
- 'local_media'
- 'archives'
opts: opts:
path: /var/matrix/media path: /var/matrix/media
archiving: archiving:
enabled: true enabled: true
selfService: ${str(node.metadata.get('matrix-media-repo/archive/self-service')).lower()} selfService: ${str(node.metadata['matrix-media-repo']['archive']['self-service']).lower()}
targetBytesPerPart: ${node.metadata.get('matrix-media-repo/archive/mb_per_part', node.metadata.get('matrix-media-repo/upload_max_mb')*2)*1024*1024} targetBytesPerPart: ${node.metadata['matrix-media-repo']['archive'].get('mb_per_part', node.metadata['matrix-media-repo']['upload_max_mb']*2)*1024*1024}
uploads: uploads:
maxBytes: ${node.metadata.get('matrix-media-repo/upload_max_mb')*1024*1024} maxBytes: ${node.metadata['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
downloads: downloads:
maxBytes: ${node.metadata.get('matrix-media-repo/download_max_mb')*1024*1024} maxBytes: ${node.metadata['matrix-media-repo']['download_max_mb']*1024*1024}
numWorkers: ${node.metadata.get('matrix-media-repo/workers')} numWorkers: ${node.metadata['matrix-media-repo']['workers']}
failureCacheMinutes: 5 failureCacheMinutes: 5
cache:
enabled: true
maxSizeBytes: ${node.metadata['matrix-media-repo']['download_max_mb']*10*1024*1024}
maxFileSizeBytes: ${node.metadata['matrix-media-repo']['upload_max_mb']*1024*1024}
trackedMinutes: 30
minDownloads: 5
minCacheTimeSeconds: 300
minEvictedTimeSeconds: 60
expireAfterDays: 0 expireAfterDays: 0
urlPreviews: urlPreviews:
enabled: true enabled: true
maxPageSizeBytes: ${node.metadata.get('matrix-media-repo/preview_max_mb')*1024*1024} maxPageSizeBytes: ${node.metadata['matrix-media-repo']['preview_max_mb']*1024*1024}
previewUnsafeCertificates: false previewUnsafeCertificates: false
numWords: 50 numWords: 50
maxLength: 200 maxLength: 200
@ -81,7 +84,7 @@ urlPreviews:
maxTitleLength: 150 maxTitleLength: 150
filePreviewTypes: filePreviewTypes:
- "image/*" - "image/*"
numWorkers: ${node.metadata.get('matrix-media-repo/workers')} numWorkers: ${node.metadata['matrix-media-repo']['workers']}
disallowedNetworks: disallowedNetworks:
- "127.0.0.1/8" - "127.0.0.1/8"
- "10.0.0.0/8" - "10.0.0.0/8"
@ -100,8 +103,8 @@ urlPreviews:
oEmbed: false oEmbed: false
thumbnails: thumbnails:
maxSourceBytes: ${node.metadata.get('matrix-media-repo/preview_max_mb')*1024*1024} maxSourceBytes: ${node.metadata['matrix-media-repo']['preview_max_mb']*1024*1024}
numWorkers: ${node.metadata.get('matrix-media-repo/workers')} numWorkers: ${node.metadata['matrix-media-repo']['workers']}
sizes: sizes:
- width: 32 - width: 32
height: 32 height: 32
@ -131,14 +134,14 @@ thumbnails:
- "video/mp4" - "video/mp4"
allowAnimated: true allowAnimated: true
defaultAnimated: false defaultAnimated: false
maxAnimateSizeBytes: ${node.metadata.get('matrix-media-repo/preview_max_mb')*1024*1024} maxAnimateSizeBytes: ${node.metadata['matrix-media-repo']['preview_max_mb']*1024*1024}
stillFrame: 0.5 stillFrame: 0.5
expireAfterDays: 0 expireAfterDays: 0
rateLimit: rateLimit:
enabled: true enabled: true
requestsPerSecond: 100 requestsPerSecond: 10
burst: 5000 burst: 50
identicons: identicons:
enabled: true enabled: true

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