Compare commits
	
		
			1 commit
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 2518cb6864 | 
					 358 changed files with 1945 additions and 10118 deletions
				
			
		
							
								
								
									
										12
									
								
								.envrc
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								.envrc
									
									
									
									
									
								
							|  | @ -1,7 +1,13 @@ | |||
| #!/usr/bin/env bash | ||||
| 
 | ||||
| python3 -m venv .venv | ||||
| source ./.venv/bin/activate | ||||
| PATH_add .venv/bin | ||||
| PATH_add bin | ||||
| python3 -m pip install --upgrade pip | ||||
| 
 | ||||
| source_env ~/.local/share/direnv/pyenv | ||||
| source_env ~/.local/share/direnv/venv | ||||
| source_env ~/.local/share/direnv/bundlewrap | ||||
| rm -rf .cache/bw/git_deploy | ||||
| export BW_GIT_DEPLOY_CACHE=.cache/bw/git_deploy | ||||
| export EXPERIMENTAL_UPLOAD_VIA_CAT=1 | ||||
| mkdir -p "$BW_GIT_DEPLOY_CACHE" | ||||
| unset PS1 | ||||
|  |  | |||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							|  | @ -2,4 +2,3 @@ | |||
| .venv | ||||
| .cache | ||||
| *.pyc | ||||
| .bw_debug_history | ||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							|  | @ -15,7 +15,7 @@ Raspberry pi as soundcard | |||
| 
 | ||||
| # install bw fork | ||||
| 
 | ||||
| pip3 install --editable git+file:///Users/mwiegand/Projekte/bundlewrap-fork@main#egg=bundlewrap | ||||
| pip3 install --editable git+file:///Users/mwiegand/Projekte/bundlewrap-fork#egg=bundlewrap | ||||
| 
 | ||||
| # monitor timers | ||||
| 
 | ||||
|  | @ -35,14 +35,3 @@ fi | |||
| ``` | ||||
| 
 | ||||
| telegraf: execd for daemons | ||||
| 
 | ||||
| TEST | ||||
| 
 | ||||
| # git signing | ||||
| 
 | ||||
| git config --global gpg.format ssh | ||||
| git config --global commit.gpgsign true | ||||
| 
 | ||||
| git config user.name CroneKorkN | ||||
| git config user.email i@ckn.li | ||||
| git config user.signingkey "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILMVroYmswD4tLk6iH+2tvQiyaMe42yfONDsPDIdFv6I" | ||||
|  |  | |||
							
								
								
									
										32
									
								
								bin/rcon
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								bin/rcon
									
									
									
									
									
								
							|  | @ -1,32 +0,0 @@ | |||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| from sys import argv | ||||
| from os.path import realpath, dirname | ||||
| from shlex import quote | ||||
| from bundlewrap.repo import Repository | ||||
| 
 | ||||
| repo = Repository(dirname(dirname(realpath(__file__)))) | ||||
| 
 | ||||
| if len(argv) == 1: | ||||
|     for node in repo.nodes: | ||||
|         for name in node.metadata.get('left4dead2/servers', {}): | ||||
|             print(name) | ||||
|     exit(0) | ||||
| 
 | ||||
| server = argv[1] | ||||
| command = argv[2] | ||||
| 
 | ||||
| remote_code = """ | ||||
| from rcon.source import Client | ||||
| 
 | ||||
| with Client('127.0.0.1', {port}, passwd='''{password}''') as client: | ||||
|     response = client.run('''{command}''') | ||||
| 
 | ||||
| print(response) | ||||
| """ | ||||
| 
 | ||||
| for node in repo.nodes: | ||||
|     for name, conf in node.metadata.get('left4dead2/servers', {}).items(): | ||||
|         if name == server: | ||||
|             response = node.run('python3 -c ' + quote(remote_code.format(port=conf['port'], password=conf['rcon_password'], command=command))) | ||||
|             print(response.stdout.decode()) | ||||
|  | @ -10,6 +10,7 @@ nodes = [ | |||
|         for node in sorted(repo.nodes_in_group('debian')) | ||||
|         if not node.dummy | ||||
| ] | ||||
| reboot_nodes = [] | ||||
| 
 | ||||
| print('updating nodes:', sorted(node.name for node in nodes)) | ||||
| 
 | ||||
|  | @ -23,13 +24,14 @@ for node in nodes: | |||
|     print(node.run('DEBIAN_FRONTEND=noninteractive apt update').stdout.decode()) | ||||
|     print(node.run('DEBIAN_FRONTEND=noninteractive apt list --upgradable').stdout.decode()) | ||||
|     if int(node.run('DEBIAN_FRONTEND=noninteractive apt list --upgradable 2> /dev/null | grep upgradable | wc -l').stdout.decode()): | ||||
|         print(node.run('DEBIAN_FRONTEND=noninteractive apt -qy full-upgrade').stdout.decode()) | ||||
|         print(node.run('DEBIAN_FRONTEND=noninteractive apt -y dist-upgrade').stdout.decode()) | ||||
|         reboot_nodes.append(node) | ||||
| 
 | ||||
| # REBOOT IN ORDER | ||||
| 
 | ||||
| wireguard_servers = [ | ||||
|     node | ||||
|         for node in nodes | ||||
|         for node in reboot_nodes | ||||
|         if node.has_bundle('wireguard') | ||||
|         and ( | ||||
|             ip_interface(node.metadata.get('wireguard/my_ip')).network.prefixlen < | ||||
|  | @ -39,7 +41,7 @@ wireguard_servers = [ | |||
| 
 | ||||
| wireguard_s2s = [ | ||||
|     node | ||||
|         for node in nodes | ||||
|         for node in reboot_nodes | ||||
|         if node.has_bundle('wireguard') | ||||
|         and ( | ||||
|             ip_interface(node.metadata.get('wireguard/my_ip')).network.prefixlen == | ||||
|  | @ -49,7 +51,7 @@ wireguard_s2s = [ | |||
| 
 | ||||
| everything_else = [ | ||||
|     node | ||||
|         for node in nodes | ||||
|         for node in reboot_nodes | ||||
|         if not node.has_bundle('wireguard') | ||||
| ] | ||||
| 
 | ||||
|  | @ -60,11 +62,8 @@ for node in [ | |||
|     *wireguard_s2s, | ||||
|     *wireguard_servers, | ||||
| ]: | ||||
|     print('rebooting', node.name) | ||||
|     try: | ||||
|         if node.run('test -e /var/run/reboot-required', may_fail=True).return_code == 0: | ||||
|             print('rebooting', node.name) | ||||
|             print(node.run('systemctl reboot').stdout.decode()) | ||||
|         else: | ||||
|             print('not rebooting', node.name) | ||||
|         print(node.run('systemctl reboot').stdout.decode()) | ||||
|     except Exception as e: | ||||
|         print(e) | ||||
|  |  | |||
|  | @ -5,17 +5,9 @@ from os.path import realpath, dirname | |||
| from sys import argv | ||||
| from ipaddress import ip_network, ip_interface | ||||
| 
 | ||||
| if len(argv) != 3: | ||||
|     print(f'usage: {argv[0]} <node> <client>') | ||||
|     exit(1) | ||||
| 
 | ||||
| repo = Repository(dirname(dirname(realpath(__file__)))) | ||||
| 
 | ||||
| server_node = repo.get_node(argv[1]) | ||||
| 
 | ||||
| if argv[2] not in server_node.metadata.get('wireguard/clients'): | ||||
|     print(f'client {argv[2]} not found in: {server_node.metadata.get("wireguard/clients").keys()}') | ||||
|     exit(1) | ||||
| 
 | ||||
| data = server_node.metadata.get(f'wireguard/clients/{argv[2]}') | ||||
| 
 | ||||
| vpn_network = ip_interface(server_node.metadata.get('wireguard/my_ip')).network | ||||
|  | @ -28,7 +20,9 @@ for peer in server_node.metadata.get('wireguard/s2s').values(): | |||
|         if not ip_network(network).subnet_of(vpn_network): | ||||
|             allowed_ips.append(ip_network(network)) | ||||
| 
 | ||||
| conf = f''' | ||||
| conf = \ | ||||
| f'''>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> | ||||
| 
 | ||||
| [Interface] | ||||
| PrivateKey = {repo.libs.wireguard.privkey(data['peer_id'])} | ||||
| ListenPort = 51820 | ||||
|  | @ -41,12 +35,11 @@ PresharedKey = {repo.libs.wireguard.psk(data['peer_id'], server_node.metadata.ge | |||
| AllowedIPs = {', '.join(str(client_route) for client_route in sorted(allowed_ips))} | ||||
| Endpoint = {ip_interface(server_node.metadata.get('network/external/ipv4')).ip}:51820 | ||||
| PersistentKeepalive = 10 | ||||
| ''' | ||||
| 
 | ||||
| print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') | ||||
| <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<''' | ||||
| 
 | ||||
| print(conf) | ||||
| print('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<') | ||||
| 
 | ||||
| if input("print qrcode? [Yn]: ").upper() in ['', 'Y']: | ||||
| if input("print qrcode? [yN]: ").upper() == 'Y': | ||||
|     import pyqrcode | ||||
|     print(pyqrcode.create(conf).terminal(quiet_zone=1)) | ||||
|  |  | |||
|  | @ -1,6 +1,3 @@ | |||
| # https://manpages.debian.org/latest/apt/sources.list.5.de.html | ||||
| # https://repolib.readthedocs.io/en/latest/deb822-format.html | ||||
| 
 | ||||
| ```python | ||||
| { | ||||
|     'apt': { | ||||
|  | @ -8,32 +5,8 @@ | |||
|             'apt-transport-https': {}, | ||||
|         }, | ||||
|         'sources': { | ||||
|             'debian': { | ||||
|                 'types': { # optional, defaults to `{'deb'}`` | ||||
|                     'deb', | ||||
|                     'deb-src', | ||||
|                 }, | ||||
|                 'options': { # optional | ||||
|                     'aarch': 'amd64', | ||||
|                 }, | ||||
|                 'urls': { | ||||
|                     'https://deb.debian.org/debian', | ||||
|                 }, | ||||
|                 'suites': { # at least one | ||||
|                     '{codename}', | ||||
|                     '{codename}-updates', | ||||
|                     '{codename}-backports', | ||||
|                 }, | ||||
|                 'components': { # optional | ||||
|                     'main', | ||||
|                     'contrib', | ||||
|                     'non-frese', | ||||
|                 }, | ||||
|                 # key: | ||||
|                 # - optional, defaults to source name (`debian` in this example) | ||||
|                 # - place key under data/apt/keys/debian-12.{asc|gpg} | ||||
|                 'key': 'debian-{version}', | ||||
|             }, | ||||
|             # place key under data/apt/keys/packages.cloud.google.com.{asc|gpg} | ||||
|             'deb https://packages.cloud.google.com/apt cloud-sdk main', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  |  | |||
|  | @ -1,15 +0,0 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| apt update -qq --silent 2> /dev/null | ||||
| 
 | ||||
| UPGRADABLE=$(apt list --upgradable -qq 2> /dev/null | cut -d '/' -f 1) | ||||
| 
 | ||||
| if test "$UPGRADABLE" != "" | ||||
| then | ||||
|   echo "$(wc -l <<< $UPGRADABLE) package(s) upgradable:" | ||||
|   echo | ||||
|   echo "$UPGRADABLE" | ||||
|   exit 1 | ||||
| else | ||||
|   exit 0 | ||||
| fi | ||||
|  | @ -1,68 +1,33 @@ | |||
| # TODO pin repo: https://superuser.com/a/1595920 | ||||
| 
 | ||||
| from os.path import join | ||||
| from urllib.parse import urlparse | ||||
| from glob import glob | ||||
| from os.path import join, basename | ||||
| 
 | ||||
| directories = { | ||||
|     '/etc/apt': { | ||||
|         'purge': True, | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     }, | ||||
|     '/etc/apt/apt.conf.d': { | ||||
|         # existance is expected | ||||
|         'purge': True, | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     }, | ||||
|     '/etc/apt/keyrings': { | ||||
|         # https://askubuntu.com/a/1307181 | ||||
|         'purge': True, | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     }, | ||||
|     # '/etc/apt/listchanges.conf.d': { | ||||
|     #     'purge': True, | ||||
|     #     'triggers': { | ||||
|     #         'action:apt_update', | ||||
|     #     }, | ||||
|     # }, | ||||
|     '/etc/apt/preferences.d': { | ||||
|         'purge': True, | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     }, | ||||
|     '/etc/apt/sources.list.d': { | ||||
|         'purge': True, | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     }, | ||||
|     '/etc/apt/trusted.gpg.d': { | ||||
|         'purge': True, | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     }, | ||||
|     '/etc/apt/preferences.d': { | ||||
|         'purge': True, | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| files = { | ||||
|     '/etc/apt/apt.conf': { | ||||
|         'content': repo.libs.apt.render_apt_conf(node.metadata.get('apt/config')), | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     }, | ||||
|     '/etc/apt/sources.list': { | ||||
|         'content': '# managed by bundlewrap\n', | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|         'content': '# managed' | ||||
|     }, | ||||
|     # '/etc/apt/listchanges.conf': { | ||||
|     #     'content': repo.libs.ini.dumps(node.metadata.get('apt/list_changes')), | ||||
|     # }, | ||||
|     '/usr/lib/nagios/plugins/check_apt_upgradable': { | ||||
|         'mode': '0755', | ||||
|     }, | ||||
|     # /etc/kernel/postinst.d/apt-auto-removal | ||||
| } | ||||
| 
 | ||||
| actions = { | ||||
|  | @ -76,22 +41,41 @@ actions = { | |||
|     }, | ||||
| } | ||||
| 
 | ||||
| # create sources.lists and respective keyfiles | ||||
| # group sources by apt server hostname | ||||
| 
 | ||||
| for name, config in node.metadata.get('apt/sources').items(): | ||||
|     # place keyfile | ||||
|     keyfile_destination_path = repo.libs.apt.format_variables(node, config['options']['Signed-By']) | ||||
|     files[keyfile_destination_path] = { | ||||
|         'source': join(repo.path, 'data', 'apt', 'keys', basename(keyfile_destination_path)), | ||||
|         'content_type': 'binary', | ||||
| hosts = {} | ||||
| 
 | ||||
| for source_string in node.metadata.get('apt/sources'): | ||||
|     source = repo.libs.apt.AptSource(source_string) | ||||
|     hosts\ | ||||
|         .setdefault(source.url.hostname, list())\ | ||||
|         .append(source) | ||||
| 
 | ||||
| # create sources lists and keyfiles | ||||
| 
 | ||||
| for host, sources in hosts.items(): | ||||
|     keyfile = basename(glob(join(repo.path, 'data', 'apt', 'keys', f'{host}.*'))[0]) | ||||
|     destination_path = f'/etc/apt/trusted.gpg.d/{keyfile}' | ||||
| 
 | ||||
|     for source in sources: | ||||
|         source.options['signed-by'] = [destination_path] | ||||
| 
 | ||||
|     files[f'/etc/apt/sources.list.d/{host}.list'] = { | ||||
|         'content': '\n'.join(sorted(set( | ||||
|             str(source).format( | ||||
|                 release=node.metadata.get('os_release'), | ||||
|                 version=node.os_version[0], # WIP crystal | ||||
|             ) | ||||
|                 for source in sources | ||||
|         ))), | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
|     # place sources.list | ||||
|     files[f'/etc/apt/sources.list.d/{name}.sources'] = { | ||||
|         'content': repo.libs.apt.render_source(node, name), | ||||
|      | ||||
|     files[destination_path] = { | ||||
|         'source': join(repo.path, 'data', 'apt', 'keys', keyfile), | ||||
|         'content_type': 'binary', | ||||
|         'triggers': { | ||||
|             'action:apt_update', | ||||
|         }, | ||||
|  | @ -99,14 +83,14 @@ for name, config in node.metadata.get('apt/sources').items(): | |||
| 
 | ||||
| # create backport pinnings | ||||
| 
 | ||||
| for package, options in node.metadata.get('apt/packages', {}).items(): | ||||
| for package, options in node.metadata.get('apt/packages', {}).items():     | ||||
|     pkg_apt[package] = options | ||||
| 
 | ||||
|     if pkg_apt[package].pop('backports', False): | ||||
|         files[f'/etc/apt/preferences.d/{package}'] = { | ||||
|             'content': '\n'.join([ | ||||
|                 f"Package: {package}", | ||||
|                 f"Pin: release a={node.metadata.get('os_codename')}-backports", | ||||
|                 f"Pin: release a={node.metadata.get('os_release')}-backports", | ||||
|                 f"Pin-Priority: 900", | ||||
|             ]), | ||||
|             'needed_by': [ | ||||
|  | @ -116,25 +100,3 @@ for package, options in node.metadata.get('apt/packages', {}).items(): | |||
|                 'action:apt_update', | ||||
|             }, | ||||
|         } | ||||
| 
 | ||||
| # unattended upgrades | ||||
| # | ||||
| # unattended-upgrades.service: delays shutdown if necessary | ||||
| # apt-daily.timer: performs apt update | ||||
| # apt-daily-upgrade.timer: performs apt upgrade | ||||
| 
 | ||||
| svc_systemd['unattended-upgrades.service'] = { | ||||
|     'needs': [ | ||||
|         'pkg_apt:unattended-upgrades', | ||||
|     ], | ||||
| } | ||||
| svc_systemd['apt-daily.timer'] = { | ||||
|     'needs': [ | ||||
|         'pkg_apt:unattended-upgrades', | ||||
|     ], | ||||
| } | ||||
| svc_systemd['apt-daily-upgrade.timer'] = { | ||||
|     'needs': [ | ||||
|         'pkg_apt:unattended-upgrades', | ||||
|     ], | ||||
| } | ||||
|  |  | |||
|  | @ -1,177 +1,6 @@ | |||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'apt-listchanges': { | ||||
|                 'installed': False, | ||||
|             }, | ||||
|         }, | ||||
|         'config': { | ||||
|             'DPkg': { | ||||
|                 'Pre-Install-Pkgs': { | ||||
|                     '/usr/sbin/dpkg-preconfigure --apt || true', | ||||
|                 }, | ||||
|                 'Post-Invoke': { | ||||
|                     # keep package cache empty | ||||
|                     '/bin/rm -f /var/cache/apt/archives/*.deb || true', | ||||
|                 }, | ||||
|                 'Options': { | ||||
|                     # https://unix.stackexchange.com/a/642541/357916 | ||||
|                     '--force-confold', | ||||
|                     '--force-confdef', | ||||
|                 }, | ||||
|             }, | ||||
|             'APT': { | ||||
|                 'NeverAutoRemove': { | ||||
|                     '^firmware-linux.*', | ||||
|                     '^linux-firmware$', | ||||
|                     '^linux-image-[a-z0-9]*$', | ||||
|                     '^linux-image-[a-z0-9]*-[a-z0-9]*$', | ||||
|                 }, | ||||
|                 'VersionedKernelPackages': { | ||||
|                     # kernels | ||||
|                     'linux-.*', | ||||
|                     'kfreebsd-.*', | ||||
|                     'gnumach-.*', | ||||
|                     # (out-of-tree) modules | ||||
|                     '.*-modules', | ||||
|                     '.*-kernel', | ||||
|                 }, | ||||
|                 'Never-MarkAuto-Sections': { | ||||
|                     'metapackages', | ||||
|                     'tasks', | ||||
|                 }, | ||||
|                 'Move-Autobit-Sections': { | ||||
|                     'oldlibs', | ||||
|                 }, | ||||
|                 'Update': { | ||||
|                     # https://unix.stackexchange.com/a/653377/357916 | ||||
|                     'Error-Mode': 'any', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         'sources': {}, | ||||
|     }, | ||||
|     'monitoring': { | ||||
|         'services': { | ||||
|             'apt upgradable': { | ||||
|                 'vars.command': '/usr/lib/nagios/plugins/check_apt_upgradable', | ||||
|                 'vars.sudo': True, | ||||
|                 'check_interval': '1h', | ||||
|             }, | ||||
|             'current kernel': { | ||||
|                 'vars.command': 'ls /boot/vmlinuz-* | sort -V | tail -n 1 | xargs -n1 basename | cut -d "-" -f 2- | grep -q "^$(uname -r)$"', | ||||
|                 'check_interval': '1h', | ||||
|             }, | ||||
|             'apt reboot-required': { | ||||
|                 'vars.command': 'ls /var/run/reboot-required 2> /dev/null && exit 1 || exit 0', | ||||
|                 'check_interval': '1h', | ||||
|             }, | ||||
|         }, | ||||
|         'packages': {}, | ||||
|         'sources': set(), | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'apt/sources', | ||||
| ) | ||||
| def key(metadata): | ||||
|     return { | ||||
|         'apt': { | ||||
|             'sources': { | ||||
|                 source_name: { | ||||
|                     'key': source_name, | ||||
|                 } | ||||
|                     for source_name, source_config in metadata.get('apt/sources').items() | ||||
|                     if 'key' not in source_config | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'apt/sources', | ||||
| ) | ||||
| def signed_by(metadata): | ||||
|     return { | ||||
|         'apt': { | ||||
|             'sources': { | ||||
|                 source_name: { | ||||
|                     'options': { | ||||
|                         'Signed-By': '/etc/apt/keyrings/' + metadata.get(f'apt/sources/{source_name}/key') + '.' + repo.libs.apt.find_keyfile_extension(node, metadata.get(f'apt/sources/{source_name}/key')), | ||||
|                     }, | ||||
|                 } | ||||
|                     for source_name in metadata.get('apt/sources') | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'apt/config', | ||||
|     'apt/packages', | ||||
| ) | ||||
| def unattended_upgrades(metadata): | ||||
|     return { | ||||
|         'apt': { | ||||
|             'config': { | ||||
|                 'APT': { | ||||
|                     'Periodic': { | ||||
|                         'Update-Package-Lists': '1', | ||||
|                         'Unattended-Upgrade': '1', | ||||
|                     }, | ||||
|                 }, | ||||
|                 'Unattended-Upgrade': { | ||||
|                     'Origins-Pattern': { | ||||
|                         "origin=*", | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|             'packages': { | ||||
|                 'unattended-upgrades': {}, | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| # @metadata_reactor.provides( | ||||
| #     'apt/config', | ||||
| #     'apt/list_changes', | ||||
| # ) | ||||
| # def listchanges(metadata): | ||||
| #     return { | ||||
| #         'apt': { | ||||
| #             'config': { | ||||
| #                 'DPkg': { | ||||
| #                     'Pre-Install-Pkgs': { | ||||
| #                         '/usr/bin/apt-listchanges --apt || test $? -lt 10', | ||||
| #                     }, | ||||
| #                     'Tools': { | ||||
| #                         'Options': { | ||||
| #                             '/usr/bin/apt-listchanges': { | ||||
| #                                 'Version': '2', | ||||
| #                                 'InfoFD': '20', | ||||
| #                             }, | ||||
| #                         }, | ||||
| #                     }, | ||||
| #                 }, | ||||
| #                 'Dir': { | ||||
| #                     'Etc': { | ||||
| #                         'apt-listchanges-main': 'listchanges.conf', | ||||
| #                         'apt-listchanges-parts': 'listchanges.conf.d', | ||||
| #                     }, | ||||
| #                 }, | ||||
| #             }, | ||||
| #             'list_changes': { | ||||
| #                 'apt': { | ||||
| #                     'frontend': 'pager', | ||||
| #                     'which': 'news', | ||||
| #                     'email_address': 'root', | ||||
| #                     'email_format': 'text', | ||||
| #                     'confirm': 'false', | ||||
| #                     'headers': 'false', | ||||
| #                     'reverse': 'false', | ||||
| #                     'save_seen': '/var/lib/apt/listchanges.db', | ||||
| #                 }, | ||||
| #             }, | ||||
| #         }, | ||||
| #     } | ||||
|  |  | |||
|  | @ -1,47 +0,0 @@ | |||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import json | ||||
| from subprocess import check_output | ||||
| from datetime import datetime, timedelta | ||||
| 
 | ||||
| 
 | ||||
| now = datetime.now() | ||||
| two_days_ago = now - timedelta(days=2) | ||||
| 
 | ||||
| with open('/etc/backup-freshness-check.json', 'r') as file: | ||||
|     config = json.load(file) | ||||
| 
 | ||||
| local_datasets = check_output(['zfs', 'list', '-H', '-o', 'name']).decode().splitlines() | ||||
| errors = set() | ||||
| 
 | ||||
| for dataset in config['datasets']: | ||||
|     if f'tank/{dataset}' not in local_datasets: | ||||
|         errors.add(f'dataset "{dataset}" not present at all') | ||||
|         continue | ||||
| 
 | ||||
|     snapshots = [ | ||||
|         snapshot | ||||
|             for snapshot in check_output(['zfs', 'list', '-H', '-o', 'name', '-t', 'snapshot', f'tank/{dataset}', '-s', 'creation']).decode().splitlines() | ||||
|             if f"@{config['prefix']}" in snapshot | ||||
|     ] | ||||
| 
 | ||||
|     if not snapshots: | ||||
|         errors.add(f'dataset "{dataset}" has no backup snapshots') | ||||
|         continue | ||||
| 
 | ||||
|     newest_backup_snapshot = snapshots[-1] | ||||
|     snapshot_datetime = datetime.utcfromtimestamp( | ||||
|         int(check_output(['zfs', 'list', '-p', '-H', '-o', 'creation', '-t', 'snapshot', newest_backup_snapshot]).decode()) | ||||
|     ) | ||||
| 
 | ||||
|     if snapshot_datetime < two_days_ago: | ||||
|         days_ago = (now - snapshot_datetime).days | ||||
|         errors.add(f'dataset "{dataset}" has not been backed up for {days_ago} days') | ||||
|         continue | ||||
| 
 | ||||
| if errors: | ||||
|     for error in errors: | ||||
|         print(error) | ||||
|     exit(2) | ||||
| else: | ||||
|     print(f"all {len(config['datasets'])} datasets have fresh backups.") | ||||
|  | @ -1,15 +0,0 @@ | |||
| from json import dumps | ||||
| from bundlewrap.metadata import MetadataJSONEncoder | ||||
| 
 | ||||
| 
 | ||||
| files = { | ||||
|     '/etc/backup-freshness-check.json': { | ||||
|         'content': dumps({ | ||||
|             'prefix': node.metadata.get('backup-freshness-check/prefix'), | ||||
|             'datasets': node.metadata.get('backup-freshness-check/datasets'), | ||||
|         }, indent=4, sort_keys=True, cls=MetadataJSONEncoder), | ||||
|     }, | ||||
|     '/usr/lib/nagios/plugins/check_backup_freshness': { | ||||
|         'mode': '0755', | ||||
|     }, | ||||
| } | ||||
|  | @ -1,37 +0,0 @@ | |||
| defaults = { | ||||
|     'backup-freshness-check': { | ||||
|         'server': node.name, | ||||
|         'prefix': 'auto-backup_', | ||||
|         'datasets': {}, | ||||
|     }, | ||||
|     'monitoring': { | ||||
|         'services': { | ||||
|             'backup freshness': { | ||||
|                 'vars.command': '/usr/lib/nagios/plugins/check_backup_freshness', | ||||
|                 'check_interval': '6h', | ||||
|                 'vars.sudo': True, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'backup-freshness-check/datasets' | ||||
| ) | ||||
| def backup_freshness_check(metadata): | ||||
|     return { | ||||
|         'backup-freshness-check': { | ||||
|             'datasets': { | ||||
|                 f"{other_node.metadata.get('id')}/{dataset}" | ||||
|                     for other_node in repo.nodes | ||||
|                     if not other_node.dummy | ||||
|                     and other_node.has_bundle('backup') | ||||
|                     and other_node.has_bundle('zfs') | ||||
|                     and other_node.metadata.get('backup/server') == metadata.get('backup-freshness-check/server') | ||||
|                     for dataset, options in other_node.metadata.get('zfs/datasets').items() | ||||
|                     if options.get('backup', True) | ||||
|                     and not options.get('mountpoint', None) in [None, 'none'] | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
|  | @ -16,14 +16,7 @@ defaults = { | |||
|             '/usr/bin/rsync', | ||||
|             '/sbin/zfs', | ||||
|         }, | ||||
|     }, | ||||
|     'zfs': { | ||||
|         'datasets': { | ||||
|             'tank': { | ||||
|                 'recordsize': "1048576", | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -32,10 +25,9 @@ defaults = { | |||
| ) | ||||
| def zfs(metadata): | ||||
|     datasets = {} | ||||
| 
 | ||||
|      | ||||
|     for other_node in repo.nodes: | ||||
|         if ( | ||||
|             not other_node.dummy and | ||||
|             other_node.has_bundle('backup') and | ||||
|             other_node.metadata.get('backup/server') == node.name | ||||
|         ): | ||||
|  | @ -50,7 +42,7 @@ def zfs(metadata): | |||
|                 'com.sun:auto-snapshot': 'false', | ||||
|                 'backup': False, | ||||
|             } | ||||
| 
 | ||||
|              | ||||
|             # for rsync backups | ||||
|             datasets[f'{base_dataset}/fs'] = { | ||||
|                 'mountpoint': f"/mnt/backups/{id}", | ||||
|  | @ -59,10 +51,10 @@ def zfs(metadata): | |||
|                 'com.sun:auto-snapshot': 'true', | ||||
|                 'backup': False, | ||||
|             } | ||||
| 
 | ||||
|              | ||||
|             # for zfs send/recv | ||||
|             if other_node.has_bundle('zfs'): | ||||
| 
 | ||||
|                  | ||||
|                 # base datasets for each tank | ||||
|                 for pool in other_node.metadata.get('zfs/pools'): | ||||
|                     datasets[f'{base_dataset}/{pool}'] = { | ||||
|  | @ -72,7 +64,7 @@ def zfs(metadata): | |||
|                         'com.sun:auto-snapshot': 'false', | ||||
|                         'backup': False, | ||||
|                     } | ||||
| 
 | ||||
|                  | ||||
|                 # actual datasets | ||||
|                 for path in other_node.metadata.get('backup/paths'): | ||||
|                     for dataset, config in other_node.metadata.get('zfs/datasets').items(): | ||||
|  | @ -99,7 +91,7 @@ def zfs(metadata): | |||
| def dns(metadata): | ||||
|     return { | ||||
|         'dns': { | ||||
|             metadata.get('backup-server/hostname'): repo.libs.ip.get_a_records(metadata), | ||||
|             metadata.get('backup-server/hostname'): repo.libs.dns.get_a_records(metadata), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,31 +1,11 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| set -u | ||||
| 
 | ||||
| # FIXME: inelegant | ||||
| % if wol_command: | ||||
| ${wol_command} | ||||
| % endif | ||||
| 
 | ||||
| exit=0 | ||||
| failed_paths="" | ||||
| 
 | ||||
| for path in $(jq -r '.paths | .[]' < /etc/backup/config.json) | ||||
| do | ||||
|   echo backing up $path | ||||
|   /opt/backup/backup_path "$path" | ||||
|   # set exit to 1 if any backup fails | ||||
|   if [ $? -ne 0 ] | ||||
|   then | ||||
|     echo ERROR: backing up $path failed >&2 | ||||
|     exit=5 | ||||
|     failed_paths="$failed_paths $path" | ||||
|   fi | ||||
| done | ||||
| 
 | ||||
| if [ $exit -ne 0 ] | ||||
| then | ||||
|   echo "ERROR: failed to backup paths: $failed_paths" >&2 | ||||
| fi | ||||
| 
 | ||||
| exit $exit | ||||
|  |  | |||
|  | @ -1,13 +1,11 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| set -exu | ||||
| 
 | ||||
| path=$1 | ||||
| 
 | ||||
| if zfs list -H -o mountpoint | grep -q "^$path$" | ||||
| if zfs list -H -o mountpoint | grep -q "$path" | ||||
| then | ||||
|   /opt/backup/backup_path_via_zfs "$path" | ||||
| elif test -e "$path" | ||||
| elif test -d "$path" | ||||
| then | ||||
|   /opt/backup/backup_path_via_rsync "$path" | ||||
| else | ||||
|  |  | |||
|  | @ -7,14 +7,5 @@ uuid=$(jq -r .client_uuid < /etc/backup/config.json) | |||
| server=$(jq -r .server_hostname < /etc/backup/config.json) | ||||
| ssh="ssh -o ConnectTimeout=5 backup-receiver@$server" | ||||
| 
 | ||||
| if test -d "$path" | ||||
| then | ||||
|   postfix="/" | ||||
| elif test -f "$path" | ||||
| then | ||||
|   postfix="" | ||||
| else | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| rsync -av --rsync-path="sudo rsync" "$path$postfix" "backup-receiver@$server:/mnt/backups/$uuid$path$postfix" | ||||
| rsync -av --rsync-path="sudo rsync" "$path/" "backup-receiver@$server:/mnt/backups/$uuid$path/" | ||||
| $ssh sudo zfs snap "tank/$uuid/fs@auto-backup_$(date +"%Y-%m-%d_%H:%M:%S")"  | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| set -eu | ||||
| set -exu | ||||
| 
 | ||||
| path=$1 | ||||
| uuid=$(jq -r .client_uuid < /etc/backup/config.json) | ||||
|  | @ -39,20 +39,20 @@ else | |||
|   echo "INCREMENTAL BACKUP" | ||||
|   last_bookmark=$(zfs list -t bookmark -H -o name | grep "^$source_dataset#$bookmark_prefix" | sort | tail -1 | cut -d '#' -f 2) | ||||
|   [[ -z "$last_bookmark" ]] && echo "ERROR - last_bookmark is empty" && exit 98 | ||||
|   $(zfs send -v -L -i "#$last_bookmark" "$source_dataset@$new_bookmark" | $ssh sudo zfs recv "$target_dataset") | ||||
|   $(zfs send -v -i "#$last_bookmark" "$source_dataset@$new_bookmark" | $ssh sudo zfs recv "$target_dataset") | ||||
| fi | ||||
| 
 | ||||
| if [[ "$?" == "0" ]] | ||||
| then | ||||
| 
 | ||||
|   # delete old local bookmarks | ||||
|   for destroyable_bookmark in $(zfs list -t bookmark -H -o name "$source_dataset" | grep "^$source_dataset#$bookmark_prefix") | ||||
|   for destroyable_bookmark in $(zfs list -t bookmark -H -o name "$dataset" | grep "^$dataset#$bookmark_prefix") | ||||
|   do | ||||
|     zfs destroy "$destroyable_bookmark" | ||||
|   done | ||||
| 
 | ||||
|   # delete remote snapshots from bookmarks (except newest, even of not necessary; maybe for resuming tho) | ||||
|   for destroyable_snapshot in $($ssh sudo zfs list -t snapshot -H -o name "$target_dataset" | grep "^$target_dataset@$bookmark_prefix" | grep -v "$new_bookmark") | ||||
|   # delete snapshots from bookmarks (except newest, even of not necessary; maybe for resuming tho) | ||||
|   for destroyable_snapshot in $($ssh sudo zfs list -t snapshot -H -o name "$dataset" | grep "^$dataset@$bookmark_prefix" | grep -v "$new_bookmark") | ||||
|   do | ||||
|     $ssh sudo zfs destroy "$destroyable_snapshot" | ||||
|   done | ||||
|  |  | |||
|  | @ -20,11 +20,7 @@ defaults = { | |||
|     'systemd-timers': { | ||||
|         f'backup': { | ||||
|             'command': '/opt/backup/backup_all', | ||||
|             'when': '1:00', | ||||
|             'persistent': True, | ||||
|             'after': { | ||||
|                 'network-online.target', | ||||
|             }, | ||||
|             'when': 'daily', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +0,0 @@ | |||
| $TTL    86400 | ||||
| @       IN      SOA     localhost. root.localhost. ( | ||||
|                               1         ; Serial | ||||
|                               604800    ; Refresh | ||||
|                               86400     ; Retry | ||||
|                               2419200   ; Expire | ||||
|                               86400 )   ; Negative Cache TTL | ||||
|         IN      NS      localhost. | ||||
|  | @ -29,7 +29,6 @@ view "${view_name}" { | |||
| 
 | ||||
|   % if view_conf['is_internal']: | ||||
|   recursion yes; | ||||
|   include "/etc/bind/zones.rfc1918"; | ||||
|   % else: | ||||
|   recursion no; | ||||
|   rate-limit { | ||||
|  | @ -63,6 +62,9 @@ view "${view_name}" { | |||
|     file "/var/lib/bind/${view_name}/${zone_name}"; | ||||
|   }; | ||||
|   % endfor | ||||
| 
 | ||||
|   include "/etc/bind/named.conf.default-zones"; | ||||
|   include "/etc/bind/zones.rfc1918"; | ||||
| }; | ||||
| 
 | ||||
| % endfor | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ options { | |||
| 
 | ||||
| % if type == 'master': | ||||
|   notify yes; | ||||
|   also-notify { ${' '.join(sorted(f'{ip};' for ip in slave_ips))} }; | ||||
|   allow-transfer { ${' '.join(sorted(f'{ip};' for ip in slave_ips))} }; | ||||
|   also-notify { ${' '.join([f'{ip};' for ip in slave_ips])} }; | ||||
|   allow-transfer { ${' '.join([f'{ip};' for ip in slave_ips])} }; | ||||
| % endif | ||||
| }; | ||||
|  |  | |||
|  | @ -1,19 +0,0 @@ | |||
| zone "10.in-addr.arpa"       { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "16.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "17.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "18.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "19.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "20.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "21.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "22.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "23.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "24.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "25.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "26.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "27.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "28.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "29.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "30.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "31.172.in-addr.arpa"   { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "168.192.in-addr.arpa"  { type master; file "/etc/bind/db.empty"; }; | ||||
| zone "254.169.in-addr.arpa"  { type master; file "/etc/bind/db.empty"; }; | ||||
|  | @ -1,5 +1,7 @@ | |||
| from ipaddress import ip_address, ip_interface | ||||
| from datetime import datetime | ||||
| import json | ||||
| from bundlewrap.metadata import MetadataJSONEncoder | ||||
| from hashlib import sha3_512 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -19,7 +21,7 @@ directories[f'/var/lib/bind'] = { | |||
|         'svc_systemd:bind9', | ||||
|     ], | ||||
|     'triggers': [ | ||||
|         'svc_systemd:bind9:reload', | ||||
|         'svc_systemd:bind9:restart', | ||||
|     ], | ||||
| } | ||||
| 
 | ||||
|  | @ -29,7 +31,7 @@ files['/etc/default/bind9'] = { | |||
|         'svc_systemd:bind9', | ||||
|     ], | ||||
|     'triggers': [ | ||||
|         'svc_systemd:bind9:reload', | ||||
|         'svc_systemd:bind9:restart', | ||||
|     ], | ||||
| } | ||||
| 
 | ||||
|  | @ -43,7 +45,7 @@ files['/etc/bind/named.conf'] = { | |||
|         'svc_systemd:bind9', | ||||
|     ], | ||||
|     'triggers': [ | ||||
|         'svc_systemd:bind9:reload', | ||||
|         'svc_systemd:bind9:restart', | ||||
|     ], | ||||
| } | ||||
| 
 | ||||
|  | @ -63,7 +65,7 @@ files['/etc/bind/named.conf.options'] = { | |||
|         'svc_systemd:bind9', | ||||
|     ], | ||||
|     'triggers': [ | ||||
|         'svc_systemd:bind9:reload', | ||||
|         'svc_systemd:bind9:restart', | ||||
|     ], | ||||
| } | ||||
| 
 | ||||
|  | @ -93,7 +95,7 @@ files['/etc/bind/named.conf.local'] = { | |||
|         'svc_systemd:bind9', | ||||
|     ], | ||||
|     'triggers': [ | ||||
|         'svc_systemd:bind9:reload', | ||||
|         'svc_systemd:bind9:restart', | ||||
|     ], | ||||
| } | ||||
| 
 | ||||
|  | @ -106,7 +108,7 @@ for view_name, view_conf in master_node.metadata.get('bind/views').items(): | |||
|             'svc_systemd:bind9', | ||||
|         ], | ||||
|         'triggers': [ | ||||
|             'svc_systemd:bind9:reload', | ||||
|             'svc_systemd:bind9:restart', | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
|  | @ -127,10 +129,10 @@ for view_name, view_conf in master_node.metadata.get('bind/views').items(): | |||
|                 'svc_systemd:bind9', | ||||
|             ], | ||||
|             'triggers': [ | ||||
|                 'svc_systemd:bind9:reload', | ||||
|                 'svc_systemd:bind9:restart', | ||||
|             ], | ||||
|         } | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
| svc_systemd['bind9'] = {} | ||||
| 
 | ||||
|  | @ -139,24 +141,6 @@ actions['named-checkconf'] = { | |||
|     'unless': 'named-checkconf -z', | ||||
|     'needs': [ | ||||
|         'svc_systemd:bind9', | ||||
|         'svc_systemd:bind9:reload', | ||||
|         'svc_systemd:bind9:restart', | ||||
|     ] | ||||
| } | ||||
| 
 | ||||
| # beantwortet Anfragen nach privaten IP-Adressen mit NXDOMAIN, statt sie ins Internet weiterzuleiten | ||||
| files['/etc/bind/zones.rfc1918'] = { | ||||
|     'needed_by': [ | ||||
|         'svc_systemd:bind9', | ||||
|     ], | ||||
|     'triggers': [ | ||||
|         'svc_systemd:bind9:reload', | ||||
|     ], | ||||
| } | ||||
| files['/etc/bind/db.empty'] = { | ||||
|     'needed_by': [ | ||||
|         'svc_systemd:bind9', | ||||
|     ], | ||||
|     'triggers': [ | ||||
|         'svc_systemd:bind9:reload', | ||||
|     ], | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ from json import dumps | |||
| h = repo.libs.hashable.hashable | ||||
| repo.libs.bind.repo = repo | ||||
| 
 | ||||
| 
 | ||||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|  | @ -42,12 +41,6 @@ defaults = { | |||
|         }, | ||||
|         'zones': set(), | ||||
|     }, | ||||
|     'nftables': { | ||||
|         'input': { | ||||
|             'tcp dport 53 accept', | ||||
|             'udp dport 53 accept', | ||||
|         }, | ||||
|     }, | ||||
|     'telegraf': { | ||||
|         'config': { | ||||
|             'inputs': { | ||||
|  | @ -93,7 +86,7 @@ def master_slave(metadata): | |||
| def dns(metadata): | ||||
|     return { | ||||
|         'dns': { | ||||
|             metadata.get('bind/hostname'): repo.libs.ip.get_a_records(metadata), | ||||
|             metadata.get('bind/hostname'): repo.libs.dns.get_a_records(metadata), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -104,7 +97,7 @@ def dns(metadata): | |||
| def collect_records(metadata): | ||||
|     if metadata.get('bind/type') == 'slave': | ||||
|         return {} | ||||
| 
 | ||||
|      | ||||
|     views = {} | ||||
| 
 | ||||
|     for view_name, view_conf in metadata.get('bind/views').items(): | ||||
|  | @ -124,7 +117,7 @@ def collect_records(metadata): | |||
| 
 | ||||
|                 name = fqdn[0:-len(zone) - 1] | ||||
| 
 | ||||
|                 for type, values in records.items(): | ||||
|                 for type, values in records.items():                     | ||||
|                     for value in values: | ||||
|                         if repo.libs.bind.record_matches_view(value, type, name, zone, view_name, metadata): | ||||
|                             views\ | ||||
|  | @ -135,7 +128,7 @@ def collect_records(metadata): | |||
|                                 .add( | ||||
|                                     h({'name': name, 'type': type, 'value': value}) | ||||
|                                 ) | ||||
| 
 | ||||
|      | ||||
|     return { | ||||
|         'bind': { | ||||
|             'views': views, | ||||
|  | @ -167,7 +160,7 @@ def ns_records(metadata): | |||
|                                 # FIXME: bw currently cant handle lists of dicts :( | ||||
|                                 h({'name': '@', 'type': 'NS', 'value': f"{nameserver}."}) | ||||
|                                     for nameserver in nameservers | ||||
|                             } | ||||
|                             }  | ||||
|                         } | ||||
|                             for zone_name, zone_conf in view_conf['zones'].items() | ||||
|                     } | ||||
|  | @ -184,7 +177,7 @@ def ns_records(metadata): | |||
| def slaves(metadata): | ||||
|     if metadata.get('bind/type') == 'slave': | ||||
|         return {} | ||||
| 
 | ||||
|      | ||||
|     return { | ||||
|         'bind': { | ||||
|             'slaves': [ | ||||
|  | @ -212,7 +205,7 @@ def generate_keys(metadata): | |||
|                             'token':repo.libs.hmac.hmac_sha512( | ||||
|                                 key, | ||||
|                                 str(repo.vault.random_bytes_as_base64_for( | ||||
|                                     f"{metadata.get('id')} bind key {key} 20250713", | ||||
|                                     f"{metadata.get('id')} bind key {key}", | ||||
|                                     length=32, | ||||
|                                 )), | ||||
|                             ) | ||||
|  |  | |||
|  | @ -1,165 +0,0 @@ | |||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import os | ||||
| import datetime | ||||
| import numpy as np | ||||
| import matplotlib.pyplot as plt | ||||
| import soundfile as sf | ||||
| from scipy.fft import rfft, rfftfreq | ||||
| import shutil | ||||
| import traceback | ||||
| 
 | ||||
| 
 | ||||
| RECORDINGS_DIR = "recordings" | ||||
| PROCESSED_RECORDINGS_DIR = "recordings/processed" | ||||
| DETECTIONS_DIR = "events" | ||||
| 
 | ||||
| DETECT_FREQUENCY = 211 # Hz | ||||
| DETECT_FREQUENCY_TOLERANCE = 2 # Hz | ||||
| ADJACENCY_FACTOR = 2 # area to look for the frequency (e.g. 2 means 100Hz to 400Hz for 200Hz detection) | ||||
| BLOCK_SECONDS = 3 # seconds (longer means more frequency resolution, but less time resolution) | ||||
| DETECTION_DISTANCE_SECONDS = 30 # seconds (minimum time between detections) | ||||
| BLOCK_OVERLAP_FACTOR = 0.9 # overlap between blocks (0.2 means 20% overlap) | ||||
| MIN_SIGNAL_QUALITY = 1000.0 # maximum noise level (relative DB) to consider a detection valid | ||||
| PLOT_PADDING_START_SECONDS = 2 # seconds (padding before and after the event in the plot) | ||||
| PLOT_PADDING_END_SECONDS = 3 # seconds (padding before and after the event in the plot) | ||||
| 
 | ||||
| DETECTION_DISTANCE_BLOCKS = DETECTION_DISTANCE_SECONDS // BLOCK_SECONDS # number of blocks to skip after a detection | ||||
| DETECT_FREQUENCY_FROM = DETECT_FREQUENCY - DETECT_FREQUENCY_TOLERANCE # Hz | ||||
| DETECT_FREQUENCY_TO = DETECT_FREQUENCY + DETECT_FREQUENCY_TOLERANCE # Hz | ||||
| 
 | ||||
| 
 | ||||
| def process_recording(filename): | ||||
|     print('processing', filename) | ||||
| 
 | ||||
|     # get ISO 8601 nanosecond recording date from filename | ||||
|     date_string_from_filename = os.path.splitext(filename)[0] | ||||
|     recording_date = datetime.datetime.strptime(date_string_from_filename, "%Y-%m-%d_%H-%M-%S.%f%z") | ||||
| 
 | ||||
|     # get data and metadata from recording | ||||
|     path = os.path.join(RECORDINGS_DIR, filename) | ||||
|     soundfile = sf.SoundFile(path) | ||||
|     samplerate = soundfile.samplerate | ||||
|     samples_per_block = int(BLOCK_SECONDS * samplerate) | ||||
|     overlapping_samples = int(samples_per_block * BLOCK_OVERLAP_FACTOR) | ||||
| 
 | ||||
|     sample_num = 0 | ||||
|     current_event = None | ||||
| 
 | ||||
|     while sample_num < len(soundfile): | ||||
|         soundfile.seek(sample_num) | ||||
|         block = soundfile.read(frames=samples_per_block, dtype='float32', always_2d=False) | ||||
| 
 | ||||
|         if len(block) == 0: | ||||
|             break | ||||
| 
 | ||||
|         # calculate FFT | ||||
|         labels = rfftfreq(len(block), d=1/samplerate) | ||||
|         complex_amplitudes = rfft(block) | ||||
|         amplitudes = np.abs(complex_amplitudes) | ||||
| 
 | ||||
|         # get the frequency with the highest amplitude within the search range | ||||
|         search_amplitudes = amplitudes[(labels >= DETECT_FREQUENCY_FROM/ADJACENCY_FACTOR) & (labels <= DETECT_FREQUENCY_TO*ADJACENCY_FACTOR)] | ||||
|         search_labels = labels[(labels >= DETECT_FREQUENCY_FROM/ADJACENCY_FACTOR) & (labels <= DETECT_FREQUENCY_TO*ADJACENCY_FACTOR)] | ||||
|         max_amplitude = max(search_amplitudes) | ||||
|         max_amplitude_index = np.argmax(search_amplitudes) | ||||
|         max_freq = search_labels[max_amplitude_index] | ||||
|         max_freq_detected = DETECT_FREQUENCY_FROM <= max_freq <= DETECT_FREQUENCY_TO | ||||
| 
 | ||||
|         # calculate signal quality | ||||
|         adjacent_amplitudes = amplitudes[(labels < DETECT_FREQUENCY_FROM) | (labels > DETECT_FREQUENCY_TO)] | ||||
|         signal_quality = max_amplitude/np.mean(adjacent_amplitudes) | ||||
|         good_signal_quality = signal_quality > MIN_SIGNAL_QUALITY | ||||
| 
 | ||||
|         # conclude detection | ||||
|         if ( | ||||
|             max_freq_detected and | ||||
|             good_signal_quality | ||||
|         ): | ||||
|             block_date = recording_date + datetime.timedelta(seconds=sample_num / samplerate) | ||||
| 
 | ||||
|             # detecting an event | ||||
|             if not current_event: | ||||
|                 current_event = { | ||||
|                     'start_at': block_date, | ||||
|                     'end_at': block_date, | ||||
|                     'start_sample': sample_num, | ||||
|                     'end_sample': sample_num + samples_per_block, | ||||
|                     'start_freq': max_freq, | ||||
|                     'end_freq': max_freq, | ||||
|                     'max_amplitude': max_amplitude, | ||||
|                 } | ||||
|             else: | ||||
|                 current_event.update({ | ||||
|                     'end_at': block_date, | ||||
|                     'end_freq': max_freq, | ||||
|                     'end_sample': sample_num + samples_per_block, | ||||
|                     'max_amplitude': max(max_amplitude, current_event['max_amplitude']), | ||||
|                 }) | ||||
|             print(f'- {block_date.strftime('%Y-%m-%d %H:%M:%S')}: {max_amplitude:.1f}rDB @ {max_freq:.1f}Hz (signal {signal_quality:.3f}x)') | ||||
|         else: | ||||
|             # not detecting an event | ||||
|             if current_event: | ||||
|                 duration = (current_event['end_at'] - current_event['start_at']).total_seconds() | ||||
|                 current_event['duration'] = duration | ||||
|                 print(f'🔊 {current_event['start_at'].strftime('%Y-%m-%d %H:%M:%S')} ({duration:.1f}s): {current_event['start_freq']:.1f}Hz->{current_event['end_freq']:.1f}Hz @{current_event['max_amplitude']:.0f}rDB') | ||||
| 
 | ||||
|                 # read full audio clip again for writing | ||||
|                 write_event(current_event=current_event, soundfile=soundfile, samplerate=samplerate) | ||||
| 
 | ||||
|                 current_event = None | ||||
|                 sample_num += DETECTION_DISTANCE_BLOCKS * samples_per_block | ||||
| 
 | ||||
|         sample_num += samples_per_block - overlapping_samples | ||||
| 
 | ||||
|     # move to PROCESSED_RECORDINGS_DIR | ||||
| 
 | ||||
|     os.makedirs(PROCESSED_RECORDINGS_DIR, exist_ok=True) | ||||
|     shutil.move(os.path.join(RECORDINGS_DIR, filename), os.path.join(PROCESSED_RECORDINGS_DIR, filename)) | ||||
| 
 | ||||
| 
 | ||||
| # write a spectrogram using the sound from start to end of the event | ||||
| def write_event(current_event, soundfile, samplerate): | ||||
|     # date and filename | ||||
|     event_date = current_event['start_at'] - datetime.timedelta(seconds=PLOT_PADDING_START_SECONDS) | ||||
|     filename_prefix = event_date.strftime('%Y-%m-%d_%H-%M-%S.%f%z') | ||||
| 
 | ||||
|     # event clip | ||||
|     event_start_sample = current_event['start_sample'] - samplerate * PLOT_PADDING_START_SECONDS | ||||
|     event_end_sample = current_event['end_sample'] + samplerate * PLOT_PADDING_END_SECONDS | ||||
|     total_samples = event_end_sample - event_start_sample | ||||
|     soundfile.seek(event_start_sample) | ||||
|     event_clip = soundfile.read(frames=total_samples, dtype='float32', always_2d=False) | ||||
| 
 | ||||
|     # write flac | ||||
|     flac_path = os.path.join(DETECTIONS_DIR, f"{filename_prefix}.flac") | ||||
|     sf.write(flac_path, event_clip, samplerate, format='FLAC') | ||||
| 
 | ||||
|     # write spectrogram | ||||
|     plt.figure(figsize=(8, 6)) | ||||
|     plt.specgram(event_clip, Fs=samplerate, NFFT=samplerate, noverlap=samplerate//2, cmap='inferno', vmin=-100, vmax=-10) | ||||
|     plt.title(f"Bootshorn @{event_date.strftime('%Y-%m-%d %H:%M:%S%z')}") | ||||
|     plt.xlabel(f"Time {current_event['duration']:.1f}s") | ||||
|     plt.ylabel(f"Frequency {current_event['start_freq']:.1f}Hz -> {current_event['end_freq']:.1f}Hz") | ||||
|     plt.colorbar(label="Intensity (rDB)") | ||||
|     plt.ylim(50, 1000) | ||||
|     plt.savefig(os.path.join(DETECTIONS_DIR, f"{filename_prefix}.png")) | ||||
|     plt.close() | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     os.makedirs(RECORDINGS_DIR, exist_ok=True) | ||||
|     os.makedirs(PROCESSED_RECORDINGS_DIR, exist_ok=True) | ||||
| 
 | ||||
|     for filename in sorted(os.listdir(RECORDINGS_DIR)): | ||||
|         if filename.endswith(".flac"): | ||||
|             try: | ||||
|                 process_recording(filename) | ||||
|             except Exception as e: | ||||
|                 print(f"Error processing {filename}: {e}") | ||||
|                 # print stacktrace | ||||
|                 traceback.print_exc() | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
|  | @ -1,25 +0,0 @@ | |||
| #!/bin/sh | ||||
| 
 | ||||
| mkdir -p recordings | ||||
| 
 | ||||
| while true | ||||
| do | ||||
|     # get date in ISO 8601 format with nanoseconds | ||||
|     PROGRAMM=$(test $(uname) = "Darwin" && echo "gdate" || echo "date") | ||||
|     DATE=$($PROGRAMM "+%Y-%m-%d_%H-%M-%S.%6N%z") | ||||
| 
 | ||||
|     # record audio using ffmpeg | ||||
|     ffmpeg \ | ||||
|         -y \ | ||||
|         -f pulse \ | ||||
|         -i "alsa_input.usb-HANMUS_USB_AUDIO_24BIT_2I2O_1612310-00.analog-stereo" \ | ||||
|         -ac 1 \ | ||||
|         -ar 96000 \ | ||||
|         -sample_fmt s32 \ | ||||
|         -t "3600" \ | ||||
|         -c:a flac \ | ||||
|         -compression_level 12 \ | ||||
|         "recordings/current/$DATE.flac" | ||||
| 
 | ||||
|     mv "recordings/current/$DATE.flac" "recordings/$DATE.flac" | ||||
| done | ||||
|  | @ -1,43 +0,0 @@ | |||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import requests | ||||
| import urllib3 | ||||
| import datetime | ||||
| import csv | ||||
| urllib3.disable_warnings() | ||||
| import os | ||||
| 
 | ||||
| 
 | ||||
| HUE_IP = "${hue_ip}"  # replace with your bridge IP | ||||
| HUE_APP_KEY = "${hue_app_key}" # local only | ||||
| HUE_DEVICE_ID = "31f58786-3242-4e88-b9ce-23f44ba27bbe" | ||||
| TEMPERATURE_LOG_DIR = "/opt/bootshorn/temperatures" | ||||
| 
 | ||||
| response = requests.get( | ||||
|     f"https://{HUE_IP}/clip/v2/resource/temperature", | ||||
|     headers={"hue-application-key": HUE_APP_KEY}, | ||||
|     verify=False, | ||||
| ) | ||||
| response.raise_for_status() | ||||
| data = response.json() | ||||
| 
 | ||||
| for item in data["data"]: | ||||
|     if item["id"] == HUE_DEVICE_ID: | ||||
|         temperature = item["temperature"]["temperature"] | ||||
|         temperature_date_string = item["temperature"]["temperature_report"]["changed"] | ||||
|         temperature_date = datetime.datetime.fromisoformat(temperature_date_string).astimezone(datetime.timezone.utc) | ||||
|         break | ||||
| 
 | ||||
| print(f"@{temperature_date}: {temperature}°C") | ||||
| 
 | ||||
| filename = temperature_date.strftime("%Y-%m-%d_00-00-00.000000%z") + ".log" | ||||
| logpath  = os.path.join(TEMPERATURE_LOG_DIR, filename) | ||||
| now_utc = datetime.datetime.now(datetime.timezone.utc) | ||||
| 
 | ||||
| with open(logpath, "a+", newline="") as logfile: | ||||
|     writer = csv.writer(logfile) | ||||
|     writer.writerow([ | ||||
|         now_utc.strftime('%Y-%m-%d_%H-%M-%S.%f%z'), # current UTC time | ||||
|         temperature_date.strftime('%Y-%m-%d_%H-%M-%S.%f%z'), # date of temperature reading | ||||
|         temperature, | ||||
|     ]) | ||||
|  | @ -1,61 +0,0 @@ | |||
| # nano /etc/selinux/config | ||||
| # SELINUX=disabled | ||||
| # reboot | ||||
| 
 | ||||
| directories = { | ||||
|     '/opt/bootshorn': { | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|     }, | ||||
|     '/opt/bootshorn/temperatures': { | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|     }, | ||||
|     '/opt/bootshorn/recordings': { | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|     }, | ||||
|     '/opt/bootshorn/recordings/current': { | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|     }, | ||||
|     '/opt/bootshorn/recordings/processed': { | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|     }, | ||||
|     '/opt/bootshorn/events': { | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| files = { | ||||
|     '/opt/bootshorn/record': { | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|         'mode': '755', | ||||
|     }, | ||||
|     '/opt/bootshorn/temperature': { | ||||
|         'content_type': 'mako', | ||||
|         'context': { | ||||
|             'hue_ip': repo.get_node('home.hue').hostname, | ||||
|             'hue_app_key': repo.vault.decrypt('encrypt$gAAAAABoc2WxZCLbxl-Z4IrSC97CdOeFgBplr9Fp5ujpd0WCCCPNBUY_WquHN86z8hKLq5Y04dwq8TdJW0PMSOSgTFbGgdp_P1q0jOBLEKaW9IIT1YM88h-JYwLf9QGDV_5oEfvnBCtO'), | ||||
|         }, | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|         'mode': '755', | ||||
|     }, | ||||
|     '/opt/bootshorn/process': { | ||||
|         'owner': 'ckn', | ||||
|         'group': 'ckn', | ||||
|         'mode': '755', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| svc_systemd = { | ||||
|     'bootshorn-record.service': { | ||||
|         'needs': { | ||||
|             'file:/opt/bootshorn/record', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  | @ -1,44 +0,0 @@ | |||
| defaults = { | ||||
|     'systemd': { | ||||
|         'units': { | ||||
|             'bootshorn-record.service': { | ||||
|                 'Unit': { | ||||
|                     'Description': 'Bootshorn Recorder', | ||||
|                     'After': 'network.target', | ||||
|                 }, | ||||
|                 'Service': { | ||||
|                     'User': 'ckn', | ||||
|                     'Group': 'ckn', | ||||
|                     'Type': 'simple', | ||||
|                     'WorkingDirectory': '/opt/bootshorn', | ||||
|                     'ExecStart': '/opt/bootshorn/record', | ||||
|                     'Restart': 'always', | ||||
|                     'RestartSec': 5, | ||||
|                     'Environment': { | ||||
|                         "XDG_RUNTIME_DIR": "/run/user/1000", | ||||
|                         "PULSE_SERVER": "unix:/run/user/1000/pulse/native", | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     'systemd-timers': { | ||||
|         'bootshorn-temperature': { | ||||
|             'command': '/opt/bootshorn/temperature', | ||||
|             'when': '*:0/10', | ||||
|             'working_dir': '/opt/bootshorn', | ||||
|             'user': 'ckn', | ||||
|             'group': 'ckn', | ||||
|         }, | ||||
|         # 'bootshorn-process': { | ||||
|         #     'command': '/opt/bootshorn/process', | ||||
|         #     'when': 'hourly', | ||||
|         #     'working_dir': '/opt/bootshorn', | ||||
|         #     'user': 'ckn', | ||||
|         #     'group': 'ckn', | ||||
|         #     'after': { | ||||
|         #         'bootshorn-process.service', | ||||
|         #     }, | ||||
|         # }, | ||||
|     }, | ||||
| } | ||||
|  | @ -1,10 +1,6 @@ | |||
| from shlex import quote | ||||
| 
 | ||||
| 
 | ||||
| defaults = { | ||||
|     'build-ci': {}, | ||||
| } | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'users/build-ci/authorized_users', | ||||
|     'sudoers/build-ci', | ||||
|  | @ -22,7 +18,7 @@ def ssh_keys(metadata): | |||
|         }, | ||||
|         'sudoers': { | ||||
|             'build-ci': { | ||||
|                 f"/usr/bin/chown -R build-ci\\:{quote(ci['group'])} {quote(ci['path'])}" | ||||
|                 f"/usr/bin/chown -R build-ci\:{quote(ci['group'])} {quote(ci['path'])}" | ||||
|                     for ci in metadata.get('build-ci').values() | ||||
|             } | ||||
|         }, | ||||
|  |  | |||
|  | @ -71,7 +71,6 @@ def nginx(metadata): | |||
|                     'context': { | ||||
|                         'target': 'http://127.0.0.1:4000', | ||||
|                     }, | ||||
|                     'check_path': '/status', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|  |  | |||
|  | @ -1,20 +1,10 @@ | |||
| debian_version = min([node.os_version, (11,)])[0] # FIXME | ||||
| 
 | ||||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'crystal': {}, | ||||
|         }, | ||||
|         'sources': { | ||||
|             'crystal': { | ||||
|                 # https://software.opensuse.org/download.html?project=devel%3Alanguages%3Acrystal&package=crystal | ||||
|                 'urls': { | ||||
|                     'http://download.opensuse.org/repositories/devel:/languages:/crystal/Debian_Testing/', | ||||
|                 }, | ||||
|                 'suites': { | ||||
|                     '/', | ||||
|                 }, | ||||
|             }, | ||||
|             'deb http://download.opensuse.org/repositories/devel:/languages:/crystal/Debian_{version}/ /', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  |  | |||
|  | @ -1,12 +1,9 @@ | |||
| DOVECOT | ||||
| ======= | ||||
| 
 | ||||
| rescan index | ||||
| ------------ | ||||
| 
 | ||||
| https://doc.dovecot.org/configuration_manual/fts/#rescan | ||||
| rescan index: https://doc.dovecot.org/configuration_manual/fts/#rescan | ||||
| 
 | ||||
| ``` | ||||
| doveadm fts rescan -u 'i@ckn.li' | ||||
| doveadm index -u 'i@ckn.li' -q '*' | ||||
|     sudo -u vmail doveadm fts rescan -u 'test@mail2.sublimity.de' | ||||
|     sudo -u vmail doveadm index -u 'test@mail2.sublimity.de' -q '*' | ||||
| ``` | ||||
|  |  | |||
|  | @ -66,7 +66,8 @@ xmlunzip() { | |||
|   trap "rm -rf $path $tempdir" 0 1 2 3 14 15 | ||||
|   cd $tempdir || exit 1 | ||||
|   unzip -q "$path" 2>/dev/null || exit 0 | ||||
|   find . -name "$name" -print0 | xargs -0 cat | /usr/lib/dovecot/xml2text | ||||
|   find . -name "$name" -print0 | xargs -0 cat | | ||||
|     $libexec_dir/xml2text | ||||
| } | ||||
| 
 | ||||
| wait_timeout() { | ||||
|  |  | |||
							
								
								
									
										10
									
								
								bundles/dovecot/files/dovecot-sql.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								bundles/dovecot/files/dovecot-sql.conf
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| connect = host=${host} dbname=${name} user=${user} password=${password} | ||||
| driver = pgsql | ||||
| default_pass_scheme = ARGON2ID | ||||
| 
 | ||||
| password_query = SELECT CONCAT(users.name, '@', domains.name) AS user, password\ | ||||
|   FROM users \ | ||||
|   LEFT JOIN domains ON users.domain_id = domains.id \ | ||||
|   WHERE redirect IS NULL \ | ||||
|   AND users.name = SPLIT_PART('%u', '@', 1) \ | ||||
|   AND domains.name = SPLIT_PART('%u', '@', 2) | ||||
|  | @ -1,79 +1,42 @@ | |||
| dovecot_config_version = ${config_version} | ||||
| dovecot_storage_version = ${storage_version} | ||||
| 
 | ||||
| protocols = imap lmtp sieve | ||||
| auth_mechanisms = plain login | ||||
| mail_privileged_group = mail | ||||
| ssl = required | ||||
| ssl_server_cert_file = /var/lib/dehydrated/certs/${hostname}/fullchain.pem | ||||
| ssl_server_key_file = /var/lib/dehydrated/certs/${hostname}/privkey.pem | ||||
| ssl_server_dh_file = /etc/dovecot/dhparam.pem | ||||
| ssl_cert = </var/lib/dehydrated/certs/${node.metadata.get('mailserver/hostname')}/fullchain.pem | ||||
| ssl_key = </var/lib/dehydrated/certs/${node.metadata.get('mailserver/hostname')}/privkey.pem | ||||
| ssl_dh = </etc/dovecot/dhparam.pem | ||||
| ssl_client_ca_dir = /etc/ssl/certs | ||||
| mail_driver     = maildir | ||||
| mail_path       = ${maildir}/%{user} | ||||
| mail_index_path = ${maildir}/index/%{user} | ||||
| mail_plugins = fts fts_flatcurve | ||||
| mail_location = maildir:~ | ||||
| mail_plugins = fts fts_xapian | ||||
| 
 | ||||
| namespace inbox { | ||||
|   inbox = yes | ||||
|   separator = . | ||||
|   mailbox Drafts { | ||||
|     auto = subscribe | ||||
|     auto = subscribe  | ||||
|     special_use = \Drafts | ||||
|   } | ||||
|   mailbox Junk { | ||||
|     auto = create | ||||
|     auto = create  | ||||
|     special_use = \Junk | ||||
|   } | ||||
|   mailbox Trash { | ||||
|     auto = subscribe | ||||
|     auto = subscribe  | ||||
|     special_use = \Trash | ||||
|   } | ||||
|   mailbox Sent { | ||||
|     auto = subscribe | ||||
|     auto = subscribe  | ||||
|     special_use = \Sent | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| # postgres passdb userdb | ||||
| 
 | ||||
| sql_driver = pgsql | ||||
| 
 | ||||
| pgsql main { | ||||
|   parameters { | ||||
|     host     = ${db_host} | ||||
|     dbname   = ${db_name} | ||||
|     user     = ${db_user} | ||||
|     password = ${db_password} | ||||
|   } | ||||
| passdb { | ||||
|   driver = sql | ||||
|   args = /etc/dovecot/dovecot-sql.conf | ||||
| } | ||||
| 
 | ||||
| passdb sql { | ||||
|   passdb_default_password_scheme = ARGON2ID | ||||
| 
 | ||||
|   query = SELECT \ | ||||
|             CONCAT(users.name, '@', domains.name) AS "user", \ | ||||
|             password \ | ||||
|           FROM users \ | ||||
|           LEFT JOIN domains ON users.domain_id = domains.id \ | ||||
|           WHERE redirect IS NULL \ | ||||
|             AND users.name   = SPLIT_PART('%{user}', '@', 1) \ | ||||
|             AND domains.name = SPLIT_PART('%{user}', '@', 2) | ||||
| } | ||||
| 
 | ||||
| mail_uid = vmail | ||||
| mail_gid = vmail | ||||
| 
 | ||||
| userdb sql { | ||||
|   query = SELECT \ | ||||
|             '/var/vmail/%{user}' AS home, \ | ||||
|             'vmail'              AS uid, \ | ||||
|             'vmail'              AS gid | ||||
| 
 | ||||
|   iterate_query = SELECT \ | ||||
|                     CONCAT(users.name, '@', domains.name) AS username \ | ||||
|                   FROM users \ | ||||
|                   LEFT JOIN domains ON users.domain_id = domains.id \ | ||||
|                   WHERE redirect IS NULL | ||||
| userdb { | ||||
|   driver = static | ||||
|   args = uid=vmail gid=vmail home=/var/vmail/%u | ||||
| } | ||||
| 
 | ||||
| service auth { | ||||
|  | @ -103,9 +66,10 @@ service stats { | |||
|   } | ||||
| } | ||||
| service managesieve-login { | ||||
|   #inet_listener sieve {} | ||||
|   process_min_avail = 1 | ||||
|   process_limit = 1 | ||||
|   inet_listener sieve { | ||||
|   } | ||||
|   process_min_avail = 0 | ||||
|   service_count = 1 | ||||
|   vsz_limit = 64 M | ||||
| } | ||||
| service managesieve { | ||||
|  | @ -113,53 +77,31 @@ service managesieve { | |||
| } | ||||
| 
 | ||||
| protocol imap { | ||||
|   mail_plugins = fts fts_flatcurve imap_sieve | ||||
|   mail_max_userip_connections = 50 | ||||
|   imap_idle_notify_interval = 29 mins | ||||
| } | ||||
|    mail_plugins = $mail_plugins imap_sieve | ||||
|    mail_max_userip_connections = 50 | ||||
|    imap_idle_notify_interval = 29 mins | ||||
| }  | ||||
| protocol lmtp { | ||||
|   mail_plugins = fts fts_flatcurve sieve | ||||
| } | ||||
| 
 | ||||
| # Persönliches Skript (deine alte Datei /var/vmail/sieve/%u.sieve) | ||||
| sieve_script personal { | ||||
|   driver      = file | ||||
|   # Verzeichnis mit (evtl. mehreren) Sieve-Skripten des Users | ||||
|   path        = /var/vmail/sieve/%{user}/ | ||||
|   # Aktives Skript (entspricht früher "sieve = /var/vmail/sieve/%u.sieve") | ||||
|   active_path = /var/vmail/sieve/%{user}.sieve | ||||
| } | ||||
| 
 | ||||
| # Globales After-Skript (dein früheres "sieve_after = …") | ||||
| sieve_script after { | ||||
|   type   = after | ||||
|   driver = file | ||||
|   path   = /var/vmail/sieve/global/spam-to-folder.sieve | ||||
|    mail_plugins = $mail_plugins sieve | ||||
| }  | ||||
| protocol sieve { | ||||
|   plugin { | ||||
|     sieve = /var/vmail/sieve/%u.sieve | ||||
|     sieve_storage = /var/vmail/sieve/%u/ | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| # fulltext search | ||||
| language en { | ||||
| plugin { | ||||
|     fts = xapian | ||||
|     fts_xapian = partial=3 full=20 verbose=0 | ||||
|     fts_autoindex = yes | ||||
|     fts_enforced = yes | ||||
|     # Index attachements | ||||
|     fts_decoder = decode2text | ||||
| } | ||||
| language de { | ||||
|   default = yes | ||||
| } | ||||
| language_tokenizers =  generic email-address | ||||
| 
 | ||||
| fts flatcurve { | ||||
|   substring_search = yes | ||||
|   # rotate_count     = 5000   # DB-Rotation nach X Mails | ||||
|   # rotate_time      = 5s     # oder zeitbasiert rotieren | ||||
|   # optimize_limit   = 10 | ||||
|   # min_term_size    = 3 | ||||
| } | ||||
| 
 | ||||
| fts_autoindex = yes | ||||
| fts_decoder_driver = script | ||||
| fts_decoder_script_socket_path = decode2text | ||||
| 
 | ||||
| service indexer-worker { | ||||
|   process_limit = ${indexer_cores} | ||||
|   vsz_limit = ${indexer_ram}M | ||||
|     vsz_limit = ${indexer_ram} | ||||
| } | ||||
| service decode2text { | ||||
|     executable = script /usr/local/libexec/dovecot/decode2text.sh | ||||
|  | @ -169,39 +111,24 @@ service decode2text { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| mailbox Junk { | ||||
|   sieve_script learn_spam { | ||||
|     driver = file | ||||
|     type   = before | ||||
|     cause  = copy | ||||
|     path   = /var/vmail/sieve/global/learn-spam.sieve | ||||
|   } | ||||
| } | ||||
| # spam filter | ||||
| plugin { | ||||
|   sieve_plugins = sieve_imapsieve sieve_extprograms | ||||
|   sieve_dir = /var/vmail/sieve/%u/ | ||||
|   sieve = /var/vmail/sieve/%u.sieve | ||||
|   sieve_pipe_bin_dir = /var/vmail/sieve/bin | ||||
|   sieve_extensions = +vnd.dovecot.pipe  | ||||
| 
 | ||||
| imapsieve_from Junk { | ||||
|   sieve_script learn_ham { | ||||
|     driver = file | ||||
|     type   = before | ||||
|     cause  = copy | ||||
|     path   = /var/vmail/sieve/global/learn-ham.sieve | ||||
|   } | ||||
| } | ||||
|   sieve_after = /var/vmail/sieve/global/spam-to-folder.sieve | ||||
| 
 | ||||
| # Extprograms-Plugin einschalten | ||||
| sieve_plugins { | ||||
|   sieve_extprograms = yes | ||||
| } | ||||
|   # From elsewhere to Spam folder | ||||
|   imapsieve_mailbox1_name = Junk | ||||
|   imapsieve_mailbox1_causes = COPY | ||||
|   imapsieve_mailbox1_before = file:/var/vmail/sieve/global/learn-spam.sieve | ||||
| 
 | ||||
| # Welche Sieve-Erweiterungen dürfen genutzt werden? | ||||
| # Empfehlung: nur global erlauben (nicht in User-Skripten): | ||||
| sieve_global_extensions { | ||||
|   vnd.dovecot.pipe = yes | ||||
|   # vnd.dovecot.filter = yes      # nur falls gebraucht | ||||
|   # vnd.dovecot.execute = yes     # nur falls gebraucht | ||||
|   # From Spam folder to elsewhere | ||||
|   imapsieve_mailbox2_name = * | ||||
|   imapsieve_mailbox2_from = Junk | ||||
|   imapsieve_mailbox2_causes = COPY | ||||
|   imapsieve_mailbox2_before = file:/var/vmail/sieve/global/learn-ham.sieve | ||||
| } | ||||
| 
 | ||||
| # Verzeichnis mit deinen Skripten/Binaries für :pipe | ||||
| sieve_pipe_bin_dir = /var/vmail/sieve/bin | ||||
| # (optional, analog für :filter / :execute) | ||||
| # sieve_filter_bin_dir  = /var/vmail/sieve/filter | ||||
| # sieve_execute_bin_dir = /var/vmail/sieve/execute | ||||
|  | @ -20,10 +20,6 @@ directories = { | |||
|         'owner': 'vmail', | ||||
|         'group': 'vmail', | ||||
|     }, | ||||
|     '/var/vmail/index': { | ||||
|         'owner': 'vmail', | ||||
|         'group': 'vmail', | ||||
|     }, | ||||
|     '/var/vmail/sieve': { | ||||
|         'owner': 'vmail', | ||||
|         'group': 'vmail', | ||||
|  | @ -44,16 +40,6 @@ files = { | |||
|         'context': { | ||||
|             'admin_email': node.metadata.get('mailserver/admin_email'), | ||||
|             'indexer_ram': node.metadata.get('dovecot/indexer_ram'), | ||||
|             'config_version': node.metadata.get('dovecot/config_version'), | ||||
|             'storage_version': node.metadata.get('dovecot/storage_version'), | ||||
|             'maildir': node.metadata.get('mailserver/maildir'), | ||||
|             'hostname': node.metadata.get('mailserver/hostname'), | ||||
|             'db_host': node.metadata.get('mailserver/database/host'), | ||||
|             'db_name': node.metadata.get('mailserver/database/name'), | ||||
|             'db_user': node.metadata.get('mailserver/database/user'), | ||||
|             'db_password': node.metadata.get('mailserver/database/password'), | ||||
|             'indexer_cores': node.metadata.get('vm/cores'), | ||||
|             'indexer_ram': node.metadata.get('vm/ram')//2, | ||||
|         }, | ||||
|         'needs': { | ||||
|             'pkg_apt:' | ||||
|  | @ -62,9 +48,29 @@ files = { | |||
|             'svc_systemd:dovecot:restart', | ||||
|         }, | ||||
|     }, | ||||
|     '/etc/dovecot/dovecot-sql.conf': { | ||||
|         'content_type': 'mako', | ||||
|         'context': node.metadata.get('mailserver/database'), | ||||
|         'needs': { | ||||
|             'pkg_apt:' | ||||
|         }, | ||||
|         'triggers': { | ||||
|             'svc_systemd:dovecot:restart', | ||||
|         }, | ||||
|     }, | ||||
|     '/etc/dovecot/dhparam.pem': { | ||||
|         'content_type': 'any', | ||||
|     }, | ||||
|     '/etc/dovecot/dovecot-sql.conf': { | ||||
|         'content_type': 'mako', | ||||
|         'context': node.metadata.get('mailserver/database'), | ||||
|         'needs': { | ||||
|             'pkg_apt:' | ||||
|         }, | ||||
|         'triggers': { | ||||
|             'svc_systemd:dovecot:restart', | ||||
|         }, | ||||
|     }, | ||||
|     '/var/vmail/sieve/global/spam-to-folder.sieve': { | ||||
|         'owner': 'vmail', | ||||
|         'group': 'vmail', | ||||
|  | @ -121,6 +127,7 @@ svc_systemd = { | |||
|             'action:letsencrypt_update_certificates', | ||||
|             'action:dovecot_generate_dhparam', | ||||
|             'file:/etc/dovecot/dovecot.conf', | ||||
|             'file:/etc/dovecot/dovecot-sql.conf', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  |  | |||
|  | @ -8,31 +8,20 @@ defaults = { | |||
|             'dovecot-sieve':        {}, | ||||
|             'dovecot-managesieved': {}, | ||||
|             # fulltext search | ||||
|             'dovecot-flatcurve':    {}, # buster-backports | ||||
|             'dovecot-fts-xapian':   {}, # buster-backports | ||||
|             'poppler-utils':        {}, # pdftotext | ||||
|             'catdoc':               {}, # catdoc, catppt, xls2csv | ||||
|         }, | ||||
|     }, | ||||
|     'dovecot': { | ||||
|         'database': { | ||||
|             'dbname': 'mailserver', | ||||
|             'dbuser': 'mailserver', | ||||
|         }, | ||||
|     }, | ||||
|     'letsencrypt': { | ||||
|         'reload_after': { | ||||
|             'dovecot', | ||||
|         }, | ||||
|     }, | ||||
|     'nftables': { | ||||
|         'input': { | ||||
|             'tcp dport {143, 993, 4190} accept', | ||||
|         }, | ||||
|     }, | ||||
|     'systemd-timers': { | ||||
|         'dovecot-optimize-index': { | ||||
|             'command': '/usr/bin/doveadm fts optimize -A', | ||||
|             'when': 'daily', | ||||
|     'dovecot': { | ||||
|         'database': { | ||||
|             'dbname': 'mailserver', | ||||
|             'dbuser': 'mailserver', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  |  | |||
							
								
								
									
										6
									
								
								bundles/download-server/items.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								bundles/download-server/items.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| # directories = { | ||||
| #     '/var/lib/downloads': { | ||||
| #         'owner': 'downloads', | ||||
| #         'group': 'www-data', | ||||
| #     } | ||||
| # } | ||||
|  | @ -1,23 +0,0 @@ | |||
| Pg Pass workaround: set manually: | ||||
| 
 | ||||
| ``` | ||||
| root@freescout /ro psql freescout | ||||
| psql (15.6 (Debian 15.6-0+deb12u1)) | ||||
| Type "help" for help. | ||||
| 
 | ||||
| freescout=# \password freescout | ||||
| Enter new password for user "freescout": | ||||
| Enter it again: | ||||
| freescout=# | ||||
| \q | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| # problems | ||||
| 
 | ||||
| # check if /opt/freescout/.env is resettet | ||||
| # ckeck `psql -h localhost -d freescout -U freescout -W`with pw from .env | ||||
| # chown -R www-data:www-data /opt/freescout | ||||
| # sudo su - www-data -c 'php /opt/freescout/artisan freescout:clear-cache' -s /bin/bash | ||||
| # javascript funny? `sudo su - www-data -c 'php /opt/freescout/artisan storage:link' -s /bin/bash` | ||||
| # benutzer bilder weg? aus dem backup holen: `/opt/freescout/.zfs/snapshot/zfs-auto-snap_hourly-2024-11-22-1700/storage/app/public/users` `./customers` | ||||
|  | @ -1,66 +0,0 @@ | |||
| # https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide | ||||
| run_as = repo.libs.tools.run_as | ||||
| php_version = node.metadata.get('php/version') | ||||
| 
 | ||||
| 
 | ||||
| directories = { | ||||
|     '/opt/freescout': { | ||||
|         'owner': 'www-data', | ||||
|         'group': 'www-data', | ||||
|         # chown -R www-data:www-data /opt/freescout | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| actions = { | ||||
|     # 'clone_freescout': { | ||||
|     #     'command': run_as('www-data', 'git clone https://github.com/freescout-helpdesk/freescout.git /opt/freescout'), | ||||
|     #     'unless': 'test -e /opt/freescout/.git', | ||||
|     #     'needs': [ | ||||
|     #         'pkg_apt:git', | ||||
|     #         'directory:/opt/freescout', | ||||
|     #     ], | ||||
|     # }, | ||||
|     # 'pull_freescout': { | ||||
|     #     'command': run_as('www-data', 'git -C /opt/freescout fetch origin dist && git -C /opt/freescout reset --hard origin/dist && git -C /opt/freescout clean -f'), | ||||
|     #     'unless': run_as('www-data', 'git -C /opt/freescout fetch origin && git -C /opt/freescout status -uno | grep -q "Your branch is up to date"'), | ||||
|     #     'needs': [ | ||||
|     #         'action:clone_freescout', | ||||
|     #     ], | ||||
|     #     'triggers': [ | ||||
|     #         'action:freescout_artisan_update', | ||||
|     #         f'svc_systemd:php{php_version}-fpm.service:restart', | ||||
|     #     ], | ||||
|     # }, | ||||
|     # 'freescout_artisan_update': { | ||||
|     #     'command': run_as('www-data', 'php /opt/freescout/artisan freescout:after-app-update'), | ||||
|     #     'triggered': True, | ||||
|     #     'needs': [ | ||||
|     #         f'svc_systemd:php{php_version}-fpm.service:restart', | ||||
|     #         'action:pull_freescout', | ||||
|     #     ], | ||||
|     # }, | ||||
| } | ||||
| 
 | ||||
| # svc_systemd = { | ||||
| #     f'freescout-cron.service': {}, | ||||
| # } | ||||
| 
 | ||||
| # files = { | ||||
| #     '/opt/freescout/.env': { | ||||
| #         # https://github.com/freescout-helpdesk/freescout/blob/dist/.env.example | ||||
| #         # Every time you are making changes in .env file, in order changes to take an effect you need to run: | ||||
| #         # ´sudo su - www-data -c 'php /opt/freescout/artisan freescout:clear-cache' -s /bin/bash´ | ||||
| #         'owner': 'www-data', | ||||
| #         'content': '\n'.join( | ||||
| #             f'{k}={v}' for k, v in | ||||
| #                 sorted(node.metadata.get('freescout/env').items()) | ||||
| #         ) + '\n', | ||||
| #         'needs': [ | ||||
| #             'directory:/opt/freescout', | ||||
| #             'action:clone_freescout', | ||||
| #         ], | ||||
| #     }, | ||||
| # } | ||||
| 
 | ||||
| #sudo su - www-data -s /bin/bash -c 'php /opt/freescout/artisan freescout:create-user --role admin --firstName M --lastName W --email freescout@freibrief.net --password gyh.jzv2bnf6hvc.HKG --no-interaction' | ||||
| #sudo su - www-data -s /bin/bash -c 'php /opt/freescout/artisan freescout:create-user --role admin --firstName M --lastName W --email freescout@freibrief.net --password gyh.jzv2bnf6hvc.HKG --no-interaction' | ||||
|  | @ -1,121 +0,0 @@ | |||
| from base64 import b64decode | ||||
| 
 | ||||
| # hash: SCRAM-SHA-256$4096:tQNfqQi7seqNDwJdHqCHbg==$r3ibECluHJaY6VRwpvPqrtCjgrEK7lAkgtUO8/tllTU=:+eeo4M0L2SowfyHFxT2FRqGzezve4ZOEocSIo11DATA= | ||||
| database_password = repo.vault.password_for(f'{node.name} postgresql freescout').value | ||||
| 
 | ||||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'git': {}, | ||||
|             'php': {}, | ||||
|             'php-pgsql': {}, | ||||
|             'php-fpm': {}, | ||||
|             'php-mbstring': {}, | ||||
|             'php-xml': {}, | ||||
|             'php-imap': {}, | ||||
|             'php-zip': {}, | ||||
|             'php-gd': {}, | ||||
|             'php-curl': {}, | ||||
|             'php-intl': {}, | ||||
|         }, | ||||
|     }, | ||||
|     'freescout': { | ||||
|         'env': { | ||||
|             'APP_TIMEZONE': 'Europe/Berlin', | ||||
|             'DB_CONNECTION': 'pgsql', | ||||
|             'DB_HOST': '127.0.0.1', | ||||
|             'DB_PORT': '5432', | ||||
|             'DB_DATABASE': 'freescout', | ||||
|             'DB_USERNAME': 'freescout', | ||||
|             'DB_PASSWORD': database_password, | ||||
|             'APP_KEY': 'base64:' + repo.vault.random_bytes_as_base64_for(f'{node.name} freescout APP_KEY', length=32).value | ||||
|         }, | ||||
|     }, | ||||
|     'php': { | ||||
|         'php.ini': { | ||||
|             'cgi': { | ||||
|                 'fix_pathinfo': '0', | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     'postgresql': { | ||||
|         'roles': { | ||||
|             'freescout': { | ||||
|                 'password_hash': repo.libs.postgres.generate_scram_sha_256( | ||||
|                     database_password, | ||||
|                     b64decode(repo.vault.random_bytes_as_base64_for(f'{node.name} postgres freescout', length=16).value.encode()), | ||||
|                 ), | ||||
|             }, | ||||
|         }, | ||||
|         'databases': { | ||||
|             'freescout': { | ||||
|                 'owner': 'freescout', | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     # 'systemd': { | ||||
|     #     'units': { | ||||
|     #         f'freescout-cron.service': { | ||||
|     #             'Unit': { | ||||
|     #                 'Description': 'Freescout Cron', | ||||
|     #                 'After': 'network.target', | ||||
|     #             }, | ||||
|     #             'Service': { | ||||
|     #                 'User': 'www-data', | ||||
|     #                 'Nice': 10, | ||||
|     #                 'ExecStart': f"/usr/bin/php /opt/freescout/artisan schedule:run" | ||||
|     #             }, | ||||
|     #             'Install': { | ||||
|     #                 'WantedBy': { | ||||
|     #                     'multi-user.target' | ||||
|     #                 } | ||||
|     #             }, | ||||
|     #         } | ||||
|     #     }, | ||||
|     # }, | ||||
|     'systemd-timers': { | ||||
|         'freescout-cron': { | ||||
|             'command': '/usr/bin/php /opt/freescout/artisan schedule:run', | ||||
|             'when': '*-*-* *:*:00', | ||||
|             'RuntimeMaxSec': '180', | ||||
|             'user': 'www-data', | ||||
|         }, | ||||
|     }, | ||||
|     'zfs': { | ||||
|         'datasets': { | ||||
|             'tank/freescout': { | ||||
|                 'mountpoint': '/opt/freescout', | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'freescout/env/APP_URL', | ||||
| ) | ||||
| def freescout(metadata): | ||||
|     return { | ||||
|         'freescout': { | ||||
|             'env': { | ||||
|                 'APP_URL': 'https://' + metadata.get('freescout/domain') + '/', | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'nginx/vhosts', | ||||
| ) | ||||
| def nginx(metadata): | ||||
|     return { | ||||
|         'nginx': { | ||||
|             'vhosts': { | ||||
|                 metadata.get('freescout/domain'): { | ||||
|                     'content': 'freescout/vhost.conf', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
|  | @ -8,15 +8,7 @@ defaults = { | |||
|             'python3-crcmod': {}, | ||||
|         }, | ||||
|         'sources': { | ||||
|             'google-cloud': { | ||||
|                 'url': 'https://packages.cloud.google.com/apt/', | ||||
|                 'suites': { | ||||
|                     'cloud-sdk', | ||||
|                 }, | ||||
|                 'components': { | ||||
|                     'main', | ||||
|                 }, | ||||
|             }, | ||||
|             'deb https://packages.cloud.google.com/apt cloud-sdk main', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| [DEFAULT] | ||||
| APP_NAME = ckn-gitea | ||||
| RUN_USER = git | ||||
| RUN_MODE = prod | ||||
|  | @ -14,24 +13,40 @@ MEMBERS_PAGING_NUM = 100 | |||
| 
 | ||||
| [server] | ||||
| PROTOCOL = http | ||||
| SSH_DOMAIN = ${domain} | ||||
| DOMAIN = ${domain} | ||||
| HTTP_ADDR = 0.0.0.0 | ||||
| HTTP_PORT = 3500 | ||||
| ROOT_URL = https://${domain}/ | ||||
| DISABLE_SSH = true | ||||
| SSH_PORT = 22 | ||||
| LFS_START_SERVER = true | ||||
| LFS_CONTENT_PATH = /var/lib/gitea/data/lfs | ||||
| LFS_JWT_SECRET = ${lfs_secret_key} | ||||
| OFFLINE_MODE = true | ||||
| START_SSH_SERVER = false | ||||
| DISABLE_ROUTER_LOG = true | ||||
| LANDING_PAGE = explore | ||||
| 
 | ||||
| [database] | ||||
| DB_TYPE = postgres | ||||
| HOST = ${database.get('host')}:${database.get('port')} | ||||
| NAME = ${database.get('database')} | ||||
| USER = ${database.get('username')} | ||||
| PASSWD = ${database.get('password')} | ||||
| SSL_MODE = disable | ||||
| LOG_SQL = false | ||||
| 
 | ||||
| [admin] | ||||
| DEFAULT_EMAIL_NOTIFICATIONS = onmention | ||||
| DISABLE_REGULAR_ORG_CREATION = true | ||||
| 
 | ||||
| [security] | ||||
| INTERNAL_TOKEN = ${internal_token} | ||||
| INSTALL_LOCK = true | ||||
| SECRET_KEY = ${security_secret_key} | ||||
| LOGIN_REMEMBER_DAYS = 30 | ||||
| DISABLE_GIT_HOOKS = ${str(not enable_git_hooks).lower()} | ||||
| 
 | ||||
| [openid] | ||||
| ENABLE_OPENID_SIGNIN = false | ||||
|  | @ -40,13 +55,19 @@ ENABLE_OPENID_SIGNUP = false | |||
| [service] | ||||
| REGISTER_EMAIL_CONFIRM = true | ||||
| ENABLE_NOTIFY_MAIL = true | ||||
| DISABLE_REGISTRATION = true | ||||
| DISABLE_REGISTRATION = false | ||||
| ALLOW_ONLY_EXTERNAL_REGISTRATION = false | ||||
| ENABLE_CAPTCHA = false | ||||
| REQUIRE_SIGNIN_VIEW = false | ||||
| DEFAULT_KEEP_EMAIL_PRIVATE = true | ||||
| DEFAULT_ALLOW_CREATE_ORGANIZATION = false | ||||
| DEFAULT_ENABLE_TIMETRACKING = true | ||||
| NO_REPLY_ADDRESS = noreply.${domain} | ||||
| 
 | ||||
| [mailer] | ||||
| ENABLED = true | ||||
| MAILER_TYPE = sendmail | ||||
| FROM = "${app_name}" <noreply@${domain}> | ||||
| 
 | ||||
| [session] | ||||
| PROVIDER = file | ||||
|  | @ -59,6 +80,9 @@ ENABLE_FEDERATED_AVATAR = false | |||
| MODE = console | ||||
| LEVEL = warn | ||||
| 
 | ||||
| [oauth2] | ||||
| JWT_SECRET = ${oauth_secret_key} | ||||
| 
 | ||||
| [other] | ||||
| SHOW_FOOTER_BRANDING = true | ||||
| SHOW_FOOTER_TEMPLATE_LOAD_TIME = false | ||||
|  | @ -66,10 +90,3 @@ SHOW_FOOTER_TEMPLATE_LOAD_TIME = false | |||
| [webhook] | ||||
| ALLOWED_HOST_LIST = * | ||||
| DELIVER_TIMEOUT = 600 | ||||
| 
 | ||||
| [indexer] | ||||
| REPO_INDEXER_ENABLED = true | ||||
| MAX_FILE_SIZE = 10240000 | ||||
| 
 | ||||
| [queue.issue_indexer] | ||||
| LENGTH = 20 | ||||
|  |  | |||
|  | @ -1,15 +1,8 @@ | |||
| from os.path import join | ||||
| from bundlewrap.utils.dicts import merge_dict | ||||
| 
 | ||||
| 
 | ||||
| version = node.metadata.get('gitea/version') | ||||
| assert not version.startswith('v') | ||||
| arch = node.metadata.get('system/architecture') | ||||
| version = version=node.metadata.get('gitea/version') | ||||
| 
 | ||||
| downloads['/usr/local/bin/gitea'] = { | ||||
|     # https://forgejo.org/releases/ | ||||
|     'url': f'https://codeberg.org/forgejo/forgejo/releases/download/v{version}/forgejo-{version}-linux-{arch}', | ||||
|     'sha256_url': '{url}.sha256', | ||||
|     'url': f'https://dl.gitea.io/gitea/{version}/gitea-{version}-linux-amd64', | ||||
|     'sha256': node.metadata.get('gitea/sha256'), | ||||
|     'triggers': { | ||||
|         'svc_systemd:gitea:restart', | ||||
|     }, | ||||
|  | @ -41,15 +34,9 @@ actions = { | |||
| } | ||||
| 
 | ||||
| files['/etc/gitea/app.ini'] = { | ||||
|     'content': repo.libs.ini.dumps( | ||||
|         merge_dict( | ||||
|             repo.libs.ini.parse(open(join(repo.path, 'bundles', 'gitea', 'files', 'app.ini')).read()), | ||||
|             node.metadata.get('gitea/conf'), | ||||
|         ), | ||||
|     ), | ||||
|     'content_type': 'mako', | ||||
|     'owner': 'git', | ||||
|     'mode': '0600', | ||||
|     'context': node.metadata.get('gitea'), | ||||
|     'context': node.metadata['gitea'], | ||||
|     'triggers': { | ||||
|         'svc_systemd:gitea:restart', | ||||
|     }, | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| database_password = repo.vault.password_for(f'{node.name} postgresql gitea').value | ||||
| database_password = repo.vault.password_for(f'{node.name} postgresql gitea') | ||||
| 
 | ||||
| defaults = { | ||||
|     'apt': { | ||||
|  | @ -11,20 +11,18 @@ defaults = { | |||
|         }, | ||||
|     }, | ||||
|     'gitea': { | ||||
|         'conf': { | ||||
|             'DEFAULT': { | ||||
|                 'WORK_PATH': '/var/lib/gitea', | ||||
|             }, | ||||
|             'database': { | ||||
|                 'DB_TYPE': 'postgres', | ||||
|                 'HOST': 'localhost:5432', | ||||
|                 'NAME': 'gitea', | ||||
|                 'USER': 'gitea', | ||||
|                 'PASSWD': database_password, | ||||
|                 'SSL_MODE': 'disable', | ||||
|                 'LOG_SQL': 'false', | ||||
|             }, | ||||
|         'database': { | ||||
|             'host': 'localhost', | ||||
|             'port': '5432', | ||||
|             'username': 'gitea', | ||||
|             'password': database_password, | ||||
|             'database': 'gitea', | ||||
|         }, | ||||
|         'app_name': 'Gitea', | ||||
|         'lfs_secret_key': repo.vault.password_for(f'{node.name} gitea lfs_secret_key', length=43), | ||||
|         'security_secret_key': repo.vault.password_for(f'{node.name} gitea security_secret_key'), | ||||
|         'oauth_secret_key': repo.vault.password_for(f'{node.name} gitea oauth_secret_key', length=43), | ||||
|         'internal_token': repo.vault.password_for(f'{node.name} gitea internal_token'), | ||||
|     }, | ||||
|     'postgresql': { | ||||
|         'roles': { | ||||
|  | @ -43,7 +41,8 @@ defaults = { | |||
|             'gitea.service': { | ||||
|                 'Unit': { | ||||
|                     'Description': 'gitea', | ||||
|                     'After': {'syslog.target', 'network.target'}, | ||||
|                     'After': 'syslog.target', | ||||
|                     'After': 'network.target', | ||||
|                     'Requires': 'postgresql.service', | ||||
|                 }, | ||||
|                 'Service': { | ||||
|  | @ -67,40 +66,21 @@ defaults = { | |||
|             'home': '/home/git', | ||||
|         }, | ||||
|     }, | ||||
|     'zfs': { | ||||
|         'datasets': { | ||||
|             'tank/gitea': { | ||||
|                 'mountpoint': '/var/lib/gitea', | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'gitea/conf', | ||||
|     'zfs/datasets', | ||||
| ) | ||||
| def conf(metadata): | ||||
|     domain = metadata.get('gitea/domain') | ||||
| def zfs(metadata): | ||||
|     if not node.has_bundle('zfs'): | ||||
|         return {} | ||||
| 
 | ||||
|     return { | ||||
|         'gitea': { | ||||
|             'conf': { | ||||
|                 'server': { | ||||
|                     'SSH_DOMAIN': domain, | ||||
|                     'DOMAIN': domain, | ||||
|                     'ROOT_URL': f'https://{domain}/', | ||||
|                     'LFS_JWT_SECRET': repo.vault.password_for(f'{node.name} gitea lfs_secret_key', length=43), | ||||
|                 }, | ||||
|                 'security': { | ||||
|                     'INTERNAL_TOKEN': repo.vault.password_for(f'{node.name} gitea internal_token'), | ||||
|                     'SECRET_KEY': repo.vault.password_for(f'{node.name} gitea security_secret_key'), | ||||
|                 }, | ||||
|                 'service': { | ||||
|                     'NO_REPLY_ADDRESS': f'noreply.{domain}', | ||||
|                 }, | ||||
|                 'oauth2': { | ||||
|                     'JWT_SECRET': repo.vault.password_for(f'{node.name} gitea oauth_secret_key', length=43), | ||||
|         'zfs': { | ||||
|             'datasets': { | ||||
|                 f"{metadata.get('zfs/storage_classes/ssd')}/gitea": { | ||||
|                     'mountpoint': '/var/lib/gitea', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|  | @ -118,7 +98,7 @@ def nginx(metadata): | |||
|                     'content': 'nginx/proxy_pass.conf', | ||||
|                     'context': { | ||||
|                         'target': 'http://127.0.0.1:3500', | ||||
|                     }, | ||||
|                     } | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|  |  | |||
|  | @ -18,17 +18,16 @@ admin_password = node.metadata.get('grafana/config/security/admin_password') | |||
| port = node.metadata.get('grafana/config/server/http_port') | ||||
| actions['reset_grafana_admin_password'] = { | ||||
|     'command': f"grafana-cli admin reset-admin-password {quote(admin_password)}", | ||||
|     'unless': f"sleep 5 && curl http://admin:{quote(admin_password)}@localhost:{port}/api/org --fail", | ||||
|     'unless': f"curl http://admin:{quote(admin_password)}@localhost:{port}/api/org", | ||||
|     'needs': [ | ||||
|         'svc_systemd:grafana-server', | ||||
|     ], | ||||
| } | ||||
| 
 | ||||
| directories = { | ||||
|     '/etc/grafana': {}, | ||||
|     '/etc/grafana': { | ||||
|     }, | ||||
|     '/etc/grafana/provisioning': { | ||||
|         'owner': 'grafana', | ||||
|         'group': 'grafana', | ||||
|     }, | ||||
|     '/etc/grafana/provisioning/datasources': { | ||||
|         'purge': True, | ||||
|  | @ -36,13 +35,8 @@ directories = { | |||
|     '/etc/grafana/provisioning/dashboards': { | ||||
|         'purge': True, | ||||
|     }, | ||||
|     '/var/lib/grafana': { | ||||
|         'owner': 'grafana', | ||||
|         'group': 'grafana', | ||||
|     }, | ||||
|     '/var/lib/grafana': {}, | ||||
|     '/var/lib/grafana/dashboards': { | ||||
|         'owner': 'grafana', | ||||
|         'group': 'grafana', | ||||
|         'purge': True, | ||||
|         'triggers': [ | ||||
|             'svc_systemd:grafana-server:restart', | ||||
|  | @ -53,8 +47,6 @@ directories = { | |||
| files = { | ||||
|     '/etc/grafana/grafana.ini': { | ||||
|         'content': repo.libs.ini.dumps(node.metadata.get('grafana/config')), | ||||
|         'owner': 'grafana', | ||||
|         'group': 'grafana', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:grafana-server:restart', | ||||
|         ], | ||||
|  | @ -64,8 +56,6 @@ files = { | |||
|             'apiVersion': 1, | ||||
|             'datasources': list(node.metadata.get('grafana/datasources').values()), | ||||
|         }), | ||||
|         'owner': 'grafana', | ||||
|         'group': 'grafana', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:grafana-server:restart', | ||||
|         ], | ||||
|  | @ -82,8 +72,6 @@ files = { | |||
|                 }, | ||||
|             }], | ||||
|         }), | ||||
|         'owner': 'grafana', | ||||
|         'group': 'grafana', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:grafana-server:restart', | ||||
|         ], | ||||
|  | @ -172,8 +160,6 @@ for dashboard_id, monitored_node in enumerate(monitored_nodes, start=1): | |||
| 
 | ||||
|     files[f'/var/lib/grafana/dashboards/{monitored_node.name}.json'] = { | ||||
|         'content': json.dumps(dashboard, indent=4), | ||||
|         'owner': 'grafana', | ||||
|         'group': 'grafana', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:grafana-server:restart', | ||||
|         ] | ||||
|  |  | |||
|  | @ -8,33 +8,16 @@ defaults = { | |||
|             'grafana': {}, | ||||
|         }, | ||||
|         'sources': { | ||||
|             'grafana': { | ||||
|                 'urls': { | ||||
|                     'https://packages.grafana.com/oss/deb', | ||||
|                 }, | ||||
|                 'suites': { | ||||
|                     'stable', | ||||
|                 }, | ||||
|                 'components': { | ||||
|                     'main', | ||||
|                 }, | ||||
|             }, | ||||
|             'deb https://packages.grafana.com/oss/deb stable main', | ||||
|         }, | ||||
| 
 | ||||
|     }, | ||||
|     'grafana': { | ||||
|         'config': { | ||||
|             'server': { | ||||
|                 'http_port': 8300, | ||||
|                 'http_addr': '127.0.0.1', | ||||
|                 'enable_gzip': True, | ||||
|             }, | ||||
|             'database': { | ||||
|                 'type': 'postgres', | ||||
|                 'host': '127.0.0.1:5432', | ||||
|                 'name': 'grafana', | ||||
|                 'user': 'grafana', | ||||
|                 'password': postgres_password, | ||||
|                 'url': f'postgres://grafana:{postgres_password}@localhost:5432/grafana', | ||||
|             }, | ||||
|             'remote_cache': { | ||||
|                 'type': 'redis', | ||||
|  | @ -83,7 +66,7 @@ def domain(metadata): | |||
|                     'domain': metadata.get('grafana/hostname'), | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         },     | ||||
|     } | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|  | @ -91,7 +74,7 @@ def domain(metadata): | |||
| ) | ||||
| def influxdb2(metadata): | ||||
|     influxdb_metadata = repo.get_node(metadata.get('grafana/influxdb_node')).metadata.get('influxdb') | ||||
| 
 | ||||
|      | ||||
|     return { | ||||
|         'grafana': { | ||||
|             'datasources': { | ||||
|  | @ -110,7 +93,7 @@ def influxdb2(metadata): | |||
|                     'isDefault': True, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         },     | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -123,7 +106,7 @@ def datasource_key_to_name(metadata): | |||
|             'datasources': { | ||||
|                 name: {'name': name} for name in metadata.get('grafana/datasources').keys() | ||||
|             }, | ||||
|         }, | ||||
|         },     | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -133,19 +116,17 @@ def datasource_key_to_name(metadata): | |||
| def dns(metadata): | ||||
|     return { | ||||
|         'dns': { | ||||
|             metadata.get('grafana/hostname'): repo.libs.ip.get_a_records(metadata), | ||||
|             metadata.get('grafana/hostname'): repo.libs.dns.get_a_records(metadata), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'nginx/has_websockets', | ||||
|     'nginx/vhosts', | ||||
| ) | ||||
| def nginx(metadata): | ||||
|     return { | ||||
|         'nginx': { | ||||
|             'has_websockets': True, | ||||
|             'vhosts': { | ||||
|                 metadata.get('grafana/hostname'): { | ||||
|                     'content': 'grafana/vhost.conf', | ||||
|  |  | |||
							
								
								
									
										20
									
								
								bundles/homeassistant/items.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								bundles/homeassistant/items.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| users = { | ||||
|     'homeassistant': { | ||||
|         'home': '/var/lib/homeassistant', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| directories = { | ||||
|     '/var/lib/homeassistant': { | ||||
|         'owner': 'homeassistant', | ||||
|     }, | ||||
|     '/var/lib/homeassistant/config': { | ||||
|         'owner': 'homeassistant', | ||||
|     }, | ||||
|     '/var/lib/homeassistant/venv': { | ||||
|         'owner': 'homeassistant', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| # https://wiki.instar.com/de/Software/Linux/Home_Assistant/ | ||||
							
								
								
									
										9
									
								
								bundles/homeassistant/metadata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								bundles/homeassistant/metadata.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'python3-dev': {}, | ||||
|             'python3-pip': {}, | ||||
|             'python3-venv': {}, | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
|  | @ -23,6 +23,6 @@ def hostname_file(metadata): | |||
| def dns(metadata): | ||||
|     return { | ||||
|         'dns': { | ||||
|             metadata.get('hostname'): repo.libs.ip.get_a_records(metadata), | ||||
|             metadata.get('hostname'): repo.libs.dns.get_a_records(metadata, external=False), | ||||
|         }, | ||||
|     } | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ header_margin=1 | |||
| detailed_cpu_time=0 | ||||
| cpu_count_from_one=1 | ||||
| show_cpu_usage=0 | ||||
| show_cpu_frequency=1 | ||||
| show_cpu_frequency=0 | ||||
| show_cpu_temperature=0 | ||||
| degree_fahrenheit=0 | ||||
| update_process_names=0 | ||||
|  |  | |||
|  | @ -2,7 +2,11 @@ | |||
| 
 | ||||
| UNKNOWN=3 | ||||
| 
 | ||||
| if [ -z "$SSHMON_COMMAND" ] | ||||
| if [ -z "$SSHMON_TEST" ] | ||||
| then | ||||
|     echo 'check_by_sshmon: Env SSHMON_TEST missing' >&2 | ||||
|     exit $UNKNOWN | ||||
| elif [ -z "$SSHMON_COMMAND" ] | ||||
| then | ||||
|     echo 'check_by_sshmon: Env SSHMON_COMMAND missing' >&2 | ||||
|     exit $UNKNOWN | ||||
|  | @ -12,14 +16,7 @@ then | |||
|     exit $UNKNOWN | ||||
| fi | ||||
| 
 | ||||
| if [ -z "$SSHMON_SUDO" ] | ||||
| then | ||||
|     PREFIX="" | ||||
| else | ||||
|     PREFIX="sudo " | ||||
| fi | ||||
| 
 | ||||
| ssh sshmon@"$SSHMON_HOST" "$PREFIX$SSHMON_COMMAND" | ||||
| ssh sshmon@"$SSHMON_HOST" "sudo $SSHMON_COMMAND" | ||||
| 
 | ||||
| exitcode=$? | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,9 +12,9 @@ object CheckCommand "sshmon" { | |||
| 
 | ||||
|     command = [ "/usr/lib/nagios/plugins/check_by_sshmon" ] | ||||
| 
 | ||||
|     env.SSHMON_TEST = "1234" | ||||
|     env.SSHMON_COMMAND = "$command$" | ||||
|     env.SSHMON_HOST = "$address$" | ||||
|     env.SSHMON_SUDO = "$sudo$" | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,9 +13,9 @@ apply Notification "mail-icingaadmin" to Host { | |||
|   user_groups = host.vars.notification.mail.groups | ||||
|   users = host.vars.notification.mail.users | ||||
| 
 | ||||
|   //interval = 2h | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   //vars.notification_logtosyslog = true | ||||
| 
 | ||||
|   assign where host.vars.notification.mail | ||||
| } | ||||
|  | @ -25,9 +25,9 @@ apply Notification "mail-icingaadmin" to Service { | |||
|   user_groups = host.vars.notification.mail.groups | ||||
|   users = host.vars.notification.mail.users | ||||
| 
 | ||||
|   //interval = 2h | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   //vars.notification_logtosyslog = true | ||||
| 
 | ||||
|   assign where host.vars.notification.mail | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +0,0 @@ | |||
| /** | ||||
|  * The JournaldLogger type writes log information to the systemd journal. | ||||
|  */ | ||||
| 
 | ||||
| object JournaldLogger "journald" { | ||||
|     severity = "warning" | ||||
| } | ||||
|  | @ -1,4 +0,0 @@ | |||
| /** | ||||
|  * This file is requires for inital apt install. | ||||
|  * The JournaldLogger type writes log information to the systemd journal. | ||||
|  */ | ||||
							
								
								
									
										3
									
								
								bundles/icinga2/files/features/syslog.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								bundles/icinga2/files/features/syslog.conf
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| object SyslogLogger "syslog" { | ||||
|     severity = "warning" | ||||
| } | ||||
|  | @ -14,8 +14,7 @@ | |||
|             if key.endswith('_interval'): | ||||
|                 return value | ||||
|             else: | ||||
|                 escaped_value = value.replace('$', '$$').replace('"', '\\"') | ||||
|                 return f'"{escaped_value}"' | ||||
|                 return f'"{value}"' | ||||
|         elif isinstance(value, (list, set)): | ||||
|             return '[' + ', '.join(render_value(e) for e in sorted(value)) + ']' | ||||
|         else: | ||||
|  |  | |||
|  | @ -5,6 +5,6 @@ include <itl> | |||
| include <plugins> | ||||
| include <plugins-contrib> | ||||
| 
 | ||||
| include "features-enabled/*.conf" | ||||
| include "features.d/*.conf" | ||||
| include_recursive "conf.d" | ||||
| include "hosts.d/*.conf" | ||||
|  |  | |||
|  | @ -10,24 +10,6 @@ directories = { | |||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|    '/etc/icinga2/pki': { # required for apt install | ||||
|        'purge': True, | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'mode': '0750', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|    '/etc/icinga2/zones.d': { # required for apt install | ||||
|        'purge': True, | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'mode': '0750', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icinga2/conf.d': { | ||||
|         'purge': True, | ||||
|         'owner': 'nagios', | ||||
|  | @ -46,16 +28,7 @@ directories = { | |||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icinga2/features-available': { | ||||
|         'purge': True, | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'mode': '0750', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icinga2/features-enabled': { | ||||
|     '/etc/icinga2/features.d': { | ||||
|         'purge': True, | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|  | @ -196,6 +169,50 @@ files = { | |||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icinga2/features.d/ido-pgsql.conf': { | ||||
|         'source': 'features/ido-pgsql.conf', | ||||
|         'content_type': 'mako', | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'context': { | ||||
|             'db_password': node.metadata.get('postgresql/roles/icinga2/password') | ||||
|         }, | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icinga2/features.d/syslog.conf': { | ||||
|         'source': 'features/syslog.conf', | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icinga2/features.d/notification.conf': { | ||||
|         'source': 'features/notification.conf', | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icinga2/features.d/checker.conf': { | ||||
|         'source': 'features/checker.conf', | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icinga2/features.d/api.conf': { | ||||
|         'source': 'features/api.conf', | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/var/lib/icinga2/certs/ca.crt': { | ||||
|         'content_type': 'download', | ||||
|         'source': f'https://letsencrypt.org/certs/isrg-root-x1-cross-signed.pem', | ||||
|  | @ -210,39 +227,6 @@ files = { | |||
|     }, | ||||
| } | ||||
| 
 | ||||
| # FEATURES | ||||
| 
 | ||||
| for feature, context in { | ||||
|     'mainlog': {}, | ||||
| #    'journald': {}, FIXME | ||||
|     'notification': {}, | ||||
|     'checker': {}, | ||||
|     'api': {}, | ||||
|     'ido-pgsql': { | ||||
|         'db_password': node.metadata.get('postgresql/roles/icinga2/password'), | ||||
|     }, | ||||
| }.items(): | ||||
|     files[f'/etc/icinga2/features-available/{feature}.conf'] = { | ||||
|         'content_type': 'mako' if context else 'text', | ||||
|         'context':  context, | ||||
|         'source': f'features/{feature}.conf', | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     } | ||||
|     symlinks[f'/etc/icinga2/features-enabled/{feature}.conf'] = { | ||||
|         'target': f'/etc/icinga2/features-available/{feature}.conf', | ||||
|         'owner': 'nagios', | ||||
|         'group': 'nagios', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:icinga2.service:restart', | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
| # HOSTS | ||||
| 
 | ||||
| for other_node in repo.nodes: | ||||
|     if other_node.dummy: | ||||
|         continue | ||||
|  | @ -269,7 +253,7 @@ svc_systemd = { | |||
|     'icinga2.service': { | ||||
|         'needs': [ | ||||
|             'pkg_apt:icinga2-ido-pgsql', | ||||
|             'svc_systemd:postgresql.service', | ||||
|             'svc_systemd:postgresql', | ||||
|         ], | ||||
|     }, | ||||
| } | ||||
|  |  | |||
|  | @ -9,21 +9,7 @@ defaults = { | |||
|             'monitoring-plugins': {}, | ||||
|         }, | ||||
|         'sources': { | ||||
|             'icinga': { | ||||
|                 'types': { | ||||
|                     'deb', | ||||
|                     'deb-src', | ||||
|                 }, | ||||
|                 'urls': { | ||||
|                     'https://packages.icinga.com/debian', | ||||
|                 }, | ||||
|                 'suites': { | ||||
|                     'icinga-{codename}', | ||||
|                 }, | ||||
|                 'components': { | ||||
|                     'main', | ||||
|                 }, | ||||
|             }, | ||||
|             'deb https://packages.icinga.com/debian icinga-{release} main', | ||||
|         }, | ||||
|     }, | ||||
|     'icinga2': { | ||||
|  | @ -34,11 +20,6 @@ defaults = { | |||
|             } | ||||
|         }, | ||||
|     }, | ||||
|     'nftables': { | ||||
|         'input': { | ||||
|             'tcp dport 5665 accept', | ||||
|         }, | ||||
|     }, | ||||
|     'postgresql': { | ||||
|         'databases': { | ||||
|             'icinga2': { | ||||
|  | @ -63,6 +44,7 @@ defaults = { | |||
|                 'mountpoint': '/var/lib/icinga2', | ||||
|                 'needed_by': { | ||||
|                     'pkg_apt:icinga2', | ||||
|                     'pkg_apt:icingaweb2', | ||||
|                     'pkg_apt:icinga2-ido-pgsql', | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -72,7 +54,7 @@ defaults = { | |||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'letsencrypt/domains', | ||||
|     'nginx/vhosts', | ||||
| ) | ||||
| def letsencrypt(metadata): | ||||
|     return { | ||||
|  |  | |||
							
								
								
									
										14
									
								
								bundles/icingadb/items.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								bundles/icingadb/items.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| import yaml, json | ||||
| from bundlewrap.metadata import MetadataJSONEncoder | ||||
| 
 | ||||
| files = { | ||||
|     '/etc/icingadb/config.yml': { | ||||
|         'content': yaml.dump( | ||||
|             json.loads( | ||||
|                 json.dumps(node.metadata.get('icingadb'), sort_keys=True, cls=MetadataJSONEncoder) | ||||
|             ), | ||||
|         ), | ||||
|         'mode': '0640', | ||||
|         'owner': 'icingadb', | ||||
|     }, | ||||
| } | ||||
							
								
								
									
										53
									
								
								bundles/icingadb/metadata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								bundles/icingadb/metadata.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'icingadb': {}, | ||||
|             'icingadb-redis': {}, | ||||
|             'icingadb-web': {}, | ||||
|         }, | ||||
|         'sources': { | ||||
|             'deb https://packages.icinga.com/debian icinga-{release} main', | ||||
|             'deb https://packages.icinga.com/debian icinga-{release}-snapshots main', | ||||
|         }, | ||||
|     }, | ||||
|     'postgresql': { | ||||
|         'databases': { | ||||
|             'icingadb': { | ||||
|                 'owner': 'icingadb', | ||||
|             }, | ||||
|         }, | ||||
|         'roles': { | ||||
|             'icingadb': { | ||||
|                 'password': repo.vault.password_for(f'psql icingadb on {node.name}'), | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     'redis': { | ||||
|         'icingadb': { | ||||
|             'port': '6381', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'icingadb', | ||||
| ) | ||||
| def config(metadata): | ||||
|     return { | ||||
|         'icingadb': { | ||||
|             'database': { | ||||
|                 'type': 'postgresql', | ||||
|                 'host': 'localhost', | ||||
|                 'port': 3306, | ||||
|                 'database': 'icingadb', | ||||
|                 'user': 'icingadb', | ||||
|                 'password': metadata.get('postgresql/roles/icingadb/password'), | ||||
|             }, | ||||
|             'redis': { | ||||
|                 'address': 'localhost:6380', | ||||
|             }, | ||||
|             'logging': { | ||||
|                 'level': 'info', | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
|  | @ -2,4 +2,3 @@ | |||
| - open /icingaweb2/setup in browser | ||||
| - fill in values from metadata | ||||
| - apply | ||||
| - make sure tls cert exists and is owned by nagios | ||||
|  |  | |||
|  | @ -4,27 +4,18 @@ directories = { | |||
|         'owner': 'www-data', | ||||
|         'group': 'icingaweb2', | ||||
|         'mode': '2770', | ||||
|         'needs': [ | ||||
|             'pkg_apt:icingaweb2', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icingaweb2/enabledModules': { | ||||
| #        'purge': True, | ||||
|         'owner': 'www-data', | ||||
|         'group': 'icingaweb2', | ||||
|         'mode': '2770', | ||||
|         'needs': [ | ||||
|             'pkg_apt:icingaweb2', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/icingaweb2/modules': { | ||||
| #        'purge': True, | ||||
|         'owner': 'www-data', | ||||
|         'group': 'icingaweb2', | ||||
|         'mode': '2770', | ||||
|         'needs': [ | ||||
|             'pkg_apt:icingaweb2', | ||||
|         ], | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
|  | @ -34,9 +25,6 @@ files = { | |||
|         'owner': 'www-data', | ||||
|         'group': 'icingaweb2', | ||||
|         'mode': '0660', | ||||
|         'needs': [ | ||||
|             'pkg_apt:icingaweb2', | ||||
|         ], | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
|  | @ -45,9 +33,6 @@ symlinks = { | |||
|         'target': '/usr/share/icingaweb2/modules/monitoring', | ||||
|         'owner': 'www-data', | ||||
|         'group': 'icingaweb2', | ||||
|         'needs': [ | ||||
|             'pkg_apt:icingaweb2', | ||||
|         ], | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
|  | @ -63,9 +48,6 @@ for name in [ | |||
|         'owner': 'www-data', | ||||
|         'group': 'icingaweb2', | ||||
|         'mode': '0660', | ||||
|         'needs': [ | ||||
|             'pkg_apt:icingaweb2', | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
| for name in [ | ||||
|  | @ -78,7 +60,4 @@ for name in [ | |||
|         'owner': 'www-data', | ||||
|         'group': 'icingaweb2', | ||||
|         'mode': '0660', | ||||
|         'needs': [ | ||||
|             'pkg_apt:icingaweb2', | ||||
|         ], | ||||
|     } | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ from hashlib import sha3_256 | |||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'icingaweb2': {}, | ||||
|             'php-ldap': {}, | ||||
|             'php-json': {}, | ||||
|             'php-intl': {}, | ||||
|  | @ -10,25 +11,11 @@ defaults = { | |||
|             'php-gd': {}, | ||||
|             'php-imagick': {}, | ||||
|             'php-pgsql': {}, | ||||
|             'icingaweb2': {}, | ||||
|             #'icingaweb2-module-monitoring': {}, # ? | ||||
|             'icingaweb2-module-monitoring': {}, | ||||
|         }, | ||||
|         'sources': { | ||||
|             'icinga': { | ||||
|                 'types': { | ||||
|                     'deb', | ||||
|                     'deb-src', | ||||
|                 }, | ||||
|                 'urls': { | ||||
|                     'https://packages.icinga.com/debian', | ||||
|                 }, | ||||
|                 'suites': { | ||||
|                     'icinga-{codename}', | ||||
|                 }, | ||||
|                 'components': { | ||||
|                     'main', | ||||
|                 }, | ||||
|             }, | ||||
|             'deb https://packages.icinga.com/debian icinga-{release} main', | ||||
|             'deb https://packages.icinga.com/debian icinga-{release}-snapshots main', | ||||
|         }, | ||||
|     }, | ||||
|     'icingaweb2': { | ||||
|  | @ -131,7 +118,7 @@ defaults = { | |||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'icingaweb2/hostname', | ||||
|     'icingaweb2/resources.ini/icinga_ido/password', | ||||
|     'icingaweb2/resources.ini/icinga_ido/icinga2/password', | ||||
|     'icingaweb2/monitoring/commandtransports.ini/icinga2/password', | ||||
| ) | ||||
| def stuff(metadata): | ||||
|  | @ -177,9 +164,7 @@ def nginx(metadata): | |||
|                 metadata.get('icingaweb2/hostname'): { | ||||
|                     'content': 'icingaweb2/vhost.conf', | ||||
|                     'context': { | ||||
|                         'php_version': metadata.get('php/version'), | ||||
|                     }, | ||||
|                     'check_path': '/icingaweb2/index.php', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|  |  | |||
|  | @ -1,3 +0,0 @@ | |||
| # svc_systemd = { | ||||
| #     'ifupdown.service': {}, | ||||
| # } | ||||
|  | @ -4,9 +4,8 @@ from shlex import quote | |||
| directories['/var/lib/influxdb'] = { | ||||
|     'owner': 'influxdb', | ||||
|     'group': 'influxdb', | ||||
|     'mode': '0750', | ||||
|     'needs': [ | ||||
|         'zfs_dataset:tank/influxdb', | ||||
|         f"zfs_dataset:{node.metadata.get('zfs/storage_classes/ssd')}/influxdb", | ||||
|     ], | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,22 +7,7 @@ defaults = { | |||
|             'influxdb2-cli': {}, | ||||
|         }, | ||||
|         'sources': { | ||||
|             'influxdata': { | ||||
|                 'urls': { | ||||
|                     'https://repos.influxdata.com/debian', | ||||
|                 }, | ||||
|                 'suites': { | ||||
|                     'stable', | ||||
|                 }, | ||||
|                 'components': { | ||||
|                     'main', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     'nftables': { | ||||
|         'input': { | ||||
|             'tcp dport 8200 accept', | ||||
|             'deb https://repos.influxdata.com/debian {release} stable', | ||||
|         }, | ||||
|     }, | ||||
|     'influxdb': { | ||||
|  | @ -62,7 +47,7 @@ def zfs(metadata): | |||
|     return { | ||||
|         'zfs': { | ||||
|             'datasets': { | ||||
|                 'tank/influxdb': { | ||||
|                 f"{metadata.get('zfs/storage_classes/ssd')}/influxdb": { | ||||
|                     'mountpoint': '/var/lib/influxdb', | ||||
|                     'recordsize': '8192', | ||||
|                     'atime': 'off', | ||||
|  | @ -78,7 +63,7 @@ def zfs(metadata): | |||
| def dns(metadata): | ||||
|     return { | ||||
|         'dns': { | ||||
|             metadata.get('influxdb/hostname'): repo.libs.ip.get_a_records(metadata), | ||||
|             metadata.get('influxdb/hostname'): repo.libs.dns.get_a_records(metadata), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ def apt(metadata): | |||
|     return { | ||||
|         'apt': { | ||||
|             'packages': { | ||||
|                 f'openjdk-{metadata.get("java/version")}-jre-headless': {}, | ||||
|                 f'openjdk-{metadata.get("java/version")}-jre': {}, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -1,21 +0,0 @@ | |||
| from json import dumps | ||||
| from bundlewrap.metadata import MetadataJSONEncoder | ||||
| 
 | ||||
| files = { | ||||
|     '/etc/kea/kea-dhcp4.conf': { | ||||
|         'content': dumps(node.metadata.get('kea'), indent=4, sort_keys=True, cls=MetadataJSONEncoder), | ||||
|         'triggers': [ | ||||
|             'svc_systemd:kea-dhcp4-server:restart', | ||||
|         ], | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| svc_systemd = { | ||||
|     'kea-dhcp4-server': { | ||||
|         'needs': [ | ||||
|             'pkg_apt:kea-dhcp4-server', | ||||
|             'file:/etc/kea/kea-dhcp4.conf', | ||||
|             'svc_systemd:systemd-networkd.service:restart', | ||||
|         ], | ||||
|     }, | ||||
| } | ||||
|  | @ -1,97 +0,0 @@ | |||
| from ipaddress import ip_interface, ip_network | ||||
| 
 | ||||
| hashable = repo.libs.hashable.hashable | ||||
| 
 | ||||
| 
 | ||||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'kea-dhcp4-server': {}, | ||||
|         }, | ||||
|     }, | ||||
|     'kea': { | ||||
|         'Dhcp4': { | ||||
|             'interfaces-config': { | ||||
|                 'interfaces': set(), | ||||
|             }, | ||||
|             'lease-database': { | ||||
|                 'type': 'memfile', | ||||
|                 'lfc-interval': 3600 | ||||
|             }, | ||||
|             'subnet4': set(), | ||||
|             'loggers': set([ | ||||
|                 hashable({ | ||||
|                     'name': 'kea-dhcp4', | ||||
|                     'output_options': [ | ||||
|                         { | ||||
|                             'output': 'syslog', | ||||
|                         } | ||||
|                     ], | ||||
|                     'severity': 'INFO', | ||||
|                 }), | ||||
|             ]), | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'kea/Dhcp4/interfaces-config/interfaces', | ||||
|     'kea/Dhcp4/subnet4', | ||||
| ) | ||||
| def subnets(metadata): | ||||
|     subnet4 = set() | ||||
|     interfaces = set() | ||||
|     reservations = set( | ||||
|         hashable({ | ||||
|             'hw-address': network_conf['mac'], | ||||
|             'ip-address': str(ip_interface(network_conf['ipv4']).ip), | ||||
|         }) | ||||
|             for other_node in repo.nodes | ||||
|             for network_conf in other_node.metadata.get('network', {}).values() | ||||
|             if 'mac' in network_conf | ||||
|     ) | ||||
| 
 | ||||
|     for id, (network_name, network_conf) in enumerate(sorted(metadata.get('network').items())): | ||||
|         dhcp_server_config = network_conf.get('dhcp_server_config', None) | ||||
| 
 | ||||
|         if dhcp_server_config: | ||||
|             _network = ip_network(dhcp_server_config['subnet']) | ||||
| 
 | ||||
|             subnet4.add(hashable({ | ||||
|                 'id': id + 1, | ||||
|                 'subnet': dhcp_server_config['subnet'], | ||||
|                 'pools': [ | ||||
|                     { | ||||
|                         'pool': f'{dhcp_server_config['pool_from']} - {dhcp_server_config['pool_to']}', | ||||
|                     }, | ||||
|                 ], | ||||
|                 'option-data': [ | ||||
|                     { | ||||
|                         'name': 'routers', | ||||
|                         'data': dhcp_server_config['router'], | ||||
|                     }, | ||||
|                     { | ||||
|                         'name': 'domain-name-servers', | ||||
|                         'data': '10.0.0.1', | ||||
|                     }, | ||||
|                 ], | ||||
|                 'reservations': set( | ||||
|                     reservation | ||||
|                         for reservation in reservations | ||||
|                         if ip_interface(reservation['ip-address']).ip in _network | ||||
|                 ), | ||||
|             })) | ||||
| 
 | ||||
|             interfaces.add(network_conf.get('interface', network_name)) | ||||
| 
 | ||||
|     return { | ||||
|         'kea': { | ||||
|             'Dhcp4': { | ||||
|                 'interfaces-config': { | ||||
|                     'interfaces': interfaces, | ||||
|                 }, | ||||
|                 'subnet4': subnet4, | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
|  | @ -1 +1,58 @@ | |||
| https://github.com/SirPlease/L4D2-Competitive-Rework/blob/master/Dedicated%20Server%20Install%20Guide/README.md | ||||
| https://developer.valvesoftware.com/wiki/List_of_L4D2_Cvars | ||||
| 
 | ||||
| Dead Center 	c1m1_hotel | ||||
| Dead Center 	c1m2_streets | ||||
| Dead Center 	c1m3_mall | ||||
| Dead Center 	c1m4_atrium | ||||
| Dark Carnival 	c2m1_highway | ||||
| Dark Carnival 	c2m2_fairgrounds | ||||
| Dark Carnival 	c2m3_coaster | ||||
| Dark Carnival 	c2m4_barns | ||||
| Dark Carnival 	c2m5_concert | ||||
| Swamp Fever 	c3m1_plankcountry | ||||
| Swamp Fever 	c3m2_swamp | ||||
| Swamp Fever 	c3m3_shantytown | ||||
| Swamp Fever 	c3m4_plantation | ||||
| Hard Rain 	c4m1_milltown_a | ||||
| Hard Rain 	c4m2_sugarmill_a | ||||
| Hard Rain 	c4m3_sugarmill_b | ||||
| Hard Rain 	c4m4_milltown_b | ||||
| Hard Rain 	c4m5_milltown_escape | ||||
| The Parish 	c5m1_waterfront_sndscape | ||||
| The Parish 	c5m1_waterfront | ||||
| The Parish 	c5m2_park | ||||
| The Parish 	c5m3_cemetery | ||||
| The Parish 	c5m4_quarter | ||||
| The Parish 	c5m5_bridge | ||||
| The Passing 	c6m1_riverbank | ||||
| The Passing 	c6m2_bedlam | ||||
| The Passing 	c6m3_port | ||||
| The Sacrifice 	c7m1_docks | ||||
| The Sacrifice 	c7m2_barge | ||||
| The Sacrifice 	c7m3_port | ||||
| No Mercy 	c8m1_apartment | ||||
| No Mercy 	c8m2_subway | ||||
| No Mercy 	c8m3_sewers | ||||
| No Mercy 	c8m4_interior | ||||
| No Mercy 	c8m5_rooftop | ||||
| Crash Course 	c9m1_alleys | ||||
| Crash Course 	c9m2_lots | ||||
| Death Toll 	c10m1_caves | ||||
| Death Toll 	c10m2_drainage | ||||
| Death Toll 	c10m3_ranchhouse | ||||
| Death Toll 	c10m4_mainstreet | ||||
| Death Toll 	c10m5_houseboat | ||||
| Dead Air 	c11m1_greenhouse | ||||
| Dead Air 	c11m2_offices | ||||
| Dead Air 	c11m3_garage | ||||
| Dead Air 	c11m4_terminal | ||||
| Dead Air 	c11m5_runway | ||||
| Blood Harvest 	c12m1_hilltop | ||||
| Blood Harvest 	c12m2_traintunnel | ||||
| Blood Harvest 	c12m3_bridge | ||||
| Blood Harvest 	c12m4_barn | ||||
| Blood Harvest 	c12m5_cornfield | ||||
| Cold Stream 	c13m1_alpinecreek | ||||
| Cold Stream 	c13m2_southpinestream | ||||
| Cold Stream 	c13m3_memorialbridge | ||||
| Cold Stream 	c13m4_cutthroatcreek | ||||
|  |  | |||
|  | @ -1,38 +0,0 @@ | |||
| #!/bin/bash | ||||
| set -xeuo pipefail | ||||
| 
 | ||||
| function steam() { | ||||
|     # for systemd, so it can terminate the process (for other things sudo would have been enough) | ||||
|     setpriv --reuid=steam --regid=steam --init-groups "$@" <&0 | ||||
|     export HOME=/opt/l4d2/steam | ||||
| } | ||||
| 
 | ||||
| declare -A addons=( | ||||
|     [Ions_Vocalizer]=698857882 | ||||
|     [EMS_Admin_System]=2524204971 | ||||
| ) | ||||
| 
 | ||||
| function install_addon() { | ||||
|     local overlay="$1" | ||||
|     local addon_name="$2" | ||||
|     local addon_id="${addons[${addon_name}]}" | ||||
|     steam mkdir -p "/opt/l4d2/overlays/${overlay}/left4dead2/addons" | ||||
|     test -f "/opt/l4d2/overlays/${overlay}/left4dead2/addons/${addon_id}.vpk" || \ | ||||
|         steam /opt/l4d2/scripts/steam-workshop-download "${addon_id}" --out "/opt/l4d2/overlays/${overlay}/left4dead2/addons" | ||||
| } | ||||
| 
 | ||||
| function install_admin_system() { | ||||
|     local overlay="$1" | ||||
|     install_addon "${overlay}" EMS_Admin_System | ||||
|     steam mkdir -p "/opt/l4d2/overlays/${overlay}/left4dead2/ems/admin system" | ||||
|     steam echo "STEAM_1:0:12376499" > "/opt/l4d2/overlays/${overlay}/left4dead2/ems/admin system/admins.txt" | ||||
| } | ||||
| 
 | ||||
| function install_tickrate_enabler() { | ||||
|     local overlay="$1" | ||||
|     steam mkdir -p "/opt/l4d2/overlays/${overlay}/left4dead2/addons" | ||||
|     for file in tickrate_enabler.dll tickrate_enabler.so tickrate_enabler.vdf | ||||
|     do | ||||
|         curl -L "https://github.com/SirPlease/L4D2-Competitive-Rework/raw/refs/heads/master/addons/${file}" -o "/opt/l4d2/overlays/${overlay}/left4dead2/addons/${file}" | ||||
|     done | ||||
| } | ||||
|  | @ -1,10 +0,0 @@ | |||
| #!/bin/bash | ||||
| set -xeuo pipefail | ||||
| source /opt/l4d2/scripts/helpers | ||||
| overlay=$(basename "$0") | ||||
| 
 | ||||
| # https://github.com/SirPlease/L4D2-Competitive-Rework | ||||
| 
 | ||||
| steam mkdir -p /opt/l4d2/overlays/$overlay/left4dead2 | ||||
| test -d /opt/l4d2/overlays/$overlay/left4dead2/cfg/cfgogl || \ | ||||
|     curl -L https://github.com/SirPlease/L4D2-Competitive-Rework/archive/refs/heads/master.tar.gz | steam tar -xz --strip-components=1 -C /opt/l4d2/overlays/$overlay/left4dead2 | ||||
|  | @ -1,128 +0,0 @@ | |||
| #!/bin/bash | ||||
| set -xeuo pipefail | ||||
| source /opt/l4d2/scripts/helpers | ||||
| overlay=$(basename "$0") | ||||
| 
 | ||||
| steam mkdir -p /opt/l4d2/overlays/$overlay/left4dead2/addons | ||||
| cd /opt/l4d2/overlays/$overlay/left4dead2/addons | ||||
| 
 | ||||
| # https://l4d2center.com/maps/servers/l4d2center_maps_sync.sh.txt -> | ||||
| 
 | ||||
| # Exit immediately if a command exits with a non-zero status. | ||||
| set -e | ||||
| 
 | ||||
| # Function to print error messages | ||||
| error_exit() { | ||||
|     echo "Error: $1" >&2 | ||||
|     exit 1 | ||||
| } | ||||
| 
 | ||||
| # Check if the current directory ends with /left4dead2/addons | ||||
| current_dir=$(pwd) | ||||
| expected_dir="/left4dead2/addons" | ||||
| 
 | ||||
| if [[ ! "$current_dir" == *"$expected_dir" ]]; then | ||||
|     error_exit "Script must be run from your L4D2 \"addons\" folder. Current directory: $current_dir" | ||||
| fi | ||||
| 
 | ||||
| # Check for required commands | ||||
| for cmd in curl md5sum 7z; do | ||||
|     if ! command -v "$cmd" >/dev/null 2>&1; then | ||||
|         error_exit "Required command '$cmd' is not installed. Please install it and retry." | ||||
|     fi | ||||
| done | ||||
| 
 | ||||
| # URL of the CSV file | ||||
| CSV_URL="https://l4d2center.com/maps/servers/index.csv" | ||||
| 
 | ||||
| # Temporary file to store CSV | ||||
| TEMP_CSV=$(mktemp) | ||||
| 
 | ||||
| # Ensure temporary file is removed on exit | ||||
| trap 'rm -f "$TEMP_CSV"' EXIT | ||||
| 
 | ||||
| echo "Downloading CSV from $CSV_URL..." | ||||
| curl -sSL -o "$TEMP_CSV" "$CSV_URL" || error_exit "Failed to download CSV." | ||||
| 
 | ||||
| declare -A map_md5 | ||||
| declare -A map_links | ||||
| 
 | ||||
| # Read CSV and populate associative arrays | ||||
| { | ||||
|     # Skip the first line (header) | ||||
|     IFS= read -r header | ||||
| 
 | ||||
|     while IFS=';' read -r Name Size MD5 DownloadLink || [[ $Name ]]; do | ||||
|         # Trim whitespace | ||||
|         Name=$(echo "$Name" | xargs) | ||||
|         MD5=$(echo "$MD5" | xargs) | ||||
|         DownloadLink=$(echo "$DownloadLink" | xargs) | ||||
| 
 | ||||
|         # Populate associative arrays | ||||
|         map_md5["$Name"]="$MD5" | ||||
|         map_links["$Name"]="$DownloadLink" | ||||
|     done | ||||
| } < "$TEMP_CSV" | ||||
| 
 | ||||
| # Get list of expected VPK files | ||||
| expected_vpk=("${!map_md5[@]}") | ||||
| 
 | ||||
| # Remove VPK files not in expected list or with mismatched MD5 | ||||
| echo "Cleaning up existing VPK files..." | ||||
| for file in *.vpk; do | ||||
|     # Check if it's a regular file | ||||
|     if [[ -f "$file" ]]; then | ||||
|         if [[ -z "${map_md5["$file"]}" ]]; then | ||||
|             echo "Removing unexpected file: $file" | ||||
|             rm -f "$file" | ||||
|         else | ||||
|             # Calculate MD5 | ||||
|             echo "Calculating MD5 for existing file: $file..." | ||||
|             current_md5=$(md5sum "$file" | awk '{print $1}') | ||||
|             expected_md5="${map_md5["$file"]}" | ||||
| 
 | ||||
|             if [[ "$current_md5" != "$expected_md5" ]]; then | ||||
|                 echo "MD5 mismatch for $file. Removing." | ||||
|                 rm -f "$file" | ||||
|             fi | ||||
|         fi | ||||
|     fi | ||||
| done | ||||
| 
 | ||||
| # Download and extract missing or updated VPK files | ||||
| echo "Processing required VPK files..." | ||||
| for vpk in "${expected_vpk[@]}"; do | ||||
|     if [[ ! -f "$vpk" ]]; then | ||||
|         echo "Downloading and extracting $vpk..." | ||||
|         download_url="${map_links["$vpk"]}" | ||||
| 
 | ||||
|         if [[ -z "$download_url" ]]; then | ||||
|             echo "No download link found for $vpk. Skipping." | ||||
|             continue | ||||
|         fi | ||||
| 
 | ||||
|         encoded_url=$(echo "$download_url" | sed 's/ /%20/g') | ||||
| 
 | ||||
|         # Download the .7z file to a temporary location | ||||
|         TEMP_7Z=$(mktemp --suffix=.7z) | ||||
|         curl -# -L -o "$TEMP_7Z" "$encoded_url" | ||||
| 
 | ||||
|         # Check if the download was successful | ||||
|         if [[ $? -ne 0 ]]; then | ||||
|             echo "Failed to download $download_url. Skipping." | ||||
|             rm -f "$TEMP_7Z" | ||||
|             continue | ||||
|         fi | ||||
| 
 | ||||
|         # Extract the .7z file | ||||
|         7z x -y "$TEMP_7Z" || { echo "Failed to extract $TEMP_7Z. Skipping."; rm -f "$TEMP_7Z"; continue; } | ||||
| 
 | ||||
|         # Remove the temporary .7z file | ||||
|         rm -f "$TEMP_7Z" | ||||
| 
 | ||||
|     else | ||||
|         echo "$vpk is already up to date." | ||||
|     fi | ||||
| done | ||||
| 
 | ||||
| echo "Synchronization complete." | ||||
|  | @ -1,21 +0,0 @@ | |||
| #!/bin/bash | ||||
| set -xeuo pipefail | ||||
| source /opt/l4d2/scripts/helpers | ||||
| overlay=$(basename "$0") | ||||
| 
 | ||||
| # server config | ||||
| # https://github.com/SirPlease/L4D2-Competitive-Rework/blob/7ecc3a32a5e2180d6607a40119ff2f3c072502a9/cfg/server.cfg#L58-L69 | ||||
| # https://www.programmersought.com/article/513810199514/ | ||||
| steam mkdir -p /opt/l4d2/overlays/$overlay/left4dead2/cfg | ||||
| steam cat <<'EOF' > /opt/l4d2/overlays/$overlay/left4dead2/cfg/server.cfg | ||||
| # https://github.com/SirPlease/L4D2-Competitive-Rework/blob/7ecc3a32a5e2180d6607a40119ff2f3c072502a9/cfg/server.cfg#L58-L69 | ||||
| sv_minrate 100000 | ||||
| sv_maxrate 100000 | ||||
| nb_update_frequency 0.014 | ||||
| net_splitpacket_maxrate 50000 | ||||
| net_maxcleartime 0.0001 | ||||
| fps_max 0 | ||||
| EOF | ||||
| 
 | ||||
| # admin system | ||||
| install_tickrate_enabler $overlay | ||||
|  | @ -1,8 +0,0 @@ | |||
| #!/bin/bash | ||||
| set -xeuo pipefail | ||||
| source /opt/l4d2/scripts/helpers | ||||
| overlay=$(basename "$0") | ||||
| 
 | ||||
| # admin system | ||||
| install_addon $overlay Ions_Vocalizer | ||||
| install_admin_system $overlay | ||||
|  | @ -1,13 +0,0 @@ | |||
| // defaults | ||||
| hostname ${server_name} | ||||
| motd_enabled 0 | ||||
| rcon_password ${rcon_password} | ||||
| sv_steamgroup "38347879" | ||||
| 
 | ||||
| mp_autoteambalance 0 | ||||
| sv_forcepreload 1 | ||||
| 
 | ||||
| // server specific | ||||
| % for line in config: | ||||
| ${line} | ||||
| % endfor | ||||
|  | @ -1,72 +0,0 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| set -xeuo pipefail | ||||
| 
 | ||||
| # -- DEFINE FUNCTIONS AND VARIABLES -- # | ||||
| 
 | ||||
| function steam() { | ||||
|     # for systemd, so it can terminate the process (for other things sudo would have been enough) | ||||
|     setpriv --reuid=steam --regid=steam --init-groups "$@" <&0 | ||||
|     export HOME=/opt/l4d2/steam | ||||
| } | ||||
| 
 | ||||
| # -- PREPARE SYSTEM -- # | ||||
| 
 | ||||
| getent passwd steam >/dev/null || useradd -M -d /opt/l4d2 -s /bin/bash steam | ||||
| mkdir -p /opt/l4d2 /tmp/dumps | ||||
| chown steam:steam /opt/l4d2 /tmp/dumps | ||||
| dpkg --add-architecture i386 | ||||
| apt update | ||||
| DEBIAN_FRONTEND=noninteractive apt install -y libc6:i386 lib32z1 | ||||
| 
 | ||||
| # workshop downloader | ||||
| test -f /opt/l4d2/steam-workshop-download || \ | ||||
|     steam wget -4 https://git.sublimity.de/cronekorkn/steam-workshop-downloader/raw/branch/master/steam-workshop-download -P /opt/l4d2/scripts | ||||
| steam chmod +x /opt/l4d2/scripts/steam-workshop-download | ||||
| 
 | ||||
| # -- STEAM -- # | ||||
| 
 | ||||
| steam mkdir -p /opt/l4d2/steam | ||||
| test -f /opt/l4d2/steam/steamcmd_linux.tar.gz || \ | ||||
|     steam wget http://media.steampowered.com/installer/steamcmd_linux.tar.gz -P /opt/l4d2/steam | ||||
| test -f /opt/l4d2/steam/steamcmd.sh || \ | ||||
|     steam tar -xvzf /opt/l4d2/steam/steamcmd_linux.tar.gz -C /opt/l4d2/steam | ||||
| 
 | ||||
| # fix for: /opt/l4d2/.steam/sdk32/steamclient.so: cannot open shared object file: No such file or directory | ||||
| steam mkdir -p /opt/l4d2/steam/.steam # needs to be in steam users home dir | ||||
| readlink /opt/l4d2/steam/.steam/sdk32 | grep -q ^/opt/l4d2/steam/linux32$ || \ | ||||
|     steam ln -sf /opt/l4d2/steam/linux32 /opt/l4d2/steam/.steam/sdk32 | ||||
| readlink /opt/l4d2/steam/.steam/sdk64 | grep -q ^/opt/l4d2/steam/linux64$ || \ | ||||
|     steam ln -sf /opt/l4d2/steam/linux64 /opt/l4d2/steam/.steam/sdk64 | ||||
| 
 | ||||
| # -- INSTALL -- # | ||||
| 
 | ||||
| # erst die windows deps zu installieren scheint ein workaround für x64 zu sein? | ||||
| steam mkdir -p /opt/l4d2/installation | ||||
| steam /opt/l4d2/steam/steamcmd.sh \ | ||||
|     +force_install_dir /opt/l4d2/installation \ | ||||
|     +login anonymous \ | ||||
|     +@sSteamCmdForcePlatformType windows \ | ||||
|     +app_update 222860 validate \ | ||||
|     +quit | ||||
| steam /opt/l4d2/steam/steamcmd.sh \ | ||||
|     +force_install_dir /opt/l4d2/installation \ | ||||
|     +login anonymous \ | ||||
|     +@sSteamCmdForcePlatformType linux \ | ||||
|     +app_update 222860 validate \ | ||||
|     +quit | ||||
| 
 | ||||
| # -- OVERLAYS -- # | ||||
| 
 | ||||
| for overlay_path in /opt/l4d2/scripts/overlays/*; do | ||||
|     overlay=$(basename "$overlay_path") | ||||
|     steam mkdir -p /opt/l4d2/overlays/$overlay | ||||
|     bash -xeuo pipefail "$overlay_path" | ||||
|     test -f /opt/l4d2/overlays/$overlay/left4dead2/cfg/server.cfg && \ | ||||
|         steam cp /opt/l4d2/overlays/$overlay/left4dead2/cfg/server.cfg /opt/l4d2/overlays/$overlay/left4dead2/cfg/server_$overlay.cfg | ||||
| done | ||||
| 
 | ||||
| # -- SERVERS -- # | ||||
| 
 | ||||
| #steam rm -rf /opt/l4d2/servers | ||||
| steam mkdir -p /opt/l4d2/servers | ||||
|  | @ -1,75 +0,0 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| set -xeuo pipefail | ||||
| 
 | ||||
| name="" | ||||
| port="" | ||||
| configfile="" | ||||
| overlays="" | ||||
| arguments="" | ||||
| 
 | ||||
| while [[ $# -gt 0 ]]; do | ||||
|   case "$1" in | ||||
|     -n|--name) | ||||
|       name="$2"; shift 2 | ||||
|       ;; | ||||
|     -p|--port) | ||||
|       port="$2"; shift 2 | ||||
|       ;; | ||||
|     -c|--config) | ||||
|       configfile="$2"; shift 2 | ||||
|       ;; | ||||
|     -o|--overlay) | ||||
|       overlays="/opt/l4d2/overlays/$2:$overlays"; shift 2 | ||||
|       ;; | ||||
|     --) | ||||
|       shift | ||||
|       arguments+="$@" | ||||
|       break | ||||
|       ;; | ||||
|     *) | ||||
|       echo "ERROR: unknown argument $1"; exit 1 | ||||
|       ;; | ||||
|   esac | ||||
| done | ||||
| 
 | ||||
| [[ -n "${name}" ]] || { echo "ERROR: -n/--name missing"; exit 1; } | ||||
| [[ -n "${port}" ]] || { echo "ERROR: -p/--port missing"; exit 1; } | ||||
| 
 | ||||
| # -- HELPER FUNCTIONS -- # | ||||
| 
 | ||||
| function steam() { | ||||
|     # für systemd, damit es den prozess beenden kann | ||||
|     setpriv --reuid=steam --regid=steam --init-groups "$@" | ||||
|     export HOME=/opt/l4d2/steam | ||||
| } | ||||
| 
 | ||||
| # -- TIDY UP -- # | ||||
| 
 | ||||
| mountpoint -q "/opt/l4d2/servers/$name/merged" && umount "/opt/l4d2/servers/$name/merged" | ||||
| steam rm -rf "/opt/l4d2/servers/$name" | ||||
| 
 | ||||
| # -- CREATE DIRECTORIES -- # | ||||
| 
 | ||||
| steam mkdir -p \ | ||||
|   "/opt/l4d2/servers/$name" \ | ||||
|   "/opt/l4d2/servers/$name/work" \ | ||||
|   "/opt/l4d2/servers/$name/upper" \ | ||||
|   "/opt/l4d2/servers/$name/merged" | ||||
| 
 | ||||
| # -- MOUNT OVERLAYFS -- # | ||||
| 
 | ||||
| mount -t overlay overlay \ | ||||
|   -o "lowerdir=$overlays/opt/l4d2/installation,upperdir=/opt/l4d2/servers/$name/upper,workdir=/opt/l4d2/servers/$name/work" \ | ||||
|   "/opt/l4d2/servers/$name/merged" | ||||
| 
 | ||||
| # -- REPLACE SERVER.CFG -- # | ||||
| 
 | ||||
| if [[ -n "$configfile" ]]; then | ||||
|   cp "$configfile" "/opt/l4d2/servers/$name/merged/left4dead2/cfg/server.cfg" | ||||
|   chown steam:steam "/opt/l4d2/servers/$name/merged/left4dead2/cfg/server.cfg" | ||||
| fi | ||||
| 
 | ||||
| # -- RUN L4D2 -- # | ||||
| 
 | ||||
| steam "/opt/l4d2/servers/$name/merged/srcds_run" -norestart -pidfile "/opt/l4d2/servers/$name/pid" -game left4dead2 -ip 0.0.0.0 -port "$port" +hostname "Crone_$name" +map c1m1_hotel $arguments | ||||
|  | @ -1,19 +0,0 @@ | |||
| #!/bin/bash | ||||
| 
 | ||||
| set -xeuo pipefail | ||||
| 
 | ||||
| name="" | ||||
| 
 | ||||
| while [[ $# -gt 0 ]]; do | ||||
|   case "$1" in | ||||
|     -n|--name) | ||||
|       name="$2"; shift 2 | ||||
|       ;; | ||||
|     *) | ||||
|       echo "ERROR: unknown argument $1"; exit 1 | ||||
|       ;; | ||||
|   esac | ||||
| done | ||||
| 
 | ||||
| mountpoint -q "/opt/l4d2/servers/$name/merged" && umount "/opt/l4d2/servers/$name/merged" | ||||
| steam rm -rf "/opt/l4d2/servers/$name" | ||||
|  | @ -1,97 +1,106 @@ | |||
| users = { | ||||
|     'steam': { | ||||
|         'home': '/opt/l4d2/steam', | ||||
|         'shell': '/bin/bash', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| directories = { | ||||
|     '/opt/l4d2': { | ||||
|         'owner': 'steam', 'group': 'steam', | ||||
|     '/opt/left4dead2': { | ||||
|         'owner': 'steam', | ||||
|     }, | ||||
|     '/opt/l4d2/steam': { | ||||
|         'owner': 'steam', 'group': 'steam', | ||||
|     '/opt/left4dead2/ems/admin system': { | ||||
|         'owner': 'steam', | ||||
|     }, | ||||
|     '/opt/l4d2/configs': { | ||||
|         'owner': 'steam', 'group': 'steam', | ||||
|         'purge': True, | ||||
|     '/opt/left4dead2/left4dead2/cfg': { | ||||
|         'owner': 'steam', | ||||
|     }, | ||||
|     '/opt/l4d2/scripts': { | ||||
|         'owner': 'steam', 'group': 'steam', | ||||
|     }, | ||||
|     '/opt/l4d2/scripts/overlays': { | ||||
|         'owner': 'steam', 'group': 'steam', | ||||
|     '/opt/left4dead2/left4dead2/addons': { | ||||
|         'owner': 'steam', | ||||
|         'purge': True, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| files = { | ||||
|     '/opt/l4d2/setup': { | ||||
|         'mode': '755', | ||||
|         'triggers': { | ||||
|             'svc_systemd:left4dead2-initialize.service:restart', | ||||
|         }, | ||||
|     }, | ||||
|     '/opt/l4d2/start': { | ||||
|         'mode': '755', | ||||
|         'triggers': { | ||||
|             f'svc_systemd:left4dead2-{server_name}.service:restart' | ||||
|                 for server_name in node.metadata.get('left4dead2/servers').keys() | ||||
|         }, | ||||
|     }, | ||||
|     '/opt/l4d2/scripts/helpers': { | ||||
|         'source': 'scripts/helpers', | ||||
|         'mode': '755', | ||||
|         'triggers': { | ||||
|             'svc_systemd:left4dead2-initialize.service:restart', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| for overlay in node.metadata.get('left4dead2/overlays'): | ||||
|     files[f'/opt/l4d2/scripts/overlays/{overlay}'] = { | ||||
|         'source': f'scripts/overlays/{overlay}', | ||||
|         'mode': '755', | ||||
|         'triggers': { | ||||
|             'svc_systemd:left4dead2-initialize.service:restart', | ||||
|         }, | ||||
|     '/opt/left4dead2/ems/admin system/admins.txt': { | ||||
|         'owner': 'steam', | ||||
|         'content': '\n'.join(node.metadata.get('left4dead2/admins')), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| svc_systemd = { | ||||
|     'left4dead2-initialize.service': { | ||||
|         'enabled': True, | ||||
|         'running': None, | ||||
|         'needs': { | ||||
|             'file:/opt/l4d2/setup', | ||||
|             'file:/usr/local/lib/systemd/system/left4dead2-initialize.service', | ||||
|         }, | ||||
|     'left4dead2-workshop': { | ||||
|         'running': False, | ||||
|         'needs': [ | ||||
|             'svc_systemd:steam-update', | ||||
|         ], | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| for server_name, config in node.metadata.get('left4dead2/servers').items(): | ||||
|     files[f'/opt/l4d2/configs/{server_name}.cfg'] = { | ||||
|         'source': 'server.cfg', | ||||
|         'content_type': 'mako', | ||||
|         'context': { | ||||
|             'server_name': server_name, | ||||
|             'rcon_password': repo.vault.decrypt('encrypt$gAAAAABpAdZhxwJ47I1AXotuZmBvyZP1ecVTt9IXFkLI28JiVS74LKs9QdgIBz-FC-iXtIHHh_GVGxxKQZprn4UrXZcvZ57kCKxfHBs3cE2JiGnbWE8_mfs=').value, | ||||
|             'config': config.get('config', []), | ||||
|         }, | ||||
| for id in node.metadata.get('left4dead2/workshop'): | ||||
|     directories[f'/opt/left4dead2/left4dead2/addons/{id}'] = { | ||||
|         'owner': 'steam', | ||||
|         'mode': '644', | ||||
|         'triggers': { | ||||
|             f'svc_systemd:left4dead2-{server_name}.service:restart', | ||||
|         }, | ||||
|         'triggers': [ | ||||
|             'svc_systemd:left4dead2-workshop:restart', | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
|     svc_systemd[f'left4dead2-{server_name}.service'] = { | ||||
|         'enabled': True, | ||||
|         'running': True, | ||||
|         'tags': { | ||||
|             'left4dead2-servers', | ||||
|         }, | ||||
|         'needs': { | ||||
|             'svc_systemd:left4dead2-initialize.service', | ||||
|             f'file:/usr/local/lib/systemd/system/left4dead2-{server_name}.service', | ||||
|         }, | ||||
| server_units = set() | ||||
| for name, config in node.metadata.get('left4dead2/servers').items(): | ||||
|     config.pop('port') | ||||
|     config = { | ||||
|         'hostname': name, | ||||
|         'sv_steamgroup': ','.join( | ||||
|             str(gid) for gid in  node.metadata.get('left4dead2/steamgroups') | ||||
|         ), | ||||
|         'z_difficulty': 'Impossible', | ||||
|         'sv_gametypes': 'realism', | ||||
|         'sv_region': 3, # europe | ||||
|         'log': 'on', | ||||
|         'sv_logecho': 1, | ||||
|         'sv_logfile': 1, | ||||
|         'sv_log_onefile': 0, | ||||
|         'sv_logbans': 1, | ||||
|         'sv_logflush': 0, | ||||
|         'sv_logsdir': 'logs', # /opt/left4dead2/left4dead2/logs | ||||
|         **config, | ||||
|     } | ||||
|      | ||||
|     files[f'/opt/left4dead2/left4dead2/cfg/server-{name}.cfg'] = { | ||||
|         'content': '\n'.join( | ||||
|             f'{key} "{value}"' for key, value in sorted(config.items()) | ||||
|         ) + '\n', | ||||
|         'owner': 'steam', | ||||
|         'triggers': [ | ||||
|             f'svc_systemd:left4dead2-server-{name}:restart', | ||||
|         ], | ||||
|     } | ||||
|     svc_systemd[f'left4dead2-server-{name}'] = { | ||||
|         'needs': [ | ||||
|             f'file:/usr/local/lib/systemd/system/left4dead2-server-{name}.service', | ||||
|         ], | ||||
|     } | ||||
|     server_units.add(f'left4dead2-server-{name}') | ||||
|      | ||||
| 
 | ||||
| for id in node.metadata.get('left4dead2/workshop'): | ||||
|     directories[f'/opt/left4dead2/addons/{id}'] = { | ||||
|         'owner': 'steam', | ||||
|         'triggers': [ | ||||
|             'svc_systemd:left4dead2-workshop:restart', | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
| # TIDYUP | ||||
| 
 | ||||
| find_obsolete_units = ( | ||||
|     'find /usr/local/lib/systemd/system -type f -name "left4dead2-server-*.service" ' + | ||||
|     ' '.join(f"! -name '{name}.service'" for name in server_units) | ||||
| ) | ||||
| actions['remove_obsolete_left4dead2_units'] = { | ||||
|     'command':  ( | ||||
|         f'for unitfile in $({find_obsolete_units}); ' | ||||
|         f'do ' | ||||
|             f'systemctl stop $(basename "$unitfile"); ' | ||||
|             f'systemctl disable $(basename "$unitfile"); ' | ||||
|             f'rm "$unitfile"; ' | ||||
|             f'systemctl daemon-reload; ' | ||||
|         f'done' | ||||
|     ), | ||||
|     'unless':    ( | ||||
|         find_obsolete_units + " | wc -l | grep -q '^0$'" | ||||
|     ), | ||||
| } | ||||
|  |  | |||
|  | @ -1,101 +1,85 @@ | |||
| from re import match | ||||
| from os import path, listdir | ||||
| assert node.has_bundle('steam') | ||||
| 
 | ||||
| from shlex import quote | ||||
| 
 | ||||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'libc6_i386': {}, # installs libc6:i386 | ||||
|             'lib32z1': {}, | ||||
|             'unzip': {}, | ||||
|             'p7zip-full': {}, # l4d2center_maps_sync.sh | ||||
|     'steam': { | ||||
|         'games': { | ||||
|             'left4dead2': '222860', | ||||
|         }, | ||||
|     }, | ||||
|     'left4dead2': { | ||||
|         'overlays': set(listdir(path.join(repo.path, 'bundles/left4dead2/files/scripts/overlays'))), | ||||
|         'servers': { | ||||
|             # 'port': 27017, | ||||
|             # 'overlays': ['competitive_rework'], | ||||
|             # 'arguments': ['-tickrate 60'], | ||||
|             # 'config': [ | ||||
|             #     'exec server_original.cfg', | ||||
|             #     'sm_forcematch zonemod', | ||||
|             # ], | ||||
|         }, | ||||
|     }, | ||||
|     'nftables': { | ||||
|         'input': { | ||||
|             'udp dport { 27005, 27020 } accept', | ||||
|         }, | ||||
|     }, | ||||
|     'systemd': { | ||||
|         'units': { | ||||
|             'left4dead2-initialize.service': { | ||||
|                 'Unit': { | ||||
|                     'Description': 'initialize left4dead2', | ||||
|                     'After': 'network-online.target', | ||||
|                 }, | ||||
|                 'Service': { | ||||
|                     'Type': 'oneshot', | ||||
|                     'RemainAfterExit': 'yes', | ||||
|                     'ExecStart': '/opt/l4d2/setup', | ||||
|                     'StandardOutput': 'journal', | ||||
|                     'StandardError': 'journal', | ||||
|                 }, | ||||
|                 'Install': { | ||||
|                     'WantedBy': {'multi-user.target'}, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         'servers': {}, | ||||
|         'admins': set(), | ||||
|         'workshop': set(), | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'systemd/units', | ||||
| ) | ||||
| def workshop(metadata): | ||||
|     command = ( | ||||
|         'set -x; ' | ||||
|         'for ID in ' + ' '.join(metadata.get('left4dead2/workshop')) + '; ' | ||||
|         'do ' | ||||
|             'if ! ls /opt/left4dead2/left4dead2/addons/$ID/*.vpk; ' | ||||
|             'then ' | ||||
|                 'cd /opt/left4dead2/left4dead2/addons/$ID; ' | ||||
|                 '/opt/steam-workshop-downloader https://steamcommunity.com/sharedfiles/filedetails\?id\=$ID; ' | ||||
|                 'unzip $ID.zip; ' | ||||
|             'fi; ' | ||||
|         'done' | ||||
|     ) | ||||
|      | ||||
|     return { | ||||
|         'systemd': { | ||||
|             'units': { | ||||
|                 'left4dead2-workshop.service': { | ||||
|                     'Unit': { | ||||
|                         'Description': 'install workshop items', | ||||
|                         'After': 'network.target', | ||||
|                         'Requires': 'steam-update.service', | ||||
|                         'PartOf': 'steam-update.service' | ||||
|                     }, | ||||
|                     'Service': { | ||||
|                         'Type': 'oneshot', | ||||
|                         'User': 'steam', | ||||
|                         'ExecStart': f'/bin/bash -c {quote(command)}', | ||||
|                     }, | ||||
|                     'Install': { | ||||
|                         'WantedBy': {'multi-user.target'}, | ||||
|                     }, | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'systemd/units', | ||||
| ) | ||||
| def server_units(metadata): | ||||
|     units = {} | ||||
| 
 | ||||
|      | ||||
|     for name, config in metadata.get('left4dead2/servers').items(): | ||||
|         assert match(r'^[A-z0-9-_-]+$', name) | ||||
|         assert 27000 <= config["port"] <= 27100 | ||||
|         for overlay in config.get('overlays', []): | ||||
|             assert overlay in metadata.get('left4dead2/overlays'), f"unknown overlay {overlay}, known: {metadata.get('left4dead2/overlays')}" | ||||
| 
 | ||||
|         cmd = f'/opt/l4d2/start -n {name} -p {config["port"]}' | ||||
| 
 | ||||
|         if 'config' in config: | ||||
|             cmd += f' -c /opt/l4d2/configs/{name}.cfg' | ||||
| 
 | ||||
|         for overlay in config.get('overlays', []): | ||||
|             cmd += f' -o {overlay}' | ||||
| 
 | ||||
|         if 'arguments' in config: | ||||
|             cmd += ' -- ' + ' '.join(config['arguments']) | ||||
| 
 | ||||
|         units[f'left4dead2-{name}.service'] = { | ||||
|         units[f'left4dead2-server-{name}.service'] = { | ||||
|             'Unit': { | ||||
|                 'Description': f'left4dead2 server {name}', | ||||
|                 'After': {'left4dead2-initialize.service'}, | ||||
|                 'Requires': {'left4dead2-initialize.service'}, | ||||
|                 'After': 'network.target', | ||||
|                 'Requires': 'steam-update.service', | ||||
|             }, | ||||
|             'Service': { | ||||
|                 'Type': 'simple', | ||||
|                 'ExecStart': cmd, | ||||
|                 'ExecStop': f'/opt/l4d2/stop -n {name}', | ||||
|                 'User': 'steam', | ||||
|                 'Group': 'steam', | ||||
|                 'WorkingDirectory': '/opt/left4dead2', | ||||
|                 'ExecStart': f'/opt/left4dead2/srcds_run -port {config["port"]} -insecure +map {config["map"]} +exec server-{name}.cfg', | ||||
|                 'Restart': 'on-failure', | ||||
|                 'Nice': -10, | ||||
|                 'CPUWeight': 200, | ||||
|                 'IOSchedulingClass': 'best-effort', | ||||
|                 'IOSchedulingPriority': 0, | ||||
|             }, | ||||
|             'Install': { | ||||
|                 'WantedBy': {'multi-user.target'}, | ||||
|             }, | ||||
|             'triggers': { | ||||
|                 f'svc_systemd:left4dead2-{name}.service:restart', | ||||
|             }, | ||||
|         } | ||||
| 
 | ||||
|     return { | ||||
|  | @ -103,18 +87,3 @@ def server_units(metadata): | |||
|             'units': units, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'nftables/input', | ||||
| ) | ||||
| def nftables(metadata): | ||||
|     ports = sorted(str(config["port"]) for config in metadata.get('left4dead2/servers').values()) | ||||
| 
 | ||||
|     return { | ||||
|         'nftables': { | ||||
|             'input': { | ||||
|                 f'ip protocol {{ tcp, udp }} th dport {{ {", ".join(ports)} }} accept' | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
|  |  | |||
|  | @ -1,58 +0,0 @@ | |||
| https://developer.valvesoftware.com/wiki/List_of_L4D2_Cvars | ||||
| 
 | ||||
| Dead Center 	c1m1_hotel | ||||
| Dead Center 	c1m2_streets | ||||
| Dead Center 	c1m3_mall | ||||
| Dead Center 	c1m4_atrium | ||||
| Dark Carnival 	c2m1_highway | ||||
| Dark Carnival 	c2m2_fairgrounds | ||||
| Dark Carnival 	c2m3_coaster | ||||
| Dark Carnival 	c2m4_barns | ||||
| Dark Carnival 	c2m5_concert | ||||
| Swamp Fever 	c3m1_plankcountry | ||||
| Swamp Fever 	c3m2_swamp | ||||
| Swamp Fever 	c3m3_shantytown | ||||
| Swamp Fever 	c3m4_plantation | ||||
| Hard Rain 	c4m1_milltown_a | ||||
| Hard Rain 	c4m2_sugarmill_a | ||||
| Hard Rain 	c4m3_sugarmill_b | ||||
| Hard Rain 	c4m4_milltown_b | ||||
| Hard Rain 	c4m5_milltown_escape | ||||
| The Parish 	c5m1_waterfront_sndscape | ||||
| The Parish 	c5m1_waterfront | ||||
| The Parish 	c5m2_park | ||||
| The Parish 	c5m3_cemetery | ||||
| The Parish 	c5m4_quarter | ||||
| The Parish 	c5m5_bridge | ||||
| The Passing 	c6m1_riverbank | ||||
| The Passing 	c6m2_bedlam | ||||
| The Passing 	c6m3_port | ||||
| The Sacrifice 	c7m1_docks | ||||
| The Sacrifice 	c7m2_barge | ||||
| The Sacrifice 	c7m3_port | ||||
| No Mercy 	c8m1_apartment | ||||
| No Mercy 	c8m2_subway | ||||
| No Mercy 	c8m3_sewers | ||||
| No Mercy 	c8m4_interior | ||||
| No Mercy 	c8m5_rooftop | ||||
| Crash Course 	c9m1_alleys | ||||
| Crash Course 	c9m2_lots | ||||
| Death Toll 	c10m1_caves | ||||
| Death Toll 	c10m2_drainage | ||||
| Death Toll 	c10m3_ranchhouse | ||||
| Death Toll 	c10m4_mainstreet | ||||
| Death Toll 	c10m5_houseboat | ||||
| Dead Air 	c11m1_greenhouse | ||||
| Dead Air 	c11m2_offices | ||||
| Dead Air 	c11m3_garage | ||||
| Dead Air 	c11m4_terminal | ||||
| Dead Air 	c11m5_runway | ||||
| Blood Harvest 	c12m1_hilltop | ||||
| Blood Harvest 	c12m2_traintunnel | ||||
| Blood Harvest 	c12m3_bridge | ||||
| Blood Harvest 	c12m4_barn | ||||
| Blood Harvest 	c12m5_cornfield | ||||
| Cold Stream 	c13m1_alpinecreek | ||||
| Cold Stream 	c13m2_southpinestream | ||||
| Cold Stream 	c13m3_memorialbridge | ||||
| Cold Stream 	c13m4_cutthroatcreek | ||||
|  | @ -1,40 +0,0 @@ | |||
| hostname "CroneKorkN : ${name}" | ||||
| sv_contact "admin@sublimity.de" | ||||
| 
 | ||||
| 
 | ||||
| sv_steamgroup "${','.join(steamgroups)}" | ||||
| 
 | ||||
| rcon_password "${rcon_password}" | ||||
| 
 | ||||
| 
 | ||||
| motd_enabled 0 | ||||
| 
 | ||||
| 
 | ||||
| sv_cheats 1 | ||||
| 
 | ||||
| 
 | ||||
| sv_consistency 0 | ||||
| 
 | ||||
| 
 | ||||
| sv_lan 0 | ||||
| 
 | ||||
| 
 | ||||
| sv_allow_lobby_connect_only 0 | ||||
| 
 | ||||
| 
 | ||||
| sv_gametypes "coop,realism,survival,versus,teamversus,scavenge,teamscavenge" | ||||
| 
 | ||||
| 
 | ||||
| sv_minrate 30000 | ||||
| sv_maxrate 60000 | ||||
| sv_mincmdrate 66 | ||||
| sv_maxcmdrate 101 | ||||
| 
 | ||||
| 
 | ||||
| sv_logsdir "logs-${name}"        //Folder in the game directory where server logs will be stored. | ||||
| log on            //Creates a logfile (on | off) | ||||
| sv_logecho 0            //default 0; Echo log information to the console. | ||||
| sv_logfile 1        //default 1; Log server information in the log file. | ||||
| sv_log_onefile 0    //default 0; Log server information to only one file. | ||||
| sv_logbans 1        //default 0;Log server bans in the server logs. | ||||
| sv_logflush 0        //default 0; Flush the log files to disk on each write (slow). | ||||
|  | @ -1,122 +0,0 @@ | |||
| assert node.has_bundle('steam') and node.has_bundle('steam-workshop-download') | ||||
| 
 | ||||
| directories = { | ||||
|     '/opt/steam/left4dead2-servers': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'mode': '0755', | ||||
|         'purge': True, | ||||
|     }, | ||||
|     # Current zfs doesnt support zfs upperdir. The support was added in October 2022. Move upperdir - unused anyway - | ||||
|     # to another dir. Also move workdir alongside it, as it has to be on same fs. | ||||
|     '/opt/steam-zfs-overlay-workarounds': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'mode': '0755', | ||||
|         'purge': True, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| # /opt/steam/steam/.steam/sdk32/steamclient.so: cannot open shared object file: No such file or directory | ||||
| symlinks = { | ||||
|     '/opt/steam/steam/.steam/sdk32': { | ||||
|         'target': '/opt/steam/steam/linux32', | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| # | ||||
| # SERVERS | ||||
| # | ||||
| 
 | ||||
| for name, config in node.metadata.get('left4dead2/servers').items(): | ||||
| 
 | ||||
|     #overlay | ||||
|     directories[f'/opt/steam/left4dead2-servers/{name}'] = { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     } | ||||
|     directories[f'/opt/steam-zfs-overlay-workarounds/{name}/upper'] = { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     } | ||||
|     directories[f'/opt/steam-zfs-overlay-workarounds/{name}/workdir'] = { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     } | ||||
| 
 | ||||
|     # conf | ||||
|     files[f'/opt/steam/left4dead2-servers/{name}/left4dead2/cfg/server.cfg'] = { | ||||
|         'content_type': 'mako', | ||||
|         'source': 'server.cfg', | ||||
|         'context': { | ||||
|             'name': name, | ||||
|             'steamgroups': node.metadata.get('left4dead2/steamgroups'), | ||||
|             'rcon_password': config['rcon_password'], | ||||
|         }, | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'triggers': [ | ||||
|             f'svc_systemd:left4dead2-{name}.service:restart', | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
|     # service | ||||
|     svc_systemd[f'left4dead2-{name}.service'] = { | ||||
|         'needs': [ | ||||
|             f'file:/opt/steam/left4dead2-servers/{name}/left4dead2/cfg/server.cfg', | ||||
|             f'file:/usr/local/lib/systemd/system/left4dead2-{name}.service', | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
|     # | ||||
|     # ADDONS | ||||
|     # | ||||
| 
 | ||||
|     # base | ||||
|     files[f'/opt/steam/left4dead2-servers/{name}/left4dead2/addons/readme.txt'] = { | ||||
|         'content_type': 'any', | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     } | ||||
|     directories[f'/opt/steam/left4dead2-servers/{name}/left4dead2/addons'] = { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'purge': True, | ||||
|         'triggers': [ | ||||
|             f'svc_systemd:left4dead2-{name}.service:restart', | ||||
|         ], | ||||
|     } | ||||
|     for id in [ | ||||
|         *config.get('workshop', []), | ||||
|         *node.metadata.get('left4dead2/workshop'), | ||||
|     ]: | ||||
|         files[f'/opt/steam/left4dead2-servers/{name}/left4dead2/addons/{id}.vpk'] = { | ||||
|             'content_type': 'any', | ||||
|             'owner': 'steam', | ||||
|             'group': 'steam', | ||||
|             'triggers': [ | ||||
|                 f'svc_systemd:left4dead2-{name}.service:restart', | ||||
|             ], | ||||
|         } | ||||
| 
 | ||||
|     # admin system | ||||
| 
 | ||||
|     directories[f'/opt/steam/left4dead2-servers/{name}/left4dead2/ems/admin system'] = { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'mode': '0755', | ||||
|         'triggers': [ | ||||
|             f'svc_systemd:left4dead2-{name}.service:restart', | ||||
|         ], | ||||
|     } | ||||
|     files[f'/opt/steam/left4dead2-servers/{name}/left4dead2/ems/admin system/admins.txt'] = { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'mode': '0755', | ||||
|         'content': '\n'.join(sorted(node.metadata.get('left4dead2/admins'))), | ||||
|         'triggers': [ | ||||
|             f'svc_systemd:left4dead2-{name}.service:restart', | ||||
|         ], | ||||
|     } | ||||
|  | @ -1,127 +0,0 @@ | |||
| assert node.has_bundle('steam') | ||||
| 
 | ||||
| from shlex import quote | ||||
| 
 | ||||
| defaults = { | ||||
|     'steam': { | ||||
|         'games': { | ||||
|             'left4dead2': 222860, | ||||
|         }, | ||||
|     }, | ||||
|     'left4dead2': { | ||||
|         'servers': {}, | ||||
|         'admins': set(), | ||||
|         'workshop': set(), | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'left4dead2/servers', | ||||
| ) | ||||
| def rconn_password(metadata): | ||||
|     # only works from localhost! | ||||
|     return { | ||||
|         'left4dead2': { | ||||
|             'servers': { | ||||
|                 server: { | ||||
|                     'rcon_password': repo.vault.password_for(f'{node.name} left4dead2 {server} rcon', length=24), | ||||
|                 } | ||||
|                     for server in metadata.get('left4dead2/servers') | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'steam-workshop-download', | ||||
|     'systemd/units', | ||||
| ) | ||||
| def server_units(metadata): | ||||
|     units = {} | ||||
|     workshop = {} | ||||
| 
 | ||||
|     for name, config in metadata.get('left4dead2/servers').items(): | ||||
|         # mount overlay | ||||
|         mountpoint = f'/opt/steam/left4dead2-servers/{name}' | ||||
|         mount_unit_name = mountpoint[1:].replace('-', '\\x2d').replace('/', '-') + '.mount' | ||||
|         units[mount_unit_name] = { | ||||
|             'Unit': { | ||||
|                 'Description': f"Mount left4dead2 server {name} overlay", | ||||
|                 'Conflicts': {'umount.target'}, | ||||
|                 'Before': {'umount.target'}, | ||||
|             }, | ||||
|             'Mount': { | ||||
|                 'What': 'overlay', | ||||
|                 'Where': mountpoint, | ||||
|                 'Type': 'overlay', | ||||
|                 'Options': ','.join([ | ||||
|                     'auto', | ||||
|                     'lowerdir=/opt/steam/left4dead2', | ||||
|                     f'upperdir=/opt/steam-zfs-overlay-workarounds/{name}/upper', | ||||
|                     f'workdir=/opt/steam-zfs-overlay-workarounds/{name}/workdir', | ||||
|                 ]), | ||||
|             }, | ||||
|             'Install': { | ||||
|                 'RequiredBy': { | ||||
|                     f'left4dead2-{name}.service', | ||||
|                 }, | ||||
|             }, | ||||
|         } | ||||
| 
 | ||||
|         # individual workshop | ||||
|         workshop_ids = config.get('workshop', set()) | metadata.get('left4dead2/workshop', set()) | ||||
|         if  workshop_ids: | ||||
|             workshop[f'left4dead2-{name}'] = { | ||||
|                 'ids': workshop_ids, | ||||
|                 'path': f'/opt/steam/left4dead2-servers/{name}/left4dead2/addons', | ||||
|                 'user': 'steam', | ||||
|                 'requires': { | ||||
|                     mount_unit_name, | ||||
|                 }, | ||||
|                 'required_by': { | ||||
|                     f'left4dead2-{name}.service', | ||||
|                 }, | ||||
|             } | ||||
| 
 | ||||
|         # left4dead2 server unit | ||||
|         units[f'left4dead2-{name}.service'] = { | ||||
|             'Unit': { | ||||
|                 'Description': f'left4dead2 server {name}', | ||||
|                 'After': {'steam-update.service'}, | ||||
|                 'Requires': {'steam-update.service'}, | ||||
|             }, | ||||
|             'Service': { | ||||
|                 'User': 'steam', | ||||
|                 'Group': 'steam', | ||||
|                 'WorkingDirectory': f'/opt/steam/left4dead2-servers/{name}', | ||||
|                 'ExecStart': f'/opt/steam/left4dead2-servers/{name}/srcds_run -port {config["port"]} +exec server.cfg', | ||||
|                 'Restart': 'on-failure', | ||||
|             }, | ||||
|             'Install': { | ||||
|                 'WantedBy': {'multi-user.target'}, | ||||
|             }, | ||||
|         } | ||||
| 
 | ||||
|     return { | ||||
|         'steam-workshop-download': workshop, | ||||
|         'systemd': { | ||||
|             'units': units, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'nftables/input', | ||||
| ) | ||||
| def firewall(metadata): | ||||
|     ports = set(str(server['port']) for server in metadata.get('left4dead2/servers').values()) | ||||
| 
 | ||||
|     return { | ||||
|         'nftables': { | ||||
|             'input': { | ||||
|                 f"tcp dport {{ {', '.join(sorted(ports))} }} accept", | ||||
|                 f"udp dport {{ {', '.join(sorted(ports))} }} accept", | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
|  | @ -1,97 +0,0 @@ | |||
| # https://github.com/SirPlease/L4D2-Competitive-Rework/blob/master/Dedicated%20Server%20Install%20Guide/README.md | ||||
| 
 | ||||
| getent passwd steam >/dev/null || useradd -M -d /opt/l4d2 -s /bin/bash steam | ||||
| mkdir -p /opt/l4d2 /tmp/dumps | ||||
| chown steam:steam /opt/l4d2 /tmp/dumps | ||||
| dpkg --add-architecture i386 | ||||
| apt update | ||||
| DEBIAN_FRONTEND=noninteractive apt install -y libc6:i386 lib32z1 | ||||
| 
 | ||||
| function steam() { sudo -Hiu steam $* } | ||||
| 
 | ||||
| # -- STEAM -- # | ||||
| 
 | ||||
| steam mkdir -p /opt/l4d2/steam | ||||
| test -f /opt/l4d2/steam/steamcmd_linux.tar.gz || \ | ||||
|   steam wget http://media.steampowered.com/installer/steamcmd_linux.tar.gz -P /opt/l4d2/steam | ||||
| test -f /opt/l4d2/steam/steamcmd.sh || \ | ||||
|   steam tar -xvzf /opt/l4d2/steam/steamcmd_linux.tar.gz -C /opt/l4d2/steam | ||||
| 
 | ||||
| # fix: /opt/l4d2/.steam/sdk32/steamclient.so: cannot open shared object file: No such file or directory | ||||
| steam mkdir -p /opt/l4d2/steam/.steam | ||||
| test -f /opt/l4d2/steam/.steam/sdk32/steamclient.so || \ | ||||
|   steam ln -s /opt/l4d2/steam/linux32 /opt/l4d2/steam/.steam/sdk32 | ||||
| 
 | ||||
| # -- INSTALL -- # | ||||
| 
 | ||||
| # erst die windows deps zu installieren scheint ein workaround für x64 zu sein? | ||||
| steam mkdir -p /opt/l4d2/installation | ||||
| steam /opt/l4d2/steam/steamcmd.sh \ | ||||
|     +force_install_dir /opt/l4d2/installation \ | ||||
|     +login anonymous \ | ||||
|     +@sSteamCmdForcePlatformType windows \ | ||||
|     +app_update 222860 validate \ | ||||
|     +quit | ||||
| steam /opt/l4d2/steam/steamcmd.sh \ | ||||
|     +force_install_dir /opt/l4d2/installation \ | ||||
|     +login anonymous \ | ||||
|     +@sSteamCmdForcePlatformType linux \ | ||||
|     +app_update 222860 validate \ | ||||
|     +quit | ||||
| 
 | ||||
| # -- OVERLAYS -- # | ||||
| 
 | ||||
| steam mkdir -p /opt/l4d2/overlays | ||||
| 
 | ||||
| # workshop downloader | ||||
| steam wget -4 https://git.sublimity.de/cronekorkn/steam-workshop-downloader/raw/branch/master/steam-workshop-download -P /opt/l4d2 | ||||
| steam chmod +x /opt/l4d2/steam-workshop-download | ||||
| 
 | ||||
| # -- OVERLAY PVE -- # | ||||
| 
 | ||||
| steam mkdir -p /opt/l4d2/overlays/pve | ||||
| 
 | ||||
| # admin system | ||||
| steam mkdir -p /opt/l4d2/overlays/pve/left4dead2/addons | ||||
| steam /opt/l4d2/steam-workshop-download 2524204971 --out /opt/l4d2/overlays/pve/left4dead2/addons | ||||
| steam mkdir -p "/opt/l4d2/overlays/pve/left4dead2/ems/admin system" | ||||
| echo "STEAM_1:0:12376499" | steam tee "/opt/l4d2/overlays/pve/left4dead2/ems/admin system/admins.txt" | ||||
| 
 | ||||
| # ions vocalizer | ||||
| steam /opt/l4d2/steam-workshop-download 698857882 --out /opt/l4d2/overlays/pve/left4dead2/addons | ||||
| 
 | ||||
| # -- OVERLAY ZONEMOD -- # | ||||
| 
 | ||||
| true | ||||
| 
 | ||||
| # -- SERVERS -- # | ||||
| 
 | ||||
| steam mkdir -p /opt/l4d2/servers | ||||
| 
 | ||||
| # -- SERVER PVE1 -- # | ||||
| 
 | ||||
| steam mkdir -p \ | ||||
|   /opt/l4d2/servers/pve1 \ | ||||
|   /opt/l4d2/servers/pve1/work \ | ||||
|   /opt/l4d2/servers/pve1/upper \ | ||||
|   /opt/l4d2/servers/pve1/merged | ||||
| 
 | ||||
| mount -t overlay overlay \ | ||||
|   -o lowerdir=/opt/l4d2/overlays/pve:/opt/l4d2/installation,upperdir=/opt/l4d2/servers/pve1/upper,workdir=/opt/l4d2/servers/pve1/work \ | ||||
|   /opt/l4d2/servers/pve1/merged | ||||
| 
 | ||||
| # run server | ||||
| steam cat <<'EOF' > /opt/l4d2/servers/pve1/merged/left4dead2/cfg/server.cfg | ||||
| hostname "CKNs Server" | ||||
| motd_enabled 0 | ||||
| 
 | ||||
| sv_steamgroup "38347879" | ||||
| #sv_steamgroup_exclusive 0 | ||||
| 
 | ||||
| sv_minrate 60000 | ||||
| sv_maxrate 0 | ||||
| net_splitpacket_maxrate 60000 | ||||
| 
 | ||||
| sv_hibernate_when_empty 0 | ||||
| EOF | ||||
| steam /opt/l4d2/servers/pve1/merged/srcds_run -game left4dead2 -ip 0.0.0.0 -port 27015 +map c1m1_hotel | ||||
|  | @ -1,183 +0,0 @@ | |||
| from shlex import quote | ||||
| 
 | ||||
| 
 | ||||
| def steam_run(cmd): | ||||
|     return f'su - steam -c {quote(cmd)}' | ||||
| 
 | ||||
| 
 | ||||
| users = { | ||||
|     'steam': { | ||||
|         'home': '/opt/steam', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| directories = { | ||||
|     '/opt/steam': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
|     '/opt/steam/.steam': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
|     '/opt/left4dead2': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
|     '/opt/left4dead2/left4dead2/ems/admin system': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
|     '/opt/left4dead2/left4dead2/addons': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
|     '/tmp/dumps': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'mode': '1770', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| symlinks = { | ||||
|     '/opt/steam/.steam/sdk32': { | ||||
|         'target': '/opt/steam/linux32', | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| files = { | ||||
|     '/opt/steam-workshop-download': { | ||||
|         'content_type': 'download', | ||||
|         'source': 'https://git.sublimity.de/cronekorkn/steam-workshop-downloader/raw/branch/master/steam-workshop-download', | ||||
|         'mode': '755', | ||||
|     }, | ||||
|     '/opt/left4dead2/left4dead2/ems/admin system/admins.txt': { | ||||
|         'unless': 'test -f /opt/left4dead2/left4dead2/ems/admin system/admins.txt', | ||||
|         'content': 'STEAM_1:0:12376499', | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| actions = { | ||||
|     'dpkg_add_architecture': { | ||||
|         'command': 'dpkg --add-architecture i386', | ||||
|         'unless': 'dpkg --print-foreign-architectures | grep -q i386', | ||||
|         'triggers': [ | ||||
|             'action:apt_update', | ||||
|         ], | ||||
|         'needed_by': [ | ||||
|             'pkg_apt:libc6_i386', | ||||
|         ], | ||||
|     }, | ||||
|     'download_steam': { | ||||
|         'command': steam_run('wget http://media.steampowered.com/installer/steamcmd_linux.tar.gz -P /opt/steam'), | ||||
|         'unless':  steam_run('test -f /opt/steam/steamcmd_linux.tar.gz'), | ||||
|         'needs': { | ||||
|             'pkg_apt:libc6_i386', | ||||
|             'directory:/opt/steam', | ||||
|         } | ||||
|     }, | ||||
|     'extract_steamcmd': { | ||||
|         'command': steam_run('tar -xvzf /opt/steam/steamcmd_linux.tar.gz -C /opt/steam'), | ||||
|         'unless': steam_run('test -f /opt/steam/steamcmd.sh'), | ||||
|         'needs': { | ||||
|             'action:download_steam', | ||||
|         } | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| for addon_id in [2524204971]: | ||||
|     actions[f'download-left4dead2-addon-{addon_id}'] = { | ||||
|         'command': steam_run(f'/opt/steam-workshop-download {addon_id} --out /opt/left4dead2/left4dead2/addons'), | ||||
|         'unless': steam_run(f'test -f /opt/left4dead2/left4dead2/addons/{addon_id}.vpk'), | ||||
|         'needs': { | ||||
|             'directory:/opt/left4dead2/left4dead2/addons', | ||||
|         }, | ||||
|         'needed_by': { | ||||
|             'tag:left4dead2-servers', | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| svc_systemd = { | ||||
|     'left4dead2-install.service': { | ||||
|         'enabled': True, | ||||
|         'running': False, | ||||
|         'needs': { | ||||
|             'file:/usr/local/lib/systemd/system/left4dead2-install.service', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| for server_name, server_config in node.metadata.get('left4dead2/servers', {}).items(): | ||||
|     svc_systemd[f'left4dead2-{server_name}.service'] = { | ||||
|         'enabled': True, | ||||
|         'running': True, | ||||
|         'tags': { | ||||
|             'left4dead2-servers', | ||||
|         }, | ||||
|         'needs': { | ||||
|             'svc_systemd:left4dead2-install.service', | ||||
|             f'file:/usr/local/lib/systemd/system/left4dead2-{server_name}.service', | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| # # https://github.com/SirPlease/L4D2-Competitive-Rework/blob/master/Dedicated%20Server%20Install%20Guide/README.md | ||||
| 
 | ||||
| # mkdir /opt/steam /tmp/dumps | ||||
| # useradd -M -d /opt/steam -s /bin/bash steam | ||||
| # chown steam:steam /opt/steam /tmp/dumps | ||||
| # dpkg --add-architecture i386 | ||||
| # apt update | ||||
| # apt install libc6:i386 lib32z1 | ||||
| # sudo su - steam -s /bin/bash | ||||
| 
 | ||||
| # #-------- | ||||
| 
 | ||||
| # wget http://media.steampowered.com/installer/steamcmd_linux.tar.gz | ||||
| # tar -xvzf steamcmd_linux.tar.gz | ||||
| 
 | ||||
| # # fix: /opt/steam/.steam/sdk32/steamclient.so: cannot open shared object file: No such file or directory | ||||
| # mkdir /opt/steam/.steam && ln -s /opt/steam/linux32 /opt/steam/.steam/sdk32 | ||||
| 
 | ||||
| # # erst die windows deps zu installieren scheint ein workaround für x64 zu sein? | ||||
| # ./steamcmd.sh \ | ||||
| #     +force_install_dir /opt/steam/left4dead2 \ | ||||
| #     +login anonymous \ | ||||
| #     +@sSteamCmdForcePlatformType windows \ | ||||
| #     +app_update 222860 validate \ | ||||
| #     +quit | ||||
| # ./steamcmd.sh \ | ||||
| #     +force_install_dir /opt/steam/left4dead2 \ | ||||
| #     +login anonymous \ | ||||
| #     +@sSteamCmdForcePlatformType linux \ | ||||
| #     +app_update 222860 validate \ | ||||
| #     +quit | ||||
| 
 | ||||
| # # download admin system | ||||
| # wget -4 https://git.sublimity.de/cronekorkn/steam-workshop-downloader/raw/branch/master/steam-workshop-download | ||||
| # chmod +x steam-workshop-download | ||||
| # ./steam-workshop-download 2524204971 --out /opt/steam/left4dead2/left4dead2/addons | ||||
| # mkdir -p "/opt/steam/left4dead2/left4dead2/ems/admin system" | ||||
| # echo "STEAM_1:0:12376499" > "/opt/steam/left4dead2/left4dead2/ems/admin system/admins.txt" | ||||
| 
 | ||||
| # /opt/steam/left4dead2/srcds_run -game left4dead2 -ip 0.0.0.0 -port 27015 +map c1m1_hotel | ||||
| 
 | ||||
| 
 | ||||
| # cat <<'EOF' > /opt/steam/left4dead2/left4dead2/cfg/server.cfg | ||||
| # hostname "CKNs Server" | ||||
| # motd_enabled 0 | ||||
| 
 | ||||
| # sv_steamgroup "38347879" | ||||
| # #sv_steamgroup_exclusive 0 | ||||
| 
 | ||||
| # sv_minrate 60000 | ||||
| # sv_maxrate 0 | ||||
| # net_splitpacket_maxrate 60000 | ||||
| 
 | ||||
| # sv_hibernate_when_empty 0 | ||||
| # EOF | ||||
|  | @ -1,107 +0,0 @@ | |||
| from re import match | ||||
| 
 | ||||
| defaults = { | ||||
|     'apt': { | ||||
|         'packages': { | ||||
|             'libc6_i386': {}, # installs libc6:i386 | ||||
|             'lib32z1': {}, | ||||
|             'unzip': {}, | ||||
|         }, | ||||
|     }, | ||||
|     'left4dead2': { | ||||
|         'servers': {}, | ||||
|     }, | ||||
|     'nftables': { | ||||
|         'input': { | ||||
|             'udp dport { 27005, 27020 } accept', | ||||
|         }, | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'nftables/input', | ||||
| ) | ||||
| def nftables(metadata): | ||||
|     ports = sorted(str(config["port"]) for config in metadata.get('left4dead2/servers', {}).values()) | ||||
| 
 | ||||
|     return { | ||||
|         'nftables': { | ||||
|             'input': { | ||||
|                 f'ip protocol {{ tcp, udp }} th dport {{ {", ".join(ports)} }} accept' | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'systemd/units', | ||||
| ) | ||||
| def initial_unit(metadata): | ||||
|     install_command = ( | ||||
|         '/opt/steam/steamcmd.sh ' | ||||
|         '+force_install_dir /opt/left4dead2 ' | ||||
|         '+login anonymous ' | ||||
|         '+@sSteamCmdForcePlatformType {platform} ' | ||||
|         '+app_update 222860 validate ' | ||||
|         '+quit ' | ||||
|     ) | ||||
| 
 | ||||
|     return { | ||||
|         'systemd': { | ||||
|             'units': { | ||||
|                 'left4dead2-install.service': { | ||||
|                     'Unit': { | ||||
|                         'Description': 'install or update left4dead2', | ||||
|                         'After': 'network-online.target', | ||||
|                     }, | ||||
|                     'Service': { | ||||
|                         'Type': 'oneshot', | ||||
|                         'RemainAfterExit': 'yes', | ||||
|                         'User': 'steam', | ||||
|                         'Group': 'steam', | ||||
|                         'WorkingDirectory': '/opt/steam', | ||||
|                         'ExecStartPre': install_command.format(platform='windows'), | ||||
|                         'ExecStart': install_command.format(platform='linux'), | ||||
|                     }, | ||||
|                     'Install': { | ||||
|                         'WantedBy': {'multi-user.target'}, | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @metadata_reactor.provides( | ||||
|     'systemd/units', | ||||
| ) | ||||
| def server_units(metadata): | ||||
|     units = {} | ||||
| 
 | ||||
|     for name, config in metadata.get('left4dead2/servers').items(): | ||||
|         assert match(r'^[A-z0-9-_-]+$', name) | ||||
| 
 | ||||
|         units[f'left4dead2-{name}.service'] = { | ||||
|             'Unit': { | ||||
|                 'Description': f'left4dead2 server {name}', | ||||
|                 'After': {'left4dead2-install.service'}, | ||||
|                 'Requires': {'left4dead2-install.service'}, | ||||
|             }, | ||||
|             'Service': { | ||||
|                 'User': 'steam', | ||||
|                 'Group': 'steam', | ||||
|                 'WorkingDirectory': '/opt/left4dead2', | ||||
|                 'ExecStart': f'/opt/left4dead2/srcds_run -port {config["port"]} +exec server_{name}.cfg', | ||||
|                 'Restart': 'on-failure', | ||||
|             }, | ||||
|             'Install': { | ||||
|                 'WantedBy': {'multi-user.target'}, | ||||
|             }, | ||||
|         } | ||||
| 
 | ||||
|     return { | ||||
|         'systemd': { | ||||
|             'units': units, | ||||
|         }, | ||||
|     } | ||||
|  | @ -1,54 +0,0 @@ | |||
| users = { | ||||
|     'steam': { | ||||
|         'home': '/opt/steam/steam', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| directories = { | ||||
|     '/opt/steam': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'needs': [ | ||||
|             'zfs_dataset:tank/steam', | ||||
|         ], | ||||
|     }, | ||||
|     '/opt/steam/steam': { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
| } | ||||
| for game in node.metadata.get('steam/games'): | ||||
|     directories[f'/opt/steam/{game}'] = { | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|         'needed_by': [ | ||||
|             'svc_systemd:steam-update.service', | ||||
|         ], | ||||
|     } | ||||
| 
 | ||||
| files = { | ||||
|     '/opt/steam/steam/steamcmd_linux.tar.gz': { | ||||
|         'content_type': 'download', | ||||
|         'source': 'https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz', | ||||
|         'owner': 'steam', | ||||
|         'group': 'steam', | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| actions = { | ||||
|     'extract_steamcmd': { | ||||
|         'command': """su - steam -c 'tar xfvz /opt/steam/steam/steamcmd_linux.tar.gz --directory /opt/steam/steam'""", | ||||
|         'unless': 'test -f /opt/steam/steam/steamcmd.sh', | ||||
|         'needs': [ | ||||
|             'file:/opt/steam/steam/steamcmd_linux.tar.gz', | ||||
|         ], | ||||
|     }, | ||||
| } | ||||
| 
 | ||||
| svc_systemd['steam-update.service'] = { | ||||
|     'running': None, | ||||
|     'enabled': True, | ||||
|     'needs': { | ||||
|         'file:/usr/local/lib/systemd/system/steam-update.service', | ||||
|     } | ||||
| } | ||||
|  | @ -1,9 +1,9 @@ | |||
| https://github.com/dehydrated-io/dehydrated/wiki/example-dns-01-nsupdate-script | ||||
| 
 | ||||
| ```sh | ||||
| ``` | ||||
| printf "server 127.0.0.1 | ||||
| zone acme.resolver.name. | ||||
| update add _acme-challenge.ckn.li.acme.resolver.name. 600 IN TXT "hello" | ||||
| send | ||||
| " | nsupdate -y hmac-sha512:acme:XXXXXX | ||||
| " | nsupdate -y hmac-sha512:acme:Y9BHl85l352BGZDXa/vg90hh2+5PYe4oJxpkq/oQvIODDkW8bAyQSFr0gKQQxjyIOyYlTjf0MGcdWFv46G/3Rg== | ||||
| ``` | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ set -o pipefail | |||
| 
 | ||||
| deploy_challenge() { | ||||
|   echo " | ||||
|     server ${server} | ||||
|     server 10.0.11.3 | ||||
|     zone ${zone}. | ||||
|     update add $1.${zone}. 60 IN TXT \"$3\" | ||||
|     send | ||||
|  | @ -13,7 +13,7 @@ deploy_challenge() { | |||
| 
 | ||||
| clean_challenge() { | ||||
|   echo " | ||||
|     server ${server} | ||||
|     server 10.0.11.3 | ||||
|     zone ${zone}. | ||||
|     update delete $1.${zone}. TXT | ||||
|     send | ||||
|  | @ -31,12 +31,6 @@ deploy_cert() { | |||
|   % for domain, conf in sorted(domains.items()): | ||||
| <%   if not conf: continue %>\ | ||||
|     ${domain}) | ||||
|       % if conf.get('scp', None): | ||||
|       scp "$KEYFILE" "${conf['scp']}/${conf.get('privkey_name', 'privkey.pem')}" | ||||
|       scp "$CERTFILE" "${conf['scp']}/${conf.get('cert_name', 'cert.pem')}" | ||||
|       scp "$FULLCHAINFILE" "${conf['scp']}/${conf.get('fullchain_name', 'fullchain.pem')}" | ||||
|       scp "$CHAINFILE" "${conf['scp']}/${conf.get('chain_name', 'chain.pem')}" | ||||
|       % endif | ||||
|       % if conf.get('location', None): | ||||
|       cat "$KEYFILE" > "${conf['location']}/${conf.get('privkey_name', 'privkey.pem')}" | ||||
|       cat "$CERTFILE" > "${conf['location']}/${conf.get('cert_name', 'cert.pem')}" | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ files = { | |||
| } | ||||
| 
 | ||||
| actions['letsencrypt_update_certificates'] = { | ||||
|     'command': 'systemctl start letsencrypt.service', | ||||
|     'command': 'dehydrated --cron --accept-terms --challenge dns-01', | ||||
|     'triggered': True, | ||||
|     'skip': delegated, | ||||
|     'needs': { | ||||
|  | @ -56,7 +56,6 @@ for domain in node.metadata.get('letsencrypt/domains').keys(): | |||
|         'unless': f'/etc/dehydrated/letsencrypt-ensure-some-certificate {domain} true', | ||||
|         'needs': { | ||||
|             'file:/etc/dehydrated/letsencrypt-ensure-some-certificate', | ||||
|             'pkg_apt:dehydrated', | ||||
|         }, | ||||
|         'needed_by': { | ||||
|             'svc_systemd:nginx', | ||||
|  |  | |||
|  | @ -1,43 +0,0 @@ | |||
| from shlex import quote | ||||
| 
 | ||||
| def generate_sysctl_key_value_pairs_from_json(json_data, parents=[]): | ||||
|     if isinstance(json_data, dict): | ||||
|         for key, value in json_data.items(): | ||||
|             yield from generate_sysctl_key_value_pairs_from_json(value, [*parents, key]) | ||||
|     elif isinstance(json_data, list): | ||||
|         raise ValueError(f"List not supported: '{json_data}'") | ||||
|     else: | ||||
|         # If it's a leaf node, yield the path | ||||
|         yield (parents, json_data) | ||||
| 
 | ||||
| key_value_pairs = generate_sysctl_key_value_pairs_from_json(node.metadata.get('sysctl')) | ||||
| 
 | ||||
| files= { | ||||
|     '/etc/sysctl.d/managed.conf': { | ||||
|         'content': '\n'.join( | ||||
|             sorted( | ||||
|                 f"{'.'.join(path)}={value}" | ||||
|                     for path, value in key_value_pairs | ||||
|             ), | ||||
|         ), | ||||
|         'triggers': [ | ||||
|             'svc_systemd:systemd-sysctl.service:restart', | ||||
|         ], | ||||
|     }, | ||||
|     '/etc/modules-load.d/managed.conf': { | ||||
|         'content': '\n'.join(sorted(node.metadata.get('modules-load'))), | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| svc_systemd = { | ||||
|     'systemd-sysctl.service': {}, | ||||
| } | ||||
| 
 | ||||
| for path, value in key_value_pairs: | ||||
|     actions[f'reload_sysctl.conf_{path}'] = { | ||||
|         'command': f"sysctl --values {'.'.join(path)}  | grep -q {quote('^'+value+'$')}", | ||||
|         'needs': [ | ||||
|             f'action:systemd-sysctl.service', | ||||
|             f'action:systemd-sysctl.service:restart', | ||||
|         ], | ||||
|     } | ||||
|  | @ -1,4 +0,0 @@ | |||
| defaults = { | ||||
|     'sysctl': {}, | ||||
|     'modules-load': set(), | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
		Reference in a new issue