Compare commits
11 commits
main
...
kunsi-wood
Author | SHA1 | Date | |
---|---|---|---|
d282d77a99 | |||
cb4d28c994 | |||
071250d798 | |||
efdff6ef28 | |||
d2caadb41b | |||
9b44bcf3a8 | |||
24f9f87734 | |||
019cc69371 | |||
eee786fabf | |||
c2e93c0abb | |||
cc767867cf |
460 changed files with 6300 additions and 8609 deletions
|
@ -22,6 +22,3 @@ indent_size = unset
|
|||
[*.vault]
|
||||
end_of_line = unset
|
||||
insert_final_newline = unset
|
||||
|
||||
[*.json]
|
||||
insert_final_newline = unset
|
||||
|
|
26
.woodpecker/bw-test.yml
Normal file
26
.woodpecker/bw-test.yml
Normal file
|
@ -0,0 +1,26 @@
|
|||
pipeline:
|
||||
install-deps:
|
||||
image: python:3.10-slim
|
||||
commands:
|
||||
- pip install -r requirements.txt
|
||||
|
||||
test-dummymode:
|
||||
image: python:3.10-slim
|
||||
commands:
|
||||
- bw test
|
||||
environment:
|
||||
BW_VAULT_DUMMY_MODE: 1
|
||||
BW_PASS_DUMMY_MODE: 1
|
||||
|
||||
test-ignore-missing-faults:
|
||||
image: python:3.10-slim
|
||||
commands:
|
||||
- bw test --ignore-missing-faults
|
||||
|
||||
test-determinism:
|
||||
image: python:3.10-slim
|
||||
commands:
|
||||
- bw test --metadata-determinism 3 --config-determinism 3
|
||||
environment:
|
||||
BW_VAULT_DUMMY_MODE: 1
|
||||
BW_PASS_DUMMY_MODE: 1
|
8
.woodpecker/editorconfig.yml
Normal file
8
.woodpecker/editorconfig.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
pipeline:
|
||||
editorconfig:
|
||||
image: alpine:latest
|
||||
commands:
|
||||
- wget -O ec-linux-amd64.tar.gz https://github.com/editorconfig-checker/editorconfig-checker/releases/latest/download/ec-linux-amd64.tar.gz
|
||||
- tar -xzf ec-linux-amd64.tar.gz
|
||||
- rm ec-linux-amd64.tar.gz
|
||||
- bin/ec-linux-amd64 -no-color -exclude '^bin/'
|
9
Jenkinsfile
vendored
9
Jenkinsfile
vendored
|
@ -25,6 +25,15 @@ pipeline {
|
|||
"""
|
||||
}
|
||||
}
|
||||
stage('syntax checking using isort') {
|
||||
steps {
|
||||
sh """
|
||||
. venv/bin/activate
|
||||
|
||||
isort --check .
|
||||
"""
|
||||
}
|
||||
}
|
||||
stage('config and metadata determinism') {
|
||||
steps {
|
||||
sh """
|
||||
|
|
|
@ -30,13 +30,13 @@ Rule of thumb: keep ports below 10000 free for stuff that reserves ports.
|
|||
| 20010 | mautrix-telegram | Bridge |
|
||||
| 20020 | mautrix-whatsapp | Bridge |
|
||||
| 20030 | matrix-dimension | Matrix Integrations Manager|
|
||||
| 20070 | matrix-synapse | sliding-sync |
|
||||
| 20080 | matrix-synapse | client, federation |
|
||||
| 20081 | matrix-synapse | prometheus metrics |
|
||||
| 20090 | matrix-media-repo | media_repo |
|
||||
| 20090 | matrix-media-repo | prometheus metrics |
|
||||
| 21000 | pleroma | pleroma |
|
||||
| 21010 | grafana | grafana |
|
||||
| 22000 | forgejo | forgejo |
|
||||
| 22000 | gitea | forgejo |
|
||||
| 22010 | jenkins-ci | Jenkins CI |
|
||||
| 22020 | travelynx | Travelynx Web |
|
||||
| 22030 | octoprint | OctoPrint Web Interface |
|
||||
|
@ -45,9 +45,8 @@ Rule of thumb: keep ports below 10000 free for stuff that reserves ports.
|
|||
| 22060 | pretalx | gunicorn |
|
||||
| 22070 | paperless-ng | gunicorn |
|
||||
| 22080 | netbox | gunicorn |
|
||||
| 22090 | jugendhackt_tools | gunicorn |
|
||||
| 22100 | powerdnsadmin | gunicorn |
|
||||
| 22110 | icinga2-statuspage | gunicorn |
|
||||
| 22100 | woodpecker-server | http |
|
||||
| 22101 | woodpecker-server | gRPC |
|
||||
| 22999 | nginx | stub_status |
|
||||
| 22100 | ntfy | http |
|
||||
|
||||
|
|
13
README.md
13
README.md
|
@ -7,16 +7,3 @@ onto shared webhosting.
|
|||
|
||||
`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/)
|
||||
|
||||
## 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).
|
||||
|
|
|
@ -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}
|
|
@ -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
|
|
@ -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 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:
|
||||
existing=$(dpkg --get-selections | grep -E '^linux-(image|headers)-[0-9]' || true)
|
||||
|
|
|
@ -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
|
1
bundles/apt/files/sources.list-raspbian-buster
Normal file
1
bundles/apt/files/sources.list-raspbian-buster
Normal file
|
@ -0,0 +1 @@
|
|||
deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi
|
|
@ -19,7 +19,7 @@ statusfile="/var/tmp/unattended_upgrades.status"
|
|||
# Workaround, because /var/tmp is usually 1777
|
||||
[[ "$UID" == 0 ]] && chown root:root "$statusfile"
|
||||
|
||||
logins=$(ps h -C sshd -o euser | awk '$1 != "root" && $1 != "sshd" && $1 != "sshmon" && $1 != "nobody"')
|
||||
logins=$(ps h -C sshd -o euser | awk '$1 != "root" && $1 != "sshd" && $1 != "sshmon"')
|
||||
if [[ -n "$logins" ]]
|
||||
then
|
||||
echo "Will abort now, there are active SSH logins: $logins"
|
||||
|
@ -46,6 +46,10 @@ fi
|
|||
|
||||
if [[ -f /var/run/reboot-required ]] && [[ "$auto_reboot_enabled" == "True" ]]
|
||||
then
|
||||
if [[ -n "$reboot_mail_to" ]]
|
||||
then
|
||||
date | mail -s "SYSREBOOTNOW $nodename" "$reboot_mail_to"
|
||||
fi
|
||||
systemctl reboot
|
||||
fi
|
||||
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
nodename="${node.name}"
|
||||
reboot_mail_to="${node.metadata.get('apt/unattended-upgrades/reboot_mail_to', '')}"
|
||||
auto_reboot_enabled="${node.metadata.get('apt/unattended-upgrades/reboot_enabled', True)}"
|
||||
|
|
|
@ -4,9 +4,11 @@ supported_os = {
|
|||
'debian': {
|
||||
10: 'buster',
|
||||
11: 'bullseye',
|
||||
12: 'bookworm',
|
||||
99: 'unstable',
|
||||
},
|
||||
'raspbian': {
|
||||
10: 'buster',
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
|
@ -24,10 +26,6 @@ actions = {
|
|||
'triggered': True,
|
||||
'cascade_skip': False,
|
||||
},
|
||||
'apt_execute_update_commands': {
|
||||
'command': ' && '.join(sorted(node.metadata.get('apt/additional_update_commands', {'true'}))),
|
||||
'triggered': True,
|
||||
},
|
||||
}
|
||||
|
||||
files = {
|
||||
|
@ -115,7 +113,7 @@ pkg_apt = {
|
|||
'mtr': {},
|
||||
'ncdu': {},
|
||||
'ncurses-term': {},
|
||||
'netcat-openbsd': {},
|
||||
'netcat': {},
|
||||
'nmap': {},
|
||||
'python3': {},
|
||||
'python3-dev': {},
|
||||
|
@ -154,9 +152,6 @@ pkg_apt = {
|
|||
'popularity-contest': {
|
||||
'installed': False,
|
||||
},
|
||||
'python3-packaging': {
|
||||
'installed': False,
|
||||
},
|
||||
'unattended-upgrades': {
|
||||
'installed': False,
|
||||
},
|
||||
|
@ -173,7 +168,6 @@ if node.os_version[0] >= 11:
|
|||
}
|
||||
|
||||
for name, data in node.metadata.get('apt/repos', {}).items():
|
||||
if 'items' in data:
|
||||
files['/etc/apt/sources.list.d/{}.list'.format(name)] = {
|
||||
'content_type': 'mako',
|
||||
'content': ("\n".join(sorted(data['items']))).format(
|
||||
|
@ -184,30 +178,8 @@ for name, data in node.metadata.get('apt/repos', {}).items():
|
|||
'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 'items' in data:
|
||||
files['/etc/apt/sources.list.d/{}.list'.format(name)]['needs'] = {
|
||||
'file:/etc/apt/trusted.gpg.d/{}.list.asc'.format(name),
|
||||
}
|
||||
|
|
|
@ -21,24 +21,16 @@ defaults = {
|
|||
'cron/jobs/upgrade-and-reboot'
|
||||
)
|
||||
def patchday(metadata):
|
||||
if not node.metadata.get('apt/unattended-upgrades/enabled', True):
|
||||
return {}
|
||||
|
||||
day = metadata.get('apt/unattended-upgrades/day')
|
||||
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 {
|
||||
'cron': {
|
||||
'jobs': {
|
||||
'upgrade-and-reboot': '{minute} {hour} * * {day} root /usr/local/sbin/upgrade-and-reboot'.format(
|
||||
minute=node.magic_number % 30,
|
||||
hour=hour,
|
||||
day=day%7,
|
||||
day=day,
|
||||
),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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" }
|
||||
]
|
|
@ -44,11 +44,6 @@ directories = {
|
|||
}
|
||||
|
||||
svc_systemd = {
|
||||
'avahi-daemon': {
|
||||
'needs': {
|
||||
'pkg_pacman:avahi',
|
||||
},
|
||||
},
|
||||
'sddm': {
|
||||
'needs': {
|
||||
'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')):
|
||||
if filename.startswith('.'):
|
||||
continue
|
||||
|
|
|
@ -9,14 +9,6 @@ defaults = {
|
|||
'icinga_options': {
|
||||
'exclude_from_monitoring': True,
|
||||
},
|
||||
'nftables': {
|
||||
'input': {
|
||||
'50-avahi': {
|
||||
'udp dport 5353 accept',
|
||||
'udp sport 5353 accept',
|
||||
},
|
||||
},
|
||||
},
|
||||
'pacman': {
|
||||
'packages': {
|
||||
# fonts
|
||||
|
@ -31,9 +23,8 @@ defaults = {
|
|||
'sddm': {},
|
||||
|
||||
# networking
|
||||
'avahi': {},
|
||||
'netctl': {},
|
||||
'util-linux': {}, # provides rfkill
|
||||
'rfkill': {},
|
||||
'wpa_supplicant': {},
|
||||
'wpa_actiond': {},
|
||||
|
||||
|
@ -54,7 +45,6 @@ defaults = {
|
|||
'pipewire': {},
|
||||
'pipewire-jack': {},
|
||||
'pipewire-pulse': {},
|
||||
'pipewire-zeroconf': {},
|
||||
'qpwgraph': {},
|
||||
|
||||
# window management
|
||||
|
|
|
@ -62,13 +62,10 @@ trap "on_exit" EXIT
|
|||
|
||||
# redirect stdout and stderr to logfile
|
||||
prepare_and_cleanup_logdir
|
||||
if [[ -z "$DEBUG" ]]
|
||||
then
|
||||
logfile="$logdir/backup--$(date '+%F--%H-%M-%S')--$$.log.gz"
|
||||
echo "All log output will go to $logfile" | logger -it backup-client
|
||||
exec > >(gzip >"$logfile")
|
||||
exec 2>&1
|
||||
fi
|
||||
logfile="$logdir/backup--$(date '+%F--%H-%M-%S')--$$.log.gz"
|
||||
echo "All log output will go to $logfile" | logger -it backup-client
|
||||
exec > >(gzip >"$logfile")
|
||||
exec 2>&1
|
||||
|
||||
# this is where the real work starts
|
||||
ts_begin=$(date +%s)
|
||||
|
|
|
@ -19,12 +19,12 @@ else:
|
|||
|
||||
if node.metadata.get('backups/exclude_from_backups', False):
|
||||
# make sure nobody tries to do something funny
|
||||
for file in {
|
||||
for file in [
|
||||
'/etc/backup.priv',
|
||||
'/usr/local/bin/generate-backup',
|
||||
'/usr/local/bin/generate-backup-with-retries',
|
||||
'/var/tmp/backup.monitoring', # status file
|
||||
}:
|
||||
]:
|
||||
files[file] = {
|
||||
'delete': True,
|
||||
}
|
||||
|
@ -33,17 +33,14 @@ else:
|
|||
backup_target = repo.get_node(node.metadata.get('backup-client/target'))
|
||||
|
||||
files['/etc/backup.priv'] = {
|
||||
'content': repo.libs.ssh.generate_ed25519_private_key(
|
||||
node.metadata.get('backup-client/user-name'),
|
||||
backup_target,
|
||||
),
|
||||
'content': repo.vault.decrypt_file(join('backup', 'keys', f'{node.name}.key.vault')),
|
||||
'mode': '0400',
|
||||
}
|
||||
|
||||
files['/usr/local/bin/generate-backup'] = {
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'username': node.metadata.get('backup-client/user-name'),
|
||||
'username': node.metadata['backup-client']['user-name'],
|
||||
'server': backup_target.metadata.get('backup-server/my_hostname'),
|
||||
'port': backup_target.metadata.get('backup-server/my_ssh_port'),
|
||||
'paths': backup_paths,
|
||||
|
|
|
@ -27,6 +27,9 @@ directories['/etc/backup-server/clients'] = {
|
|||
sudoers = {}
|
||||
|
||||
for nodename, config in node.metadata.get('backup-server/clients', {}).items():
|
||||
with open(join(repo.path, 'data', 'backup', 'keys', f'{nodename}.pub'), 'r') as f:
|
||||
pubkey = f.read().strip()
|
||||
|
||||
sudoers[config['user']] = nodename
|
||||
|
||||
users[config['user']] = {
|
||||
|
@ -38,10 +41,7 @@ for nodename, config in node.metadata.get('backup-server/clients', {}).items():
|
|||
}
|
||||
|
||||
files[f'/srv/backups/{nodename}/.ssh/authorized_keys'] = {
|
||||
'content': repo.libs.ssh.generate_ed25519_public_key(
|
||||
config['user'],
|
||||
node,
|
||||
),
|
||||
'content': pubkey,
|
||||
'owner': config['user'],
|
||||
'mode': '0400',
|
||||
'needs': {
|
||||
|
|
|
@ -35,15 +35,8 @@ def get_my_clients(metadata):
|
|||
continue
|
||||
|
||||
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'),
|
||||
'one_backup_every_hours': rnode.metadata.get('backup-client/one_backup_every_hours', 24),
|
||||
'retain': {
|
||||
'daily': rnode.metadata.get('backups/retain/daily', retain_defaults['daily']),
|
||||
'weekly': rnode.metadata.get('backups/retain/weekly', retain_defaults['weekly']),
|
||||
|
@ -160,7 +153,7 @@ def monitoring(metadata):
|
|||
client,
|
||||
config['one_backup_every_hours'],
|
||||
),
|
||||
'vars.sshmon_timeout': 40,
|
||||
'vars.sshmon_timeout': 20,
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -32,8 +32,8 @@ account_guest_in_cpu_meter=0
|
|||
color_scheme=0
|
||||
enable_mouse=0
|
||||
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
|
||||
right_meters=Hostname CPU RightCPUs2
|
||||
right_meters=Hostname CPU RightCPUs
|
||||
right_meter_modes=2 3 1
|
||||
hide_function_bar=0
|
||||
|
|
|
@ -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()
|
||||
for locale in sorted(node.metadata.get('locale/installed')):
|
||||
for locale in sorted(node.metadata['locale']['installed']):
|
||||
actions[f'ensure_locale_{locale}_is_enabled'] = {
|
||||
'command': f"sed -i '/{locale}/s/^# *//g' /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'}
|
||||
|
||||
actions['locale-gen'] = {
|
||||
actions = {
|
||||
'locale-gen': {
|
||||
'triggered': True,
|
||||
'command': 'locale-gen',
|
||||
},
|
||||
}
|
||||
|
||||
description = []
|
||||
|
||||
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'):
|
||||
description.append('Grafana: https://grafana.kunsmann.eu/d/{}'.format(UUID(int=node.magic_number).hex[:10]))
|
||||
|
|
|
@ -19,9 +19,7 @@ protocol static {
|
|||
ipv4;
|
||||
|
||||
% 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 ${config['local_ip']};
|
||||
% endfor
|
||||
route ${route} via ${node.metadata.get('bird/my_ip')};
|
||||
% endfor
|
||||
}
|
||||
% endif
|
||||
|
|
|
@ -24,7 +24,7 @@ defaults = {
|
|||
},
|
||||
'sysctl': {
|
||||
'options': {
|
||||
'net.ipv4.conf.all.forwarding': '1',
|
||||
'net.ipv4.ip_forward': '1',
|
||||
'net.ipv6.conf.all.forwarding': '1',
|
||||
},
|
||||
},
|
||||
|
@ -43,9 +43,6 @@ def neighbor_info_from_wireguard(metadata):
|
|||
except NoSuchNode:
|
||||
continue
|
||||
|
||||
if not rnode.has_bundle('bird'):
|
||||
continue
|
||||
|
||||
neighbors[name] = {
|
||||
'local_ip': config['my_ip'],
|
||||
'local_as': my_as,
|
||||
|
@ -65,10 +62,7 @@ def neighbor_info_from_wireguard(metadata):
|
|||
)
|
||||
def my_ip(metadata):
|
||||
if node.has_bundle('wireguard'):
|
||||
wg_ifaces = sorted({iface for iface in metadata.get('interfaces').keys() if iface.startswith('wg_')})
|
||||
if not wg_ifaces:
|
||||
return {}
|
||||
my_ip = sorted(metadata.get(f'interfaces/{wg_ifaces[0]}/ips'))[0].split('/')[0]
|
||||
my_ip = sorted(metadata.get('interfaces/wg0/ips'))[0].split('/')[0]
|
||||
else:
|
||||
my_ip = str(sorted(repo.libs.tools.resolve_identifier(repo, node.name))[0])
|
||||
|
||||
|
@ -90,7 +84,7 @@ def firewall(metadata):
|
|||
return {
|
||||
'firewall': {
|
||||
'port_rules': {
|
||||
'179/tcp': atomic(sources),
|
||||
'179': atomic(sources),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,19 +1,5 @@
|
|||
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 = {
|
||||
'apt',
|
||||
'nginx',
|
||||
|
@ -71,18 +57,6 @@ actions = {
|
|||
'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 = {
|
||||
|
@ -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():
|
||||
files['/etc/cron.d/{}'.format(crontab)] = {
|
||||
'source': 'cron_template',
|
||||
|
|
|
@ -17,7 +17,7 @@ files = {
|
|||
directories = {
|
||||
'/etc/cron.d': {
|
||||
'purge': True,
|
||||
'after': {
|
||||
'needs': {
|
||||
'pkg_apt:',
|
||||
},
|
||||
},
|
||||
|
|
36
bundles/dhcpd/files/dhcpd.conf
Normal file
36
bundles/dhcpd/files/dhcpd.conf
Normal 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
|
18
bundles/dhcpd/files/isc-dhcp-server
Normal file
18
bundles/dhcpd/files/isc-dhcp-server
Normal 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
41
bundles/dhcpd/items.py
Normal 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
54
bundles/dhcpd/metadata.py
Normal 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),
|
||||
},
|
||||
}
|
||||
}
|
11
bundles/docker-ce/items.py
Normal file
11
bundles/docker-ce/items.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from bundlewrap.metadata import metadata_to_json
|
||||
|
||||
files['/etc/docker/daemon.json'] = {
|
||||
'content': metadata_to_json({
|
||||
'iptables': False,
|
||||
}),
|
||||
'before': {
|
||||
'pkg_apt:docker-ce',
|
||||
'pkg_apt:docker-ce-cli',
|
||||
}
|
||||
}
|
36
bundles/docker-ce/metadata.py
Normal file
36
bundles/docker-ce/metadata.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
defaults = {
|
||||
'apt': {
|
||||
'repos': {
|
||||
'docker': {
|
||||
'items': {
|
||||
'deb https://download.docker.com/linux/debian {os_release} stable',
|
||||
},
|
||||
},
|
||||
},
|
||||
'packages': {
|
||||
'docker-ce': {},
|
||||
'docker-ce-cli': {},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'nftables/rules/00-docker-ce',
|
||||
)
|
||||
def nftables_nat(metadata):
|
||||
rules = {
|
||||
'inet filter forward ct state { related, established } accept',
|
||||
'inet filter forward iifname docker0 accept',
|
||||
}
|
||||
|
||||
for iface in metadata.get('interfaces'):
|
||||
rules.add(f'nat postrouting oifname {iface} masquerade')
|
||||
|
||||
return {
|
||||
'nftables': {
|
||||
'rules': {
|
||||
'00-docker-ce': sorted(rules),
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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}',
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -3,4 +3,3 @@ driver = pgsql
|
|||
default_pass_scheme = MD5-CRYPT
|
||||
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
|
||||
iterate_query = SELECT username as user FROM mailbox WHERE active = true
|
||||
|
|
|
@ -28,19 +28,19 @@ namespace inbox {
|
|||
mail_location = maildir:/var/mail/vmail/%d/%n
|
||||
protocols = imap lmtp sieve
|
||||
|
||||
ssl = required
|
||||
ssl_cert = </var/lib/dehydrated/certs/${node.metadata.get('postfix/myhostname')}/fullchain.pem
|
||||
ssl_key = </var/lib/dehydrated/certs/${node.metadata.get('postfix/myhostname')}/privkey.pem
|
||||
ssl_dh = </etc/ssl/certs/dhparam.pem
|
||||
ssl = yes
|
||||
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', node.metadata['hostname'])}/privkey.pem
|
||||
ssl_dh = </etc/dovecot/ssl/dhparam.pem
|
||||
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_prefer_server_ciphers = no
|
||||
ssl_cipher_list = EECDH+AESGCM:EDH+AESGCM
|
||||
ssl_prefer_server_ciphers = yes
|
||||
|
||||
login_greeting = IMAPd ready
|
||||
auth_mechanisms = plain login
|
||||
first_valid_uid = 65534
|
||||
disable_plaintext_auth = yes
|
||||
mail_plugins = $mail_plugins zlib old_stats fts fts_xapian
|
||||
mail_plugins = $mail_plugins zlib old_stats
|
||||
|
||||
plugin {
|
||||
zlib_save_level = 6
|
||||
|
@ -56,15 +56,6 @@ plugin {
|
|||
old_stats_refresh = 30 secs
|
||||
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'):
|
||||
sieve_before = /var/mail/vmail/sieve/global/spam-global.sieve
|
||||
|
||||
|
@ -95,19 +86,14 @@ service auth {
|
|||
}
|
||||
}
|
||||
|
||||
service decode2text {
|
||||
executable = script /usr/lib/dovecot/decode2text.sh
|
||||
user = dovecot
|
||||
unix_listener decode2text {
|
||||
mode = 0666
|
||||
service lmtp {
|
||||
unix_listener /var/spool/postfix/private/dovecot-lmtp {
|
||||
group = postfix
|
||||
mode = 0600
|
||||
user = postfix
|
||||
}
|
||||
}
|
||||
|
||||
service indexer-worker {
|
||||
vsz_limit = 0
|
||||
process_limit = 0
|
||||
}
|
||||
|
||||
service imap {
|
||||
executable = imap
|
||||
}
|
||||
|
@ -118,14 +104,6 @@ service imap-login {
|
|||
vsz_limit = 64M
|
||||
}
|
||||
|
||||
service lmtp {
|
||||
unix_listener /var/spool/postfix/private/dovecot-lmtp {
|
||||
group = postfix
|
||||
mode = 0600
|
||||
user = postfix
|
||||
}
|
||||
}
|
||||
|
||||
service managesieve-login {
|
||||
inet_listener sieve {
|
||||
port = 4190
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
# by this bundle
|
||||
repo.libs.tools.require_bundle(node, 'postfix')
|
||||
|
||||
directories = {
|
||||
'/etc/dovecot/ssl': {},
|
||||
}
|
||||
|
||||
files = {
|
||||
'/etc/dovecot/dovecot.conf': {
|
||||
'content_type': 'mako',
|
||||
|
@ -45,17 +49,25 @@ files = {
|
|||
},
|
||||
}
|
||||
|
||||
symlinks['/usr/lib/dovecot/decode2text.sh'] = {
|
||||
'target': '/usr/share/doc/dovecot-core/examples/decode2text.sh',
|
||||
'before': {
|
||||
'svc_systemd:dovecot',
|
||||
actions = {
|
||||
'dovecot_generate_dhparam': {
|
||||
'command': 'openssl dhparam -out /etc/dovecot/ssl/dhparam.pem 2048',
|
||||
'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 = {
|
||||
'dovecot': {
|
||||
'needs': {
|
||||
'action:generate-dhparam',
|
||||
'action:dovecot_generate_dhparam',
|
||||
'file:/etc/dovecot/dovecot.conf',
|
||||
'file:/etc/dovecot/dovecot-sql.conf',
|
||||
},
|
||||
|
|
|
@ -3,7 +3,6 @@ from bundlewrap.metadata import atomic
|
|||
defaults = {
|
||||
'apt': {
|
||||
'packages': {
|
||||
'dovecot-fts-xapian': {},
|
||||
'dovecot-imapd': {},
|
||||
'dovecot-lmtpd': {},
|
||||
'dovecot-managesieved': {},
|
||||
|
@ -36,16 +35,6 @@ defaults = {
|
|||
'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'):
|
||||
|
@ -87,19 +76,19 @@ def import_database_settings_from_postfixadmin(metadata):
|
|||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'firewall/port_rules',
|
||||
'firewall/port_rules',
|
||||
'firewall/port_rules',
|
||||
'firewall/port_rules/143',
|
||||
'firewall/port_rules/993',
|
||||
'firewall/port_rules/4190',
|
||||
)
|
||||
def firewall(metadata):
|
||||
return {
|
||||
'firewall': {
|
||||
'port_rules': {
|
||||
# imap(s)
|
||||
'143/tcp': atomic(metadata.get('dovecot/restrict-to', {'*'})),
|
||||
'993/tcp': atomic(metadata.get('dovecot/restrict-to', {'*'})),
|
||||
'143': atomic(metadata.get('dovecot/restrict-to', {'*'})),
|
||||
'993': atomic(metadata.get('dovecot/restrict-to', {'*'})),
|
||||
# managesieve
|
||||
'4190/tcp': atomic(metadata.get('dovecot/restrict-to', {'*'})),
|
||||
'4190': atomic(metadata.get('dovecot/restrict-to', {'*'})),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ directories = {
|
|||
|
||||
git_deploy = {
|
||||
'/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',
|
||||
'triggers': {
|
||||
'action:element-web_yarn',
|
||||
|
@ -18,22 +18,28 @@ git_deploy = {
|
|||
|
||||
files = {
|
||||
'/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': {
|
||||
'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 = {
|
||||
'element-web_yarn': {
|
||||
'command': ' && '.join([
|
||||
*extra_install_cmds,
|
||||
'cd /opt/element-web',
|
||||
'yarn install --pure-lockfile --ignore-scripts',
|
||||
'yarn build',
|
||||
]),
|
||||
'needs': {
|
||||
'action:apt_execute_update_commands',
|
||||
'action:nodejs_install_yarn',
|
||||
'pkg_apt:nodejs',
|
||||
},
|
||||
'triggered': True,
|
||||
|
|
|
@ -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': 20,
|
||||
},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'nodejs': {
|
||||
'version': 18,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'nginx/vhosts/element-web',
|
||||
)
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
}
|
33
bundles/gce-workaround/items.py
Normal file
33
bundles/gce-workaround/items.py
Normal 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,
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
APP_NAME = ${app_name}
|
||||
RUN_USER = git
|
||||
RUN_MODE = prod
|
||||
WORK_PATH = /var/lib/forgejo
|
||||
|
||||
[repository]
|
||||
ROOT = /var/lib/forgejo/repositories
|
||||
ROOT = /home/git/gitea-repositories
|
||||
MAX_CREATION_LIMIT = 0
|
||||
DEFAULT_BRANCH = main
|
||||
|
|
@ -5,13 +5,14 @@ After=network.target
|
|||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
RestartSec=10
|
||||
RestartSec=2s
|
||||
Type=simple
|
||||
User=git
|
||||
Group=git
|
||||
WorkingDirectory=/var/lib/forgejo
|
||||
ExecStart=/usr/local/bin/forgejo web -c /etc/forgejo/app.ini
|
||||
WorkingDirectory=/var/lib/gitea/
|
||||
ExecStart=/usr/local/bin/gitea web -c /etc/gitea/app.ini
|
||||
Restart=always
|
||||
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
68
bundles/gitea/items.py
Normal file
68
bundles/gitea/items.py
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
|
@ -1,31 +1,33 @@
|
|||
defaults = {
|
||||
'backups': {
|
||||
'paths': {
|
||||
'/var/lib/forgejo',
|
||||
'/home/git',
|
||||
'/var/lib/gitea',
|
||||
},
|
||||
},
|
||||
'forgejo': {
|
||||
'gitea': {
|
||||
'app_name': 'Forgejo',
|
||||
'database': {
|
||||
'username': 'forgejo',
|
||||
'password': repo.vault.password_for('{} postgresql forgejo'.format(node.name)),
|
||||
'database': 'forgejo',
|
||||
'username': 'gitea',
|
||||
'password': repo.vault.password_for('{} postgresql gitea'.format(node.name)),
|
||||
'database': 'gitea',
|
||||
},
|
||||
'disable_registration': True,
|
||||
'email_domain_blocklist': set(),
|
||||
'enable_git_hooks': False,
|
||||
'internal_token': repo.vault.password_for('{} forgejo internal_token'.format(node.name)),
|
||||
'lfs_secret_key': repo.vault.password_for('{} forgejo lfs_secret_key'.format(node.name)),
|
||||
'oauth_secret_key': repo.vault.password_for('{} forgejo oauth_secret_key'.format(node.name)),
|
||||
'security_secret_key': repo.vault.password_for('{} forgejo security_secret_key'.format(node.name)),
|
||||
'internal_token': repo.vault.password_for('{} gitea internal_token'.format(node.name)),
|
||||
'lfs_secret_key': repo.vault.password_for('{} gitea lfs_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('{} gitea security_secret_key'.format(node.name)),
|
||||
},
|
||||
'icinga2_api': {
|
||||
'forgejo': {
|
||||
'gitea': {
|
||||
'services': {
|
||||
'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': {
|
||||
'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,
|
||||
'check_interval': '60m',
|
||||
},
|
||||
|
@ -39,22 +41,29 @@ defaults = {
|
|||
},
|
||||
'postgresql': {
|
||||
'roles': {
|
||||
'forgejo': {
|
||||
'password': repo.vault.password_for('{} postgresql forgejo'.format(node.name)),
|
||||
'gitea': {
|
||||
'password': repo.vault.password_for('{} postgresql gitea'.format(node.name)),
|
||||
},
|
||||
},
|
||||
'databases': {
|
||||
'forgejo': {
|
||||
'owner': 'forgejo',
|
||||
'gitea': {
|
||||
'owner': 'gitea',
|
||||
},
|
||||
},
|
||||
},
|
||||
'zfs': {
|
||||
'datasets': {
|
||||
'tank/forgejo': {
|
||||
'mountpoint': '/var/lib/forgejo',
|
||||
'tank/gitea': {},
|
||||
'tank/gitea/home': {
|
||||
'mountpoint': '/home/git',
|
||||
'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(
|
||||
'nginx/vhosts/forgejo',
|
||||
)
|
||||
|
@ -90,7 +82,7 @@ def nginx(metadata):
|
|||
'nginx': {
|
||||
'vhosts': {
|
||||
'forgejo': {
|
||||
'domain': metadata.get('forgejo/domain'),
|
||||
'domain': metadata.get('gitea/domain'),
|
||||
'locations': {
|
||||
'/': {
|
||||
'target': 'http://127.0.0.1:22000',
|
||||
|
@ -100,8 +92,16 @@ def nginx(metadata):
|
|||
},
|
||||
},
|
||||
'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 {
|
||||
}
|
|
@ -47,7 +47,7 @@ def dashboard_row_smartd(panel_id, node):
|
|||
'renderer': 'flot',
|
||||
'seriesOverrides': [],
|
||||
'spaceLength': 10,
|
||||
'span': 12,
|
||||
'span': 8,
|
||||
'stack': False,
|
||||
'steppedLine': False,
|
||||
'targets': [
|
||||
|
@ -114,5 +114,115 @@ def dashboard_row_smartd(panel_id, node):
|
|||
'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
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ def nginx(metadata):
|
|||
'locations': {
|
||||
'/': {
|
||||
'target': 'http://127.0.0.1:21010',
|
||||
'websockets': True,
|
||||
},
|
||||
'/api/ds/query': {
|
||||
'target': 'http://127.0.0.1:21010',
|
||||
|
|
|
@ -33,11 +33,7 @@ ProtectSystem=strict
|
|||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
SystemCallArchitectures=native
|
||||
# FIXME
|
||||
# causes problems on bookworm
|
||||
# see https://github.com/hedgedoc/hedgedoc/issues/4686
|
||||
# cmmented out for now ...
|
||||
#SystemCallFilter=@system-service
|
||||
SystemCallFilter=@system-service
|
||||
|
||||
# You may have to adjust these settings
|
||||
User=hedgedoc
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from semver import compare
|
||||
|
||||
repo.libs.tools.require_bundle(node, 'nodejs')
|
||||
|
||||
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 = {
|
||||
'hedgedoc_yarn': {
|
||||
'command': ' && '.join([
|
||||
'cd /opt/hedgedoc',
|
||||
'yarn install --immutable',
|
||||
'yarn install --production=true --pure-lockfile --ignore-scripts',
|
||||
'yarn install --ignore-scripts',
|
||||
'yarn build',
|
||||
]),
|
||||
'needs': {
|
||||
'action:nodejs_install_yarn',
|
||||
'file:/opt/hedgedoc/config.json',
|
||||
'git_deploy:/opt/hedgedoc',
|
||||
'pkg_apt:nodejs',
|
||||
|
|
|
@ -2,42 +2,48 @@
|
|||
|
||||
from sys import exit
|
||||
|
||||
from packaging.version import parse
|
||||
from requests import get
|
||||
import requests
|
||||
from packaging import version
|
||||
|
||||
API_TOKEN = "${token}"
|
||||
DOMAIN = "${domain}"
|
||||
bearer = "${bearer}"
|
||||
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:
|
||||
r = get("https://version.home-assistant.io/stable.json")
|
||||
r.raise_for_status()
|
||||
stable_version = parse(r.json()["homeassistant"]["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"},
|
||||
stable_version = version.parse(
|
||||
s.get("https://version.home-assistant.io/stable.json").json()["homeassistant"][
|
||||
"generic-x86-64"
|
||||
]
|
||||
)
|
||||
r.raise_for_status()
|
||||
running_version = parse(r.json()["version"])
|
||||
except Exception as e:
|
||||
print(f"Could not get running version information from homeassistant: {e!r}")
|
||||
exit(3)
|
||||
|
||||
try:
|
||||
if stable_version > running_version:
|
||||
print(
|
||||
f"There is a newer version available: {stable_version} (currently installed: {running_version})"
|
||||
s.headers.update(
|
||||
{"Authorization": f"Bearer {bearer}", "Content-Type": "application/json"}
|
||||
)
|
||||
exit(2)
|
||||
running_version = version.parse(
|
||||
s.get(f"https://{domain}/api/config").json()["version"]
|
||||
)
|
||||
if running_version == stable_version:
|
||||
status = 0
|
||||
message = f"OK - running version {running_version} equals stable version {stable_version}"
|
||||
elif running_version > stable_version:
|
||||
status = 1
|
||||
message = f"WARNING - stable version {stable_version} is lower than running version {running_version}, check if downgrade is necessary."
|
||||
else:
|
||||
print(
|
||||
f"Currently running version {running_version} matches newest release on home-assistant.io"
|
||||
)
|
||||
exit(0)
|
||||
status = 2
|
||||
message = f"CRITICAL - update necessary, running version {running_version} is lower than stable version {stable_version}"
|
||||
except Exception as e:
|
||||
print(repr(e))
|
||||
exit(3)
|
||||
message = f"{message}: {repr(e)}"
|
||||
|
||||
print(message)
|
||||
exit(status)
|
||||
|
|
|
@ -5,13 +5,9 @@ After=network-online.target
|
|||
[Service]
|
||||
Type=simple
|
||||
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
|
||||
ExecStart=/opt/homeassistant/venv/bin/hass -c "/var/opt/homeassistant"
|
||||
RestartForceExitStatus=100
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
@ -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 = {
|
||||
'homeassistant': {
|
||||
'home': '/var/opt/homeassistant',
|
||||
"groups": ["dialout"],
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -30,7 +23,7 @@ files = {
|
|||
'/usr/local/share/icinga/plugins/check_homeassistant_update': {
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'token': node.metadata.get('homeassistant/api_secret'),
|
||||
'bearer': repo.vault.decrypt(node.metadata.get('homeassistant/api_secret')),
|
||||
'domain': node.metadata.get('homeassistant/domain'),
|
||||
},
|
||||
'mode': '0755',
|
||||
|
@ -39,18 +32,11 @@ files = {
|
|||
|
||||
actions = {
|
||||
'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/',
|
||||
'needs': {
|
||||
'directory:/opt/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:libffi-dev',
|
||||
'pkg_apt:libssl-dev',
|
||||
|
@ -59,10 +45,17 @@ actions = {
|
|||
'pkg_apt:autoconf',
|
||||
'pkg_apt:build-essential',
|
||||
'pkg_apt:libopenjp2-7',
|
||||
'pkg_apt:libtiff6',
|
||||
'pkg_apt:libtiff5',
|
||||
'pkg_apt:libturbojpeg0-dev',
|
||||
'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': {
|
||||
'svc_systemd:homeassistant:restart',
|
||||
},
|
||||
|
|
|
@ -4,12 +4,11 @@ defaults = {
|
|||
'autoconf': {},
|
||||
'bluez': {},
|
||||
'build-essential': {},
|
||||
'ffmpeg': {},
|
||||
'libffi-dev': {},
|
||||
'libjpeg-dev': {},
|
||||
'libopenjp2-7': {},
|
||||
'libssl-dev': {},
|
||||
'libtiff6': {},
|
||||
'libtiff5': {},
|
||||
'libturbojpeg0-dev': {},
|
||||
'python3-packaging': {},
|
||||
'tzdata': {},
|
||||
|
@ -23,8 +22,6 @@ defaults = {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'icinga2_api/homeassistant/services',
|
||||
)
|
||||
|
@ -34,17 +31,15 @@ def icinga_check_for_new_release(metadata):
|
|||
'homeassistant': {
|
||||
'services': {
|
||||
'HOMEASSISTANT UPDATE': {
|
||||
'check_interval': '60m',
|
||||
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_homeassistant_update',
|
||||
'vars.notification.mail': True,
|
||||
'vars.sshmon_timeout': 20,
|
||||
'check_interval': '60m',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'nginx/vhosts/homeassistant',
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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()
|
|
@ -1,17 +1,16 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from json import load
|
||||
from sys import exit
|
||||
|
||||
from requests import get
|
||||
|
||||
with open('/etc/icinga2/notification_config.json') as f:
|
||||
CONFIG = load(f)
|
||||
SIPGATE_USER = '${node.metadata['icinga2']['sipgate_user']}'
|
||||
SIPGATE_PASS = '${node.metadata['icinga2']['sipgate_pass']}'
|
||||
|
||||
try:
|
||||
r = get(
|
||||
'https://api.sipgate.com/v2/balance',
|
||||
auth=(CONFIG['sipgate']['user'], CONFIG['sipgate']['password']),
|
||||
auth=(SIPGATE_USER, SIPGATE_PASS),
|
||||
headers={'Accept': 'application/json'},
|
||||
)
|
||||
|
||||
|
|
|
@ -5,33 +5,30 @@ from ipaddress import IPv6Address, ip_address
|
|||
from subprocess import check_output
|
||||
from sys import argv, exit
|
||||
|
||||
BLOCKLISTS = {
|
||||
'0spam.fusionzero.com': set(),
|
||||
'bl.mailspike.org': set(),
|
||||
'bl.spamcop.net': set(),
|
||||
'blackholes.brainerd.net': set(),
|
||||
'dnsbl-1.uceprotect.net': set(),
|
||||
'l2.spews.dnsbl.sorbs.net': set(),
|
||||
'list.dsbl.org': set(),
|
||||
'multihop.dsbl.org': set(),
|
||||
'ns1.unsubscore.com': set(),
|
||||
'opm.blitzed.org': set(),
|
||||
'psbl.surriel.com': set(),
|
||||
'rbl.efnet.org': set(),
|
||||
'rbl.schulte.org': set(),
|
||||
'spamguard.leadmon.net': set(),
|
||||
'ubl.unsubscore.com': set(),
|
||||
'unconfirmed.dsbl.org': set(),
|
||||
'virbl.dnsbl.bit.nl': set(),
|
||||
'zen.spamhaus.org': {
|
||||
# https://www.spamhaus.org/news/article/807/using-our-public-mirrors-check-your-return-codes-now.
|
||||
'127.255.255.252', # Typing Error
|
||||
'127.255.255.254', # public resolver / generic rdns
|
||||
'127.255.255.255', # rate limited
|
||||
},
|
||||
}
|
||||
BLOCKLISTS = [
|
||||
'0spam.fusionzero.com',
|
||||
'bl.mailspike.org',
|
||||
'bl.spamcop.net',
|
||||
'blackholes.brainerd.net',
|
||||
'dnsbl-1.uceprotect.net',
|
||||
'dnsbl-2.uceprotect.net',
|
||||
'l2.spews.dnsbl.sorbs.net',
|
||||
'list.dsbl.org',
|
||||
'map.spam-rbl.com',
|
||||
'multihop.dsbl.org',
|
||||
'ns1.unsubscore.com',
|
||||
'opm.blitzed.org',
|
||||
'psbl.surriel.com',
|
||||
'rbl.efnet.org',
|
||||
'rbl.schulte.org',
|
||||
'spamguard.leadmon.net',
|
||||
'ubl.unsubscore.com',
|
||||
'unconfirmed.dsbl.org',
|
||||
'virbl.dnsbl.bit.nl',
|
||||
'zen.spamhaus.org',
|
||||
]
|
||||
|
||||
def check_list(ip_list, blocklist, warn_ips):
|
||||
def check_list(ip_list, blocklist):
|
||||
dns_name = '{}.{}'.format(
|
||||
'.'.join(ip_list),
|
||||
blocklist,
|
||||
|
@ -44,21 +41,16 @@ def check_list(ip_list, blocklist, warn_ips):
|
|||
result = check_output([
|
||||
'dig',
|
||||
'+tries=2',
|
||||
'+time=10',
|
||||
'+time=5',
|
||||
'+short',
|
||||
dns_name
|
||||
]).decode().splitlines()
|
||||
for item in result:
|
||||
if item.startswith(';;'):
|
||||
continue
|
||||
msgs.append('{} listed in {} as {}'.format(
|
||||
ip,
|
||||
blocklist,
|
||||
item,
|
||||
))
|
||||
if item in warn_ips and returncode < 2:
|
||||
returncode = 1
|
||||
else:
|
||||
returncode = 2
|
||||
except Exception as e:
|
||||
if e.returncode == 9:
|
||||
|
@ -86,8 +78,8 @@ exitcode = 0
|
|||
with ThreadPoolExecutor(max_workers=len(BLOCKLISTS)) as executor:
|
||||
futures = set()
|
||||
|
||||
for blocklist, warn_ips in BLOCKLISTS.items():
|
||||
futures.add(executor.submit(check_list, ip_list, blocklist, warn_ips))
|
||||
for blocklist in BLOCKLISTS:
|
||||
futures.add(executor.submit(check_list, ip_list, blocklist))
|
||||
|
||||
for future in as_completed(futures):
|
||||
msgs, this_exitcode = future.result()
|
||||
|
|
|
@ -1,18 +1,31 @@
|
|||
% for dt in downtimes:
|
||||
object ScheduledDowntime "${dt['name']}" {
|
||||
host_name = "${dt['host']}"
|
||||
% for monitored_node in sorted(repo.nodes):
|
||||
<%
|
||||
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']}"
|
||||
comment = "${dt['comment']}"
|
||||
author = "unattended-upgrades"
|
||||
comment = "Downtime for upgrade-and-reboot of node ${monitored_node.name}"
|
||||
|
||||
fixed = true
|
||||
|
||||
ranges = {
|
||||
% for d,t in dt['times'].items():
|
||||
"${d}" = "${t}"
|
||||
% endfor
|
||||
% if monitored_node.has_bundle('pacman'):
|
||||
"${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}"
|
||||
% 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"
|
||||
}
|
||||
% endif
|
||||
% endfor
|
||||
|
|
|
@ -33,11 +33,3 @@ object ServiceGroup "checks_with_sms" {
|
|||
assign where service.vars.notification.sms == true
|
||||
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
|
||||
}
|
||||
|
|
|
@ -14,8 +14,7 @@ object Host "${rnode.name}" {
|
|||
vars.os = "${rnode.os}"
|
||||
|
||||
# used for status page
|
||||
vars.pretty_name = "${rnode.metadata.get('icinga_options/pretty_name', rnode.metadata.get('hostname'))}"
|
||||
vars.show_on_statuspage = ${str(rnode.metadata.get('icinga_options/show_on_statuspage', True)).lower()}
|
||||
vars.pretty_name = "${rnode.metadata.get('icinga_options/pretty_name', rnode.name)}"
|
||||
|
||||
vars.period = "${rnode.metadata.get('icinga_options/period', '24x7')}"
|
||||
|
||||
|
|
|
@ -9,11 +9,6 @@ app = Flask(__name__)
|
|||
@app.route('/status')
|
||||
def statuspage():
|
||||
everything_fine = True
|
||||
try:
|
||||
check_output(['/usr/local/share/icinga/plugins/check_mounts'])
|
||||
except:
|
||||
everything_fine = False
|
||||
|
||||
try:
|
||||
check_output(['/usr/lib/nagios/plugins/check_procs', '-C', 'icinga2', '-c', '1:'])
|
||||
except:
|
||||
|
|
|
@ -3,6 +3,8 @@ Description=Icinga2 Statusmonitor
|
|||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=nagios
|
||||
Group=nagios
|
||||
Environment="FLASK_APP=/etc/icinga2/icinga_statusmonitor.py"
|
||||
ExecStart=/usr/bin/python3 -m flask run
|
||||
WorkingDirectory=/tmp
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
[settings]
|
||||
acknowledge_sticky = 1
|
||||
hostdowntime_all_services = 1
|
||||
hostdowntime_end_fixed = P1W
|
||||
servicedowntime_end_fixed = P2D
|
|
@ -3,14 +3,22 @@
|
|||
import email.mime.text
|
||||
import smtplib
|
||||
from argparse import ArgumentParser
|
||||
from json import dumps, load
|
||||
from json import dumps
|
||||
from subprocess import run
|
||||
from sys import argv
|
||||
|
||||
from requests import post
|
||||
|
||||
with open('/etc/icinga2/notification_config.json') as f:
|
||||
CONFIG = load(f)
|
||||
SIPGATE_USER='${node.metadata['icinga2']['sipgate_user']}'
|
||||
SIPGATE_PASS='${node.metadata['icinga2']['sipgate_pass']}'
|
||||
|
||||
STATUS_TO_EMOJI = {
|
||||
'critical': '🔥',
|
||||
'down': '🚨🚨🚨',
|
||||
'ok': '🆗',
|
||||
'up': '👌',
|
||||
'warning': '⚡',
|
||||
}
|
||||
|
||||
parser = ArgumentParser(
|
||||
prog='icinga_notification_wrapper',
|
||||
|
@ -65,31 +73,36 @@ def notify_per_sms():
|
|||
output_text = ''
|
||||
else:
|
||||
output_text = '\n\n{}'.format(args.output)
|
||||
|
||||
if args.state.lower() in STATUS_TO_EMOJI:
|
||||
message_text = '{emoji} {host}{service} {emoji}{output}'.format(
|
||||
emoji=STATUS_TO_EMOJI[args.state.lower()],
|
||||
host=args.host_name,
|
||||
service=('/'+args.service_name if args.service_name else ''),
|
||||
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_text,
|
||||
'smsId': 's0', # XXX what does this mean? Documentation is unclear
|
||||
'recipient': args.sms
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
try:
|
||||
r = post(
|
||||
'https://api.sipgate.com/v2/sessions/sms',
|
||||
json=message,
|
||||
headers=headers,
|
||||
auth=(CONFIG['sipgate']['user'], CONFIG['sipgate']['password']),
|
||||
auth=(SIPGATE_USER, SIPGATE_PASS),
|
||||
)
|
||||
|
||||
if r.status_code == 204:
|
||||
|
@ -100,42 +113,6 @@ def notify_per_sms():
|
|||
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']),
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
except Exception as e:
|
||||
log_to_syslog('Sending a Notification failed: {}'.format(repr(e)))
|
||||
|
||||
|
||||
def notify_per_mail():
|
||||
if args.notification_type.lower() == 'recovery':
|
||||
# Do not send recovery emails.
|
||||
|
@ -199,7 +176,4 @@ if __name__ == '__main__':
|
|||
notify_per_mail()
|
||||
|
||||
if args.sms:
|
||||
if not args.service_name:
|
||||
notify_per_sms()
|
||||
if CONFIG['ntfy']['user']:
|
||||
notify_per_ntfy()
|
||||
|
|
|
@ -76,6 +76,8 @@ files = {
|
|||
},
|
||||
'/usr/local/share/icinga/plugins/check_sipgate_account_balance': {
|
||||
'mode': '0755',
|
||||
'content_type': 'mako',
|
||||
'cascade_skip': False, # contains faults
|
||||
},
|
||||
'/usr/local/share/icinga/plugins/check_freifunk_node': {
|
||||
'mode': '0755',
|
||||
|
@ -112,22 +114,11 @@ files = {
|
|||
'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': {
|
||||
'source': 'scripts/icinga_notification_wrapper',
|
||||
'content_type': 'mako',
|
||||
'mode': '0755',
|
||||
'cascade_skip': False, # contains faults
|
||||
},
|
||||
'/etc/icinga2/features-available/ido-pgsql.conf': {
|
||||
'source': 'icinga2/ido-pgsql.conf',
|
||||
|
@ -254,11 +245,6 @@ files = {
|
|||
'mode': '0660',
|
||||
'group': 'icingaweb2',
|
||||
},
|
||||
'/etc/icingaweb2/modules/monitoring/config.ini': {
|
||||
'source': 'icingaweb2/monitoring_config.ini',
|
||||
'mode': '0660',
|
||||
'group': 'icingaweb2',
|
||||
},
|
||||
'/etc/icingaweb2/groups.ini': {
|
||||
'source': 'icingaweb2/groups.ini',
|
||||
'mode': '0660',
|
||||
|
@ -276,13 +262,13 @@ files = {
|
|||
'group': 'icingaweb2',
|
||||
},
|
||||
|
||||
# monitoring
|
||||
# Statusmonitor
|
||||
'/etc/icinga2/icinga_statusmonitor.py': {
|
||||
'triggers': {
|
||||
'svc_systemd:icinga_statusmonitor:restart',
|
||||
},
|
||||
},
|
||||
'/usr/local/lib/systemd/system/icinga_statusmonitor.service': {
|
||||
'/etc/systemd/system/icinga_statusmonitor.service': {
|
||||
'triggers': {
|
||||
'action:systemd-reload',
|
||||
'svc_systemd:icinga_statusmonitor:restart',
|
||||
|
@ -290,12 +276,8 @@ files = {
|
|||
},
|
||||
}
|
||||
|
||||
svc_systemd['icinga_statusmonitor'] = {
|
||||
'needs': {
|
||||
'file:/etc/icinga2/icinga_statusmonitor.py',
|
||||
'file:/usr/local/lib/systemd/system/icinga_statusmonitor.service',
|
||||
'pkg_apt:python3-flask',
|
||||
},
|
||||
pkg_pip = {
|
||||
'easysnmp': {}, # for check_usv_snmp
|
||||
}
|
||||
|
||||
actions = {
|
||||
|
@ -337,22 +319,36 @@ for name in files:
|
|||
for name in symlinks:
|
||||
icinga_run_deps.add(f'symlink:{name}')
|
||||
|
||||
svc_systemd['icinga2'] = {
|
||||
svc_systemd = {
|
||||
'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
|
||||
bundles = set()
|
||||
downtimes = []
|
||||
for rnode in sorted(repo.nodes):
|
||||
for rnode in repo.nodes:
|
||||
if rnode.metadata.get('icinga_options/exclude_from_monitoring', False):
|
||||
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 = {}
|
||||
|
||||
# XXX for the love of god, PLEASE remove this once DNS is no longer
|
||||
# hosted at GCE
|
||||
if rnode.in_group('gce'):
|
||||
icinga_ips['ipv4'] = rnode.metadata.get('external_ipv4')
|
||||
else:
|
||||
for ip_type in ('ipv4', 'ipv6'):
|
||||
for ip in sorted(host_ips[ip_type]):
|
||||
if ip.is_private and not ip.is_link_local:
|
||||
|
@ -383,41 +379,6 @@ for rnode in sorted(repo.nodes):
|
|||
|
||||
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'] = {
|
||||
'source': 'icinga2/groups.conf',
|
||||
'content_type': 'mako',
|
||||
|
@ -438,7 +399,7 @@ files['/etc/icinga2/conf.d/downtimes.conf'] = {
|
|||
'source': 'icinga2/downtimes.conf',
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'downtimes': downtimes,
|
||||
'days': DAYS_TO_STRING,
|
||||
},
|
||||
'owner': 'nagios',
|
||||
'group': 'nagios',
|
||||
|
|
|
@ -17,9 +17,12 @@ defaults = {
|
|||
'icinga2': {},
|
||||
'icinga2-ido-pgsql': {},
|
||||
'icingaweb2': {},
|
||||
'python3-easysnmp': {},
|
||||
|
||||
# apparently no longer needed
|
||||
#'icingaweb2-module-monitoring': {},
|
||||
|
||||
# neeeded for statusmonitor
|
||||
'python3-flask': {},
|
||||
'snmp': {},
|
||||
}
|
||||
},
|
||||
'icinga2': {
|
||||
|
@ -40,6 +43,9 @@ defaults = {
|
|||
'check_interval': '30m',
|
||||
'vars.notification.mail': True,
|
||||
},
|
||||
'ICINGA STATUSMONITOR': {
|
||||
'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_systemd_unit icinga_statusmonitor',
|
||||
},
|
||||
'IDO-PGSQL': {
|
||||
'check_command': 'ido',
|
||||
'vars.ido_type': 'IdoPgsqlConnection',
|
||||
|
@ -53,21 +59,6 @@ defaults = {
|
|||
'icingaweb2': {
|
||||
'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': {
|
||||
'roles': {
|
||||
'icinga2': {
|
||||
|
@ -114,29 +105,13 @@ def add_users_from_json(metadata):
|
|||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'nginx/vhosts/icingaweb2',
|
||||
'nginx/vhosts/icinga_statusmonitor',
|
||||
'firewall/port_rules/5665',
|
||||
)
|
||||
def nginx(metadata):
|
||||
if not node.has_bundle('nginx'):
|
||||
raise DoNotRunAgain
|
||||
|
||||
def firewall(metadata):
|
||||
return {
|
||||
'nginx': {
|
||||
'vhosts': {
|
||||
'icingaweb2': {
|
||||
'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,
|
||||
},
|
||||
'firewall': {
|
||||
'port_rules': {
|
||||
'5665': atomic(metadata.get('icinga2/restrict-to', set())),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ defaults = {
|
|||
'repos': {
|
||||
'influxdb': {
|
||||
'items': {
|
||||
'deb https://repos.influxdata.com/{os} stable main',
|
||||
'deb https://repos.influxdata.com/{os} {os_release} stable',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,8 +4,7 @@ After=network.target
|
|||
Requires=infobeamer-cms.service
|
||||
|
||||
[Service]
|
||||
Environment=SETTINGS=/opt/infobeamer-cms/settings.toml
|
||||
WorkingDirectory=/opt/infobeamer-cms/src
|
||||
User=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
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Description=Run infobeamer-cms sync
|
||||
|
||||
[Timer]
|
||||
OnCalendar=minutely
|
||||
OnCalendar=*:0/5
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
|
|
4
bundles/infobeamer-cms/files/settings.toml
Normal file
4
bundles/infobeamer-cms/files/settings.toml
Normal 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))}
|
|
@ -1,4 +1,8 @@
|
|||
actions = {
|
||||
'infobeamer-cms_set_directory_permissions': {
|
||||
'triggered': True,
|
||||
'command': 'chown -R infobeamer-cms:infobeamer-cms /opt/infobeamer-cms/src/static/'
|
||||
},
|
||||
'infobeamer-cms_create_virtualenv': {
|
||||
'command': '/usr/bin/python3 -m virtualenv -p python3 /opt/infobeamer-cms/venv/',
|
||||
'unless': 'test -d /opt/infobeamer-cms/venv/',
|
||||
|
@ -8,11 +12,7 @@ actions = {
|
|||
},
|
||||
},
|
||||
'infobeamer-cms_install_requirements': {
|
||||
'command': ' && '.join([
|
||||
'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/',
|
||||
]),
|
||||
'command': 'cd /opt/infobeamer-cms/src && /opt/infobeamer-cms/venv/bin/pip install --upgrade pip gunicorn -r requirements.txt',
|
||||
'needs': {
|
||||
'action:infobeamer-cms_create_virtualenv',
|
||||
},
|
||||
|
@ -23,12 +23,13 @@ actions = {
|
|||
git_deploy = {
|
||||
'/opt/infobeamer-cms/src': {
|
||||
'rev': 'master',
|
||||
'repo': 'https://github.com/voc/infobeamer-cms.git',
|
||||
'repo': 'https://github.com/sophieschi/36c3-cms.git',
|
||||
'needs': {
|
||||
'directory:/opt/infobeamer-cms/src',
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:infobeamer-cms:restart',
|
||||
'action:infobeamer-cms_set_directory_permissions',
|
||||
'action:infobeamer-cms_install_requirements',
|
||||
},
|
||||
},
|
||||
|
@ -36,9 +37,6 @@ git_deploy = {
|
|||
|
||||
directories = {
|
||||
'/opt/infobeamer-cms/src': {},
|
||||
'/opt/infobeamer-cms/static': {
|
||||
'owner': 'infobeamer-cms',
|
||||
},
|
||||
}
|
||||
|
||||
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 = {
|
||||
'/opt/infobeamer-cms/settings.toml': {
|
||||
'content': repo.libs.faults.dict_as_toml(config),
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'config': config,
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:infobeamer-cms:restart',
|
||||
},
|
||||
|
@ -96,11 +97,19 @@ files = {
|
|||
},
|
||||
}
|
||||
|
||||
pkg_pip = {
|
||||
'github-flask': {
|
||||
'needed_by': {
|
||||
'svc_systemd:infobeamer-cms',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
svc_systemd = {
|
||||
'infobeamer-cms': {
|
||||
'needs': {
|
||||
'action:infobeamer-cms_install_requirements',
|
||||
'directory:/opt/infobeamer-cms/static',
|
||||
'action:infobeamer-cms_set_directory_permissions',
|
||||
'file:/etc/systemd/system/infobeamer-cms.service',
|
||||
'file:/opt/infobeamer-cms/settings.toml',
|
||||
'git_deploy:/opt/infobeamer-cms/src',
|
||||
|
@ -108,12 +117,8 @@ svc_systemd = {
|
|||
},
|
||||
'infobeamer-cms-runperiodic.timer': {
|
||||
'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:/opt/infobeamer-cms/settings.toml',
|
||||
'git_deploy:/opt/infobeamer-cms/src',
|
||||
'file:/etc/systemd/system/infobeamer-cms-runperiodic.service',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ defaults = {
|
|||
'MAX_UPLOADS': 5,
|
||||
'PREFERRED_URL_SCHEME': 'https',
|
||||
'SESSION_COOKIE_NAME': '__Host-sess',
|
||||
'STATIC_PATH': '/opt/infobeamer-cms/static',
|
||||
'URL_KEY': repo.vault.password_for(f'{node.name} infobeamer-cms url key'),
|
||||
'VERSION': 1,
|
||||
},
|
||||
|
@ -30,13 +29,15 @@ def nginx(metadata):
|
|||
'/': {
|
||||
'target': 'http://127.0.0.1:8000',
|
||||
},
|
||||
'/sync': {
|
||||
'return': 403,
|
||||
},
|
||||
'/static': {
|
||||
'alias': '/opt/infobeamer-cms/static',
|
||||
'alias': '/opt/infobeamer-cms/src/static',
|
||||
},
|
||||
},
|
||||
'website_check_path': '/',
|
||||
'website_check_string': 'Share your projects',
|
||||
'do_not_set_content_security_headers': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -44,7 +45,6 @@ def nginx(metadata):
|
|||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'infobeamer-cms/config/DOMAIN',
|
||||
'infobeamer-cms/config/TIME_MAX',
|
||||
'infobeamer-cms/config/TIME_MIN',
|
||||
)
|
||||
|
@ -57,7 +57,6 @@ def event_times(metadata):
|
|||
return {
|
||||
'infobeamer-cms': {
|
||||
'config': {
|
||||
'DOMAIN': metadata.get('infobeamer-cms/domain'),
|
||||
'TIME_MAX': int(event_end.timestamp()),
|
||||
'TIME_MIN': int(event_start.timestamp()),
|
||||
},
|
||||
|
|
|
@ -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
|
|
@ -1,227 +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")
|
||||
MLOG = logging.getLogger("mqtt")
|
||||
|
||||
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']}"
|
||||
message = f"[{device['description']}] {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,
|
||||
)
|
||||
|
||||
|
||||
mqtt_out("Monitor starting up")
|
||||
while True:
|
||||
try:
|
||||
online_devices = set()
|
||||
available_credits = None
|
||||
|
||||
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 data from info-beamer")
|
||||
mqtt_out(
|
||||
f"Could not get data from info-beamer: {e!r}",
|
||||
level="WARN",
|
||||
)
|
||||
else:
|
||||
new_state = {}
|
||||
for device in ib_state:
|
||||
did = str(device["id"])
|
||||
|
||||
if did in new_state:
|
||||
mqtt_out("DUPLICATE DETECTED!", level="ERROR", device=device)
|
||||
continue
|
||||
|
||||
new_state[did] = device
|
||||
must_dump_state = False
|
||||
|
||||
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"]))
|
||||
|
||||
if device["is_online"]:
|
||||
online_devices.add(
|
||||
"{} ({})".format(
|
||||
device["id"],
|
||||
device["description"],
|
||||
)
|
||||
)
|
||||
|
||||
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 data from info-beamer")
|
||||
mqtt_out(
|
||||
f"Could not get data from info-beamer: {e!r}",
|
||||
level="WARN",
|
||||
)
|
||||
else:
|
||||
available_credits = ib_account["balance"]
|
||||
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",
|
||||
)
|
||||
|
||||
if datetime.now(ZoneInfo("Europe/Berlin")).strftime("%H%M") == "0900":
|
||||
if available_credits is not None:
|
||||
mqtt_out(f"Available Credits: {available_credits}")
|
||||
|
||||
if online_devices:
|
||||
mqtt_out(
|
||||
"Online Devices: {}".format(", ".join(sorted(online_devices)))
|
||||
)
|
||||
|
||||
sleep(60)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
mqtt_out("Monitor exiting")
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
files['/etc/sudoers.d/jellyfin-sudoers'] = {
|
||||
'after': {
|
||||
'pkg_apt:jellyfin',
|
||||
},
|
||||
}
|
|
@ -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())),
|
||||
},
|
||||
},
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
defaults = {
|
||||
'apt': {
|
||||
'packages': {
|
||||
'jool-dkms': {},
|
||||
'jool-tools': {},
|
||||
'linux-headers-amd64': {},
|
||||
},
|
||||
},
|
||||
'modules': {
|
||||
'jool': [
|
||||
'jool',
|
||||
],
|
||||
},
|
||||
}
|
|
@ -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
|
|
@ -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',
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -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())
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
files['/etc/modules'] = {
|
||||
'content_type': 'mako',
|
||||
}
|
|
@ -43,15 +43,15 @@ defaults = {
|
|||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'firewall/port_rules',
|
||||
'firewall/port_rules',
|
||||
'firewall/port_rules/8080',
|
||||
'firewall/port_rules/9090',
|
||||
)
|
||||
def firewall(metadata):
|
||||
return {
|
||||
'firewall': {
|
||||
'port_rules': {
|
||||
'8080/tcp': atomic(metadata.get('kodi/restrict-to', {'*'})),
|
||||
'9090/tcp': atomic(metadata.get('kodi/restrict-to', {'*'})),
|
||||
'8080': atomic(metadata.get('kodi/restrict-to', {'*'})),
|
||||
'9090': atomic(metadata.get('kodi/restrict-to', {'*'})),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ def cron(metadata):
|
|||
'/usr/bin/dehydrated --cleanup',
|
||||
],
|
||||
'when': '04:{}:00'.format(node.magic_number % 60),
|
||||
'exclude_from_monitoring': True,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
if node.os != 'routeros':
|
||||
directories = {
|
||||
directories = {
|
||||
'/etc/lldpd.d': {
|
||||
'purge': True,
|
||||
'triggers': {
|
||||
'svc_systemd:lldpd:restart',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
files = {
|
||||
files = {
|
||||
'/etc/lldpd.conf': {
|
||||
'delete': True,
|
||||
},
|
||||
|
@ -18,12 +17,12 @@ if node.os != 'routeros':
|
|||
'svc_systemd:lldpd:restart',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
svc_systemd = {
|
||||
svc_systemd = {
|
||||
'lldpd': {
|
||||
'needs': {
|
||||
'file:/etc/lldpd.d/bundlewrap.conf',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue