From 1f73b04351b90a0c52c36ec64969aca2f7311f79 Mon Sep 17 00:00:00 2001 From: Rico Ullmann Date: Tue, 21 Dec 2021 15:56:24 +0100 Subject: [PATCH] cp over all the bundles from kunsis bw repo --- bundles/apt/files/apt-listchanges.conf | 6 + bundles/apt/files/check_unattended_upgrades | 32 ++ bundles/apt/files/do-unattended-upgrades | 43 ++ bundles/apt/files/kernel-postinst.d | 15 + .../apt/files/sources.list-debian-bullseye | 3 + bundles/apt/files/sources.list-debian-buster | 3 + .../apt/files/sources.list-debian-unstable | 1 + .../apt/files/sources.list-raspbian-buster | 1 + bundles/apt/files/upgrade-and-reboot | 56 +++ bundles/apt/files/upgrade-and-reboot.conf | 2 + bundles/apt/items.py | 191 +++++++ bundles/apt/metadata.py | 35 ++ bundles/basic/files/hosts | 12 + bundles/basic/files/htoprc | 39 ++ bundles/basic/files/locale | 1 + bundles/basic/files/locale.gen | 3 + bundles/basic/items.py | 88 ++++ bundles/basic/metadata.py | 25 + bundles/letsencrypt/files/config | 5 + bundles/letsencrypt/files/domains.txt | 3 + bundles/letsencrypt/files/hook.sh | 37 ++ .../files/letsencrypt-ensure-some-certificate | 31 ++ bundles/letsencrypt/items.py | 52 ++ bundles/letsencrypt/metadata.py | 24 + bundles/nftables/files/nftables.conf | 47 ++ bundles/nftables/files/override.conf | 10 + bundles/nftables/files/rules-template | 3 + bundles/nftables/items.py | 56 +++ bundles/nftables/metadata.py | 96 ++++ bundles/nginx/files/arch-override.conf | 9 + bundles/nginx/files/check_nginx_status | 475 ++++++++++++++++++ bundles/nginx/files/fastcgi.conf | 26 + bundles/nginx/files/logrotate.conf | 28 ++ bundles/nginx/files/nginx.conf | 61 +++ bundles/nginx/files/port80.conf | 13 + bundles/nginx/files/security.txt | 9 + bundles/nginx/files/site_template | 156 ++++++ bundles/nginx/files/ssl_template | 6 + bundles/nginx/files/stub_status | 6 + bundles/nginx/items.py | 178 +++++++ bundles/nginx/metadata.py | 209 ++++++++ bundles/openssh/files/override.conf | 8 + bundles/openssh/files/sshd_config | 42 ++ bundles/openssh/items.py | 55 ++ bundles/openssh/metadata.py | 28 ++ bundles/php/files/7.3/fpm.conf | 23 + bundles/php/files/7.3/php.ini | 99 ++++ bundles/php/files/7.4/fpm.conf | 23 + bundles/php/files/7.4/php.ini | 99 ++++ bundles/php/files/8.0/fpm.conf | 27 + bundles/php/files/8.0/php.ini | 99 ++++ bundles/php/items.py | 58 +++ bundles/php/metadata.py | 37 ++ bundles/postgresql/files/backup-pre-hook | 9 + bundles/postgresql/files/pg_hba.conf | 8 + bundles/postgresql/files/postgresql.conf | 32 ++ bundles/postgresql/items.py | 124 +++++ bundles/postgresql/metadata.py | 127 +++++ bundles/redis/files/redis.conf | 50 ++ bundles/redis/items.py | 22 + bundles/redis/metadata.py | 40 ++ bundles/sudo/files/bwusers | 9 + bundles/sudo/files/sudoers | 10 + bundles/sudo/items.py | 27 + bundles/sudo/metadata.py | 12 + bundles/sysctl/files/98-sysctl.conf | 3 + bundles/sysctl/items.py | 40 ++ bundles/systemd-networkd/files/resolv.conf | 3 + .../files/template-bond.netdev | 13 + .../files/template-bond.network | 5 + .../files/template-bridge-vlan.network | 7 + .../files/template-bridge.netdev | 6 + .../files/template-bridge.network | 6 + .../files/template-dummy.netdev | 3 + .../files/template-iface-dhcp.network | 27 + .../files/template-iface-nodhcp.network | 50 ++ .../files/template-iface-vlan.netdev | 7 + bundles/systemd-networkd/items.py | 181 +++++++ bundles/systemd-networkd/metadata.py | 46 ++ bundles/systemd/files/journald.conf | 19 + bundles/systemd/items.py | 48 ++ bundles/users/README.md | 27 + bundles/users/files/bashrc | 72 +++ bundles/users/files/fish.conf | 23 + bundles/users/files/fish_variables | 32 ++ bundles/users/files/tmux.conf | 51 ++ bundles/users/files/vimrc | 28 ++ bundles/users/items.py | 89 ++++ bundles/users/metadata.py | 41 ++ 89 files changed, 3991 insertions(+) create mode 100644 bundles/apt/files/apt-listchanges.conf create mode 100644 bundles/apt/files/check_unattended_upgrades create mode 100644 bundles/apt/files/do-unattended-upgrades create mode 100644 bundles/apt/files/kernel-postinst.d create mode 100644 bundles/apt/files/sources.list-debian-bullseye create mode 100644 bundles/apt/files/sources.list-debian-buster create mode 100644 bundles/apt/files/sources.list-debian-unstable create mode 100644 bundles/apt/files/sources.list-raspbian-buster create mode 100644 bundles/apt/files/upgrade-and-reboot create mode 100644 bundles/apt/files/upgrade-and-reboot.conf create mode 100644 bundles/apt/items.py create mode 100644 bundles/apt/metadata.py create mode 100644 bundles/basic/files/hosts create mode 100644 bundles/basic/files/htoprc create mode 100644 bundles/basic/files/locale create mode 100644 bundles/basic/files/locale.gen create mode 100644 bundles/basic/items.py create mode 100644 bundles/basic/metadata.py create mode 100644 bundles/letsencrypt/files/config create mode 100644 bundles/letsencrypt/files/domains.txt create mode 100644 bundles/letsencrypt/files/hook.sh create mode 100644 bundles/letsencrypt/files/letsencrypt-ensure-some-certificate create mode 100644 bundles/letsencrypt/items.py create mode 100644 bundles/letsencrypt/metadata.py create mode 100644 bundles/nftables/files/nftables.conf create mode 100644 bundles/nftables/files/override.conf create mode 100644 bundles/nftables/files/rules-template create mode 100644 bundles/nftables/items.py create mode 100644 bundles/nftables/metadata.py create mode 100644 bundles/nginx/files/arch-override.conf create mode 100644 bundles/nginx/files/check_nginx_status create mode 100644 bundles/nginx/files/fastcgi.conf create mode 100644 bundles/nginx/files/logrotate.conf create mode 100644 bundles/nginx/files/nginx.conf create mode 100644 bundles/nginx/files/port80.conf create mode 100644 bundles/nginx/files/security.txt create mode 100644 bundles/nginx/files/site_template create mode 100644 bundles/nginx/files/ssl_template create mode 100644 bundles/nginx/files/stub_status create mode 100644 bundles/nginx/items.py create mode 100644 bundles/nginx/metadata.py create mode 100644 bundles/openssh/files/override.conf create mode 100644 bundles/openssh/files/sshd_config create mode 100644 bundles/openssh/items.py create mode 100644 bundles/openssh/metadata.py create mode 100644 bundles/php/files/7.3/fpm.conf create mode 100644 bundles/php/files/7.3/php.ini create mode 100644 bundles/php/files/7.4/fpm.conf create mode 100644 bundles/php/files/7.4/php.ini create mode 100644 bundles/php/files/8.0/fpm.conf create mode 100644 bundles/php/files/8.0/php.ini create mode 100644 bundles/php/items.py create mode 100644 bundles/php/metadata.py create mode 100644 bundles/postgresql/files/backup-pre-hook create mode 100644 bundles/postgresql/files/pg_hba.conf create mode 100644 bundles/postgresql/files/postgresql.conf create mode 100644 bundles/postgresql/items.py create mode 100644 bundles/postgresql/metadata.py create mode 100644 bundles/redis/files/redis.conf create mode 100644 bundles/redis/items.py create mode 100644 bundles/redis/metadata.py create mode 100644 bundles/sudo/files/bwusers create mode 100644 bundles/sudo/files/sudoers create mode 100644 bundles/sudo/items.py create mode 100644 bundles/sudo/metadata.py create mode 100644 bundles/sysctl/files/98-sysctl.conf create mode 100644 bundles/sysctl/items.py create mode 100644 bundles/systemd-networkd/files/resolv.conf create mode 100644 bundles/systemd-networkd/files/template-bond.netdev create mode 100644 bundles/systemd-networkd/files/template-bond.network create mode 100644 bundles/systemd-networkd/files/template-bridge-vlan.network create mode 100644 bundles/systemd-networkd/files/template-bridge.netdev create mode 100644 bundles/systemd-networkd/files/template-bridge.network create mode 100644 bundles/systemd-networkd/files/template-dummy.netdev create mode 100644 bundles/systemd-networkd/files/template-iface-dhcp.network create mode 100644 bundles/systemd-networkd/files/template-iface-nodhcp.network create mode 100644 bundles/systemd-networkd/files/template-iface-vlan.netdev create mode 100644 bundles/systemd-networkd/items.py create mode 100644 bundles/systemd-networkd/metadata.py create mode 100644 bundles/systemd/files/journald.conf create mode 100644 bundles/systemd/items.py create mode 100644 bundles/users/README.md create mode 100644 bundles/users/files/bashrc create mode 100644 bundles/users/files/fish.conf create mode 100644 bundles/users/files/fish_variables create mode 100644 bundles/users/files/tmux.conf create mode 100644 bundles/users/files/vimrc create mode 100644 bundles/users/items.py create mode 100644 bundles/users/metadata.py diff --git a/bundles/apt/files/apt-listchanges.conf b/bundles/apt/files/apt-listchanges.conf new file mode 100644 index 0000000..511e955 --- /dev/null +++ b/bundles/apt/files/apt-listchanges.conf @@ -0,0 +1,6 @@ +[apt] +frontend=pager +email_address=${data['mail']} +confirm=0 +save_seen=/var/lib/apt/listchanges.db +which=both diff --git a/bundles/apt/files/check_unattended_upgrades b/bundles/apt/files/check_unattended_upgrades new file mode 100644 index 0000000..aa315ce --- /dev/null +++ b/bundles/apt/files/check_unattended_upgrades @@ -0,0 +1,32 @@ +#!/bin/bash + +statusfile="/var/tmp/unattended_upgrades.status" +if ! [[ -f "$statusfile" ]] +then + echo "Status file not found" + exit 3 +fi + +mtime=$(stat -c %Y $statusfile) +now=$(date +%s) +if (( $now - $mtime > 60*60*24*8 )) +then + echo "Status file is older than 8 days!" + exit 3 +fi + +exitcode=$(cat $statusfile) +case "$exitcode" in + abort_ssh) + echo "Upgrades skipped due to active SSH login" + exit 1 + ;; + 0) + echo "OK" + exit 0 + ;; + *) + echo "Last exitcode was $exitcode" + exit 2 + ;; +esac diff --git a/bundles/apt/files/do-unattended-upgrades b/bundles/apt/files/do-unattended-upgrades new file mode 100644 index 0000000..be56863 --- /dev/null +++ b/bundles/apt/files/do-unattended-upgrades @@ -0,0 +1,43 @@ +#!/bin/bash + +set -xeuo pipefail + +apt-get update + +DEBIAN_FRONTEND=noninteractive apt-get -y -q -o Dpkg::Options::=--force-confold dist-upgrade + +DEBIAN_FRONTEND=noninteractive apt-get -y -q autoclean + +DEBIAN_FRONTEND=noninteractive apt-get -y -q autoremove + +% if clean_old_kernels: +existing=$(dpkg --get-selections | grep -E '^linux-(image|headers)-[0-9]' || true) + +if [[ -z "$existing" ]] +then + echo "ERROR: No installed kernels found! Aborting!" >&2 + exit 1 +fi + +current=$(uname -r | sed -r 's/-[a-zA-Z]+$//') +latest=$(echo "$existing" | sort --version-sort -t- -k 3,4 | tail -n 1 | sed -r 's/[^0-9]+([0-9]\.[^-]+-[0-9]+).*/\1/') +todelete=$(echo "$existing" | grep -v -E "($current|$latest)" | awk '{ print $1 }' || true) + +if [[ -n "$todelete" ]] +then + DEBIAN_FRONTEND=noninteractive apt-get -qy purge $todelete +fi +% endif + +% for affected, restarts in sorted(restart_triggers.items()): + up_since=$(systemctl show "${affected}" | sed -n 's/^ActiveEnterTimestamp=//p' || echo 0) + up_since_ts=$(date -d "$up_since" +%s || echo 0) + now=$(date +%s) + + if [ $((now - up_since_ts)) -lt 3600 ] + then +% for restart in sorted(restarts): + systemctl restart "${restart}" || true +% endfor + fi +% endfor diff --git a/bundles/apt/files/kernel-postinst.d b/bundles/apt/files/kernel-postinst.d new file mode 100644 index 0000000..91641b6 --- /dev/null +++ b/bundles/apt/files/kernel-postinst.d @@ -0,0 +1,15 @@ +#!/bin/sh + +# /etc/kernel/postinst.d/unattended-upgrades + +case "$DPKG_MAINTSCRIPT_PACKAGE::$DPKG_MAINTSCRIPT_NAME" in + linux-image-extra*::postrm) + exit 0;; +esac + +if [ -d /var/run ]; then + touch /var/run/reboot-required + if ! grep -q "^$DPKG_MAINTSCRIPT_PACKAGE$" /var/run/reboot-required.pkgs 2> /dev/null ; then + echo "$DPKG_MAINTSCRIPT_PACKAGE" >> /var/run/reboot-required.pkgs + fi +fi diff --git a/bundles/apt/files/sources.list-debian-bullseye b/bundles/apt/files/sources.list-debian-bullseye new file mode 100644 index 0000000..a0446a2 --- /dev/null +++ b/bundles/apt/files/sources.list-debian-bullseye @@ -0,0 +1,3 @@ +deb http://deb.debian.org/debian/ bullseye main non-free contrib +deb http://security.debian.org/debian-security bullseye-security main contrib non-free +deb http://deb.debian.org/debian/ bullseye-updates main contrib non-free diff --git a/bundles/apt/files/sources.list-debian-buster b/bundles/apt/files/sources.list-debian-buster new file mode 100644 index 0000000..dec296f --- /dev/null +++ b/bundles/apt/files/sources.list-debian-buster @@ -0,0 +1,3 @@ +deb http://deb.debian.org/debian/ buster main non-free contrib +deb http://security.debian.org/debian-security buster/updates main contrib non-free +deb http://deb.debian.org/debian/ buster-updates main contrib non-free diff --git a/bundles/apt/files/sources.list-debian-unstable b/bundles/apt/files/sources.list-debian-unstable new file mode 100644 index 0000000..e6c36eb --- /dev/null +++ b/bundles/apt/files/sources.list-debian-unstable @@ -0,0 +1 @@ +deb http://deb.debian.org/debian/ unstable main non-free contrib diff --git a/bundles/apt/files/sources.list-raspbian-buster b/bundles/apt/files/sources.list-raspbian-buster new file mode 100644 index 0000000..d52d1f9 --- /dev/null +++ b/bundles/apt/files/sources.list-raspbian-buster @@ -0,0 +1 @@ +deb http://raspbian.raspberrypi.org/raspbian/ buster main contrib non-free rpi diff --git a/bundles/apt/files/upgrade-and-reboot b/bundles/apt/files/upgrade-and-reboot new file mode 100644 index 0000000..22c93a7 --- /dev/null +++ b/bundles/apt/files/upgrade-and-reboot @@ -0,0 +1,56 @@ +#!/bin/bash + +# With systemd, we can force logging to the journal. This is better than +# spamming the world with cron mails. You can then view these logs using +# "journalctl -rat upgrade-and-reboot". +if which logger >/dev/null 2>&1 +then + # Dump stdout and stderr to logger, which will then put everything + # into the journal. + exec 1> >(logger -t upgrade-and-reboot -p user.info) + exec 2> >(logger -t upgrade-and-reboot -p user.error) +fi + +. /etc/upgrade-and-reboot.conf + +echo "Starting upgrade-and-reboot for node $nodename ..." + +statusfile="/var/tmp/unattended_upgrades.status" +# Workaround, because /var/tmp is usually 1777 +[[ "$UID" == 0 ]] && chown root:root "$statusfile" + +logins=$(ps h -C sshd -o euser | awk '$1 != "root" && $1 != "sshd" && $1 != "sshmon"') +if [[ -n "$logins" ]] +then + echo "Will abort now, there are active SSH logins: $logins" + echo "abort_ssh" > "$statusfile" + exit 1 +fi + +softlockdir=/var/lib/bundlewrap/soft-$nodename +mkdir -p "$softlockdir" +printf '{"comment": "UPDATE", "date": %s, "expiry": %s, "id": "UNATTENDED", "items": ["*"], "user": "root@localhost"}\n' \ + $(date +%s) \ + $(date -d 'now + 30 mins' +%s) \ + >"$softlockdir"/UNATTENDED +trap 'rm -f "$softlockdir"/UNATTENDED' EXIT + +do-unattended-upgrades +ret=$? + +echo "$ret" > "$statusfile" +if (( $ret != 0 )) +then + exit 1 +fi + +if [[ -f /var/run/reboot-required ]] +then + if [[ -n "$reboot_mail_to" ]] + then + date | mail -s "SYSREBOOTNOW $nodename" "$reboot_mail_to" + fi + systemctl reboot +else + echo "upgrade-and-reboot for node $nodename is DONE" +fi diff --git a/bundles/apt/files/upgrade-and-reboot.conf b/bundles/apt/files/upgrade-and-reboot.conf new file mode 100644 index 0000000..e973246 --- /dev/null +++ b/bundles/apt/files/upgrade-and-reboot.conf @@ -0,0 +1,2 @@ +nodename="${node.name}" +reboot_mail_to="${node.metadata.get('apt/unattended-upgrades/reboot_mail_to', '')}" diff --git a/bundles/apt/items.py b/bundles/apt/items.py new file mode 100644 index 0000000..7d40483 --- /dev/null +++ b/bundles/apt/items.py @@ -0,0 +1,191 @@ +from bundlewrap.exceptions import BundleError + +supported_os = { + 'debian': { + 10: 'buster', + 11: 'bullseye', + 99: 'unstable', + }, + 'raspbian': { + 10: 'buster', + }, +} + +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') + + +actions = { + 'apt_update': { + 'command': 'apt-get update', + 'needed_by': { + 'pkg_apt:', + }, + 'triggered': True, + 'cascade_skip': False, + }, +} + +files = { + '/etc/apt/sources.list': { + 'source': 'sources.list-{}-{}'.format(node.os, supported_os[node.os][node.os_version[0]]), + 'triggers': { + 'action:apt_update', + }, + }, + '/etc/cloud': { + 'delete': True, + }, + '/etc/kernel/postinst.d/unattended-upgrades': { + 'source': 'kernel-postinst.d', + 'mode': '0755', + }, + '/etc/netplan': { + 'delete': True, + }, + '/etc/upgrade-and-reboot.conf': { + 'content_type': 'mako', + }, + '/usr/local/sbin/upgrade-and-reboot': { + 'mode': '0700', + }, + '/usr/local/sbin/do-unattended-upgrades': { + 'content_type': 'mako', + 'mode': '0700', + 'context': { + 'clean_old_kernels': node.metadata.get('apt/clean_old_kernels', True), + 'restart_triggers': node.metadata.get('apt/restart_triggers', {}), + } + }, + '/usr/local/share/icinga/plugins/check_unattended_upgrades': { + 'mode': '0755', + }, + '/var/lib/cloud': { + 'delete': True, + }, +} + +directories = { + '/etc/apt/sources.list.d': { + 'purge': True, + 'triggers': { + 'action:apt_update', + }, + }, +} + +svc_systemd = { + 'apt-daily.timer': { + 'running': False, + 'enabled': False, + }, + 'apt-daily-upgrade.timer': { + 'running': False, + 'enabled': False, + }, +} + +pkg_apt = { + 'apt-transport-https': {}, + + 'arping': {}, + 'at': {}, + 'build-essential': {}, + 'bzip2': {}, + 'curl': {}, + 'diffutils': {}, + 'dnsutils': {}, + 'git': {}, + 'grep': {}, + 'gzip': {}, + 'htop': {}, + 'jq': {}, + 'less': {}, + 'logrotate': {}, + 'lsof': {}, + 'mailutils': {}, + 'manpages': {}, + 'moreutils': {}, + 'mount': {}, + 'mtr': {}, + 'ncdu': {}, + 'ncurses-term': {}, + 'netcat': {}, + 'nmap': {}, + 'python3': {}, + 'python3-dev': {}, + 'python3-setuptools': { + 'needed_by': { + 'pkg_pip:', + }, + }, + 'python3-pip': { + 'needed_by': { + 'pkg_pip:', + }, + }, + 'python3-virtualenv': {}, + 'rsync': {}, + 'tar': {}, + 'tcpdump': {}, + 'telnet': {}, + 'tmux': {}, + 'tree': {}, + 'unzip': {}, + 'vim': {}, + 'wget': {}, + 'whois': {}, + 'zip': {}, + + 'cloud-init': { + 'installed': False, + }, + 'netplan.io': { + 'installed': False, + }, + 'popularity-contest': { + 'installed': False, + }, + 'unattended-upgrades': { + 'installed': False, + }, +} + +if node.os_version[0] >= 11: + symlinks = { + '/usr/bin/python': { + 'target': '/usr/bin/python3', + 'needs': { + 'pkg_apt:python3', + }, + }, + } + +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 package, options in node.metadata.get('apt/packages', {}).items(): + pkg_apt[package] = options diff --git a/bundles/apt/metadata.py b/bundles/apt/metadata.py new file mode 100644 index 0000000..ecb0775 --- /dev/null +++ b/bundles/apt/metadata.py @@ -0,0 +1,35 @@ +defaults = { + 'apt': { + 'unattended_upgrades': { + 'day': 5, + 'hour': 21, + }, + }, + 'icinga2_api': { + 'apt': { + 'services': { + 'UNATTENDED UPGRADES': { + 'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_unattended_upgrades', + }, + }, + }, + }, +} + + +@metadata_reactor.provides( + 'cron/upgrade-and-reboot' +) +def patchday(metadata): + day = metadata.get('apt/unattended_upgrades/day') + hour = metadata.get('apt/unattended_upgrades/hour') + + return { + 'cron': { + 'upgrade-and-reboot': '{minute} {hour} * * {day} root /usr/local/sbin/upgrade-and-reboot'.format( + minute=node.magic_number % 30, + hour=hour, + day=day, + ), + }, + } diff --git a/bundles/basic/files/hosts b/bundles/basic/files/hosts new file mode 100644 index 0000000..2ecc921 --- /dev/null +++ b/bundles/basic/files/hosts @@ -0,0 +1,12 @@ +127.0.0.1 localhost ${node.name} ${node.metadata['hostname']} + +::1 ip6-localhost +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters +ff02::3 ip6-allhosts + +% for ip, entries in sorted(node.metadata.get('hosts/entries', {}).items()): +${ip} ${' '.join(sorted(entries))} +% endfor diff --git a/bundles/basic/files/htoprc b/bundles/basic/files/htoprc new file mode 100644 index 0000000..68ef687 --- /dev/null +++ b/bundles/basic/files/htoprc @@ -0,0 +1,39 @@ +# Beware! This file is rewritten by htop when settings are changed in the interface. +# The parser is also very primitive, and not human-friendly. +fields=0 48 17 18 38 39 40 2 46 47 49 1 +sort_key=46 +sort_direction=-1 +tree_sort_key=0 +tree_sort_direction=1 +hide_kernel_threads=1 +hide_userland_threads=0 +shadow_other_users=0 +show_thread_names=0 +show_program_path=1 +highlight_base_name=1 +highlight_megabytes=0 +highlight_threads=1 +highlight_changes=0 +highlight_changes_delay_secs=5 +find_comm_in_cmdline=1 +strip_exe_from_cmdline=1 +show_merged_command=0 +tree_view=0 +tree_view_always_by_pid=0 +header_margin=1 +detailed_cpu_time=1 +cpu_count_from_one=1 +show_cpu_usage=1 +show_cpu_frequency=0 +show_cpu_temperature=0 +degree_fahrenheit=0 +update_process_names=0 +account_guest_in_cpu_meter=0 +color_scheme=0 +enable_mouse=0 +delay=10 +left_meters=Tasks LoadAverage Uptime Memory CPU LeftCPUs CPU +left_meter_modes=2 2 2 1 1 1 2 +right_meters=Hostname CPU RightCPUs +right_meter_modes=2 3 1 +hide_function_bar=0 diff --git a/bundles/basic/files/locale b/bundles/basic/files/locale new file mode 100644 index 0000000..c8227c5 --- /dev/null +++ b/bundles/basic/files/locale @@ -0,0 +1 @@ +LANG=${node.metadata['locale']['default']} diff --git a/bundles/basic/files/locale.gen b/bundles/basic/files/locale.gen new file mode 100644 index 0000000..f41230e --- /dev/null +++ b/bundles/basic/files/locale.gen @@ -0,0 +1,3 @@ +% for locale in sorted(node.metadata['locale']['installed']): +${locale} ${locale.split('.')[-1]} +% endfor diff --git a/bundles/basic/items.py b/bundles/basic/items.py new file mode 100644 index 0000000..ed794d4 --- /dev/null +++ b/bundles/basic/items.py @@ -0,0 +1,88 @@ +from inspect import cleandoc +from uuid import UUID + +from bundlewrap.utils.text import italic + +files = { + '/etc/default/locale': { + 'content_type': 'mako', + 'needs': { + 'action:locale-gen', + }, + }, + '/etc/hosts': { + 'content_type': 'mako', + }, + '/etc/htoprc.global': { + 'source': 'htoprc', + }, + '/etc/motd': { + 'content': '', + }, +} + +locale_needs = set() +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", + 'triggers': { + 'action:locale-gen', + }, + 'needs': locale_needs, + } + locale_needs = {f'action:ensure_locale_{locale}_is_enabled'} + +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.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])) + +if ( + not node.metadata.get('icinga_options/exclude_from_monitoring', False) or + node.has_bundle('telegraf') +): + description.append('') # divider line + +if node.metadata.get('nginx/vhosts', {}): + description.append('nginx vhosts:') + + for vname, vconfig in sorted(node.metadata.get('nginx/vhosts', {}).items()): + if vconfig.get('ssl', 'letsencrypt') is not None: + proto = 'https' + else: + proto = 'http' + + domain = vconfig.get('domain', vname) + + description.append(' {}: {}://{}{}'.format( + vname, + proto, + domain, + vconfig.get('website_check_path', '/'), + )) + + if node.metadata.get('description', []): + description.append('') # divider line + +for line in node.metadata.get('description', []): + description.append('# {}'.format(italic(line))) + +if description: + files['/etc/node.description'] = { + 'content': '\n'.join(description) + '\n', + } +else: + files['/etc/node.description'] = { + 'delete': True, + } diff --git a/bundles/basic/metadata.py b/bundles/basic/metadata.py new file mode 100644 index 0000000..29d666e --- /dev/null +++ b/bundles/basic/metadata.py @@ -0,0 +1,25 @@ +defaults = { + 'bash_functions': { + 'h': 'cp /etc/htoprc.global ~/.htoprc; mkdir -p ~/.config/htop; cp /etc/htoprc.global ~/.config/htop/htoprc; htop', + }, + 'locale': { + 'default': 'en_US.UTF-8', + 'installed': { + 'de_DE.UTF-8', + 'en_US.UTF-8', + }, + }, +} + + +@metadata_reactor.provides( + 'locale/installed', +) +def ensure_default_is_installed(metadata): + return { + 'locale': { + 'installed': { + metadata.get('locale/default'), + }, + }, + } diff --git a/bundles/letsencrypt/files/config b/bundles/letsencrypt/files/config new file mode 100644 index 0000000..2d4b2b6 --- /dev/null +++ b/bundles/letsencrypt/files/config @@ -0,0 +1,5 @@ +CONFIG_D=/etc/dehydrated/conf.d +BASEDIR=/var/lib/dehydrated +WELLKNOWN="${BASEDIR}/acme-challenges" +DOMAINS_TXT="/etc/dehydrated/domains.txt" +HOOK="/etc/dehydrated/hook.sh" diff --git a/bundles/letsencrypt/files/domains.txt b/bundles/letsencrypt/files/domains.txt new file mode 100644 index 0000000..ea7e427 --- /dev/null +++ b/bundles/letsencrypt/files/domains.txt @@ -0,0 +1,3 @@ +% for domain, aliases in sorted(node.metadata.get('letsencrypt/domains', {}).items()): +${domain} ${' '.join(sorted(aliases))} +% endfor diff --git a/bundles/letsencrypt/files/hook.sh b/bundles/letsencrypt/files/hook.sh new file mode 100644 index 0000000..4cdf79d --- /dev/null +++ b/bundles/letsencrypt/files/hook.sh @@ -0,0 +1,37 @@ +deploy_cert() {<%text> + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" +% for service, config in node.metadata.get('letsencrypt/concat_and_deploy', {}).items(): + + # concat_and_deploy ${service} + if [ "$DOMAIN" = "${config['match_domain']}" ]; then + cat $KEYFILE > ${config['target']} + cat $FULLCHAINFILE >> ${config['target']} +% if 'chown' in config: + chown ${config['chown']} ${config['target']} +% endif +% if 'chmod' in config: + chmod ${config['chmod']} ${config['target']} +% endif +% if 'commands' in config: +% for command in config['commands']: + ${command} +% endfor +% endif + fi +% endfor +} + + +exit_hook() {<%text> + local ERROR="${1:-}" + +% for service in sorted(node.metadata.get('letsencrypt/reload_after', set())): + systemctl reload-or-restart ${service} +% endfor +} + +<%text> +HANDLER="$1"; shift +if [[ "${HANDLER}" =~ ^(deploy_cert|exit_hook)$ ]]; then + "$HANDLER" "$@" +fi diff --git a/bundles/letsencrypt/files/letsencrypt-ensure-some-certificate b/bundles/letsencrypt/files/letsencrypt-ensure-some-certificate new file mode 100644 index 0000000..f9b961d --- /dev/null +++ b/bundles/letsencrypt/files/letsencrypt-ensure-some-certificate @@ -0,0 +1,31 @@ +#!/bin/sh + +domain=$1 +just_check=$2 + +cert_path="/var/lib/dehydrated/certs/$domain" + +already_exists=false +if [ -f "$cert_path/privkey.pem" -a -f "$cert_path/fullchain.pem" -a -f "$cert_path/chain.pem" ] +then + already_exists=true +fi + +if [ "$just_check" = true ] +then + if [ "$already_exists" = true ] + then + exit 0 + else + exit 1 + fi +fi + +if [ "$already_exists" != true ] +then + rm -r "$cert_path" + mkdir -p "$cert_path" + openssl req -x509 -newkey rsa:4096 -nodes -days 1 -subj "/CN=$domain" -keyout "$cert_path/privkey.pem" -out "$cert_path/fullchain.pem" + chmod 0600 "$cert_path/privkey.pem" + cp "$cert_path/fullchain.pem" "$cert_path/chain.pem" +fi diff --git a/bundles/letsencrypt/items.py b/bundles/letsencrypt/items.py new file mode 100644 index 0000000..585cf8e --- /dev/null +++ b/bundles/letsencrypt/items.py @@ -0,0 +1,52 @@ +repo.libs.tools.require_bundle(node, 'nginx', 'letsencrypt bundle needs nginx for http challenge') + +directories = { + '/etc/dehydrated/conf.d': {}, + '/var/lib/dehydrated/acme-challenges': {}, +} + +actions = { + 'letsencrypt_update_certificates': { + 'command': 'dehydrated --cron --accept-terms --challenge http-01', + 'triggered': True, + 'needs': { + 'svc_systemd:nginx', + }, + }, +} + +for domain, _ in node.metadata.get('letsencrypt/domains').items(): + actions['letsencrypt_ensure-some-certificate_{}'.format(domain)] = { + 'command': '/etc/dehydrated/letsencrypt-ensure-some-certificate {}'.format(domain), + 'unless': '/etc/dehydrated/letsencrypt-ensure-some-certificate {} true'.format(domain), + 'needs': { + 'file:/etc/dehydrated/letsencrypt-ensure-some-certificate', + }, + 'needed_by': { + 'svc_systemd:nginx', + }, + 'triggers': { + 'action:letsencrypt_update_certificates', + }, + } + +files = { + '/etc/dehydrated/domains.txt': { + 'content_type': 'mako', + 'triggers': { + 'action:letsencrypt_update_certificates', + }, + }, + '/etc/dehydrated/config': { + 'triggers': { + 'action:letsencrypt_update_certificates', + }, + }, + '/etc/dehydrated/hook.sh': { + 'content_type': 'mako', + 'mode': '0755', + }, + '/etc/dehydrated/letsencrypt-ensure-some-certificate': { + 'mode': '0755', + }, +} diff --git a/bundles/letsencrypt/metadata.py b/bundles/letsencrypt/metadata.py new file mode 100644 index 0000000..d5b6d1d --- /dev/null +++ b/bundles/letsencrypt/metadata.py @@ -0,0 +1,24 @@ +defaults = { + 'apt': { + 'packages': { + 'dehydrated': { + 'needed_by': { + 'action:letsencrypt_update_certificates', + }, + }, + }, + }, + 'cron': { + 'letsencrypt_renew': '{} 4 * * * root /usr/bin/dehydrated --cron --accept-terms --challenge http-01 > /dev/null'.format((node.magic_number % 60)), + 'letsencrypt_cleanup': '{} 4 * * 0 root /usr/bin/dehydrated --cleanup > /dev/null'.format((node.magic_number % 60)), + }, + 'pacman': { + 'packages': { + 'dehydrated': { + 'needed_by': { + 'action:letsencrypt_update_certificates', + }, + }, + }, + }, +} diff --git a/bundles/nftables/files/nftables.conf b/bundles/nftables/files/nftables.conf new file mode 100644 index 0000000..4034ad4 --- /dev/null +++ b/bundles/nftables/files/nftables.conf @@ -0,0 +1,47 @@ +#!/usr/sbin/nft -f + +flush ruleset + +table inet filter { + chain input { + type filter hook input priority 0 + policy drop + + tcp flags syn tcp option maxseg size 1-500 drop + + ct state { established, related } accept + ct state invalid drop + + iif lo accept + + icmp type timestamp-request drop + icmp type timestamp-reply drop + ip protocol icmp accept + + ip6 nexthdr ipv6-icmp accept + } + + chain output { + type filter hook output priority 0 + policy accept + } + + chain forward { + type filter hook forward priority 0 + policy drop + + icmp type timestamp-request drop + icmp type timestamp-reply drop + } +} + +table nat { + chain prerouting { + type nat hook prerouting priority -100 + } + chain postrouting { + type nat hook postrouting priority 100 + } +} + +include "/etc/nftables-rules.d/*-*" diff --git a/bundles/nftables/files/override.conf b/bundles/nftables/files/override.conf new file mode 100644 index 0000000..4168df3 --- /dev/null +++ b/bundles/nftables/files/override.conf @@ -0,0 +1,10 @@ +[Service] +RemainAfterExit=yes + +ExecStart= +ExecStart=/usr/sbin/nft -f /etc/nftables.conf +ExecStart=/usr/local/sbin/apply-sysctl + +ExecReload= +ExecReload=/usr/sbin/nft -f /etc/nftables.conf +ExecReload=/usr/local/sbin/apply-sysctl diff --git a/bundles/nftables/files/rules-template b/bundles/nftables/files/rules-template new file mode 100644 index 0000000..7680393 --- /dev/null +++ b/bundles/nftables/files/rules-template @@ -0,0 +1,3 @@ +% for rule in rules: +add rule ${rule} +% endfor diff --git a/bundles/nftables/items.py b/bundles/nftables/items.py new file mode 100644 index 0000000..8975d45 --- /dev/null +++ b/bundles/nftables/items.py @@ -0,0 +1,56 @@ +if node.has_bundle('pacman'): + package = 'pkg_pacman:nftables' +else: + package = 'pkg_apt:nftables' + +directories = { + # used by other bundles + '/etc/nftables-rules.d': { + 'purge': True, + 'triggers': { + 'svc_systemd:nftables:reload', + }, + }, +} + +files = { + '/etc/nftables.conf': { + 'needs': { + 'directory:/etc/nftables-rules.d', + }, + 'triggers': { + 'svc_systemd:nftables:reload', + }, + }, + '/etc/systemd/system/nftables.service.d/bundlewrap.conf': { + 'source': 'override.conf', + 'triggers': { + 'action:systemd-reload', + 'svc_systemd:nftables:reload', + }, + }, +} + +for ruleset, rules in node.metadata.get('nftables/rules', {}).items(): + files[f'/etc/nftables-rules.d/{ruleset}'] = { + 'source': 'rules-template', + 'content_type': 'mako', + 'context': { + 'rules': rules, + }, + 'needed_by': { + 'svc_systemd:nftables', + }, + 'triggers': { + 'svc_systemd:nftables:reload', + }, + } + +svc_systemd = { + 'nftables': { + 'needs': { + 'file:/etc/nftables.conf', + package, + }, + }, +} diff --git a/bundles/nftables/metadata.py b/bundles/nftables/metadata.py new file mode 100644 index 0000000..08396ce --- /dev/null +++ b/bundles/nftables/metadata.py @@ -0,0 +1,96 @@ +from bundlewrap.exceptions import BundleError + +defaults = { + 'apt': { + 'packages': { + 'nftables': {}, + }, + }, + 'pacman': { + 'packages': { + 'nftables': {}, +# https://github.com/bundlewrap/bundlewrap/issues/688 +# 'iptables': { +# 'installed': False, +# 'needed_by': { +# 'pkg_pacman:iptables-nft', +# }, +# }, + 'iptables-nft': { + 'needed_by': { + 'pkg_pacman:nftables', + }, + }, + }, + }, +} + +if not node.has_bundle('vmhost'): + # see comment in bundles/vmhost/items.py + defaults['apt']['packages']['iptables'] = { + 'installed': False, + 'needed_by': { + 'pkg_apt:nftables', + }, + } + +@metadata_reactor.provides( + 'nftables/rules/99-port_rules', +) +def port_rules_to_nftables(metadata): + # Using this, bundles can simply set up port based rules. This + # reactor will then take care of converting those rules to actual + # nftables rules + ruleset = set() + + # Plese note we do not set any defaults for ports. Bundles are + # expected to know themselves which default to use. + for portdef, targets in metadata.get('firewall/port_rules', {}).items(): + if '/' in portdef: + port, proto = portdef.split('/', 2) + + if proto not in {'udp'}: + raise BundleError(f'firewall/port_rules: illegal identifier {portdef} in metadata for {node.name}') + else: + port = portdef + proto = 'tcp' + + for target in targets: + if port == '*' and target == '*': + raise BundleError('firewall/port_rules: setting both port and target to * is unsupported') + + comment = f'comment "port_rules {target}"' + + if port != '*': + if ':' in port: + parts = port.split(':') + port_str = f'{proto} dport {{ {parts[0]}-{parts[1]} }}' + else: + port_str = f'{proto} dport {port}' + else: + port_str = f'meta l4proto {proto}' + + if target in ('ipv4', 'ipv6'): + version_str = f'meta nfproto {target}' + else: + version_str = '' + + if target in ('*', 'ipv4', 'ipv6'): + ruleset.add(f'inet filter input {version_str} {port_str} accept {comment}') + else: + resolved = repo.libs.tools.resolve_identifier(repo, target) + + for address in resolved['ipv4']: + ruleset.add(f'inet filter input meta nfproto ipv4 {port_str} ip saddr {address} accept {comment}') + + for address in resolved['ipv6']: + ruleset.add(f'inet filter input meta nfproto ipv6 {port_str} ip6 saddr {address} accept {comment}') + + return { + 'nftables': { + 'rules': { + # order does not matter here. + '99-port_rules': sorted(ruleset), + }, + }, + } diff --git a/bundles/nginx/files/arch-override.conf b/bundles/nginx/files/arch-override.conf new file mode 100644 index 0000000..5496fe6 --- /dev/null +++ b/bundles/nginx/files/arch-override.conf @@ -0,0 +1,9 @@ +[Service] +ExecStart= +ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf + +ExecReload= +ExecReload=/bin/sh -c "/bin/kill -s HUP $(/bin/cat /var/run/nginx.pid)" + +ExecStop= +ExecStop=/bin/sh -c "/bin/kill -s TERM $(/bin/cat /var/run/nginx.pid)" diff --git a/bundles/nginx/files/check_nginx_status b/bundles/nginx/files/check_nginx_status new file mode 100644 index 0000000..d80e76b --- /dev/null +++ b/bundles/nginx/files/check_nginx_status @@ -0,0 +1,475 @@ +#!/usr/bin/env perl +# editorconfig-checker-disable-file +# check_nginx_status.pl +# Author : regis.leroy at makina-corpus.com +# Licence : GPL - http://www.fsf.org/licenses/gpl.txt +# +# help : ./check_nginx_status.pl -h +# +# issues & updates: http://github.com/regilero/check_nginx_status +use warnings; +use strict; +use Getopt::Long; +use LWP::UserAgent; +use Time::HiRes qw(gettimeofday tv_interval); +use Digest::MD5 qw(md5 md5_hex); +use FindBin; + +# ensure all outputs are in UTF-8 +binmode(STDOUT, ":utf8"); + +# Nagios specific +use lib "/usr/lib/nagios/plugins"; +use utils qw($TIMEOUT); + +# Globals +my $Version='0.20'; +my $Name=$0; + +my $o_host = undef; # hostname +my $o_help= undef; # want some help ? +my $o_port= undef; # port +my $o_url = undef; # url to use, if not the default +my $o_user= undef; # user for auth +my $o_pass= ''; # password for auth +my $o_realm= ''; # password for auth +my $o_version= undef; # print version +my $o_warn_a_level= -1; # Number of active connections that will cause a warning +my $o_crit_a_level= -1; # Number of active connections that will cause an error +my $o_warn_rps_level= -1; # Number of Request per second that will cause a warning +my $o_crit_rps_level= -1; # Number of request Per second that will cause an error +my $o_warn_cps_level= -1; # Number of Connections per second that will cause a warning +my $o_crit_cps_level= -1; # Number of Connections per second that will cause an error +my $o_timeout= 15; # Default 15s Timeout +my $o_warn_thresold= undef; # warning thresolds entry +my $o_crit_thresold= undef; # critical thresolds entry +my $o_debug= undef; # debug mode +my $o_servername= undef; # ServerName (host header in http request) +my $o_https= undef; # SSL (HTTPS) mode +my $o_disable_sslverifyhostname = 0; + +my $TempPath = '/tmp/'; # temp path +my $MaxTimeDif = 60*30; # Maximum uptime difference (seconds), default 30 minutes + +my $nginx = 'NGINX'; # Could be used to store version also + +# functions +sub show_versioninfo { print "$Name version : $Version\n"; } + +sub print_usage { + print "Usage: $Name -H [-p ] [-s servername] [-t ] [-w -c ] [-V] [-d] [-u ] [-U user -P pass -r realm]\n"; +} +sub nagios_exit { + my ( $nickname, $status, $message, $perfdata , $silent) = @_; + my %STATUSCODE = ( + 'OK' => 0 + , 'WARNING' => 1 + , 'CRITICAL' => 2 + , 'UNKNOWN' => 3 + , 'PENDING' => 4 + ); + if(!defined($silent)) { + my $output = undef; + $output .= sprintf('%1$s %2$s - %3$s', $nickname, $status, $message); + if ($perfdata) { + $output .= sprintf('|%1$s', $perfdata); + } + $output .= chr(10); + print $output; + } + exit $STATUSCODE{$status}; +} + +# Get the alarm signal +$SIG{'ALRM'} = sub { + nagios_exit($nginx,"CRITICAL","ERROR: Alarm signal (Nagios timeout)"); +}; + +sub help { + print "Nginx Monitor for Nagios version ",$Version,"\n"; + print "GPL licence, (c)2012 Leroy Regis\n\n"; + print_usage(); + print </nginx_status" +-s, --servername=SERVERNAME + ServerName, (host header of HTTP request) use it if you specified an IP in -H to match the good Virtualhost in your target +-S, --ssl + Wether we should use HTTPS instead of HTTP +--disable-sslverifyhostname + Disable SSL hostname verification +-U, --user=user + Username for basic auth +-P, --pass=PASS + Password for basic auth +-r, --realm=REALM + Realm for basic auth +-d, --debug + Debug mode (show http request response) +-m, --maxreach=MAX + Number of max processes reached (since last check) that should trigger an alert +-t, --timeout=INTEGER + timeout in seconds (Default: $o_timeout) +-w, --warn=ACTIVE_CONN,REQ_PER_SEC,CONN_PER_SEC + number of active connections, ReqPerSec or ConnPerSec that will cause a WARNING + -1 for no warning +-c, --critical=ACTIVE_CONN,REQ_PER_SEC,CONN_PER_SEC + number of active connections, ReqPerSec or ConnPerSec that will cause a CRITICAL + -1 for no CRITICAL +-V, --version + prints version number + +Note : + 3 items can be managed on this check, this is why -w and -c parameters are using 3 values thresolds + - ACTIVE_CONN: Number of all opened connections, including connections to backends + - REQ_PER_SEC: Average number of request per second between this check and the previous one + - CONN_PER_SEC: Average number of connections per second between this check and the previous one + +Examples: + + This one will generate WARNING and CRITICIAL alerts if you reach 10 000 or 20 000 active connection; or + 100 or 200 request per second; or 200 or 300 connections per second +check_nginx_status.pl -H 10.0.0.10 -u /foo/nginx_status -s mydomain.example.com -t 8 -w 10000,100,200 -c 20000,200,300 + + this will generate WARNING and CRITICAL alerts only on the number of active connections (with low numbers for nginx) +check_nginx_status.pl -H 10.0.0.10 -s mydomain.example.com -t 8 -w 10,-1,-1 -c 20,-1,-1 + + theses two equivalents will not generate any alert (if the nginx_status page is reachable) but could be used for graphics +check_nginx_status.pl -H 10.0.0.10 -s mydomain.example.com -w -1,-1,-1 -c -1,-1,-1 +check_nginx_status.pl -H 10.0.0.10 -s mydomain.example.com + +EOT +} + +sub check_options { + Getopt::Long::Configure ("bundling"); + GetOptions( + 'h' => \$o_help, 'help' => \$o_help, + 'd' => \$o_debug, 'debug' => \$o_debug, + 'H:s' => \$o_host, 'hostname:s' => \$o_host, + 's:s' => \$o_servername, 'servername:s' => \$o_servername, + 'S:s' => \$o_https, 'ssl:s' => \$o_https, + 'u:s' => \$o_url, 'url:s' => \$o_url, + 'U:s' => \$o_user, 'user:s' => \$o_user, + 'P:s' => \$o_pass, 'pass:s' => \$o_pass, + 'r:s' => \$o_realm, 'realm:s' => \$o_realm, + 'p:i' => \$o_port, 'port:i' => \$o_port, + 'V' => \$o_version, 'version' => \$o_version, + 'w:s' => \$o_warn_thresold,'warn:s' => \$o_warn_thresold, + 'c:s' => \$o_crit_thresold,'critical:s' => \$o_crit_thresold, + 't:i' => \$o_timeout, 'timeout:i' => \$o_timeout, + 'disable-sslverifyhostname' => \$o_disable_sslverifyhostname, + ); + + if (defined ($o_help)) { + help(); + nagios_exit($nginx,"UNKNOWN","leaving","",1); + } + if (defined($o_version)) { + show_versioninfo(); + nagios_exit($nginx,"UNKNOWN","leaving","",1); + }; + + if (defined($o_warn_thresold)) { + ($o_warn_a_level,$o_warn_rps_level,$o_warn_cps_level) = split(',', $o_warn_thresold); + } + if (defined($o_crit_thresold)) { + ($o_crit_a_level,$o_crit_rps_level,$o_crit_cps_level) = split(',', $o_crit_thresold); + } + if (defined($o_debug)) { + print("\nDebug thresolds: \nWarning: ($o_warn_thresold) => Active: $o_warn_a_level ReqPerSec :$o_warn_rps_level ConnPerSec: $o_warn_cps_level"); + print("\nCritical ($o_crit_thresold) => : Active: $o_crit_a_level ReqPerSec: $o_crit_rps_level ConnPerSec : $o_crit_cps_level\n"); + } + if ((defined($o_warn_a_level) && defined($o_crit_a_level)) && + (($o_warn_a_level != -1) && ($o_crit_a_level != -1) && ($o_warn_a_level >= $o_crit_a_level)) ) { + nagios_exit($nginx,"UNKNOWN","Check warning and critical values for Active Process (1st part of thresold), warning level must be < crit level!"); + } + if ((defined($o_warn_rps_level) && defined($o_crit_rps_level)) && + (($o_warn_rps_level != -1) && ($o_crit_rps_level != -1) && ($o_warn_rps_level >= $o_crit_rps_level)) ) { + nagios_exit($nginx,"UNKNOWN","Check warning and critical values for ReqPerSec (2nd part of thresold), warning level must be < crit level!"); + } + if ((defined($o_warn_cps_level) && defined($o_crit_cps_level)) && + (($o_warn_cps_level != -1) && ($o_crit_cps_level != -1) && ($o_warn_cps_level >= $o_crit_cps_level)) ) { + nagios_exit($nginx,"UNKNOWN","Check warning and critical values for ConnPerSec (3rd part of thresold), warning level must be < crit level!"); + } + # Check compulsory attributes + if (!defined($o_host)) { + print_usage(); + nagios_exit($nginx,"UNKNOWN","-H host argument required"); + } +} + +########## MAIN ########## + +check_options(); + +my $override_ip = $o_host; +my $ua = LWP::UserAgent->new( + protocols_allowed => ['http', 'https'], + timeout => $o_timeout +); + +if ( $o_disable_sslverifyhostname ) { + $ua->ssl_opts( 'verify_hostname' => 0 ); +} + +# we need to enforce the HTTP request is made on the Nagios Host IP and +# not on the DNS related IP for that domain +@LWP::Protocol::http::EXTRA_SOCK_OPTS = ( PeerAddr => $override_ip ); +# this prevent used only once warning in -w mode +my $ua_settings = @LWP::Protocol::http::EXTRA_SOCK_OPTS; + +my $timing0 = [gettimeofday]; +my $response = undef; +my $url = undef; + +if (!defined($o_url)) { + $o_url='/nginx_status'; +} else { + # ensure we have a '/' as first char + $o_url = '/'.$o_url unless $o_url =~ m(^/) +} +my $proto='http://'; +if(defined($o_https)) { + $proto='https://'; + if (defined($o_port) && $o_port!=443) { + if (defined ($o_debug)) { + print "\nDEBUG: Notice: port is defined at $o_port and not 443, check you really want that in SSL mode! \n"; + } + } +} +if (defined($o_servername)) { + if (!defined($o_port)) { + $url = $proto . $o_servername . $o_url; + } else { + $url = $proto . $o_servername . ':' . $o_port . $o_url; + } +} else { + if (!defined($o_port)) { + $url = $proto . $o_host . $o_url; + } else { + $url = $proto . $o_host . ':' . $o_port . $o_url; + } +} +if (defined ($o_debug)) { + print "\nDEBUG: HTTP url: \n"; + print $url; +} + +my $req = HTTP::Request->new( GET => $url ); + +if (defined($o_servername)) { + $req->header('Host' => $o_servername); +} +if (defined($o_user)) { + $req->authorization_basic($o_user, $o_pass); +} + +if (defined ($o_debug)) { + print "\nDEBUG: HTTP request: \n"; + print "IP used (better if it's an IP):" . $override_ip . "\n"; + print $req->as_string; +} +$response = $ua->request($req); +my $timeelapsed = tv_interval ($timing0, [gettimeofday]); + +my $InfoData = ''; +my $PerfData = ''; +#my @Time = (localtime); # list context and not scalar as we want the brutal timestamp +my $Time = time; + +my $webcontent = undef; +if ($response->is_success) { + $webcontent=$response->decoded_content; + if (defined ($o_debug)) { + print "\nDEBUG: HTTP response:"; + print $response->status_line; + print "\n".$response->header('Content-Type'); + print "\n"; + print $webcontent; + } + if ($response->header('Content-Type') =~ m/text\/html/) { + nagios_exit($nginx,"CRITICAL", "We have a response page for our request, but it's an HTML page, quite certainly not the status report of nginx"); + } + # example of response content expected: + #Active connections: 10 + #server accepts handled requests + #38500 38500 50690 + #Reading: 5 Writing: 5 Waiting: 0 + + # number of all open connections including connections to backends + my $ActiveConn = 0; + if($webcontent =~ m/Active connections: (.*?)\n/) { + $ActiveConn = $1; + # triming + $ActiveConn =~ s/^\s+|\s+$//g; + } + + + # 3 counters with a space: accepted conn, handled conn and number of requests + my $counters = ''; + my $AcceptedConn = 0; + my $HandledConn = 0; + my $NbRequests = 0; + if($webcontent =~ m/\nserver accepts handled requests\n(.*?)\n/) { + $counters = $1; + # triming + $counters =~ s/^\s+|\s+$//g; + #splitting + ($AcceptedConn,$HandledConn,$NbRequests) = split(' ', $counters); + # triming + $AcceptedConn =~ s/^\s+|\s+$//g; + $HandledConn =~ s/^\s+|\s+$//g; + $NbRequests =~ s/^\s+|\s+$//g; + } + + # nginx reads request header + my $Reading = 0; + # nginx reads request body, processes request, or writes response to a client + my $Writing = 0; + # keep-alive connections, actually it is active - (reading + writing) + my $Waiting = 0; + if($webcontent =~ m/Reading: (.*?)Writing: (.*?)Waiting: (.*?)$/) { + $Reading = $1; + $Writing = $2; + $Waiting = $3; + # triming + $Reading =~ s/^\s+|\s+$//g; + $Writing =~ s/^\s+|\s+$//g; + $Waiting =~ s/^\s+|\s+$//g; + } + + # Debug + if (defined ($o_debug)) { + print ("\nDEBUG Parse results => Active :" . $ActiveConn . "\nAcceptedConn :" . $AcceptedConn . "\nHandledConn :" . $HandledConn . "\nNbRequests :".$NbRequests . "\nReading :" .$Reading . "\nWriting :" . $Writing . "\nWaiting :" . $Waiting . "\n"); + } + + my $TempFile = $TempPath.$o_host.'_check_nginx_status'.md5_hex($url); + my $FH; + + my $LastTime = 0; + my $LastAcceptedConn = 0; + my $LastHandledConn = 0; + my $LastNbRequests = 0; + if ((-e $TempFile) && (-r $TempFile) && (-w $TempFile)) { + open ($FH, '<',$TempFile) or nagios_exit($nginx,"UNKNOWN","unable to read temporary data from :".$TempFile); + $LastTime = <$FH>; + $LastAcceptedConn = <$FH>; + $LastHandledConn = <$FH>; + $LastNbRequests = <$FH>; + close ($FH); + if (defined ($o_debug)) { + print ("\nDebug: data from temporary file: $TempFile\n"); + print (" LastTime: $LastTime LastAcceptedConn: $LastAcceptedConn LastHandledConn: $LastHandledConn LastNbRequests: $LastNbRequests \n"); + } + } + + open ($FH, '>'.$TempFile) or nagios_exit($nginx,"UNKNOWN","unable to write temporary data in :".$TempFile); + #print $FH (@Time),"\n"; + print $FH "$Time\n"; + print $FH "$AcceptedConn\n"; + print $FH "$HandledConn\n"; + print $FH "$NbRequests\n"; + close ($FH); + + my $ConnPerSec = 0; + my $ReqPerSec = 0; + my $RequestsNew = 0; + # by default the average + my $ReqPerConn = 0; + if ($AcceptedConn > 0) { + $ReqPerConn = $NbRequests/$AcceptedConn; + } + my $elapsed = $Time - $LastTime ; + if (defined ($o_debug)) { + print ("\nDebug: pre-computation\n"); + print ("Average ReqPerconn: $ReqPerConn, Seconds elapsed Since last check: $elapsed\n"); + } + # check only if the counters may have been incremented + # but not if it may have been too much incremented + # if nginx was restarted ($NbRequests is now lower than previous value), just skip + if ( ($elapsed < $MaxTimeDif) && ($elapsed != 0) && ($NbRequests >= $LastNbRequests) ) { + $ConnPerSec = ($AcceptedConn-$LastAcceptedConn)/$elapsed; + $RequestsNew = $NbRequests-$LastNbRequests; + $ReqPerSec = $RequestsNew/$elapsed; + # get finer value + if ( $ConnPerSec!=0 ) { + my $ReqPerConn = $ReqPerSec/$ConnPerSec; + } else { + my $ReqPerConn = 0; + } + } + if (defined ($o_debug)) { + print ("\nDebug: data computed\n"); + print ("ConnPerSec: $ConnPerSec ReqPerSec: $ReqPerSec ReqPerConn: $ReqPerConn\n"); + } + $InfoData = sprintf (" %.3f sec. response time, Active: %d (Writing: %d Reading: %d Waiting: %d)" + . " ReqPerSec: %.3f ConnPerSec: %.3f ReqPerConn: %.3f" + ,$timeelapsed,$ActiveConn,$Writing,$Reading,$Waiting,$ReqPerSec,$ConnPerSec,$ReqPerConn); + + # Manage warn and crit values for the perfdata + my $p_warn_a_level = "$o_warn_a_level"; + my $p_crit_a_level = "$o_crit_a_level"; + my $p_warn_rps_level = "$o_warn_rps_level"; + my $p_crit_rps_level = "$o_crit_rps_level"; + my $p_warn_cps_level = "$o_warn_cps_level"; + my $p_crit_cps_level = "$o_crit_cps_level"; + + if ($p_warn_a_level == "-1") { + $p_warn_a_level = ""; + } + if ($p_crit_a_level == "-1") { + $p_crit_a_level = ""; + } + if ($p_warn_rps_level == "-1") { + $p_warn_rps_level = ""; + } + if ($p_crit_rps_level == "-1") { + $p_crit_rps_level = ""; + } + if ($p_warn_cps_level == "-1") { + $p_warn_cps_level = ""; + } + if ($p_crit_cps_level == "-1") { + $p_crit_cps_level = ""; + } + + $PerfData = sprintf ("Writing=%d;;;; Reading=%d;;;; Waiting=%d;;;; Active=%d;%s;%s;; " + . "ReqPerSec=%f;%s;%s;; ConnPerSec=%f;%s;%s;; ReqPerConn=%f;;;;" + ,($Writing),($Reading),($Waiting),($ActiveConn) + ,($p_warn_a_level),($p_crit_a_level) + ,($ReqPerSec),($p_warn_rps_level),($p_crit_rps_level) + ,($ConnPerSec),($p_warn_cps_level),($p_crit_cps_level) + ,($ReqPerConn)); + # first all critical exists by priority + if (defined($o_crit_a_level) && (-1!=$o_crit_a_level) && ($ActiveConn >= $o_crit_a_level)) { + nagios_exit($nginx,"CRITICAL", "Active Connections are critically high " . $InfoData,$PerfData); + } + if (defined($o_crit_rps_level) && (-1!=$o_crit_rps_level) && ($ReqPerSec >= $o_crit_rps_level)) { + nagios_exit($nginx,"CRITICAL", "Request per second ratios is critically high " . $InfoData,$PerfData); + } + if (defined($o_crit_cps_level) && (-1!=$o_crit_cps_level) && ($ConnPerSec >= $o_crit_cps_level)) { + nagios_exit($nginx,"CRITICAL", "Connection per second ratio is critically high " . $InfoData,$PerfData); + } + # Then WARNING exits by priority + if (defined($o_warn_a_level) && (-1!=$o_warn_a_level) && ($ActiveConn >= $o_warn_a_level)) { + nagios_exit($nginx,"WARNING", "Active Connections are high " . $InfoData,$PerfData); + } + if (defined($o_warn_rps_level) && (-1!=$o_warn_rps_level) && ($ReqPerSec >= $o_warn_rps_level)) { + nagios_exit($nginx,"WARNING", "Requests per second ratio is high " . $InfoData,$PerfData); + } + if (defined($o_warn_cps_level) && (-1!=$o_warn_cps_level) && ($ConnPerSec >= $o_warn_cps_level)) { + nagios_exit($nginx,"WARNING", "Connection per second ratio is high " . $InfoData,$PerfData); + } + + nagios_exit($nginx,"OK",$InfoData,$PerfData); + +} else { + nagios_exit($nginx,"CRITICAL", $response->status_line); +} diff --git a/bundles/nginx/files/fastcgi.conf b/bundles/nginx/files/fastcgi.conf new file mode 100644 index 0000000..53c0edd --- /dev/null +++ b/bundles/nginx/files/fastcgi.conf @@ -0,0 +1,26 @@ +fastcgi_param QUERY_STRING $query_string; +fastcgi_param REQUEST_METHOD $request_method; +fastcgi_param CONTENT_TYPE $content_type; +fastcgi_param CONTENT_LENGTH $content_length; + +fastcgi_param SCRIPT_NAME $fastcgi_script_name; +fastcgi_param REQUEST_URI $request_uri; +fastcgi_param DOCUMENT_URI $document_uri; +fastcgi_param DOCUMENT_ROOT $document_root; +fastcgi_param SERVER_PROTOCOL $server_protocol; +fastcgi_param REQUEST_SCHEME $scheme; +fastcgi_param HTTPS $https if_not_empty; + +fastcgi_param GATEWAY_INTERFACE CGI/1.1; +fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + +fastcgi_param REMOTE_ADDR $remote_addr; +fastcgi_param REMOTE_PORT $remote_port; +fastcgi_param SERVER_ADDR $server_addr; +fastcgi_param SERVER_PORT $server_port; +fastcgi_param SERVER_NAME $server_name; + +fastcgi_param REDIRECT_STATUS 200; + +# This is the only thing that's different to the debian default. +fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; diff --git a/bundles/nginx/files/logrotate.conf b/bundles/nginx/files/logrotate.conf new file mode 100644 index 0000000..7547e77 --- /dev/null +++ b/bundles/nginx/files/logrotate.conf @@ -0,0 +1,28 @@ +/var/log/nginx/*.log { + compress + copytruncate + create 0640 www-data adm + daily + dateext + missingok + notifempty + rotate ${node.metadata.get('nginx/log_retention_days', 7)} + sharedscripts + prerotate + if [ -d /etc/logrotate.d/httpd-prerotate ]; then \ + run-parts /etc/logrotate.d/httpd-prerotate; \ + fi + endscript +} + +/var/log/nginx-timing/*.log { + compress + copytruncate + create 0644 www-data adm + dateext + missingok + notifempty + rotate 3 + sharedscripts + size 1M +} diff --git a/bundles/nginx/files/nginx.conf b/bundles/nginx/files/nginx.conf new file mode 100644 index 0000000..3f4a9a9 --- /dev/null +++ b/bundles/nginx/files/nginx.conf @@ -0,0 +1,61 @@ +user ${username}; +worker_processes ${worker_processes}; + +pid /var/run/nginx.pid; + + +events { + worker_connections ${worker_connections}; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + charset UTF-8; + override_charset on; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 15; + client_body_timeout 12; + client_header_timeout 12; + send_timeout 10; + + access_log off; + error_log off; + + client_body_buffer_size 16K; + client_header_buffer_size 4k; + client_max_body_size 1M; + large_client_header_buffers 4 8k; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + # GDPR compatible IP smashinator 5000000 + map $remote_addr $ip_anonym1 { + default 0.0.0; + "~(?P(\d+)\.(\d+))\.(\d+)\.\d+" $ip; + "~(?P[^:]+:[^:]+):" $ip; + } + map $remote_addr $ip_anonym2 { + default .0.0; + "~(?P(\d+)\.(\d+)\.(\d+))\.\d+" .0.0; + "~(?P[^:]+:[^:]+):" ::; + } + map $ip_anonym1$ip_anonym2 $ip_anonymized { + default 0.0.0.0; + "~(?P.*)" $ip; + } + + log_format gdpr '$ip_anonymized - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"" "$http_user_agent"'; + + log_format anon_timing '[$time_local] $request_time $upstream_response_time "$request" $status'; + + include /etc/nginx/sites/*; +} diff --git a/bundles/nginx/files/port80.conf b/bundles/nginx/files/port80.conf new file mode 100644 index 0000000..e6e9e67 --- /dev/null +++ b/bundles/nginx/files/port80.conf @@ -0,0 +1,13 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + location /.well-known/acme-challenge/ { + alias /var/lib/dehydrated/acme-challenges/; + } + + location / { + return 404; + } +} diff --git a/bundles/nginx/files/security.txt b/bundles/nginx/files/security.txt new file mode 100644 index 0000000..d1c7726 --- /dev/null +++ b/bundles/nginx/files/security.txt @@ -0,0 +1,9 @@ +Contact: ${vhost.get('contact', repo.libs.defaults.security_email)} +Expires: ${vhost.get('expires', expiry)} +Preferred-Languages: ${','.join(sorted(vhost.get('lang', repo.libs.defaults.security_lang)))} +Canonical: ${proto}://${domain}/.well-known/security.txt +% for key, value in sorted(vhost.items()): +% if key[0].isupper(): +${key}: ${value} +% endif +% endfor diff --git a/bundles/nginx/files/site_template b/bundles/nginx/files/site_template new file mode 100644 index 0000000..9d55282 --- /dev/null +++ b/bundles/nginx/files/site_template @@ -0,0 +1,156 @@ +server { +% if domain_aliases: + server_name ${domain} ${' '.join(sorted(domain_aliases))}; +% else: + server_name ${domain}; +% endif + root ${webroot if webroot else '/var/www/{}/'.format(vhost)}; + index ${' '.join(index)}; + + listen 80; + listen [::]:80; + +% if ssl: + location / { + return 308 https://$host$request_uri; + } + +% if ssl == 'letsencrypt': + location /.well-known/acme-challenge/ { + alias /var/lib/dehydrated/acme-challenges/; + } +% endif +} + +server { +% if domain_aliases: + server_name ${domain} ${' '.join(sorted(domain_aliases))}; +% else: + server_name ${domain}; +% endif + root ${webroot if webroot else '/var/www/{}/'.format(vhost)}; + index ${' '.join(index)}; + + listen 443 ssl http2; + listen [::]:443 ssl http2; + +% if ssl == 'letsencrypt': + ssl_certificate /var/lib/dehydrated/certs/${domain}/fullchain.pem; + ssl_certificate_key /var/lib/dehydrated/certs/${domain}/privkey.pem; +% else: + ssl_certificate /etc/nginx/ssl/${vhost}.crt; + ssl_certificate_key /etc/nginx/ssl/${vhost}.key; +% endif + ssl_dhparam /etc/ssl/certs/dhparam.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_tickets off; + + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains"; +% endif + + resolver 8.8.8.8 8.8.4.4 valid=300s; + resolver_timeout 5s; + +% if create_access_log: + access_log /var/log/nginx/access-${vhost}.log gdpr; +% endif + access_log /var/log/nginx-timing/${vhost}.log anon_timing; + # error_log is disabled globally + +% if max_body_size: + client_max_body_size ${max_body_size}; +% endif + +% if not do_not_set_content_security_headers: + add_header Referrer-Policy same-origin; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; +% endif + add_header Permissions-Policy interest-cohort=(); + + error_page 404 /not_found.html; + location = /not_found.html { + root /var/www/; + internal; + } + + error_page 500 502 503 504 /error.html; + location = /error.html { + root /var/www/; + internal; + } + +% if ssl == 'letsencrypt': + location /.well-known/acme-challenge/ { + alias /var/lib/dehydrated/acme-challenges/; + } +% endif + +% if security_txt: + location = /.well-known/security.txt { + alias /etc/nginx/security.txt.d/${vhost}; + } +% endif + +% if locations: +% for location, options in sorted(locations.items()): + location ${location} { +% if 'target' in options: + proxy_pass ${options['target']}; + proxy_http_version ${options.get('http_version', '1.1')}; + proxy_set_header Host ${domain}; +% if options.get('websockets', False): + proxy_set_header Connection "upgrade"; + proxy_set_header Upgrade $http_upgrade; +% endif + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host ${options.get('x_forwarded_host', domain)}; +% for option, value in options.get('proxy_set_header', {}).items(): + proxy_set_header ${option} ${value}; +% endfor +% if location != '/': + proxy_set_header X-Script-Name ${location}; +% endif + proxy_buffering off; + proxy_read_timeout ${options.get('proxy_read_timeout', 60)}; + client_max_body_size ${options.get('max_body_size', '5M')}; +% elif 'redirect' in options: + return ${options.get('mode', 308)} ${options['redirect']}; +% elif 'return' in options: + return ${options.get('mode', 200)} '${options['return']}'; +% elif 'root' in options: + root ${options['root']}; +% elif 'alias' in options: + alias ${options['alias']}; +% endif +% if 'auth' in options: + auth_basic "${options['auth'].get('realm', vhost)}"; + auth_basic_user_file ${options['auth']['file']}; +% endif +% for opt in sorted(options.get('additional_config', set())): + ${opt}; +% endfor + } + +% endfor +% endif +% if php: + location ~ \.php$ { + include fastcgi.conf; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:/run/php/php${php_version}-fpm.sock; + } +% if not max_body_size: + client_max_body_size 5M; +% endif + +% endif +% if extras: +<%include file="extras/${node.name}/${vhost}" /> +% endif +} diff --git a/bundles/nginx/files/ssl_template b/bundles/nginx/files/ssl_template new file mode 100644 index 0000000..d886bed --- /dev/null +++ b/bundles/nginx/files/ssl_template @@ -0,0 +1,6 @@ +<% + from os.path import isfile, join +%><%include file="ssl/${domain}.crt.pem"/> +% if isfile(join(repo.path, 'data', 'ssl', f'{domain}.crt_intermediate.pem')): +<%include file="ssl/${domain}.crt_intermediate.pem"/> +% endif diff --git a/bundles/nginx/files/stub_status b/bundles/nginx/files/stub_status new file mode 100644 index 0000000..c86ce79 --- /dev/null +++ b/bundles/nginx/files/stub_status @@ -0,0 +1,6 @@ +server { + listen 127.0.0.1:22999 default_server; + server_name _; + + stub_status; +} diff --git a/bundles/nginx/items.py b/bundles/nginx/items.py new file mode 100644 index 0000000..88852e0 --- /dev/null +++ b/bundles/nginx/items.py @@ -0,0 +1,178 @@ +from datetime import datetime, timedelta + +if node.has_bundle('pacman'): + package = 'pkg_pacman:nginx' + username = 'http' +else: + package = 'pkg_apt:nginx' + username = 'www-data' + +directories = { + '/etc/nginx/sites': { + 'purge': True, + 'triggers': { + 'svc_systemd:nginx:restart', + }, + }, + '/etc/nginx/security.txt.d': { + 'purge': True, + }, + '/etc/nginx/ssl': { + 'purge': True, + 'triggers': { + 'svc_systemd:nginx:restart', + }, + }, + '/var/log/nginx-timing': { + 'owner': username, + 'needs': { + package, + }, + }, + '/var/www': {}, +} + +files = { + '/etc/logrotate.d/nginx': { + 'source': 'logrotate.conf', + }, + '/etc/nginx/nginx.conf': { + 'content_type': 'mako', + 'context': { + 'username': username, + **node.metadata['nginx'], + }, + 'triggers': { + 'svc_systemd:nginx:restart', + }, + }, + '/etc/nginx/fastcgi.conf': { + 'triggers': { + 'svc_systemd:nginx:restart', + }, + }, + '/etc/nginx/sites/stub_status': { + 'triggers': { + 'svc_systemd:nginx:restart', + }, + }, + '/etc/nginx/sites/000-port80.conf': { + 'source': 'port80.conf', + 'triggers': { + 'svc_systemd:nginx:restart', + }, + }, + '/usr/local/share/icinga/plugins/check_nginx_status': { + 'mode': '0755', + }, + '/var/www/error.html': {}, + '/var/www/not_found.html': {}, +} +if node.has_bundle('pacman'): + files['/etc/systemd/system/nginx.service.d/bundlewrap.conf'] = { + 'source': 'arch-override.conf', + 'triggers': { + 'action:systemd-reload', + 'svc_systemd:nginx:restart', + }, + } + +actions = { + 'nginx-generate-dhparam': { + 'command': 'openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048', + 'unless': 'test -f /etc/ssl/certs/dhparam.pem', + }, +} + +svc_systemd = { + 'nginx': { + 'needs': { + 'action:nginx-generate-dhparam', + 'directory:/var/log/nginx-timing', + package, + }, + }, +} + +now = datetime.now() +in_three_months = now + timedelta(days=90) +default_security_expiry = in_three_months.strftime('%Y-%m') + '-01T00:00:00.000Z' + +for vhost, config in node.metadata.get('nginx/vhosts', {}).items(): + if not 'domain' in config: + config['domain'] = vhost + + security_txt_enabled = False + if ( + node.metadata.get('nginx/security.txt/enabled', True) and + config.get('security.txt', {}).get('enabled', True) + ): + security_txt_enabled = True + + files[f'/etc/nginx/security.txt.d/{vhost}'] = { + 'source': 'security.txt', + 'content_type': 'mako', + 'context': { + 'domain': config['domain'], + 'expiry': default_security_expiry, + 'proto': 'https' if config.get('ssl', 'letsencrypt') else 'http', + 'vhost': config.get('security.txt', node.metadata.get('nginx/security.txt', {})), + }, + } + + files[f'/etc/nginx/sites/{vhost}'] = { + 'source': 'site_template', + 'content_type': 'mako', + 'context': { + 'create_access_log': config.get('access_log', node.metadata.get('nginx/access_log', False)), + 'php_version': node.metadata.get('php/version', ''), + 'security_txt': security_txt_enabled, + 'vhost': vhost, + **config, + }, + 'needs': set(), + 'needed_by': { + 'svc_systemd:nginx', + 'svc_systemd:nginx:restart', + }, + 'triggers': { + 'svc_systemd:nginx:restart', + }, + } + + if not 'webroot' in config: + directories[f'/var/www/{vhost}'] = config.get('webroot_config', {}) + + if config.get('ssl', 'letsencrypt') == 'letsencrypt': + files[f'/etc/nginx/sites/{vhost}']['needs'].add('action:letsencrypt_ensure-some-certificate_{}'.format(config['domain'])) + files[f'/etc/nginx/sites/{vhost}']['needed_by'].add('action:letsencrypt_update_certificates') + + elif config.get('ssl', 'letsencrypt'): + files[f'/etc/nginx/ssl/{vhost}.crt'] = { + 'content_type': 'mako', + 'source': 'ssl_template', + 'context': { + 'domain': config['ssl'], + }, + 'needed_by': { + 'svc_systemd:nginx', + 'svc_systemd:nginx:restart', + }, + 'triggers': { + 'svc_systemd:nginx:reload', + }, + } + files[f'/etc/nginx/ssl/{vhost}.key'] = { + 'content': repo.vault.decrypt_file('ssl/{}.key.pem.vault'.format(config['ssl'])), + 'mode': '0600', + 'needed_by': { + 'svc_systemd:nginx', + 'svc_systemd:nginx:restart', + }, + 'triggers': { + 'svc_systemd:nginx:reload', + }, + } + + files[f'/etc/nginx/sites/{vhost}']['needs'].add(f'file:/etc/nginx/ssl/{vhost}.crt') + files[f'/etc/nginx/sites/{vhost}']['needs'].add(f'file:/etc/nginx/ssl/{vhost}.key') diff --git a/bundles/nginx/metadata.py b/bundles/nginx/metadata.py new file mode 100644 index 0000000..81bc5aa --- /dev/null +++ b/bundles/nginx/metadata.py @@ -0,0 +1,209 @@ +from bundlewrap.metadata import atomic + +defaults = { + 'apt': { + 'repos': { + 'nginx': { + 'items': { + 'deb http://nginx.org/packages/{os} {os_release} nginx', + }, + }, + }, + 'packages': { + 'nginx': {}, + }, + }, + 'backups': { + 'paths': { + '/var/www', + }, + }, + 'icinga2_api': { + 'nginx': { + 'services': { + 'NGINX PROCESS': { + 'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_systemd_unit nginx', + }, + 'NGINX STATUS': { + 'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_nginx_status', + }, + }, + }, + }, + 'nginx': { + 'worker_connections': 768, + }, + 'pacman': { + 'packages': { + 'nginx': {}, + }, + }, +} + +if node.has_bundle('telegraf'): + defaults['telegraf'] = { + 'input_plugins': { + 'builtin': { + 'nginx': [{ + 'urls': ['http://localhost:22999/server_status'], + }], + }, + }, + } + + +@metadata_reactor.provides( + 'nginx/worker_processes', +) +def worker_processes(metadata): + return { + 'nginx': { + 'worker_processes': metadata.get('vm/cpu', 2), + }, + } + + +@metadata_reactor.provides( + 'letsencrypt/domains', + 'letsencrypt/reload_after', + 'nginx/vhosts', +) +def letsencrypt(metadata): + if not node.has_bundle('letsencrypt'): + raise DoNotRunAgain + + domains = {} + vhosts = {} + + for vhost, config in metadata.get('nginx/vhosts', {}).items(): + if config.get('ssl', 'letsencrypt') == 'letsencrypt': + domain = config.get('domain', vhost) + domains[domain] = config.get('domain_aliases', set()) + vhosts[vhost] = { + 'ssl': 'letsencrypt', + } + + return { + 'letsencrypt': { + 'domains': domains, + 'reload_after': { + 'nginx', + }, + }, + 'nginx': { + 'vhosts': vhosts, + }, + } + + +@metadata_reactor.provides( + 'nginx/vhosts', +) +def index_files(metadata): + vhosts = {} + + for vhost, config in metadata.get('nginx/vhosts', {}).items(): + vhosts[vhost] = { + 'index': [ + 'index.html', + 'index.htm', + ], + } + + if config.get('php', False): + # If we're using PHP, make sure index.php is tried first + vhosts[vhost]['index'].insert(0, 'index.php') + + + return { + 'nginx': { + 'vhosts': vhosts, + }, + } + + +@metadata_reactor.provides( + 'icinga2_api/nginx/services', +) +def monitoring(metadata): + services = {} + + for vname, vconfig in metadata.get('nginx/vhosts', {}).items(): + domain = vconfig.get('domain', vname) + + if vconfig['ssl']: + scheme = 'https' + else: + scheme = 'http' + + if 'website_check_path' in vconfig and 'website_check_string' in vconfig: + services['NGINX VHOST {} CONTENT'.format(vname)] = { + 'check_command': 'check_http_wget', + 'vars.http_wget_contains': vconfig['website_check_string'], + 'vars.http_wget_url': '{}://{}{}'.format(scheme, domain, vconfig['website_check_path']), + 'vars.notification.sms': True, + } + + if vconfig.get('check_ssl', vconfig['ssl']): + services['NGINX VHOST {} CERTIFICATE'.format(vname)] = { + 'check_command': 'check_https_cert_at_url', + 'vars.domain': domain, + 'vars.notification.mail': True, + } + + max_connections = metadata.get('nginx/worker_connections') * metadata.get('nginx/worker_processes') + connections_warn = int(max_connections * 0.8) + connections_crit = int(max_connections * 0.9) + + services['NGINX STATUS'] = { + 'command_on_monitored_host': '/usr/local/share/icinga/plugins/check_nginx_status --warn={},-1,-1 --critical={},-1,-1 -H 127.0.0.1:22999'.format(connections_warn, connections_crit), + } + + return { + 'icinga2_api': { + 'nginx': { + 'services': services, + }, + }, + } + + +@metadata_reactor.provides( + 'firewall/port_rules/80', + 'firewall/port_rules/443', +) +def firewall(metadata): + return { + 'firewall': { + 'port_rules': { + '80': atomic(metadata.get('nginx/restrict-to', {'*'})), + '443': atomic(metadata.get('nginx/restrict-to', {'*'})), + }, + }, + } + + +@metadata_reactor.provides( + 'telegraf/input_plugins/tail', +) +def telegraf_anon_timing(metadata): + result = {} + + for vhost in metadata.get('nginx/vhosts', {}): + result[f'nginx-{vhost}'] = { + 'files': [f'/var/log/nginx-timing/{vhost}.log'], + 'from_beginning': False, + 'grok_patterns': ['%{LOGPATTERN}'], + 'grok_custom_patterns': 'LOGPATTERN \[%{HTTPDATE:ts:ts-httpd}\] %{NUMBER:request_time:float} (?:%{NUMBER:upstream_response_time:float}|-) "%{WORD:verb:tag} %{NOTSPACE:request} HTTP/%{NUMBER:http_version:float}" %{NUMBER:resp_code:tag}', + 'data_format': 'grok', + 'name_override': 'nginx_timing', + } + + return { + 'telegraf': { + 'input_plugins': { + 'tail': result, + }, + }, + } + diff --git a/bundles/openssh/files/override.conf b/bundles/openssh/files/override.conf new file mode 100644 index 0000000..e19b593 --- /dev/null +++ b/bundles/openssh/files/override.conf @@ -0,0 +1,8 @@ +# The default unit already has "Restart=on-failure", but it has set +# "RestartPreventExitStatus=255", which prevents a restart on that +# specific exit code. I don't think we want that. Please, just restart +# ssh. + +[Service] +RestartPreventExitStatus= +RestartSec=1 diff --git a/bundles/openssh/files/sshd_config b/bundles/openssh/files/sshd_config new file mode 100644 index 0000000..60e8e43 --- /dev/null +++ b/bundles/openssh/files/sshd_config @@ -0,0 +1,42 @@ +Port 22 + +PermitRootLogin No +Protocol 2 +AuthorizedKeysFile .ssh/authorized_keys +GSSAPIAuthentication no +KerberosAuthentication no +ChallengeResponseAuthentication no +PasswordAuthentication no +PubkeyAuthentication yes +UseDNS no + +LogLevel INFO +X11Forwarding no +IgnoreRhosts yes +HostbasedAuthentication no +PermitEmptyPasswords no +PermitUserEnvironment no +Ciphers chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com +MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-256,hmac-sha2-512 +KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp256,ecdh-sha2-nistp384,diffie-hellman-group-exchange-sha256 +LoginGraceTime 60 +AllowUsers ${' '.join(sorted(login_users))} +UsePAM yes +AllowTcpForwarding no +PrintMotd no + +MaxSessions 512 +MaxStartups 512:30:768 + +Subsystem sftp internal-sftp + +Match Group sftp + ChrootDirectory %h + ForceCommand internal-sftp + PasswordAuthentication no + +Match User ${','.join(sorted(admin_users))} + AllowTcpForwarding yes +% if enable_x_forwarding_for_admins: + X11Forwarding yes +% endif diff --git a/bundles/openssh/items.py b/bundles/openssh/items.py new file mode 100644 index 0000000..a93b873 --- /dev/null +++ b/bundles/openssh/items.py @@ -0,0 +1,55 @@ +users_from_metadata = set() +additional_users = node.metadata.get('openssh/allowed_users', set()) + +for user, config in node.metadata.get('users', {}).items(): + if 'ssh_pubkey' in config and not config.get('delete', False): + users_from_metadata.add(user) + +login_users = users_from_metadata.union(additional_users) + +files = { + '/etc/ssh/sshd_config': { + 'content_type': 'mako', + 'context': { + 'login_users': login_users, + 'admin_users': users_from_metadata, + 'enable_x_forwarding_for_admins': node.metadata.get('openssh/enable_x_forwarding_for_admins', False), + }, + 'triggers': { + 'action:sshd_check_config', + }, + }, + '/etc/systemd/system/ssh.service.d/bundlewrap.conf': { + 'source': 'override.conf', + 'triggers': { + 'action:sshd_check_config', + }, + }, +} + +if node.has_bundle('pacman'): + package = 'pkg_pacman:openssh' + service = 'sshd' +else: + package = 'pkg_apt:openssh-server' + service = 'ssh' + +actions = { + 'sshd_check_config': { + 'command': 'sshd -T -C user=root -C host=localhost -C addr=localhost', + 'triggered': True, + 'triggers': { + 'svc_systemd:{}:restart'.format(service), + }, + }, +} + +svc_systemd = { + service: { + 'needs': { + 'file:/etc/systemd/system/ssh.service.d/bundlewrap.conf', + 'file:/etc/ssh/sshd_config', + package, + }, + }, +} diff --git a/bundles/openssh/metadata.py b/bundles/openssh/metadata.py new file mode 100644 index 0000000..3cad1b9 --- /dev/null +++ b/bundles/openssh/metadata.py @@ -0,0 +1,28 @@ +from bundlewrap.metadata import atomic + +defaults = { + 'apt': { + 'packages': { + 'openssh-client': {}, + 'openssh-server': {}, + 'openssh-sftp-server': {}, + }, + }, + 'pacman': { + 'packages': { + 'openssh': {}, + }, + }, +} + +@metadata_reactor.provides( + 'firewall/port_rules/22', +) +def firewall(metadata): + return { + 'firewall': { + 'port_rules': { + '22': atomic(metadata.get('openssh/restrict-to', {'*'})), + }, + }, + } diff --git a/bundles/php/files/7.3/fpm.conf b/bundles/php/files/7.3/fpm.conf new file mode 100644 index 0000000..bc745f5 --- /dev/null +++ b/bundles/php/files/7.3/fpm.conf @@ -0,0 +1,23 @@ +[global] +pid=/run/php/php7.4-fpm.pid +; We're using journal, put logs there +error_log=/var/log/php7.4-fpm.log +daemonize=yes + +; The one and only worker pool we have +[www] +user=www-data +group=www-data +listen=/run/php/php7.4-fpm.sock +listen.owner=www-data +listen.group=www-data +listen.mode=0600 + +; Process Manager Settings +pm=dynamic +pm.max_children=${num_cpus*4} +pm.start_servers=${num_cpus} +pm.max_spare_servers=${num_cpus*2} +pm.min_spare_servers=${num_cpus} +pm.process_idle_timeout=30s +pm.max_requests=1024 diff --git a/bundles/php/files/7.3/php.ini b/bundles/php/files/7.3/php.ini new file mode 100644 index 0000000..45b78bf --- /dev/null +++ b/bundles/php/files/7.3/php.ini @@ -0,0 +1,99 @@ +[PHP] +; Only needed for libapache2-mod-php? +engine = On +short_open_tag = Off +precision = 14 +output_buffering = 4096 +zlib.output_compression = Off +implicit_flush = Off +serialize_precision = -1 +disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals +ignore_user_abort = Off +zend.enable_gc = On +expose_php = Off + +max_execution_time = 30 +max_input_time = 60 +memory_limit = 256M + +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT +display_startup_errors = Off +log_errors = On +log_errors_max_len = 1024 +ignore_repeated_errors = Off +ignore_repeated_source = Off +report_memleaks = On +html_errors = On +error_log = syslog +syslog.ident = php7.4 +syslog.filter = ascii + +arg_separator.output = "&" +variables_order = "GPCS" +request_order = "GP" +register_argc_argv = Off +auto_globals_jit = On +post_max_size = ${post_max_size}M +default_mimetype = "text/html" +default_charset = "UTF-8" + +enable_dl = Off +file_uploads = On +upload_max_filesize = ${post_max_size}M +max_file_uploads = 20 + +allow_url_fopen = On +allow_url_include = Off +default_socket_timeout = 10 + +[CLI Server] +cli_server.color = On + +[mail function] +mail.add_x_header = Off + +[ODBC] +odbc.allow_persistent = On +odbc.check_persistent = On +odbc.max_persistent = -1 +odbc.max_links = -1 +odbc.defaultlrl = 4096 +odbc.defaultbinmode = 1 + +[PostgreSQL] +pgsql.allow_persistent = On +pgsql.auto_reset_persistent = Off +pgsql.max_persistent = -1 +pgsql.max_links = -1 +pgsql.ignore_notice = 0 +pgsql.log_notice = 0 + +[bcmath] +bcmath.scale = 0 + +[Session] +session.save_handler = files +session.use_strict_mode = 0 +session.use_cookies = 1 +session.use_only_cookies = 1 +session.name = PHPSESSID +session.auto_start = 0 +session.cookie_lifetime = 0 +session.cookie_path = / +session.cookie_domain = +session.cookie_httponly = +session.cookie_samesite = +session.serialize_handler = php +session.gc_probability = 1 +session.gc_divisor = 1000 +session.gc_maxlifetime = 1440 +session.referer_check = +session.cache_limiter = nocache +session.cache_expire = 180 +session.use_trans_sid = 0 +session.sid_length = 32 +session.trans_sid_tags = "a=href,area=href,frame=src,form=" +session.sid_bits_per_character = 6 + +[Assertion] +zend.assertions = -1 diff --git a/bundles/php/files/7.4/fpm.conf b/bundles/php/files/7.4/fpm.conf new file mode 100644 index 0000000..bc745f5 --- /dev/null +++ b/bundles/php/files/7.4/fpm.conf @@ -0,0 +1,23 @@ +[global] +pid=/run/php/php7.4-fpm.pid +; We're using journal, put logs there +error_log=/var/log/php7.4-fpm.log +daemonize=yes + +; The one and only worker pool we have +[www] +user=www-data +group=www-data +listen=/run/php/php7.4-fpm.sock +listen.owner=www-data +listen.group=www-data +listen.mode=0600 + +; Process Manager Settings +pm=dynamic +pm.max_children=${num_cpus*4} +pm.start_servers=${num_cpus} +pm.max_spare_servers=${num_cpus*2} +pm.min_spare_servers=${num_cpus} +pm.process_idle_timeout=30s +pm.max_requests=1024 diff --git a/bundles/php/files/7.4/php.ini b/bundles/php/files/7.4/php.ini new file mode 100644 index 0000000..45b78bf --- /dev/null +++ b/bundles/php/files/7.4/php.ini @@ -0,0 +1,99 @@ +[PHP] +; Only needed for libapache2-mod-php? +engine = On +short_open_tag = Off +precision = 14 +output_buffering = 4096 +zlib.output_compression = Off +implicit_flush = Off +serialize_precision = -1 +disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals +ignore_user_abort = Off +zend.enable_gc = On +expose_php = Off + +max_execution_time = 30 +max_input_time = 60 +memory_limit = 256M + +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT +display_startup_errors = Off +log_errors = On +log_errors_max_len = 1024 +ignore_repeated_errors = Off +ignore_repeated_source = Off +report_memleaks = On +html_errors = On +error_log = syslog +syslog.ident = php7.4 +syslog.filter = ascii + +arg_separator.output = "&" +variables_order = "GPCS" +request_order = "GP" +register_argc_argv = Off +auto_globals_jit = On +post_max_size = ${post_max_size}M +default_mimetype = "text/html" +default_charset = "UTF-8" + +enable_dl = Off +file_uploads = On +upload_max_filesize = ${post_max_size}M +max_file_uploads = 20 + +allow_url_fopen = On +allow_url_include = Off +default_socket_timeout = 10 + +[CLI Server] +cli_server.color = On + +[mail function] +mail.add_x_header = Off + +[ODBC] +odbc.allow_persistent = On +odbc.check_persistent = On +odbc.max_persistent = -1 +odbc.max_links = -1 +odbc.defaultlrl = 4096 +odbc.defaultbinmode = 1 + +[PostgreSQL] +pgsql.allow_persistent = On +pgsql.auto_reset_persistent = Off +pgsql.max_persistent = -1 +pgsql.max_links = -1 +pgsql.ignore_notice = 0 +pgsql.log_notice = 0 + +[bcmath] +bcmath.scale = 0 + +[Session] +session.save_handler = files +session.use_strict_mode = 0 +session.use_cookies = 1 +session.use_only_cookies = 1 +session.name = PHPSESSID +session.auto_start = 0 +session.cookie_lifetime = 0 +session.cookie_path = / +session.cookie_domain = +session.cookie_httponly = +session.cookie_samesite = +session.serialize_handler = php +session.gc_probability = 1 +session.gc_divisor = 1000 +session.gc_maxlifetime = 1440 +session.referer_check = +session.cache_limiter = nocache +session.cache_expire = 180 +session.use_trans_sid = 0 +session.sid_length = 32 +session.trans_sid_tags = "a=href,area=href,frame=src,form=" +session.sid_bits_per_character = 6 + +[Assertion] +zend.assertions = -1 diff --git a/bundles/php/files/8.0/fpm.conf b/bundles/php/files/8.0/fpm.conf new file mode 100644 index 0000000..2de6015 --- /dev/null +++ b/bundles/php/files/8.0/fpm.conf @@ -0,0 +1,27 @@ +[global] +pid=/run/php/php8.0-fpm.pid +; We're using journal, put logs there +error_log=/var/log/php8.0-fpm.log +daemonize=yes + +; The one and only worker pool we have +[www] +user=www-data +group=www-data +listen=/run/php/php8.0-fpm.sock +listen.owner=www-data +listen.group=www-data +listen.mode=0600 + +; Process Manager Settings +pm=dynamic +pm.max_children=${num_cpus*4} +pm.start_servers=${num_cpus} +pm.max_spare_servers=${num_cpus*2} +pm.min_spare_servers=${num_cpus} +pm.process_idle_timeout=30s +pm.max_requests=1024 + +% if not clear_env: +clear_env=no +% endif diff --git a/bundles/php/files/8.0/php.ini b/bundles/php/files/8.0/php.ini new file mode 100644 index 0000000..c8ef0e9 --- /dev/null +++ b/bundles/php/files/8.0/php.ini @@ -0,0 +1,99 @@ +[PHP] +; Only needed for libapache2-mod-php? +engine = On +short_open_tag = Off +precision = 14 +output_buffering = 4096 +zlib.output_compression = Off +implicit_flush = Off +serialize_precision = -1 +disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals +ignore_user_abort = Off +zend.enable_gc = On +expose_php = Off + +max_execution_time = 30 +max_input_time = 60 +memory_limit = ${memory_limit}M + +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT +display_startup_errors = Off +log_errors = On +log_errors_max_len = 1024 +ignore_repeated_errors = Off +ignore_repeated_source = Off +report_memleaks = On +html_errors = On +error_log = syslog +syslog.ident = php7.4 +syslog.filter = ascii + +arg_separator.output = "&" +variables_order = "GPCS" +request_order = "GP" +register_argc_argv = Off +auto_globals_jit = On +post_max_size = ${post_max_size}M +default_mimetype = "text/html" +default_charset = "UTF-8" + +enable_dl = Off +file_uploads = On +upload_max_filesize = ${post_max_size}M +max_file_uploads = 20 + +allow_url_fopen = On +allow_url_include = Off +default_socket_timeout = 10 + +[CLI Server] +cli_server.color = On + +[mail function] +mail.add_x_header = Off + +[ODBC] +odbc.allow_persistent = On +odbc.check_persistent = On +odbc.max_persistent = -1 +odbc.max_links = -1 +odbc.defaultlrl = 4096 +odbc.defaultbinmode = 1 + +[PostgreSQL] +pgsql.allow_persistent = On +pgsql.auto_reset_persistent = Off +pgsql.max_persistent = -1 +pgsql.max_links = -1 +pgsql.ignore_notice = 0 +pgsql.log_notice = 0 + +[bcmath] +bcmath.scale = 0 + +[Session] +session.save_handler = files +session.use_strict_mode = 0 +session.use_cookies = 1 +session.use_only_cookies = 1 +session.name = PHPSESSID +session.auto_start = 0 +session.cookie_lifetime = 0 +session.cookie_path = / +session.cookie_domain = +session.cookie_httponly = +session.cookie_samesite = +session.serialize_handler = php +session.gc_probability = 1 +session.gc_divisor = 1000 +session.gc_maxlifetime = 1440 +session.referer_check = +session.cache_limiter = nocache +session.cache_expire = 180 +session.use_trans_sid = 0 +session.sid_length = 32 +session.trans_sid_tags = "a=href,area=href,frame=src,form=" +session.sid_bits_per_character = 6 + +[Assertion] +zend.assertions = -1 diff --git a/bundles/php/items.py b/bundles/php/items.py new file mode 100644 index 0000000..f4479d7 --- /dev/null +++ b/bundles/php/items.py @@ -0,0 +1,58 @@ +version = node.metadata['php']['version'] + +files = { + f'/etc/php/{version}/fpm/php-fpm.conf': { + 'source': f'{version}/fpm.conf', + 'content_type': 'mako', + 'context': { + 'num_cpus': node.metadata['vm']['cpu'], + 'clear_env': node.metadata.get('php/clear_env', True), + }, + 'needs': { + # "all php packages" + 'pkg_apt:' + }, + 'triggers': { + f'svc_systemd:php{version}-fpm:restart', + }, + }, + f'/etc/php/{version}/fpm/php.ini': { + 'source': f'{version}/php.ini', + 'content_type': 'mako', + 'context': { + 'num_cpus': node.metadata['vm']['cpu'], + 'post_max_size': node.metadata['php'].get('post_max_size', 10), + 'memory_limit': node.metadata.get('php/memory_limit', 256), + }, + 'needs': { + # "all php packages" + 'pkg_apt:' + }, + 'triggers': { + f'svc_systemd:php{version}-fpm:restart', + }, + }, + f'/etc/php/{version}/cli/php.ini': { + 'source': f'{version}/php.ini', + 'content_type': 'mako', + 'context': { + 'num_cpus': node.metadata['vm']['cpu'], + 'post_max_size': node.metadata['php'].get('post_max_size', 10), + 'memory_limit': node.metadata.get('php/memory_limit', 256), + }, + 'needs': { + # "all php packages" + 'pkg_apt:' + }, + }, +} + +svc_systemd = { + f'php{version}-fpm': { + 'needs': { + 'pkg_apt:', + f'file:/etc/php/{version}/fpm/php-fpm.conf', + f'file:/etc/php/{version}/fpm/php.ini', + }, + }, +} diff --git a/bundles/php/metadata.py b/bundles/php/metadata.py new file mode 100644 index 0000000..d14954e --- /dev/null +++ b/bundles/php/metadata.py @@ -0,0 +1,37 @@ +defaults = { + 'apt': { + 'repos': { + 'php': { + 'items': { + 'deb https://packages.sury.org/php/ {os_release} main', + }, + }, + }, + }, +} + + +@metadata_reactor.provides( + 'apt/packages', +) +def php_packages_with_features(metadata): + version = metadata.get('php/version') + + packages = { + f'php{version}': {}, + f'php{version}-cli': {}, + f'php{version}-fpm': {}, + } + + for package in metadata.get('php/packages', set()): + packages[f'php{version}-{package}'] = { + 'triggers': { + f'svc_systemd:php{version}-fpm:restart', + } + } + + return { + 'apt': { + 'packages': packages, + }, + } diff --git a/bundles/postgresql/files/backup-pre-hook b/bundles/postgresql/files/backup-pre-hook new file mode 100644 index 0000000..9a56a13 --- /dev/null +++ b/bundles/postgresql/files/backup-pre-hook @@ -0,0 +1,9 @@ +#!/bin/bash + +target="/var/tmp/postgresdumps" + +pg_dumpall --globals-only | gzip --quiet --rsyncable >"$target/globals.sql.gz" + +% for db in sorted(databases): +pg_dump -C "${db}" | gzip --quiet --rsyncable >"$target/db_${db}.sql.gz" +% endfor diff --git a/bundles/postgresql/files/pg_hba.conf b/bundles/postgresql/files/pg_hba.conf new file mode 100644 index 0000000..8aeee1c --- /dev/null +++ b/bundles/postgresql/files/pg_hba.conf @@ -0,0 +1,8 @@ +% for custom_rule in sorted(node.metadata.get('postgresql/custom_rules', [])): +${custom_rule} +% endfor +local all postgres peer +local all all peer +host all all 127.0.0.1/32 md5 +host all all ::1/128 md5 +host all all all md5 diff --git a/bundles/postgresql/files/postgresql.conf b/bundles/postgresql/files/postgresql.conf new file mode 100644 index 0000000..56fa5af --- /dev/null +++ b/bundles/postgresql/files/postgresql.conf @@ -0,0 +1,32 @@ +data_directory = '/var/lib/postgresql/${version}/main' +hba_file = '/etc/postgresql/${version}/main/pg_hba.conf' +ident_file = '/etc/postgresql/${version}/main/pg_ident.conf' +external_pid_file = '/var/run/postgresql/${version}-main.pid' +unix_socket_directories = '/var/run/postgresql' +port = 5432 +listen_addresses = 'localhost' +max_connections = ${max_connections} +autovacuum_max_workers = ${autovacuum_max_workers} +maintenance_work_mem = ${maintenance_work_mem}MB +work_mem = ${work_mem}MB +shared_buffers = ${shared_buffers}MB +temp_buffers = ${temp_buffers}MB +log_destination = syslog +datestyle = 'iso, ymd' +timezone = 'localtime' +lc_messages = 'en_US.UTF-8' +lc_monetary = 'en_US.UTF-8' +lc_numeric = 'en_US.UTF-8' +lc_time = 'en_US.UTF-8' +default_text_search_config = 'pg_catalog.english' +% if slow_query_log_sec > 0: +log_min_duration_statement = ${slow_query_log_sec*1000} +% else: +log_min_duration_statement = -1 +% endif +effective_io_concurrency = ${effective_io_concurrency} +max_worker_processes = ${max_worker_processes} +% if version_list >= [10]: +max_parallel_workers = ${max_parallel_workers} +% endif +max_parallel_workers_per_gather = ${max_parallel_workers_per_gather} diff --git a/bundles/postgresql/items.py b/bundles/postgresql/items.py new file mode 100644 index 0000000..5f21b42 --- /dev/null +++ b/bundles/postgresql/items.py @@ -0,0 +1,124 @@ +postgresql_version = node.metadata['postgresql']['version'] + +pkg_apt = { + 'postgresql-common': {}, + 'postgresql-client-common': {}, + 'postgresql-{}'.format(postgresql_version): {}, + 'postgresql-client-{}'.format(postgresql_version): {}, + 'postgresql-server-dev-{}'.format(postgresql_version): {} +} + +if node.has_bundle('zfs'): + for pkgname, pkgconfig in pkg_apt.items(): + pkg_apt[pkgname]['needs'] = { + 'zfs_dataset:tank/postgresql', + } + + +directories = { + '/etc/postgresql': { + 'owner': None, + 'group': None, + 'mode': None, + # Keeping old configs messes with cluster-auto-detection. + 'purge': True, + }, + # This is needed so the above purge does not remove the version + # currently installed. + '/etc/postgresql/{}/main'.format(postgresql_version): { + 'owner': 'postgres', + 'group': 'postgres', + 'mode': '0755', + 'needs': {f'pkg_apt:{i}' for i in pkg_apt.keys()}, + }, +} + +files = { + "/etc/postgresql/{}/main/pg_hba.conf".format(postgresql_version): { + 'content_type': 'mako', + 'owner': 'postgres', + 'group': 'postgres', + 'triggers': { + 'svc_systemd:postgresql:restart', + }, + }, + "/etc/postgresql/{}/main/postgresql.conf".format(postgresql_version): { + 'content_type': 'mako', + 'context': { + 'version_list': [int(i) for i in node.metadata['postgresql']['version'].split('.')], + **node.metadata['postgresql'], + }, + 'owner': 'postgres', + 'group': 'postgres', + 'needs': { + # postgresql won't start if the configured locale isn't available + 'action:locale-gen', + } if node.has_bundle('basic') else set(), + 'triggers': { + 'svc_systemd:postgresql:restart', + }, + }, +} + +if node.has_bundle('backup-client') and not node.has_bundle('zfs'): + files['/etc/backup-pre-hooks.d/90-postgresql-dump-all'] = { + 'source': 'backup-pre-hook', + 'content_type': 'mako', + 'context': { + 'databases': node.metadata.get('postgresql/databases', {}).keys(), + }, + 'mode': '0700', + } + directories['/var/tmp/postgresdumps'] = {} +else: + files['/var/tmp/postgresdumps'] = { + 'delete': True, + } + +postgres_roles = { + 'root': { + 'password': repo.vault.password_for('{} postgresql root'.format(node.name)), + 'superuser': True, + 'needs': { + 'svc_systemd:postgresql', + }, + }, +} + +restart_deps = { + f'file:/etc/postgresql/{postgresql_version}/main/pg_hba.conf', + f'file:/etc/postgresql/{postgresql_version}/main/postgresql.conf', + *{f'pkg_apt:{i}' for i in pkg_apt.keys()}, +} + +svc_systemd = { + 'postgresql': { + 'needs': restart_deps, + 'triggers': { + 'action:postgresql_wait_after_restart', + }, + }, +} + +actions = { + 'postgresql_wait_after_restart': { + # postgresql doesn't accept connections immediately after restarting + 'command': 'sleep 10', + 'triggered': True, + 'before': { + 'postgres_role:', + 'postgres_db:', + }, + }, +} + +for user, config in node.metadata.get('postgresql/roles', {}).items(): + postgres_roles[user] = { + 'password': config['password'], + 'needs': { + 'svc_systemd:postgresql', + }, + } + +for database, config in node.metadata.get('postgresql/databases', {}).items(): + postgres_dbs[database] = config diff --git a/bundles/postgresql/metadata.py b/bundles/postgresql/metadata.py new file mode 100644 index 0000000..6a76107 --- /dev/null +++ b/bundles/postgresql/metadata.py @@ -0,0 +1,127 @@ +defaults = { + 'backups': { + 'paths': { + '/var/lib/postgresql', + }, + }, + 'bash_functions': { + 'pg_query_mon': "watch -n 2 \"echo \\\"SELECT pid, age(clock_timestamp(), query_start), usename, query FROM pg_stat_activity WHERE query != '' AND query NOT ILIKE '%pg_stat_activity%' ORDER BY query_start desc;\\\" | psql postgres\"" + }, + 'icinga2_api': { + 'postgresql': { + 'services': { + 'POSTGRESQL PROCESS': { + 'command_on_monitored_host': '/usr/lib/nagios/plugins/check_procs -C postgres -c 1:', + }, + }, + }, + }, + 'postgresql': { + 'max_connections': 100, + 'autovacuum_max_workers': 3, + 'maintenance_work_mem': 64, + 'work_mem': 4, + 'shared_buffers': 128, + 'temp_buffers': 8, + 'slow_query_log_sec': 0, + }, +} + +if node.has_bundle('telegraf'): + defaults['telegraf'] = { + 'input_plugins': { + 'builtin': { + 'postgresql': [{ + 'address': repo.vault.password_for(f'{node.name} postgresql telegraf').format_into('postgres://telegraf:{}@localhost:5432/telegraf?sslmode=disable'), + 'ignored_databases': [ + 'template0', + 'template1', + 'telegraf', + ], + }], + }, + }, + } + defaults['postgresql'].update({ + 'roles': { + 'telegraf': { + 'password': repo.vault.password_for(f'{node.name} postgresql telegraf'), + }, + }, + 'databases': { + 'telegraf': { + 'owner': 'telegraf', + }, + }, + }) + +if node.has_bundle('zfs'): + defaults['zfs'] = { + 'datasets': { + 'tank/postgresql': { + 'mountpoint': '/var/lib/postgresql', + }, + }, + } +else: + defaults['backups']['paths'].add('/var/tmp/postgresdumps') + + +@metadata_reactor.provides( + 'apt/repos/postgresql', + 'postgresql/version', +) +def default_postgresql_version_for_debian(metadata): + # + versions_in_debian = { + '10': '11', # buster + '11': '13', # bullseye + } + os = str(node.os_version[0]) + version_to_be_installed = metadata.get('postgresql/version', versions_in_debian[os]) + + if version_to_be_installed != versions_in_debian[os]: + return { + 'apt': { + 'repos': { + 'postgresql': { + 'items': { + 'deb https://apt.postgresql.org/pub/repos/apt/ {os_release}-pgdg main', + }, + }, + }, + }, + 'postgresql': { + 'version': version_to_be_installed, + }, + } + + return { + 'postgresql': { + 'version': version_to_be_installed, + }, + } + + +@metadata_reactor.provides( + 'postgresql/effective_io_concurrency', + 'postgresql/max_worker_processes', + 'postgresql/max_parallel_workers', + 'postgresql/max_parallel_workers_per_gather', +) +def worker_processes(metadata): + return { + 'postgresql': { + # This is the amount of parallel I/O Operations the + # postgresql process is allowed to do on disk. We set + # this to max_connections by default. + 'effective_io_concurrency': metadata.get('postgresql/max_connections'), + + # Try to request one worker process per 10 configured + # connections. The default is 8 for both of these values. + 'max_worker_processes': int(metadata.get('postgresql/max_connections')/10), + 'max_parallel_workers': int(metadata.get('postgresql/max_connections')/10), + # default 2 + 'max_parallel_workers_per_gather': max(int(metadata.get('postgresql/max_connections')/100), 2), + }, + } diff --git a/bundles/redis/files/redis.conf b/bundles/redis/files/redis.conf new file mode 100644 index 0000000..f479be2 --- /dev/null +++ b/bundles/redis/files/redis.conf @@ -0,0 +1,50 @@ +activerehashing yes +aof-load-truncated yes +aof-rewrite-incremental-fsync yes +appendfilename "appendonly.aof" +appendfsync everysec +appendonly ${node.metadata.get('redis/appendonly', "no")} +auto-aof-rewrite-min-size 64mb +auto-aof-rewrite-percentage 100 +bind ${node.metadata.get('redis/bind', '127.0.0.1')} +client-output-buffer-limit normal 0 0 0 +client-output-buffer-limit pubsub 32mb 8mb 60 +client-output-buffer-limit slave 256mb 64mb 60 +daemonize yes +databases 16 +dbfilename dump.rdb +dir /var/lib/redis +hash-max-ziplist-entries 512 +hash-max-ziplist-value 64 +hll-sparse-max-bytes 3000 +hz 10 +latency-monitor-threshold 0 +list-max-ziplist-entries 512 +list-max-ziplist-value 64 +logfile /var/log/redis/redis-server.log +loglevel notice +lua-time-limit 5000 +no-appendfsync-on-rewrite no +notify-keyspace-events "" +pidfile /var/run/redis/redis-server.pid +port 6379 +rdbchecksum yes +rdbcompression yes +repl-disable-tcp-nodelay no +repl-diskless-sync no +repl-diskless-sync-delay 5 +save 300 10 +save 60 10000 +save 900 1 +set-max-intset-entries 512 +slave-priority 100 +slave-read-only yes +slave-serve-stale-data yes +slowlog-log-slower-than 10000 +slowlog-max-len 128 +stop-writes-on-bgsave-error yes +tcp-backlog 511 +tcp-keepalive 0 +timeout 0 +zset-max-ziplist-entries 128 +zset-max-ziplist-value 64 diff --git a/bundles/redis/items.py b/bundles/redis/items.py new file mode 100644 index 0000000..b68e28b --- /dev/null +++ b/bundles/redis/items.py @@ -0,0 +1,22 @@ +svc_systemd = { + 'redis-server': { + 'needs': { + 'file:/etc/redis/redis.conf', + }, + }, +} + +files = { + '/etc/redis/redis.conf': { + 'content_type': 'mako', + 'mode': '0640', + 'owner': 'redis', + 'group': 'redis', + 'needs': { + 'pkg_apt:redis-server', + }, + 'triggers': { + 'svc_systemd:redis-server:restart', + }, + }, +} diff --git a/bundles/redis/metadata.py b/bundles/redis/metadata.py new file mode 100644 index 0000000..f339c4a --- /dev/null +++ b/bundles/redis/metadata.py @@ -0,0 +1,40 @@ +defaults = { + 'apt': { + 'packages': { + 'redis-server': {}, + }, + }, + 'backups': { + 'paths': { + '/var/lib/redis', + }, + }, + 'icinga2_api': { + 'redis': { + 'services': { + 'REDIS PROCESS': { + 'command_on_monitored_host': '/usr/lib/nagios/plugins/check_procs -C redis-server -c 1:', + }, + } + }, + }, + 'zfs': { + 'datasets': { + 'tank/redis': { + 'mountpoint': '/var/lib/redis', + 'needed_by': { + 'pkg_apt:redis-server', + }, + }, + }, + }, +} + +if node.has_bundle('telegraf'): + defaults['telegraf'] = { + 'input_plugins': { + 'builtin': { + 'redis': [{}], + }, + }, + } diff --git a/bundles/sudo/files/bwusers b/bundles/sudo/files/bwusers new file mode 100644 index 0000000..6c47ecd --- /dev/null +++ b/bundles/sudo/files/bwusers @@ -0,0 +1,9 @@ +% for user, config in sorted(node.metadata['users'].items()): +% if config.get('is_admin', False): +${user} ALL=(ALL) NOPASSWD:ALL +% else: +% for p in sorted(config.get('sudo_commands', [])): +${user} ALL=(ALL) NOPASSWD:${p} +% endfor +% endif +% endfor diff --git a/bundles/sudo/files/sudoers b/bundles/sudo/files/sudoers new file mode 100644 index 0000000..b87e2dc --- /dev/null +++ b/bundles/sudo/files/sudoers @@ -0,0 +1,10 @@ +Defaults timestamp_timeout=5 +Defaults passwd_timeout=10 +Defaults env_reset +Defaults secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +Defaults !syslog +Defaults !pam_session + +root ALL=(ALL) ALL + +#includedir /etc/sudoers.d diff --git a/bundles/sudo/items.py b/bundles/sudo/items.py new file mode 100644 index 0000000..1f1aa70 --- /dev/null +++ b/bundles/sudo/items.py @@ -0,0 +1,27 @@ +groups = { + 'sudo': {}, +} + +directories = { + '/etc/sudoers.d': { + 'purge': True, + }, +} + +files = { + '/etc/sudoers': { + 'mode': '0440', + 'needs': { + 'file:/etc/sudoers.d/bwusers', + }, + }, + '/etc/sudoers.d/bwusers': { + 'content_type': 'mako', + }, +} + +for filename, content in node.metadata.get('sudo/extra_configs', {}).items(): + files[f'/etc/sudoers.d/{filename}'] = { + 'content': '\n'.join(sorted(content)) + '\n', + 'mode': '0440', + } diff --git a/bundles/sudo/metadata.py b/bundles/sudo/metadata.py new file mode 100644 index 0000000..82b007d --- /dev/null +++ b/bundles/sudo/metadata.py @@ -0,0 +1,12 @@ +defaults = { + 'apt': { + 'packages': { + 'sudo': {}, + }, + }, + 'pacman': { + 'packages': { + 'sudo': {}, + }, + }, +} diff --git a/bundles/sysctl/files/98-sysctl.conf b/bundles/sysctl/files/98-sysctl.conf new file mode 100644 index 0000000..9a29a4b --- /dev/null +++ b/bundles/sysctl/files/98-sysctl.conf @@ -0,0 +1,3 @@ +% for option, value in sorted(node.metadata.get('sysctl/options', {}).items()): +${option}=${value} +% endfor diff --git a/bundles/sysctl/items.py b/bundles/sysctl/items.py new file mode 100644 index 0000000..4b804e8 --- /dev/null +++ b/bundles/sysctl/items.py @@ -0,0 +1,40 @@ +files = { + '/usr/local/sbin/apply-sysctl': { + 'content': + '#!/bin/sh\n' + '\n' + 'cat /etc/sysctl.d/*.conf | sysctl -e -p -', + 'mode': '0700', + }, + '/etc/sysctl.d/98-sysctl.conf': { + 'content_type': 'mako', + 'triggers': { + 'action:apply-sysctl-settings', + }, + }, + '/etc/sysctl.conf': { + 'delete': True, + 'triggers': { + 'action:apply-sysctl-settings', + }, + }, +} + +directories = { + '/etc/sysctl.d': { + 'purge': True, + 'triggers': { + 'action:apply-sysctl-settings', + }, + }, +} + +actions = { + 'apply-sysctl-settings': { + 'command': '/usr/local/sbin/apply-sysctl', + 'triggered': True, + 'needs': { + 'file:/usr/local/sbin/apply-sysctl', + }, + }, +} diff --git a/bundles/systemd-networkd/files/resolv.conf b/bundles/systemd-networkd/files/resolv.conf new file mode 100644 index 0000000..8f9ee33 --- /dev/null +++ b/bundles/systemd-networkd/files/resolv.conf @@ -0,0 +1,3 @@ +% for nameserver in sorted(node.metadata.get('nameservers', {'9.9.9.10', '2620:fe::10'})): +nameserver ${nameserver} +% endfor diff --git a/bundles/systemd-networkd/files/template-bond.netdev b/bundles/systemd-networkd/files/template-bond.netdev new file mode 100644 index 0000000..ec8f29d --- /dev/null +++ b/bundles/systemd-networkd/files/template-bond.netdev @@ -0,0 +1,13 @@ +[NetDev] +Name=${bond} +Kind=bond + +[Bond] +Mode=${mode} +% if mode in {'balance-rr', '802.3ad', 'balance-tlp'}: +TransmitHashPolicy=layer3+4 +% endif +MIIMonitorSec=0.1 +% if mode == '802.3ad': +LACPTransmitRate=fast +% endif diff --git a/bundles/systemd-networkd/files/template-bond.network b/bundles/systemd-networkd/files/template-bond.network new file mode 100644 index 0000000..e7849fa --- /dev/null +++ b/bundles/systemd-networkd/files/template-bond.network @@ -0,0 +1,5 @@ +[Match] +Name=${' '.join(sorted(match))} + +[Network] +Bond=${bond} diff --git a/bundles/systemd-networkd/files/template-bridge-vlan.network b/bundles/systemd-networkd/files/template-bridge-vlan.network new file mode 100644 index 0000000..7a0a3f3 --- /dev/null +++ b/bundles/systemd-networkd/files/template-bridge-vlan.network @@ -0,0 +1,7 @@ +[Match] +Name=${bridge} + +[Network] +% for vlan in sorted(vlans): +VLAN=${bridge}.${vlan} +% endfor diff --git a/bundles/systemd-networkd/files/template-bridge.netdev b/bundles/systemd-networkd/files/template-bridge.netdev new file mode 100644 index 0000000..5cf3bcf --- /dev/null +++ b/bundles/systemd-networkd/files/template-bridge.netdev @@ -0,0 +1,6 @@ +[NetDev] +Name=${bridge} +Kind=bridge + +[Bridge] +STP=off diff --git a/bundles/systemd-networkd/files/template-bridge.network b/bundles/systemd-networkd/files/template-bridge.network new file mode 100644 index 0000000..b008d84 --- /dev/null +++ b/bundles/systemd-networkd/files/template-bridge.network @@ -0,0 +1,6 @@ +[Match] +Name=${' '.join(sorted(match))} + +[Network] +Bridge=${bridge} +BindCarrier=${' '.join(sorted(match))} diff --git a/bundles/systemd-networkd/files/template-dummy.netdev b/bundles/systemd-networkd/files/template-dummy.netdev new file mode 100644 index 0000000..ba6b2d0 --- /dev/null +++ b/bundles/systemd-networkd/files/template-dummy.netdev @@ -0,0 +1,3 @@ +[NetDev] +Name=${name} +Kind=dummy diff --git a/bundles/systemd-networkd/files/template-iface-dhcp.network b/bundles/systemd-networkd/files/template-iface-dhcp.network new file mode 100644 index 0000000..19fd0d8 --- /dev/null +++ b/bundles/systemd-networkd/files/template-iface-dhcp.network @@ -0,0 +1,27 @@ +<% + from ipaddress import ip_network +%>\ +[Match] +Name=${interface} + +[Network] +DHCP=yes +IPv6AcceptRA=yes + +[DHCPv4] +UseDomains=${str(config.get('use_dhcp_domains', False)).lower()} +UseHostname=no +UseMTU=${str(config.get('use_dhcp_mtu', True)).lower()} +UseNTP=${str(config.get('use_dhcp_ntp', False)).lower()} +UseTimezone=no + +% if config.get('send_hostname', True): +SendHostname=yes +Hostname=${node.name.split('.')[-1]} +% else: +SendHostname=no +% endif + +% if config.get('forwarding', False): +IPForward=yes +%endif diff --git a/bundles/systemd-networkd/files/template-iface-nodhcp.network b/bundles/systemd-networkd/files/template-iface-nodhcp.network new file mode 100644 index 0000000..bdbaed0 --- /dev/null +++ b/bundles/systemd-networkd/files/template-iface-nodhcp.network @@ -0,0 +1,50 @@ +<% + from ipaddress import ip_network +%>\ +[Match] +Name=${interface} + +% for addr in sorted(config.get('ips', set())): +[Address] +<% + if '/' in addr: + ip, prefix = addr.split('/') + else: + ip = addr + prefix = '32' +%>\ +Address=${ip}/${prefix} + +% endfor +% for route, rconfig in sorted(config.get('routes', {}).items()): +[Route] +% if 'via' in rconfig: +Gateway=${rconfig['via']} +% endif +Destination=${route} +GatewayOnlink=yes + +% endfor +% if 'gateway4' in config: +[Route] +Gateway=${config['gateway4']} +GatewayOnlink=yes + +% endif +% if 'gateway6' in config: +[Route] +Gateway=${config['gateway6']} +GatewayOnlink=yes + +% endif +[Network] +DHCP=no +IPv6AcceptRA=no + +% if config.get('forwarding', False): +IPForward=yes +%endif + +% for vlan in sorted(config.get('vlans', set())): +VLAN=${interface}.${vlan} +% endfor diff --git a/bundles/systemd-networkd/files/template-iface-vlan.netdev b/bundles/systemd-networkd/files/template-iface-vlan.netdev new file mode 100644 index 0000000..0dfd45b --- /dev/null +++ b/bundles/systemd-networkd/files/template-iface-vlan.netdev @@ -0,0 +1,7 @@ +[NetDev] +Name=${interface} +Kind=vlan +MACAddress=${mac} + +[VLAN] +Id=${vlan} diff --git a/bundles/systemd-networkd/items.py b/bundles/systemd-networkd/items.py new file mode 100644 index 0000000..5f9ab5b --- /dev/null +++ b/bundles/systemd-networkd/items.py @@ -0,0 +1,181 @@ +from bundlewrap.exceptions import BundleError + +repo.libs.tools.require_bundle(node, 'systemd') + +files = { + '/etc/network/interfaces': { + 'delete': True, + }, +} + +if node.metadata.get('systemd-networkd/enable-resolved', False): + symlinks['/etc/resolv.conf'] = { + 'target': '/run/systemd/resolve/stub-resolv.conf', + } + svc_systemd['systemd-resolved'] = {} +else: + files['/etc/resolv.conf'] = { + 'content_type': 'mako', + } + + +directories = { + '/etc/systemd/network': { + 'purge': True, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + }, +} + +mac_host_prefix = '%04x' % (node.magic_number % 65534) +generated_mac = f'52:54:00:{mac_host_prefix[0:2]}:{mac_host_prefix[2:4]}:{{}}' + +# Don't use .get() here. We might end up with a node without a network +# config! +for interface, config in node.metadata['interfaces'].items(): + if config.get('dhcp', False): + if 'vlans' in config: + raise BundleError(f'{node.name} interface {interface} cannot use vlans and dhcp!') + template = 'template-iface-dhcp.network' + else: + template = 'template-iface-nodhcp.network' + + if '.' in interface: + vlan_id = int(interface.split('.')[1]) + vlan_hex = '%02x' % (vlan_id % 255) + files[f'/etc/systemd/network/{interface}.netdev'] = { + 'source': 'template-iface-vlan.netdev', + 'content_type': 'mako', + 'context': { + 'interface': interface, + 'vlan': vlan_id, + 'mac': generated_mac.format(vlan_hex) + }, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + } + elif interface.startswith('dummy'): + files[f'/etc/systemd/network/{interface}.netdev'] = { + 'source': 'template-dummy.netdev', + 'content_type': 'mako', + 'context': { + 'name': interface, + }, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + } + + if not config.get('ignore', False): + files[f'/etc/systemd/network/{interface}.network'] = { + 'source': template, + 'content_type': 'mako', + 'context': { + 'interface': interface, + 'config': config, + }, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + } + +for bond, config in node.metadata.get('systemd-networkd/bonds', {}).items(): + files[f'/etc/systemd/network/{bond}.netdev'] = { + 'source': 'template-bond.netdev', + 'content_type': 'mako', + 'context': { + 'bond': bond, + 'mode': config.get('mode', '802.3ad'), + 'prio': config.get('priority', '32768'), + }, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + } + + files[f'/etc/systemd/network/{bond}.network'] = { + 'source': 'template-bond.network', + 'content_type': 'mako', + 'context': { + 'bond': bond, + 'match': config['match'], + }, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + } + +for brname, config in node.metadata.get('systemd-networkd/bridges', {}).items(): + filename = '{}-match-{}'.format( + brname, + '-'.join(sorted(config['match'])), + ) + + files[f'/etc/systemd/network/{brname}.netdev'] = { + 'source': 'template-bridge.netdev', + 'content_type': 'mako', + 'context': { + 'bridge': brname, + }, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + } + + files[f'/etc/systemd/network/{filename}.network'] = { + 'source': 'template-bridge.network', + 'content_type': 'mako', + 'context': { + 'bridge': brname, + 'match': config['match'], + }, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + } + + if config.get('vlans', set()): + files[f'/etc/systemd/network/{brname}.network'] = { + 'source': 'template-bridge-vlan.network', + 'content_type': 'mako', + 'context': { + 'bridge': brname, + 'vlans': config.get('vlans', set()), + }, + 'needed_by': { + 'svc_systemd:systemd-networkd', + }, + 'triggers': { + 'svc_systemd:systemd-networkd:restart', + }, + } + +svc_systemd = { + 'systemd-networkd': {}, +} diff --git a/bundles/systemd-networkd/metadata.py b/bundles/systemd-networkd/metadata.py new file mode 100644 index 0000000..303e0f3 --- /dev/null +++ b/bundles/systemd-networkd/metadata.py @@ -0,0 +1,46 @@ +defaults = { + 'apt': { + 'packages': { + 'resolvconf': { + 'installed': False, + }, + }, + }, +} + + +@metadata_reactor.provides( + 'interfaces', + 'systemd-networkd/bridges', +) +def add_vlan_infos_to_interface(metadata): + interfaces = {} + bridges = {} + + for iface in metadata.get('interfaces', {}): + if '.' not in iface: + continue + + interface,vlan = iface.split('.') + + interfaces.setdefault(interface, {}).setdefault('vlans', set()) + interfaces[interface]['vlans'].add(vlan) + + for bridge, config in metadata.get('systemd-networkd/bridges', {}).items(): + for iface in config.get('match', {}): + if '.' not in iface: + continue + + interface,vlan = iface.split('.') + + bridges.setdefault(interface, {}).setdefault('vlans', set()) + bridges[interface]['vlans'].add(vlan) + + interfaces.setdefault(iface, {'ignore': True}) + + return { + 'interfaces': interfaces, + 'systemd-networkd': { + 'bridges': bridges, + }, + } diff --git a/bundles/systemd/files/journald.conf b/bundles/systemd/files/journald.conf new file mode 100644 index 0000000..a062649 --- /dev/null +++ b/bundles/systemd/files/journald.conf @@ -0,0 +1,19 @@ +[Journal] +Storage=${journal.get('storage', 'persistent')} +Compress=yes +SplitMode=uid + +# Disable rate limiting. +RateLimitIntervalSec=0 +RateLimitBurst=0 + +SystemMaxUse=${journal.get('maxuse', '500M')} +SystemKeepFree=${journal.get('keepfree', '2G')} +SystemMaxFileSize=100M +RuntimeMaxUse=${journal.get('maxuse', '500M')} +RuntimeKeepFree=${journal.get('keepfree', '2G')} +RuntimeMaxFileSize=100M +MaxFileSec=1d + +# Disable auditing +Audit=no diff --git a/bundles/systemd/items.py b/bundles/systemd/items.py new file mode 100644 index 0000000..6c8f671 --- /dev/null +++ b/bundles/systemd/items.py @@ -0,0 +1,48 @@ +timezone = node.metadata.get('timezone', 'UTC') + +actions = { + 'systemd-reload': { + 'command': 'systemctl daemon-reload', + 'cascade_skip': False, + 'triggered': True, + 'needed_by': { + 'svc_systemd:', + }, + }, + 'systemd-hostname': { + 'command': 'hostnamectl set-hostname {}'.format(node.metadata['hostname']), + 'unless': '[ "$(hostnamectl --static)" = "{}" ]'.format(node.metadata['hostname']), + # Provided by bundle:basic + 'needs': { + 'file:/etc/hosts', + }, + }, + 'systemd-timezone': { + 'command': 'timedatectl set-timezone {}'.format(timezone), + 'unless': 'timedatectl status | grep -Fi \'time zone\' | grep -i \'{}\''.format(timezone.lower()), + }, + 'systemd-enable-ntp': { + 'command': 'timedatectl set-ntp true', + 'unless': 'timedatectl status | grep -Fi \'ntp service\' | grep -i \'active\'', + }, +} + +files = { + '/etc/systemd/journald.conf': { + 'content_type': 'mako', + 'context': { + 'journal': node.metadata.get('systemd/journal', {}), + }, + 'triggers': { + 'svc_systemd:systemd-journald:restart', + }, + }, +} + +svc_systemd = { + 'systemd-journald': { + 'needs': { + 'file:/etc/systemd/journald.conf', + }, + }, +} diff --git a/bundles/users/README.md b/bundles/users/README.md new file mode 100644 index 0000000..e318177 --- /dev/null +++ b/bundles/users/README.md @@ -0,0 +1,27 @@ +# bundles/users + +This bundle evaluates node metadata to determine which users should +exist on the system. +It will also create a home directory, add ssh keys and deploy shell +configs, if user-specific configuration exists. + +## metadata + 'users': { + 'username': { + 'home': '/home/username', # this is the default + 'shell': '/bin/bash', # this is the default + 'groups': { + # list of groups the user should be in + }, + 'ssh_pubkey': [ + # list of ssh pubkeys that are allowed to log in + ], + }, + } + +## custom shell config +Deploy your custom config to these paths: + +* data/users/files/tmux/username.conf +* data/users/files/fish/username.conf +* data/users/files/bash/username.bashrc diff --git a/bundles/users/files/bashrc b/bundles/users/files/bashrc new file mode 100644 index 0000000..c04fed1 --- /dev/null +++ b/bundles/users/files/bashrc @@ -0,0 +1,72 @@ +[ -z "$PS1" ] && return + +__node_name="${node.name}" + +<%text> +if [[ "$(id -u)" -eq 0 ]] +then + export PS1='\[\e[1;34m\][\[\e[1;91m\]'"$__node_name"'\[\e[1;34m\]][\[\e[1;91m\]\u\[\e[1;34m\]@\[\e[1;91m\]$PWD\[\e[1;34m\]] > \[\e[0m\]' +else + export PS1='\[\e[1;34m\][\[\e[1;32m\]'"$__node_name"'\[\e[1;34m\]][\[\e[1;32m\]\u\[\e[1;34m\]@\[\e[1;32m\]\w\[\e[1;34m\]] > \[\e[0m\]' +fi +case $TERM in + xterm*|rxvt*) + export PROMPT_COMMAND='echo -ne "\a\e]0;'"$__node_name"':${PWD}\a"' + ;; + screen*) + export PROMPT_COMMAND='echo -ne "\a\ek'"$__node_name"':${PWD}\e\\"' + ;; + *) + unset PROMPT_COMMAND + ;; +esac + +if [[ -f "/etc/node.description" ]] +then + echo + cat "/etc/node.description" + echo +fi + +uptime +last | grep 'still logged in' + +export HISTCONTROL=ignoredups +export HISTSIZE=50000 +export HISTTIMEFORMAT="%d/%m/%y %T " +export SAVEHIST=50000 +shopt -s checkjobs +shopt -s checkwinsize +shopt -s globstar +shopt -s histreedit + +export LESS="-iRS -# 2" + +export EDITOR=vim +export VISUAL=vim + +alias ipb='ip -brief' +alias l='ls -lAh' +alias s='sudo -i' +alias v='vim -p' + +% for k, v in sorted(node.metadata.get('bash_aliases', {}).items()): +alias ${k}='${v}' +% endfor + +rsback() +{ + for i + do + [ -e "$i" ] || { echo "ERROR: $i does not exist" >&2; continue; } + printf 'rsync -zaP -e ssh %q ' '--rsync-path=sudo rsync' + printf '%q:%q .' "${node.hostname}" "$(printf '%q' "$(readlink -e -- "$i")")" + printf '\n' + done +} +% for k, v in sorted(node.metadata.get('bash_functions', {}).items()): + +${k}() { + ${v} +} +% endfor diff --git a/bundles/users/files/fish.conf b/bundles/users/files/fish.conf new file mode 100644 index 0000000..ddf576b --- /dev/null +++ b/bundles/users/files/fish.conf @@ -0,0 +1,23 @@ +function fish_greeting + uptime + last | grep 'still logged in' +end + +function fish_prompt + set -l last_status $status + + echo -n "$USER@${node.name}:" + + set_color $fish_color_cwd + echo -n (pwd) + set_color normal + + if not test $last_status -eq 0 + set_color $fish_color_error + echo -n " [$status]" + set_color normal + end + + echo -n '➤ ' + set_color normal +end diff --git a/bundles/users/files/fish_variables b/bundles/users/files/fish_variables new file mode 100644 index 0000000..573512c --- /dev/null +++ b/bundles/users/files/fish_variables @@ -0,0 +1,32 @@ +# This file contains fish universal variable definitions. +# VERSION: 3.0 +SETUVAR fish_color_autosuggestion:555\x1ebrblack +SETUVAR fish_color_cancel:\x2dr +SETUVAR fish_color_command:005fd7 +SETUVAR fish_color_comment:990000 +SETUVAR fish_color_cwd:green +SETUVAR fish_color_cwd_root:red +SETUVAR fish_color_end:009900 +SETUVAR fish_color_error:ff0000 +SETUVAR fish_color_escape:00a6b2 +SETUVAR fish_color_history_current:\x2d\x2dbold +SETUVAR fish_color_host:normal +SETUVAR fish_color_host_remote:yellow +SETUVAR fish_color_match:\x2d\x2dbackground\x3dbrblue +SETUVAR fish_color_normal:normal +SETUVAR fish_color_operator:00a6b2 +SETUVAR fish_color_param:00afff +SETUVAR fish_color_quote:999900 +SETUVAR fish_color_redirection:00afff +SETUVAR fish_color_search_match:bryellow\x1e\x2d\x2dbackground\x3dbrblack +SETUVAR fish_color_selection:white\x1e\x2d\x2dbold\x1e\x2d\x2dbackground\x3dbrblack +SETUVAR fish_color_status:red +SETUVAR fish_color_user:brgreen +SETUVAR fish_color_valid_path:\x2d\x2dunderline +SETUVAR fish_features:stderr\x2dnocaret\x1eqmark\x2dnoglob +SETUVAR fish_greeting:Welcome\x20to\x20fish\x2c\x20the\x20friendly\x20interactive\x20shell +SETUVAR fish_key_bindings:fish_default_key_bindings +SETUVAR fish_pager_color_completion:\x1d +SETUVAR fish_pager_color_description:B3A06D\x1eyellow +SETUVAR fish_pager_color_prefix:white\x1e\x2d\x2dbold\x1e\x2d\x2dunderline +SETUVAR fish_pager_color_progress:brwhite\x1e\x2d\x2dbackground\x3dcyan diff --git a/bundles/users/files/tmux.conf b/bundles/users/files/tmux.conf new file mode 100644 index 0000000..393e99c --- /dev/null +++ b/bundles/users/files/tmux.conf @@ -0,0 +1,51 @@ +bind C-a send-prefix # Pass on ctrl-a for other apps + +set-option -g set-titles on +set-option -g set-titles-string '#W [#(logname)@${node.name}]' + +# Status bar +set -g status on +set -g status-interval 1 +set -g status-left '[#(logname)@${node.name}] ' +set -g status-left-length 100 +set -g status-right ' [#(lsb_release -ds)] [#(date --rfc-3339=seconds)]' +set -g status-right-length 200 + +# Numbering starts at 1 +set -g base-index 1 +setw -g pane-base-index 1 +set-option -g renumber-windows on + +# Activity monitoring +setw -g monitor-activity on +set -g visual-activity on + +# Terminal +set-option -g default-shell $SHELL +set -g default-terminal "tmux-256color" + +# Keybindings +bind-key -n M-Left select-pane -L +bind-key -n M-Right select-pane -R +bind-key -n M-Up select-pane -U +bind-key -n M-Down select-pane -D + +# Vim-Keybindings, adapted for neo layout, shifted one key to the right +bind-key -n M-n select-pane -L +bind-key -n M-d select-pane -R +bind-key -n M-t select-pane -U +bind-key -n M-r select-pane -D + +#### COLOUR (Solarized dark) +set-option -g status-bg black #base02 +set-option -g status-fg yellow #yellow +set-option -g status-style default +set-window-option -g window-status-style fg=brightblue,bg=default +set-window-option -g window-status-activity-style fg=green,bg=default,none +set-window-option -g window-status-current-style fg=brightred,bg=default,none +set-option -g pane-border-style fg=black +set-option -g pane-active-border-style fg=brightgreen +set-option -g message-style bg=black,fg=brightred +set-option -g display-panes-active-colour blue #blue +set-option -g display-panes-colour brightred #orange +set-window-option -g clock-mode-colour green #green diff --git a/bundles/users/files/vimrc b/bundles/users/files/vimrc new file mode 100644 index 0000000..a87fc1d --- /dev/null +++ b/bundles/users/files/vimrc @@ -0,0 +1,28 @@ +set number +set lbr +set fdc=0 +set vb +set expandtab +set shiftwidth=4 +set tabstop=4 +set softtabstop=4 +set linespace=0 +set autoindent +set smartindent +set mouse=a +set cursorline +syntax on +set showcmd +set encoding=utf-8 +set autowrite +set noautochdir +set list +set listchars=trail:␣,tab:→\ ,extends:>,precedes:< +set hlsearch +map :nohlsearch + +set colorcolumn=72,120 +hi colorcolumn ctermbg=NONE ctermfg=red cterm=bold guibg=NONE guifg=red gui=bold + +au BufRead /tmp/neomutt-* set tw=72 +au BufRead /tmp/mutt-* set tw=72 diff --git a/bundles/users/items.py b/bundles/users/items.py new file mode 100644 index 0000000..457c46a --- /dev/null +++ b/bundles/users/items.py @@ -0,0 +1,89 @@ +from os.path import join, exists + +files = { + '/etc/bash.bashrc': { + 'source': 'bashrc', + 'content_type': 'mako', + }, + '/etc/tmux.conf': { + 'source': 'tmux.conf', + 'content_type': 'mako', + }, + '/etc/vim/vimrc.local': { + 'source': 'vimrc', + }, +} + +for group, attrs in node.metadata.get('groups', {}).items(): + groups[group] = attrs + +for username, attrs in node.metadata['users'].items(): + home = attrs.get('home', '/home/{}'.format(username)) + + if attrs.get('delete', False): + users[username] = {'delete': True} + files[home] = {'delete': True} + + else: + user = users.setdefault(username, {}) + + user['home'] = home + user['shell'] = attrs.get('shell', '/bin/bash') + + if 'password' in attrs: + user['password'] = attrs['password'] + else: + user['password_hash'] = 'x' if node.use_shadow_passwords else '*' + + if 'groups' in attrs: + user['groups'] = attrs['groups'] + + directories[home] = { + 'owner': username, + 'mode': attrs.get('home-mode', '0700'), + } + + if 'ssh_pubkey' in attrs: + files[home + '/.ssh/authorized_keys'] = { + 'content': '\n'.join(sorted(set(attrs['ssh_pubkey']))) + '\n', + 'owner': username, + 'mode': '0600', + } + + elif not attrs.get('do_not_remove_authorized_keys_from_home', False): + files[home + '/.ssh/authorized_keys'] = {'delete': True} + + if exists(join(repo.path, 'data', 'users', 'files', 'tmux', '{}.conf'.format(username))): + files[home + '/.tmux.conf'] = { + 'content_type': 'mako', + 'source': 'tmux/{}.conf'.format(username), + } + else: + files[home + '/.tmux.conf'] = { + 'delete': True, + } + + if exists(join(repo.path, 'data', 'users', 'files', 'bash', '{}.bashrc'.format(username))): + files[home + '/.bashrc'] = { + 'content_type': 'mako', + 'source': 'bash/{}.bashrc'.format(username), + } + else: + files[home + '/.bashrc'] = { + 'delete': True, + } + + if attrs.get('enable_linger', False): + linger_test = '' + linger_command = 'enable' + else: + linger_test = '!' + linger_command = 'disable' + + actions[f'ensure_linger_state_for_user_{username}'] = { + 'command': f'loginctl {linger_command}-linger {username}', + 'unless': f'{linger_test} test -f /var/lib/systemd/linger/{username}', + 'needs': { + f'user:{username}', + }, + } diff --git a/bundles/users/metadata.py b/bundles/users/metadata.py new file mode 100644 index 0000000..29006ff --- /dev/null +++ b/bundles/users/metadata.py @@ -0,0 +1,41 @@ +from json import loads +from os.path import join + +defaults = { + 'users': { + 'root': { + 'home': '/root', + 'shell': '/bin/bash', + 'password': repo.vault.human_password_for('root on {}'.format(node.name)), + }, + }, +} + + +@metadata_reactor.provides( + 'users', +) +def add_users_from_json(metadata): + with open(join(repo.path, 'users.json'), 'r') as f: + json = loads(f.read()) + + users = {} + metadata_users = metadata.get('users', {}) + # First, add all admin users + for uname, config in json.items(): + if config.get('is_admin', False) or uname in metadata_users: + users[uname] = { + 'ssh_pubkey': set(config['ssh_pubkey']), + 'is_admin': config.get('is_admin', False), + } + + # Then, run again to get all 'to be deleted' users + for uname, config in json.items(): + if uname not in metadata_users: + users.setdefault(uname, { + 'delete': True, + }) + + return { + 'users': users, + }