Compare commits

..

No commits in common. "master" and "ipv6_picking" have entirely different histories.

134 changed files with 1069 additions and 7926 deletions

1
.gitignore vendored
View file

@ -2,4 +2,3 @@
.venv .venv
.cache .cache
*.pyc *.pyc
.bw_debug_history

View file

@ -1,148 +0,0 @@
#!/usr/bin/env python3
from argparse import ArgumentParser
from time import sleep
from bundlewrap.exceptions import RemoteException
from bundlewrap.utils.cmdline import get_target_nodes
from bundlewrap.utils.ui import io
from bundlewrap.repo import Repository
from os.path import realpath, dirname
# parse args
parser = ArgumentParser()
parser.add_argument("targets", nargs="*", default=['bundle:routeros'], help="bw nodes selector")
parser.add_argument("--yes", action="store_true", default=False, help="skip confirmation prompts")
args = parser.parse_args()
def wait_up(node):
sleep(5)
while True:
try:
node.run_routeros('/system/resource/print')
except RemoteException:
sleep(2)
continue
else:
io.debug(f"{node.name}: is up")
sleep(10)
return
def upgrade_switch_os(node):
# get versions for comparison
with io.job(f"{node.name}: checking OS version"):
response = node.run_routeros('/system/package/update/check-for-updates').raw[-1]
installed_os = bw.libs.version.Version(response['installed-version'])
latest_os = bw.libs.version.Version(response['latest-version'])
io.debug(f"{node.name}: installed: {installed_os} >= latest: {latest_os}")
# compare versions
if installed_os >= latest_os:
# os is up to date
io.stdout(f"{node.name}: os up to date ({installed_os})")
else:
# confirm os upgrade
if not args.yes and not io.ask(
f"{node.name}: upgrade os from {installed_os} to {latest_os}?", default=True
):
io.stdout(f"{node.name}: skipped by user")
return
# download os
with io.job(f"{node.name}: downloading OS"):
response = node.run_routeros('/system/package/update/download').raw[-1]
io.debug(f"{node.name}: OS upgrade download response: {response['status']}")
# install and wait for reboot
with io.job(f"{node.name}: upgrading OS"):
try:
response = node.run_routeros('/system/package/update/install').raw[-1]
except RemoteException:
pass
wait_up(node)
# verify new os version
with io.job(f"{node.name}: checking new OS version"):
new_os = bw.libs.version.Version(node.run_routeros('/system/package/update/check-for-updates').raw[-1]['installed-version'])
if new_os == latest_os:
io.stdout(f"{node.name}: OS successfully upgraded from {installed_os} to {new_os}")
else:
raise Exception(f"{node.name}: OS upgrade failed, expected {latest_os}, got {new_os}")
def upgrade_switch_firmware(node):
# get versions for comparison
with io.job(f"{node.name}: checking Firmware version"):
response = node.run_routeros('/system/routerboard/print').raw[-1]
current_firmware = bw.libs.version.Version(response['current-firmware'])
upgrade_firmware = bw.libs.version.Version(response['upgrade-firmware'])
io.debug(f"{node.name}: firmware installed: {current_firmware}, upgrade: {upgrade_firmware}")
# compare versions
if current_firmware >= upgrade_firmware:
# firmware is up to date
io.stdout(f"{node.name}: firmware is up to date ({current_firmware})")
else:
# confirm firmware upgrade
if not args.yes and not io.ask(
f"{node.name}: upgrade firmware from {current_firmware} to {upgrade_firmware}?", default=True
):
io.stdout(f"{node.name}: skipped by user")
return
# upgrade firmware
with io.job(f"{node.name}: upgrading Firmware"):
node.run_routeros('/system/routerboard/upgrade')
# reboot and wait
with io.job(f"{node.name}: rebooting"):
try:
node.run_routeros('/system/reboot')
except RemoteException:
pass
wait_up(node)
# verify firmware version
new_firmware = bw.libs.version.Version(node.run_routeros('/system/routerboard/print').raw[-1]['current-firmware'])
if new_firmware == upgrade_firmware:
io.stdout(f"{node.name}: firmware successfully upgraded from {current_firmware} to {new_firmware}")
else:
raise Exception(f"firmware upgrade failed, expected {upgrade_firmware}, got {new_firmware}")
def upgrade_switch(node):
with io.job(f"{node.name}: checking"):
# check if routeros
if node.os != 'routeros':
io.progress_advance(2)
io.stdout(f"{node.name}: skipped, unsupported os {node.os}")
return
# check switch reachability
try:
node.run_routeros('/system/resource/print')
except RemoteException as error:
io.progress_advance(2)
io.stdout(f"{node.name}: skipped, error {error}")
return
upgrade_switch_os(node)
io.progress_advance(1)
upgrade_switch_firmware(node)
io.progress_advance(1)
with io:
bw = Repository(dirname(dirname(realpath(__file__))))
nodes = get_target_nodes(bw, args.targets)
io.progress_set_total(len(nodes) * 2)
io.stdout(f"upgrading {len(nodes)} switches: {', '.join([node.name for node in sorted(nodes)])}")
for node in sorted(nodes):
upgrade_switch(node)

View file

@ -1,22 +0,0 @@
#!/usr/bin/env python3
from bundlewrap.repo import Repository
from os.path import realpath, dirname
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('node', help='Node to generate passwords for')
args = parser.parse_args()
bw = Repository(dirname(dirname(realpath(__file__))))
node = bw.get_node(args.node)
if node.password:
print(f"password: {node.password}")
for metadata_key in sorted([
'users/root/password',
]):
if value := node.metadata.get(metadata_key, None):
print(f"{metadata_key}: {value}")

View file

@ -3,4 +3,4 @@
from bundlewrap.repo import Repository from bundlewrap.repo import Repository
from os.path import realpath, dirname from os.path import realpath, dirname
bw = Repository(dirname(dirname(realpath(__file__)))) repo = Repository(dirname(dirname(realpath(__file__))))

View file

@ -1,132 +0,0 @@
#!/usr/bin/env python3
from bundlewrap.repo import Repository
from os.path import realpath, dirname
import json
import os
import subprocess
from dataclasses import dataclass
from typing import Optional, List
bw = Repository(dirname(dirname(realpath(__file__))))
VAULT=bw.vault.decrypt('encrypt$gAAAAABpLgX_xxb5NmNCl3cgHM0JL65GT6PHVXO5gwly7IkmWoEgkCDSuAcSAkNFB8Tb4RdnTdpzVQEUL1XppTKVto_O7_b11GjATiyQYiSfiQ8KZkTKLvk=').value
BW_TAG = "bw"
BUNDLEWRAP_FIELD_LABEL = "bundlewrap node id"
@dataclass
class OpResult:
stdout: str
stderr: str
returncode: int
def main():
for node in bw.nodes_in_group('routeros'):
upsert_node_item(
node_name=node.name,
node_uuid=node.metadata.get('id'),
username=node.username,
password=node.password,
url=f'http://{node.hostname}',
)
def run_op(args):
proc = subprocess.run(
["op", "--vault", VAULT] + args,
env=os.environ.copy(),
capture_output=True,
text=True,
)
if proc.returncode != 0:
raise RuntimeError(
f"op {' '.join(args)} failed with code {proc.returncode}:\n"
f"STDOUT:\n{proc.stdout}\n\nSTDERR:\n{proc.stderr}"
)
return OpResult(stdout=proc.stdout, stderr=proc.stderr, returncode=proc.returncode)
def op_item_list_bw():
out = run_op([
"item", "list",
"--tags", BW_TAG,
"--format", "json",
])
stdout = out.stdout.strip()
return json.loads(stdout) if stdout else []
def op_item_get(item_id):
args = ["item", "get", item_id, "--format", "json"]
return json.loads(run_op(args).stdout)
def op_item_create(title, node_uuid, username, password, url):
print(f"creating {title}")
return json.loads(run_op([
"item", "create",
"--category", "LOGIN",
"--title", title,
"--tags", BW_TAG,
"--url", url,
"--format", "json",
f"username={username}",
f"password={password}",
f"{BUNDLEWRAP_FIELD_LABEL}[text]={node_uuid}",
]).stdout)
def op_item_edit(item_id, title, username, password, url):
print(f"updating {title}")
return json.loads(run_op([
"item", "edit",
item_id,
"--title", title,
"--url", url,
"--format", "json",
f"username={username}",
f"password={password}",
]).stdout)
def find_node_item_id(node_uuid):
for summary in op_item_list_bw():
item_id = summary.get("id")
if not item_id:
continue
item = op_item_get(item_id)
for field in item.get("fields") or []:
label = field.get("label")
value = field.get("value")
if label == BUNDLEWRAP_FIELD_LABEL and value == node_uuid:
return item_id
return None
def upsert_node_item(node_name, node_uuid, username, password, url):
if item_id := find_node_item_id(node_uuid):
return op_item_edit(
item_id=item_id,
title=node_name,
username=username,
password=password,
url=url,
)
else:
return op_item_create(
title=node_name,
node_uuid=node_uuid,
username=username,
password=password,
url=url,
)
if __name__ == "__main__":
main()

View file

@ -1,216 +0,0 @@
#!/usr/bin/env python3
from subprocess import check_output, CalledProcessError
from datetime import datetime, timedelta
from pathlib import Path
import json
from argparse import ArgumentParser
from concurrent.futures import ThreadPoolExecutor, as_completed
from os import cpu_count
from time import sleep
EXT_GROUPS = {
"quicktime": {".mp4", ".mov", ".heic", ".cr3"},
"exif": {".jpg", ".jpeg", ".cr2"},
}
DATETIME_KEYS = [
("Composite", "SubSecDateTimeOriginal"),
("Composite", "SubSecCreateDate"),
('ExifIFD', 'DateTimeOriginal'),
('ExifIFD', 'CreateDate'),
('XMP-xmp', 'CreateDate'),
('Keys', 'CreationDate'),
('QuickTime', 'CreateDate'),
('XMP-photoshop', 'DateCreated'),
]
def run(command):
return check_output(command, text=True).strip()
def mdls_timestamp(file):
for i in range(5): # retry a few times in case of transient mdls failures
try:
output = run(('mdls', '-raw', '-name', 'kMDItemContentCreationDate', file))
except CalledProcessError as e:
print(f"{file}: Error running mdls (attempt {i+1}/5): {e}")
continue
try:
return datetime.strptime(output, "%Y-%m-%d %H:%M:%S %z")
except ValueError as e:
print(f"{file}: Error parsing mdls output (attempt {i+1}/5): {e}")
continue
sleep(1)
raise RuntimeError(f"Failed to get mdls timestamp for {file} after 5 attempts")
def exiftool_data(file):
try:
output = run((
'exiftool',
'-j', # json
'-a', # unknown tags
'-u', # unknown values
'-g1', # group by category
'-time:all', # all time tags
'-api', 'QuickTimeUTC=1', # use UTC for QuickTime timestamps
'-d', '%Y-%m-%dT%H:%M:%S%z',
file,
))
except CalledProcessError as e:
print(f"Error running exiftool: {e}")
return None
else:
return json.loads(output)[0]
def exiftool_timestamp(file):
data = exiftool_data(file)
for category, key in DATETIME_KEYS:
try:
value = data[category][key]
return category, key, datetime.strptime(value, '%Y-%m-%dT%H:%M:%S%z')
except (TypeError, KeyError, ValueError) as e:
continue
print(f"⚠️ {file}: No timestamp found in exiftool: " + json.dumps(data, indent=2))
return None, None, None
def photo_has_embedded_timestamp(file):
mdls_ts = mdls_timestamp(file)
category, key, exiftool_ts = exiftool_timestamp(file)
if not exiftool_ts:
print(f"⚠️ {file}: No timestamp found in exiftool")
return False
# normalize timezone for comparison
exiftool_ts = exiftool_ts.astimezone(mdls_ts.tzinfo)
delta = abs(mdls_ts - exiftool_ts)
if delta < timedelta(hours=1): # allow for small differences
print(f"✅ {file}: {mdls_ts.isoformat()} (#{category}:{key})")
return True
else:
print(f"⚠️ {file}: {mdls_ts.isoformat()} != {exiftool_ts} (Δ={delta})")
return False
def photos_without_embedded_timestamps(directory):
executor = ThreadPoolExecutor(max_workers=cpu_count()//2)
try:
futures = {
executor.submit(photo_has_embedded_timestamp, file): file
for file in directory.iterdir()
if file.is_file()
if file.suffix.lower() not in {".aae"}
if not file.name.startswith('.')
}
for future in as_completed(futures):
file = futures[future]
has_ts = future.result() # raises immediately on first failed future
if has_ts:
file.rename(file.parent / 'ok' / file.name)
else:
yield file
except Exception:
executor.shutdown(wait=False, cancel_futures=True)
raise
else:
executor.shutdown(wait=True)
def exiftool_write(file, assignments):
print(f"🔵 {file}: Writing -- {assignments}")
return run((
"exiftool", "-overwrite_original",
"-api", "QuickTimeUTC=1",
*[
f"-{group}:{tag}={value}"
for group, tag, value in assignments
],
str(file),
))
def add_missing_timestamp(file):
data = exiftool_data(file)
mdls_ts = mdls_timestamp(file)
offset = mdls_ts.strftime("%z")
offset = f"{offset[:3]}:{offset[3:]}" if len(offset) == 5 else offset
exif_ts = mdls_ts.strftime("%Y:%m:%d %H:%M:%S")
qt_ts = mdls_ts.strftime("%Y:%m:%d %H:%M:%S")
qt_ts_tz = f"{qt_ts}{offset}"
ext = file.suffix.lower()
try:
if ext in {".heic"}:
exiftool_write(file, [
("ExifIFD", "DateTimeOriginal", qt_ts),
("ExifIFD", "CreateDate", qt_ts),
("ExifIFD", "OffsetTime", offset),
("ExifIFD", "OffsetTimeOriginal", offset),
("ExifIFD", "OffsetTimeDigitized", offset),
("QuickTime", "CreateDate", qt_ts_tz),
("Keys", "CreationDate", qt_ts_tz),
("XMP-xmp", "CreateDate", qt_ts_tz),
])
elif "QuickTime" in data or ext in {".mp4", ".mov", ".heic", ".cr3"}:
exiftool_write(file, [
("QuickTime", "CreateDate", qt_ts_tz),
("Keys", "CreationDate", qt_ts_tz),
])
elif "ExifIFD" in data or ext in {".jpg", ".jpeg", ".cr2", ".webp"}:
exiftool_write(file, [
("ExifIFD", "DateTimeOriginal", exif_ts),
("ExifIFD", "CreateDate", exif_ts),
("IFD0", "ModifyDate", exif_ts),
("ExifIFD", "OffsetTime", offset),
("ExifIFD", "OffsetTimeOriginal", offset),
("ExifIFD", "OffsetTimeDigitized", offset),
])
elif ext in {".png", ".gif", ".avif"}:
exiftool_write(file, [
("XMP-xmp", "CreateDate", qt_ts_tz),
("XMP-photoshop", "DateCreated", exif_ts),
])
else:
print(f"❌ {file}: unsupported type, skipped")
return
if photo_has_embedded_timestamp(file):
print(f"✅ {file}: Timestamp successfully added: {mdls_ts.isoformat()}")
file.rename(file.parent / 'processed' / file.name)
return
else:
category, key, exiftool_ts = exiftool_timestamp(file)
print(f"❌ {file}: Timestamp still wrong/missing after write '{category}:{key}:{exiftool_ts}': #{json.dumps(data, indent=4)}")
return
except CalledProcessError as e:
print(f"❌ {file}: Failed to write timestamp: {e}")
return
if __name__ == "__main__":
parser = ArgumentParser(description="Print timestamps of photos in the current directory.")
parser.add_argument("-d", "--directory", help="Directory to scan for photos")
args = parser.parse_args()
directory = Path(args.directory)
(directory/'ok').mkdir(exist_ok=True)
(directory/'processed').mkdir(exist_ok=True)
_photos_without_embedded_timestamps = list(photos_without_embedded_timestamps(directory))
print(f"{len(_photos_without_embedded_timestamps)} photos without embedded timestamps found.")
print("Press Enter to add missing timestamps...")
input()
for file in _photos_without_embedded_timestamps:
add_missing_timestamp(file)

View file

@ -4,21 +4,20 @@ from bundlewrap.repo import Repository
from os.path import realpath, dirname from os.path import realpath, dirname
from sys import argv from sys import argv
from ipaddress import ip_network, ip_interface from ipaddress import ip_network, ip_interface
import argparse
if len(argv) != 3:
print(f'usage: {argv[0]} <node> <client>')
exit(1)
# get info from repo
repo = Repository(dirname(dirname(realpath(__file__)))) repo = Repository(dirname(dirname(realpath(__file__))))
server_node = repo.get_node('htz.mails') server_node = repo.get_node(argv[1])
available_clients = server_node.metadata.get('wireguard/clients').keys()
# parse args if argv[2] not in server_node.metadata.get('wireguard/clients'):
parser = argparse.ArgumentParser(description='Generate WireGuard client configuration.') print(f'client {argv[2]} not found in: {server_node.metadata.get("wireguard/clients").keys()}')
parser.add_argument('client', choices=available_clients, help='The client name to generate the configuration for.') exit(1)
args = parser.parse_args()
data = server_node.metadata.get(f'wireguard/clients/{argv[2]}')
# get cert
data = server_node.metadata.get(f'wireguard/clients/{args.client}')
vpn_network = ip_interface(server_node.metadata.get('wireguard/my_ip')).network vpn_network = ip_interface(server_node.metadata.get('wireguard/my_ip')).network
allowed_ips = [ allowed_ips = [
vpn_network, vpn_network,
@ -44,15 +43,10 @@ Endpoint = {ip_interface(server_node.metadata.get('network/external/ipv4')).ip}:
PersistentKeepalive = 10 PersistentKeepalive = 10
''' '''
answer = input("print config or qrcode? [Cq]: ").strip().upper() print('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
match answer: print(conf)
case '' | 'C': print('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<')
print('>>>>>>>>>>>>>>>')
print(conf) if input("print qrcode? [Yn]: ").upper() in ['', 'Y']:
print('<<<<<<<<<<<<<<<')
case 'Q':
import pyqrcode import pyqrcode
print(pyqrcode.create(conf).terminal(quiet_zone=1)) print(pyqrcode.create(conf).terminal(quiet_zone=1))
case _:
print(f'Invalid option "{answer}".')
exit(1)

View file

@ -13,14 +13,16 @@ defaults = {
}, },
}, },
'telegraf': { 'telegraf': {
'config': {
'inputs': { 'inputs': {
'exec': { 'exec': {
'apcupsd': { repo.libs.hashable.hashable({
'commands': ["sudo /usr/local/share/telegraf/apcupsd"], 'commands': ["sudo /usr/local/share/telegraf/apcupsd"],
'name_override': "apcupsd", 'name_override': "apcupsd",
'data_format': "influx", 'data_format': "influx",
'interval': '30s', 'interval': '30s',
'flush_interval': '30s', 'flush_interval': '30s',
}),
}, },
}, },
}, },

View file

@ -4,7 +4,6 @@ defaults = {
'apt-listchanges': { 'apt-listchanges': {
'installed': False, 'installed': False,
}, },
'ca-certificates': {},
}, },
'config': { 'config': {
'DPkg': { 'DPkg': {

View file

@ -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.

View file

@ -29,7 +29,6 @@ view "${view_name}" {
% if view_conf['is_internal']: % if view_conf['is_internal']:
recursion yes; recursion yes;
include "/etc/bind/zones.rfc1918";
% else: % else:
recursion no; recursion no;
rate-limit { rate-limit {
@ -63,6 +62,9 @@ view "${view_name}" {
file "/var/lib/bind/${view_name}/${zone_name}"; file "/var/lib/bind/${view_name}/${zone_name}";
}; };
% endfor % endfor
include "/etc/bind/named.conf.default-zones";
include "/etc/bind/zones.rfc1918";
}; };
% endfor % endfor

View file

@ -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"; };

View file

@ -142,21 +142,3 @@ actions['named-checkconf'] = {
'svc_systemd:bind9:reload', 'svc_systemd:bind9:reload',
] ]
} }
# 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',
],
}

View file

@ -49,13 +49,13 @@ defaults = {
}, },
}, },
'telegraf': { 'telegraf': {
'config': {
'inputs': { 'inputs': {
'bind': { 'bind': [{
'default': {
'urls': ['http://localhost:8053/xml/v3'], 'urls': ['http://localhost:8053/xml/v3'],
'gather_memory_contexts': False, 'gather_memory_contexts': False,
'gather_views': True, 'gather_views': True,
}, }],
}, },
}, },
}, },

View file

@ -112,11 +112,6 @@ def process_recording(filename):
sample_num += samples_per_block - overlapping_samples 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 # write a spectrogram using the sound from start to end of the event
def write_event(current_event, soundfile, samplerate): def write_event(current_event, soundfile, samplerate):

View file

@ -19,7 +19,5 @@ do
-t "3600" \ -t "3600" \
-c:a flac \ -c:a flac \
-compression_level 12 \ -compression_level 12 \
"recordings/current/$DATE.flac" "recordings/$DATE.flac"
mv "recordings/current/$DATE.flac" "recordings/$DATE.flac"
done done

View file

@ -8,7 +8,7 @@ urllib3.disable_warnings()
import os import os
HUE_IP = "${hue_ip}" # replace with your bridge IP HUE_IP = "10.0.0.134" # replace with your bridge IP
HUE_APP_KEY = "${hue_app_key}" # local only HUE_APP_KEY = "${hue_app_key}" # local only
HUE_DEVICE_ID = "31f58786-3242-4e88-b9ce-23f44ba27bbe" HUE_DEVICE_ID = "31f58786-3242-4e88-b9ce-23f44ba27bbe"
TEMPERATURE_LOG_DIR = "/opt/bootshorn/temperatures" TEMPERATURE_LOG_DIR = "/opt/bootshorn/temperatures"

View file

@ -7,15 +7,11 @@ directories = {
'owner': 'ckn', 'owner': 'ckn',
'group': 'ckn', 'group': 'ckn',
}, },
'/opt/bootshorn/temperatures': {
'owner': 'ckn',
'group': 'ckn',
},
'/opt/bootshorn/recordings': { '/opt/bootshorn/recordings': {
'owner': 'ckn', 'owner': 'ckn',
'group': 'ckn', 'group': 'ckn',
}, },
'/opt/bootshorn/recordings/current': { '/opt/bootshorn/temperatures': {
'owner': 'ckn', 'owner': 'ckn',
'group': 'ckn', 'group': 'ckn',
}, },
@ -38,7 +34,6 @@ files = {
'/opt/bootshorn/temperature': { '/opt/bootshorn/temperature': {
'content_type': 'mako', 'content_type': 'mako',
'context': { 'context': {
'hue_ip': repo.get_node('home.hue').hostname,
'hue_app_key': repo.vault.decrypt('encrypt$gAAAAABoc2WxZCLbxl-Z4IrSC97CdOeFgBplr9Fp5ujpd0WCCCPNBUY_WquHN86z8hKLq5Y04dwq8TdJW0PMSOSgTFbGgdp_P1q0jOBLEKaW9IIT1YM88h-JYwLf9QGDV_5oEfvnBCtO'), 'hue_app_key': repo.vault.decrypt('encrypt$gAAAAABoc2WxZCLbxl-Z4IrSC97CdOeFgBplr9Fp5ujpd0WCCCPNBUY_WquHN86z8hKLq5Y04dwq8TdJW0PMSOSgTFbGgdp_P1q0jOBLEKaW9IIT1YM88h-JYwLf9QGDV_5oEfvnBCtO'),
}, },
'owner': 'ckn', 'owner': 'ckn',

View file

@ -27,7 +27,7 @@ def ssh_keys(metadata):
'users': { 'users': {
'build-agent': { 'build-agent': {
'authorized_users': { 'authorized_users': {
f'build-server@{other_node.name}': {} f'build-server@{other_node.name}'
for other_node in repo.nodes for other_node in repo.nodes
if other_node.has_bundle('build-server') if other_node.has_bundle('build-server')
for architecture in other_node.metadata.get('build-server/architectures').values() for architecture in other_node.metadata.get('build-server/architectures').values()

View file

@ -14,7 +14,7 @@ def ssh_keys(metadata):
'users': { 'users': {
'build-ci': { 'build-ci': {
'authorized_users': { 'authorized_users': {
f'build-server@{other_node.name}': {} f'build-server@{other_node.name}'
for other_node in repo.nodes for other_node in repo.nodes
if other_node.has_bundle('build-server') if other_node.has_bundle('build-server')
}, },

View file

@ -8,7 +8,6 @@ defaults = {
'sources': { 'sources': {
'crystal': { 'crystal': {
# https://software.opensuse.org/download.html?project=devel%3Alanguages%3Acrystal&package=crystal # https://software.opensuse.org/download.html?project=devel%3Alanguages%3Acrystal&package=crystal
# curl -fsSL https://download.opensuse.org/repositories/devel:/languages:/crystal/Debian_Testing/Release.key
'urls': { 'urls': {
'http://download.opensuse.org/repositories/devel:/languages:/crystal/Debian_Testing/', 'http://download.opensuse.org/repositories/devel:/languages:/crystal/Debian_Testing/',
}, },

View file

@ -0,0 +1,17 @@
connect = host=${host} dbname=${name} user=${user} password=${password}
driver = pgsql
default_pass_scheme = ARGON2ID
user_query = SELECT '/var/vmail/%u' AS home, 'vmail' AS uid, 'vmail' AS gid
iterate_query = SELECT CONCAT(users.name, '@', domains.name) AS user \
FROM users \
LEFT JOIN domains ON users.domain_id = domains.id \
WHERE redirect IS NULL
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)

View file

@ -1,17 +1,13 @@
dovecot_config_version = ${config_version}
dovecot_storage_version = ${storage_version}
protocols = imap lmtp sieve protocols = imap lmtp sieve
auth_mechanisms = plain login auth_mechanisms = plain login
mail_privileged_group = mail
ssl = required ssl = required
ssl_server_cert_file = /var/lib/dehydrated/certs/${hostname}/fullchain.pem ssl_cert = </var/lib/dehydrated/certs/${node.metadata.get('mailserver/hostname')}/fullchain.pem
ssl_server_key_file = /var/lib/dehydrated/certs/${hostname}/privkey.pem ssl_key = </var/lib/dehydrated/certs/${node.metadata.get('mailserver/hostname')}/privkey.pem
ssl_server_dh_file = /etc/dovecot/dhparam.pem ssl_dh = </etc/dovecot/dhparam.pem
ssl_client_ca_dir = /etc/ssl/certs ssl_client_ca_dir = /etc/ssl/certs
mail_driver = maildir mail_location = maildir:${node.metadata.get('mailserver/maildir')}/%u:INDEX=${node.metadata.get('mailserver/maildir')}/index/%u
mail_path = ${maildir}/%{user} mail_plugins = fts fts_xapian
mail_index_path = ${maildir}/index/%{user}
mail_plugins = fts fts_flatcurve
namespace inbox { namespace inbox {
inbox = yes inbox = yes
@ -34,46 +30,14 @@ namespace inbox {
} }
} }
# postgres passdb userdb passdb {
driver = sql
sql_driver = pgsql args = /etc/dovecot/dovecot-sql.conf
pgsql main {
parameters {
host = ${db_host}
dbname = ${db_name}
user = ${db_user}
password = ${db_password}
}
} }
# use sql for userdb too, to enable iterate_query
passdb sql { userdb {
passdb_default_password_scheme = ARGON2ID driver = sql
args = /etc/dovecot/dovecot-sql.conf
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
} }
service auth { service auth {
@ -103,9 +67,10 @@ service stats {
} }
} }
service managesieve-login { service managesieve-login {
#inet_listener sieve {} inet_listener sieve {
process_min_avail = 1 }
process_limit = 1 process_min_avail = 0
service_count = 1
vsz_limit = 64 M vsz_limit = 64 M
} }
service managesieve { service managesieve {
@ -113,53 +78,31 @@ service managesieve {
} }
protocol imap { protocol imap {
mail_plugins = fts fts_flatcurve imap_sieve mail_plugins = $mail_plugins imap_sieve
mail_max_userip_connections = 50 mail_max_userip_connections = 50
imap_idle_notify_interval = 29 mins imap_idle_notify_interval = 29 mins
} }
protocol lmtp { protocol lmtp {
mail_plugins = fts fts_flatcurve sieve mail_plugins = $mail_plugins sieve
} }
protocol sieve {
# Persönliches Skript (deine alte Datei /var/vmail/sieve/%u.sieve) plugin {
sieve_script personal { sieve = /var/vmail/sieve/%u.sieve
driver = file sieve_storage = /var/vmail/sieve/%u/
# 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
} }
# fulltext search # 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 { service indexer-worker {
process_limit = ${indexer_cores} vsz_limit = ${indexer_ram}
vsz_limit = ${indexer_ram}M
} }
service decode2text { service decode2text {
executable = script /usr/local/libexec/dovecot/decode2text.sh executable = script /usr/local/libexec/dovecot/decode2text.sh
@ -169,39 +112,24 @@ service decode2text {
} }
} }
mailbox Junk { # spam filter
sieve_script learn_spam { plugin {
driver = file sieve_plugins = sieve_imapsieve sieve_extprograms
type = before sieve_dir = /var/vmail/sieve/%u/
cause = copy sieve = /var/vmail/sieve/%u.sieve
path = /var/vmail/sieve/global/learn-spam.sieve sieve_pipe_bin_dir = /var/vmail/sieve/bin
} sieve_extensions = +vnd.dovecot.pipe
}
imapsieve_from Junk { sieve_after = /var/vmail/sieve/global/spam-to-folder.sieve
sieve_script learn_ham {
driver = file
type = before
cause = copy
path = /var/vmail/sieve/global/learn-ham.sieve
}
}
# Extprograms-Plugin einschalten # From elsewhere to Spam folder
sieve_plugins { imapsieve_mailbox1_name = Junk
sieve_extprograms = yes imapsieve_mailbox1_causes = COPY
} imapsieve_mailbox1_before = file:/var/vmail/sieve/global/learn-spam.sieve
# Welche Sieve-Erweiterungen dürfen genutzt werden? # From Spam folder to elsewhere
# Empfehlung: nur global erlauben (nicht in User-Skripten): imapsieve_mailbox2_name = *
sieve_global_extensions { imapsieve_mailbox2_from = Junk
vnd.dovecot.pipe = yes imapsieve_mailbox2_causes = COPY
# vnd.dovecot.filter = yes # nur falls gebraucht imapsieve_mailbox2_before = file:/var/vmail/sieve/global/learn-ham.sieve
# vnd.dovecot.execute = yes # nur falls gebraucht
} }
# 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

View file

@ -44,16 +44,6 @@ files = {
'context': { 'context': {
'admin_email': node.metadata.get('mailserver/admin_email'), 'admin_email': node.metadata.get('mailserver/admin_email'),
'indexer_ram': node.metadata.get('dovecot/indexer_ram'), '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': { 'needs': {
'pkg_apt:' 'pkg_apt:'
@ -62,9 +52,29 @@ files = {
'svc_systemd:dovecot:restart', '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': { '/etc/dovecot/dhparam.pem': {
'content_type': 'any', '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': { '/var/vmail/sieve/global/spam-to-folder.sieve': {
'owner': 'vmail', 'owner': 'vmail',
'group': 'vmail', 'group': 'vmail',
@ -121,6 +131,7 @@ svc_systemd = {
'action:letsencrypt_update_certificates', 'action:letsencrypt_update_certificates',
'action:dovecot_generate_dhparam', 'action:dovecot_generate_dhparam',
'file:/etc/dovecot/dovecot.conf', 'file:/etc/dovecot/dovecot.conf',
'file:/etc/dovecot/dovecot-sql.conf',
}, },
}, },
} }

View file

@ -8,7 +8,7 @@ defaults = {
'dovecot-sieve': {}, 'dovecot-sieve': {},
'dovecot-managesieved': {}, 'dovecot-managesieved': {},
# fulltext search # fulltext search
'dovecot-flatcurve': {}, # buster-backports 'dovecot-fts-xapian': {}, # buster-backports
'poppler-utils': {}, # pdftotext 'poppler-utils': {}, # pdftotext
'catdoc': {}, # catdoc, catppt, xls2csv 'catdoc': {}, # catdoc, catppt, xls2csv
}, },

View file

@ -57,7 +57,7 @@ def ssh_keys(metadata):
'users': { 'users': {
'downloads': { 'downloads': {
'authorized_users': { 'authorized_users': {
f'build-server@{other_node.name}': {} f'build-server@{other_node.name}'
for other_node in repo.nodes for other_node in repo.nodes
if other_node.has_bundle('build-server') if other_node.has_bundle('build-server')
}, },

View file

@ -49,7 +49,7 @@ files['/etc/gitea/app.ini'] = {
), ),
'owner': 'git', 'owner': 'git',
'mode': '0600', 'mode': '0600',
'context': node.metadata.get('gitea'), 'context': node.metadata['gitea'],
'triggers': { 'triggers': {
'svc_systemd:gitea:restart', 'svc_systemd:gitea:restart',
}, },

View file

@ -127,7 +127,7 @@ for dashboard_id, monitored_node in enumerate(monitored_nodes, start=1):
panel['gridPos']['y'] = (row_id - 1) * panel['gridPos']['h'] panel['gridPos']['y'] = (row_id - 1) * panel['gridPos']['h']
if 'display_name' in panel_config: if 'display_name' in panel_config:
panel['fieldConfig']['defaults']['displayName'] = panel_config['display_name'] panel['fieldConfig']['defaults']['displayName'] = '${'+panel_config['display_name']+'}'
if panel_config.get('stacked'): if panel_config.get('stacked'):
panel['fieldConfig']['defaults']['custom']['stacking']['mode'] = 'normal' panel['fieldConfig']['defaults']['custom']['stacking']['mode'] = 'normal'
@ -158,14 +158,13 @@ for dashboard_id, monitored_node in enumerate(monitored_nodes, start=1):
host=monitored_node.name, host=monitored_node.name,
negative=query_config.get('negative', False), negative=query_config.get('negative', False),
boolean_to_int=query_config.get('boolean_to_int', False), boolean_to_int=query_config.get('boolean_to_int', False),
over=query_config.get('over', None), minimum=query_config.get('minimum', None),
filters={ filters={
'host': monitored_node.name, 'host': monitored_node.name,
**query_config['filters'], **query_config['filters'],
}, },
exists=query_config.get('exists', []), exists=query_config.get('exists', []),
function=query_config.get('function', None), function=query_config.get('function', None),
multiply=query_config.get('multiply', None),
).strip() ).strip()
}) })
@ -179,3 +178,4 @@ for dashboard_id, monitored_node in enumerate(monitored_nodes, start=1):
'svc_systemd:grafana-server:restart', 'svc_systemd:grafana-server:restart',
] ]
} }

View file

@ -2,7 +2,7 @@ files = {
'/usr/local/share/telegraf/cpu_frequency': { '/usr/local/share/telegraf/cpu_frequency': {
'mode': '0755', 'mode': '0755',
'triggers': { 'triggers': {
'svc_systemd:telegraf.service:restart', 'svc_systemd:telegraf:restart',
}, },
}, },
} }

View file

@ -14,18 +14,17 @@ defaults = {
}, },
}, },
'telegraf': { 'telegraf': {
'config': {
'inputs': { 'inputs': {
'sensors': { 'sensors': {repo.libs.hashable.hashable({
'default': {
'timeout': '2s', 'timeout': '2s',
}, })},
},
'exec': { 'exec': {
'cpu_frequency': { repo.libs.hashable.hashable({
'commands': ["sudo /usr/local/share/telegraf/cpu_frequency"], 'commands': ["sudo /usr/local/share/telegraf/cpu_frequency"],
'name_override': "cpu_frequency", 'name_override': "cpu_frequency",
'data_format': "influx", 'data_format': "influx",
}, }),
# repo.libs.hashable.hashable({ # repo.libs.hashable.hashable({
# 'commands': ["/bin/bash -c 'expr $(cat /sys/class/thermal/thermal_zone0/temp) / 1000'"], # 'commands': ["/bin/bash -c 'expr $(cat /sys/class/thermal/thermal_zone0/temp) / 1000'"],
# 'name_override': "cpu_temperature", # 'name_override': "cpu_temperature",
@ -35,4 +34,5 @@ defaults = {
}, },
}, },
}, },
},
} }

View file

@ -52,14 +52,13 @@ def subnets(metadata):
if 'mac' in network_conf if 'mac' in network_conf
) )
for id, (network_name, network_conf) in enumerate(sorted(metadata.get('network').items())): for network_name, network_conf in metadata.get('network').items():
dhcp_server_config = network_conf.get('dhcp_server_config', None) dhcp_server_config = network_conf.get('dhcp_server_config', None)
if dhcp_server_config: if dhcp_server_config:
_network = ip_network(dhcp_server_config['subnet']) _network = ip_network(dhcp_server_config['subnet'])
subnet4.add(hashable({ subnet4.add(hashable({
'id': id + 1,
'subnet': dhcp_server_config['subnet'], 'subnet': dhcp_server_config['subnet'],
'pools': [ 'pools': [
{ {

View file

@ -1,22 +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
```python Dead Center c1m1_hotel
'tick60_maps': { Dead Center c1m2_streets
'port': 27030, Dead Center c1m3_mall
# add command line arguments Dead Center c1m4_atrium
'arguments': ['-tickrate 60'], Dark Carnival c2m1_highway
# stack overlays, first is uppermost Dark Carnival c2m2_fairgrounds
'overlays': ['tickrate', 'standard'], Dark Carnival c2m3_coaster
# server.cfg contents Dark Carnival c2m4_barns
'config': [ Dark Carnival c2m5_concert
# configs from overlays are accessible via server_${overlay}.cfg Swamp Fever c3m1_plankcountry
'exec server_tickrate.cfg', Swamp Fever c3m2_swamp
# add more options Swamp Fever c3m3_shantytown
'sv_minupdaterate 101', Swamp Fever c3m4_plantation
'sv_maxupdaterate 101', Hard Rain c4m1_milltown_a
'sv_mincmdrate 101', Hard Rain c4m2_sugarmill_a
'sv_maxcmdrate 101', Hard Rain c4m3_sugarmill_b
'sv_consistency 0', 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

View file

@ -1,13 +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
}
function workshop() {
steam mkdir -p "/opt/l4d2/overlays/${overlay}/left4dead2/addons"
steam /opt/l4d2/scripts/steam-workshop-download --out "/opt/l4d2/overlays/${overlay}/left4dead2/addons" "$@"
}

View file

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

View file

@ -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."

View file

@ -1,12 +0,0 @@
#!/bin/bash
set -xeuo pipefail
source /opt/l4d2/scripts/helpers
overlay=$(basename "$0")
# Ions Vocalizer
workshop -i 698857882
# admin system
workshop --item 2524204971
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"

View file

@ -1,25 +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
# install tickrate enabler
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

View file

@ -1,13 +0,0 @@
#!/bin/bash
set -xeuo pipefail
source /opt/l4d2/scripts/helpers
overlay=$(basename "$0")
# workshop --collection 121115793 # Back To School
# workshop --item 2957035482 # hehe30-part1
# workshop --item 2973628334 # hehe30-part2
# workshop --item 3013844371 # hehe30-part3
# workshop --item 3478461158 # 虚伪黎明(Dawn's Deception)
# workshop --item 3478934394 # 虚伪黎明(Dawn's Deception)PART2

View file

@ -1,13 +1,40 @@
// defaults hostname "CroneKorkN : ${name}"
hostname ${server_name} sv_contact "admin@sublimity.de"
sv_steamgroup "${','.join(steamgroups)}"
rcon_password "${rcon_password}"
motd_enabled 0 motd_enabled 0
rcon_password ${rcon_password}
sv_steamgroup "38347879"
mp_autoteambalance 0
sv_forcepreload 1
// server specific sv_cheats 1
% for line in config:
${line}
% endfor 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).

View file

@ -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/scripts/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

View file

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

View file

@ -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"

View file

@ -1,105 +1,122 @@
users = { assert node.has_bundle('steam') and node.has_bundle('steam-workshop-download')
'steam': {
'home': '/opt/l4d2/steam',
'shell': '/bin/bash',
},
}
directories = { directories = {
'/opt/l4d2': { '/opt/steam/left4dead2-servers': {
'owner': 'steam', 'group': 'steam', 'owner': 'steam',
}, 'group': 'steam',
'/opt/l4d2/steam': { 'mode': '0755',
'owner': 'steam', 'group': 'steam',
},
'/opt/l4d2/configs': {
'owner': 'steam', 'group': 'steam',
'purge': True, 'purge': True,
}, },
'/opt/l4d2/scripts': { # Current zfs doesnt support zfs upperdir. The support was added in October 2022. Move upperdir - unused anyway -
'owner': 'steam', 'group': 'steam', # to another dir. Also move workdir alongside it, as it has to be on same fs.
}, '/opt/steam-zfs-overlay-workarounds': {
'/opt/l4d2/scripts/overlays': { 'owner': 'steam',
'owner': 'steam', 'group': 'steam', 'group': 'steam',
'mode': '0755',
'purge': True, 'purge': True,
}, },
} }
files = { # /opt/steam/steam/.steam/sdk32/steamclient.so: cannot open shared object file: No such file or directory
'/opt/l4d2/setup': { symlinks = {
'mode': '755', '/opt/steam/steam/.steam/sdk32': {
'triggers': { 'target': '/opt/steam/steam/linux32',
'svc_systemd:left4dead2-initialize.service:restart', 'owner': 'steam',
}, 'group': 'steam',
}, }
'/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/stop': {
'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}'] = { # SERVERS
'source': f'scripts/overlays/{overlay}', #
'mode': '755',
'triggers': { for name, config in node.metadata.get('left4dead2/servers').items():
'svc_systemd:left4dead2-initialize.service:restart',
}, #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',
} }
svc_systemd = { # conf
'left4dead2-initialize.service': { files[f'/opt/steam/left4dead2-servers/{name}/left4dead2/cfg/server.cfg'] = {
'enabled': True,
'running': None,
'needs': {
'tag:left4dead2-packages',
'file:/opt/l4d2/setup',
'file:/usr/local/lib/systemd/system/left4dead2-initialize.service',
},
},
}
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', 'content_type': 'mako',
'source': 'server.cfg',
'context': { 'context': {
'server_name': server_name, 'name': name,
'rcon_password': repo.vault.decrypt('encrypt$gAAAAABpAdZhxwJ47I1AXotuZmBvyZP1ecVTt9IXFkLI28JiVS74LKs9QdgIBz-FC-iXtIHHh_GVGxxKQZprn4UrXZcvZ57kCKxfHBs3cE2JiGnbWE8_mfs=').value, 'steamgroups': node.metadata.get('left4dead2/steamgroups'),
'config': config.get('config', []), 'rcon_password': config['rcon_password'],
}, },
'owner': 'steam', 'owner': 'steam',
'mode': '644', 'group': 'steam',
'triggers': { 'triggers': [
f'svc_systemd:left4dead2-{server_name}.service:restart', f'svc_systemd:left4dead2-{name}.service:restart',
}, ],
} }
svc_systemd[f'left4dead2-{server_name}.service'] = { # service
'enabled': True, svc_systemd[f'left4dead2-{name}.service'] = {
'running': True, 'needs': [
'tags': { f'file:/opt/steam/left4dead2-servers/{name}/left4dead2/cfg/server.cfg',
'left4dead2-servers', f'file:/usr/local/lib/systemd/system/left4dead2-{name}.service',
}, ],
'needs': { }
'svc_systemd:left4dead2-initialize.service',
f'file:/usr/local/lib/systemd/system/left4dead2-{server_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',
],
} }

View file

@ -1,112 +1,110 @@
from re import match assert node.has_bundle('steam')
from os import path, listdir
from shlex import quote
defaults = { defaults = {
'apt': { 'steam': {
'packages': { 'games': {
'libc6_i386': { # installs libc6:i386 'left4dead2': 222860,
'tags': {'left4dead2-packages'},
},
'lib32z1': {
'tags': {'left4dead2-packages'},
},
'unzip': {
'tags': {'left4dead2-packages'},
},
'p7zip-full': { # l4d2center_maps_sync.sh
'tags': {'left4dead2-packages'},
},
}, },
}, },
'left4dead2': { 'left4dead2': {
'overlays': set(listdir(path.join(repo.path, 'bundles/left4dead2/files/scripts/overlays'))), 'servers': {},
'servers': { 'admins': set(),
# 'port': 27017, 'workshop': set(),
# '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'},
},
},
},
}, },
} }
@metadata_reactor.provides( @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', 'systemd/units',
) )
def server_units(metadata): def server_units(metadata):
units = {} units = {}
workshop = {}
for name, config in metadata.get('left4dead2/servers').items(): for name, config in metadata.get('left4dead2/servers').items():
assert match(r'^[A-z0-9-_-]+$', name) # mount overlay
assert 27000 <= config["port"] <= 27100 mountpoint = f'/opt/steam/left4dead2-servers/{name}'
for overlay in config.get('overlays', []): mount_unit_name = mountpoint[1:].replace('-', '\\x2d').replace('/', '-') + '.mount'
assert overlay in metadata.get('left4dead2/overlays'), f"unknown overlay {overlay}, known: {metadata.get('left4dead2/overlays')}" 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',
},
},
}
cmd = f'/opt/l4d2/start -n {name} -p {config["port"]}' # individual workshop
workshop_ids = config.get('workshop', set()) | metadata.get('left4dead2/workshop', set())
if 'config' in config: if workshop_ids:
cmd += f' -c /opt/l4d2/configs/{name}.cfg' workshop[f'left4dead2-{name}'] = {
'ids': workshop_ids,
for overlay in config.get('overlays', []): 'path': f'/opt/steam/left4dead2-servers/{name}/left4dead2/addons',
cmd += f' -o {overlay}' 'user': 'steam',
'requires': {
if 'arguments' in config: mount_unit_name,
cmd += ' -- ' + ' '.join(config['arguments']) },
'required_by': {
f'left4dead2-{name}.service',
},
}
# left4dead2 server unit
units[f'left4dead2-{name}.service'] = { units[f'left4dead2-{name}.service'] = {
'Unit': { 'Unit': {
'Description': f'left4dead2 server {name}', 'Description': f'left4dead2 server {name}',
'After': {'left4dead2-initialize.service'}, 'After': {'steam-update.service'},
'Requires': {'left4dead2-initialize.service'}, 'Requires': {'steam-update.service'},
}, },
'Service': { 'Service': {
'Type': 'simple', 'User': 'steam',
'ExecStart': cmd, 'Group': 'steam',
'ExecStopPost': f'/opt/l4d2/stop -n {name}', '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', 'Restart': 'on-failure',
'Nice': -10,
'CPUWeight': 200,
'IOSchedulingClass': 'best-effort',
'IOSchedulingPriority': 0,
}, },
'Install': { 'Install': {
'WantedBy': {'multi-user.target'}, 'WantedBy': {'multi-user.target'},
}, },
'triggers': {
f'svc_systemd:left4dead2-{name}.service:restart',
},
} }
return { return {
'steam-workshop-download': workshop,
'systemd': { 'systemd': {
'units': units, 'units': units,
}, },
@ -116,13 +114,14 @@ def server_units(metadata):
@metadata_reactor.provides( @metadata_reactor.provides(
'nftables/input', 'nftables/input',
) )
def nftables(metadata): def firewall(metadata):
ports = sorted(str(config["port"]) for config in metadata.get('left4dead2/servers').values()) ports = set(str(server['port']) for server in metadata.get('left4dead2/servers').values())
return { return {
'nftables': { 'nftables': {
'input': { 'input': {
f'ip protocol {{ tcp, udp }} th dport {{ {", ".join(ports)} }} accept' f"tcp dport {{ {', '.join(sorted(ports))} }} accept",
f"udp dport {{ {', '.join(sorted(ports))} }} accept",
}, },
}, },
} }

View file

@ -12,8 +12,9 @@ def generate_sysctl_key_value_pairs_from_json(json_data, parents=[]):
key_value_pairs = generate_sysctl_key_value_pairs_from_json(node.metadata.get('sysctl')) key_value_pairs = generate_sysctl_key_value_pairs_from_json(node.metadata.get('sysctl'))
files= { files= {
'/etc/sysctl.d/managed.conf': { '/etc/sysctl.conf': {
'content': '\n'.join( 'content': '\n'.join(
sorted( sorted(
f"{'.'.join(path)}={value}" f"{'.'.join(path)}={value}"
@ -24,9 +25,6 @@ files= {
'svc_systemd:systemd-sysctl.service:restart', 'svc_systemd:systemd-sysctl.service:restart',
], ],
}, },
'/etc/modules-load.d/managed.conf': {
'content': '\n'.join(sorted(node.metadata.get('modules-load'))),
}
} }
svc_systemd = { svc_systemd = {

View file

@ -1,6 +1,3 @@
defaults = { defaults = {
'sysctl': { 'sysctl': {},
'net.ipv4.icmp_ratelimit': '100',
},
'modules-load': set(),
} }

View file

@ -7,7 +7,12 @@ defaults = {
'locale': { 'locale': {
'default': ('en_US.UTF-8', 'UTF-8'), 'default': ('en_US.UTF-8', 'UTF-8'),
'installed': { 'installed': {
('de_AT.UTF-8', 'UTF-8'),
('de_CH.UTF-8', 'UTF-8'),
('de_DE.UTF-8', 'UTF-8'), ('de_DE.UTF-8', 'UTF-8'),
('de_LU.UTF-8', 'UTF-8'),
('en_CA.UTF-8', 'UTF-8'),
('en_GB.UTF-8', 'UTF-8'),
('en_US.UTF-8', 'UTF-8'), ('en_US.UTF-8', 'UTF-8'),
}, },
}, },

View file

@ -32,14 +32,10 @@ defaults = {
'tank/vmail': { 'tank/vmail': {
'mountpoint': '/var/vmail', 'mountpoint': '/var/vmail',
'compression': 'on', 'compression': 'on',
'atime': 'off',
'recordsize': '16384',
}, },
'tank/vmail/index': { 'tank/vmail/index': {
'mountpoint': '/var/vmail/index', 'mountpoint': '/var/vmail/index',
'compression': 'on', 'compression': 'on',
'atime': 'off',
'recordsize': '4096',
'com.sun:auto-snapshot': 'false', 'com.sun:auto-snapshot': 'false',
'backup': False, 'backup': False,
}, },
@ -82,7 +78,6 @@ def dns(metadata):
'dns': dns, 'dns': dns,
} }
@metadata_reactor.provides( @metadata_reactor.provides(
'letsencrypt/domains', 'letsencrypt/domains',
) )

View file

@ -42,7 +42,7 @@ def user(metadata):
'users': { 'users': {
'sshmon': { 'sshmon': {
'authorized_users': { 'authorized_users': {
'nagios@' + metadata.get('monitoring/icinga2_node'): {}, 'nagios@' + metadata.get('monitoring/icinga2_node'),
} }
}, },
}, },

View file

@ -34,12 +34,10 @@ def dhcp(metadata):
@metadata_reactor.provides( @metadata_reactor.provides(
'systemd/units', 'systemd/units',
'modules-load',
) )
def units(metadata): def units(metadata):
if node.has_bundle('systemd-networkd'): if node.has_bundle('systemd-networkd'):
units = {} units = {}
modules_load = set()
for network_name, network_conf in metadata.get('network').items(): for network_name, network_conf in metadata.get('network').items():
interface_type = network_conf.get('type', None) interface_type = network_conf.get('type', None)
@ -98,15 +96,13 @@ def units(metadata):
# cake WIP # cake WIP
if 'cake' in network_conf: # if 'cake' in network_conf:
units[f'{network_name}.network']['CAKE'] = network_conf['cake'] # units[f'{network_name}.network']['CAKE'] = network_conf['cake']
modules_load.add('sch_cake')
return { return {
'systemd': { 'systemd': {
'units': units, 'units': units,
}, }
'modules-load': modules_load,
} }
else: else:
return {} return {}

View file

@ -1,209 +1,78 @@
#!/usr/bin/env python3 #!/bin/bash
import argparse
import base64
import hashlib
import os
import shutil
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from pathlib import Path
USER="$1"
ALLOWED_EXTS = { REL_SOURCE_PATH="/$1/files/$2"
".png", ".jpg", ".jpeg", ".heic", ".cr2", ".cr3", ".mp4", ".mov", ABS_SOURCE_PATH="/var/lib/nextcloud/$1/files/$2"
".webp", ".avif", ".gif",
}
DATETIME_KEYS = [ REL_DEST_PATH="/$1/files/$3"
("Composite", "SubSecDateTimeOriginal"), ABS_DEST_PATH="/var/lib/nextcloud/$1/files/$3"
("Composite", "SubSecCreateDate"),
("ExifIFD", "DateTimeOriginal"),
("ExifIFD", "CreateDate"),
("XMP-xmp", "CreateDate"),
("Keys", "CreationDate"),
("QuickTime", "CreateDate"),
("XMP-photoshop", "DateCreated"),
]
REL_UNSORTABLE_PATH="/$1/files/$4"
ABS_UNSORTABLE_PATH="/var/lib/nextcloud/$1/files/$4"
def run(command: list[str], check: bool = True) -> subprocess.CompletedProcess: echo "STARTING..."
return subprocess.run(command, text=True, capture_output=True, check=check)
chown -R www-data:www-data "$ABS_SOURCE_PATH"
chmod -R 770 "$ABS_SOURCE_PATH"
def exiftool_data(file: Path) -> dict | None: SCAN="FALSE"
result = run([ IFS=$'\n'
"exiftool", for f in `find "$ABS_SOURCE_PATH" -iname *.PNG -o -iname *.JPG -o -iname *.JPEG -o -iname *.HEIC -o -iname *.CR2 -o -iname *.CR3 -o -iname *.MP4 -o -iname *.MOV`; do
"-j", SCAN="TRUE"
"-a", echo "PROCESSING: $f"
"-u",
"-g1",
"-time:all",
"-api", "QuickTimeUTC=1",
"-d", "%Y-%m-%dT%H:%M:%S%z",
str(file),
], check=False)
if result.returncode != 0:
return None
try:
data = __import__("json").loads(result.stdout)
return data[0] if data else None
except Exception:
return None
EXIF=`exiftool "$f"`
def exiftool_timestamp(file: Path) -> datetime | None: if grep -q '^Create Date' <<< $EXIF
data = exiftool_data(file) then
if not data: DATETIME=`grep -m 1 "^Create Date" <<< $EXIF | cut -d: -f2- | xargs`
return None elif grep -q '^File Modification Date' <<< $EXIF
then
for category, key in DATETIME_KEYS: DATETIME=`grep -m 1 '^File Modification Date' <<< $EXIF | cut -d: -f2- | xargs`
try: else
value = data[category][key] RELPATH=$(realpath --relative-to="$ABS_SOURCE_PATH" "$f")
except (KeyError, TypeError): DIRNAME=$(dirname "$ABS_UNSORTABLE_PATH/$RELPATH")
continue echo "UNSORTABLE: $f"
try: mkdir -p "$DIRNAME"
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S%z") mv "$f" "$DIRNAME"
except ValueError:
continue continue
fi
return None DATE=`cut -d' ' -f1 <<< $DATETIME`
TIME=`cut -d' ' -f2 <<< $DATETIME | cut -d'+' -f1`
YEAR=`cut -d':' -f1 <<< $DATE`
MONTH=`cut -d':' -f2 <<< $DATE`
DAY=`cut -d':' -f3 <<< $DATE`
HOUR=`cut -d':' -f1 <<< $TIME`
MINUTE=`cut -d':' -f2 <<< $TIME`
SECOND=`cut -d':' -f3 <<< $TIME`
def short_hash(file: Path) -> str: HASH=`sha256sum "$f" | xxd -r -p | base64 | head -c 3 | tr '/+' '_-'`
h = hashlib.sha256() EXT=`echo "${f##*.}" | tr '[:upper:]' '[:lower:]'`
with file.open("rb") as fh: if [[ "$EXT" = "cr2" ]] || [[ "$EXT" = "cr3" ]]
for chunk in iter(lambda: fh.read(1024 * 1024), b""): then
h.update(chunk) RAW="raw/"
digest = h.digest() else
b64 = base64.b64encode(digest).decode("ascii") RAW=""
return b64[:3].replace("/", "_").replace("+", "-") fi
FILE="$ABS_DEST_PATH/$YEAR-$MONTH/$RAW$YEAR$MONTH$DAY"-"$HOUR$MINUTE$SECOND"_"$HASH"."$EXT"
echo "DESTINATION: $FILE"
mkdir -p "$(dirname "$FILE")"
mv -v "$f" "$FILE"
done
if [ "$SCAN" == "TRUE" ]; then
echo "SCANNING..."
# find "$ABS_SOURCE_PATH/"* -type d -empty -delete >> /var/echo/nc-picsort.echo # nextcloud app bug when deleting folders
chown -R www-data:www-data "$ABS_DEST_PATH"
chown -R www-data:www-data "$ABS_UNSORTABLE_PATH"
chmod -R 770 "$ABS_DEST_PATH"
chmod -R 770 "$ABS_UNSORTABLE_PATH"
sudo -u www-data php /opt/nextcloud/occ files:scan --path "$REL_SOURCE_PATH"
sudo -u www-data php /opt/nextcloud/occ files:scan --path "$REL_UNSORTABLE_PATH"
sudo -u www-data php /opt/nextcloud/occ files:scan --path "$REL_DEST_PATH"
#sudo -u www-data php /opt/nextcloud/occ preview:pre-generate
fi
def build_destination(dest_root: Path, file: Path, ts: datetime) -> Path: echo "FINISH."
ext = file.suffix.lower().lstrip(".")
year = ts.strftime("%Y")
month = ts.strftime("%m")
day = ts.strftime("%d")
hour = ts.strftime("%H")
minute = ts.strftime("%M")
second = ts.strftime("%S")
hash_part = short_hash(file)
raw_subdir = "raw" if ext in {"cr2", "cr3"} else None
month_dir = dest_root / f"{year}-{month}"
if raw_subdir:
month_dir = month_dir / raw_subdir
filename = f"{year}{month}{day}-{hour}{minute}{second}_{hash_part}.{ext}"
return month_dir / filename
def move_unsortable(file: Path, source_root: Path, unsortable_root: Path) -> None:
relpath = file.relative_to(source_root)
target_dir = (unsortable_root / relpath).parent
target_dir.mkdir(parents=True, exist_ok=True)
shutil.chown(str(target_dir), user="www-data", group="www-data")
target = target_dir / file.name
if target.exists():
return
shutil.move(str(file), str(target))
shutil.chown(str(target), user="www-data", group="www-data")
def move_sorted(file: Path, target: Path) -> None:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.chown(str(target.parent), user="www-data", group="www-data")
shutil.move(str(file), str(target))
shutil.chown(str(target), user="www-data", group="www-data")
def process_file(file: Path, source_root: Path, dest_root: Path, unsortable_root: Path) -> tuple[Path, str]:
print(f"PROCESSING: {file}")
ts = exiftool_timestamp(file)
if ts is None:
print(f"UNSORTABLE: {file}")
move_unsortable(file, source_root, unsortable_root)
return file, "unsortable"
target = build_destination(dest_root, file, ts)
print(f"DESTINATION: {target}")
move_sorted(file, target)
return file, "sorted"
def scan_nextcloud(rel_source: str, rel_unsortable: str, rel_dest: str) -> None:
print("SCANNING...")
# run(["chown", "-R", "www-data:www-data", abs_source_path], check=True)
# run(["chmod", "-R", "770", abs_source_path], check=True)
# run(["chown", "-R", "www-data:www-data", abs_dest_path], check=True)
# run(["chown", "-R", "www-data:www-data", abs_unsortable_path], check=True)
# run(["chmod", "-R", "770", abs_dest_path], check=True)
# run(["chmod", "-R", "770", abs_unsortable_path], check=True)
run(["sudo", "-u", "www-data", "php", "/opt/nextcloud/occ", "files:scan", "--path", rel_source], check=True)
run(["sudo", "-u", "www-data", "php", "/opt/nextcloud/occ", "files:scan", "--path", rel_unsortable], check=True)
run(["sudo", "-u", "www-data", "php", "/opt/nextcloud/occ", "files:scan", "--path", rel_dest], check=True)
run(["systemctl", "start", "nextcloud-generate-new-previews.service"], check=True)
def iter_files(source_root: Path):
for path in source_root.rglob("*"):
if path.is_file() and path.suffix.lower() in ALLOWED_EXTS:
yield path
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Sort Nextcloud media files by embedded timestamp."
)
parser.add_argument("nc_user")
parser.add_argument("source_subdir")
parser.add_argument("dest_subdir")
parser.add_argument("unsortable_subdir")
parser.add_argument("--workers", type=int, default=os.cpu_count() or 1)
args = parser.parse_args()
nc_user = args.nc_user
source_subdir = args.source_subdir
dest_subdir = args.dest_subdir
unsortable_subdir = args.unsortable_subdir
rel_source_path = f"/{nc_user}/files/{source_subdir}"
abs_source_path = f"/var/lib/nextcloud/{nc_user}/files/{source_subdir}"
rel_dest_path = f"/{nc_user}/files/{dest_subdir}"
abs_dest_path = f"/var/lib/nextcloud/{nc_user}/files/{dest_subdir}"
rel_unsortable_path = f"/{nc_user}/files/{unsortable_subdir}"
abs_unsortable_path = f"/var/lib/nextcloud/{nc_user}/files/{unsortable_subdir}"
source_root = Path(abs_source_path)
dest_root = Path(abs_dest_path)
unsortable_root = Path(abs_unsortable_path)
print("STARTING...")
run(["chown", "-R", "www-data:www-data", str(source_root)], check=True)
run(["chmod", "-R", "770", str(source_root)], check=True)
files = list(iter_files(source_root))
if not files:
print("NO MATCHING FILES FOUND.")
print("FINISH.")
raise SystemExit(0)
with ThreadPoolExecutor(max_workers=max(1, args.workers)) as executor:
futures = {
executor.submit(process_file, file, source_root, dest_root, unsortable_root): file
for file in files
}
for future in as_completed(futures):
future.result()
scan_nextcloud(rel_source_path, rel_unsortable_path, rel_dest_path)
print("FINISH.")

View file

@ -1,71 +0,0 @@
Nextcloud
=========
import iphone pictures
----------------------
Use Photos app on macOS
- select library in the left sidebar
- select the pictures
- in menu bar open File > Export Unmodified Original for X Photos
The only reliable way to get some files creation time is being lost with rsync, so
we need to embed those timestamps on macos first:
```sh
PHOTOS_PATH="/Users/mwiegand/Desktop/photos"
bin/timestamp_icloud_photos_for_nextcloud -d "$PHOTOS_PATH"
rsync -avh --progress --rsync-path="sudo rsync" "$PHOTOS_PATH/" ckn@10.0.0.2:/var/lib/nextcloud/ckn/files/SofortUpload/AutoSort/
```
preview generator
-----------------
```
sudo -u www-data php /opt/nextcloud/occ preview:generate-all -w "$(nproc)" -n -vvv
```
This index speeds up preview generator dramatically:
```sh
CREATE INDEX CONCURRENTLY oc_filecache_path_hash_idx
ON oc_filecache (path_hash);
```
delete previews:
```sh
psql nextcloud -x -c "DELETE FROM oc_previews;"
rm -rf /var/lib/nextcloud/appdata_oci6dw1woodz/preview/*
```
https://docs.nextcloud.com/server/stable/admin_manual/configuration_files/previews_configuration.html#maximum-preview-size
```php
'preview_max_x' => 1920,
'preview_max_y' => 1920,
'preview_max_scale_factor' => 4,
```
https://github.com/nextcloud/previewgenerator?tab=readme-ov-file#i-dont-want-to-generate-all-the-preview-sizes
```sh
sudo -u www-data php /opt/nextcloud/occ config:app:set --value="64 256" previewgenerator squareSizes
sudo -u www-data php /opt/nextcloud/occ config:app:set --value="" previewgenerator fillWidthHeightSizes # changed
sudo -u www-data php /opt/nextcloud/occ config:app:set --value="" previewgenerator widthSizes
sudo -u www-data php /opt/nextcloud/occ config:app:set --value="" previewgenerator heightSizes
sudo -u www-data php /opt/nextcloud/occ config:app:set preview jpeg_quality --value="75"
sudo -u www-data php /opt/nextcloud/occ config:app:set --value=0 --type=integer previewgenerator job_max_previews # in favour of systemd timer
```
gen previews
```sh
php /opt/nextcloud/occ preview:generate-all --workers="$(nproc)" --no-interaction -vvv
```
check preview geenration
```sh
find /var/lib/nextcloud/appdata_oci6dw1woodz/preview
# /var/lib/nextcloud/appdata_oci6dw1woodz/preview/6/9/1/f/7/b/4/2822419/64-64-crop.jpg
# /var/lib/nextcloud/appdata_oci6dw1woodz/preview/6/9/1/f/7/b/4/2822419/256-256-crop.jpg
# /var/lib/nextcloud/appdata_oci6dw1woodz/preview/6/9/1/f/7/b/4/2822419/1280-1920-max.jpg
du -sh /var/lib/nextcloud/appdata_oci6dw1woodz/preview
# 28G /var/lib/nextcloud/appdata_oci6dw1woodz/preview
```

View file

@ -0,0 +1,5 @@
#!/bin/bash
php /opt/nextcloud/occ files:scan --all
php /opt/nextcloud/occ files:scan-app-data
#php /opt/nextcloud/occ preview:generate-all

View file

@ -146,3 +146,15 @@ actions['nextcloud_add_missing_inidces'] = {
f'action:extract_nextcloud', f'action:extract_nextcloud',
], ],
} }
# RESCAN
files['/opt/nextcloud_rescan'] = {
'source': 'rescan',
'owner': 'www-data',
'group': 'www-data',
'mode': '550',
'needs': [
'action:extract_nextcloud',
],
}

View file

@ -1,5 +1,5 @@
from shlex import quote import string
from uuid import UUID
defaults = { defaults = {
'apt': { 'apt': {
@ -85,35 +85,11 @@ defaults = {
'user': 'www-data', 'user': 'www-data',
'kill_mode': 'process', 'kill_mode': 'process',
}, },
'nextcloud-scan-app-data': { 'nextcloud-rescan': {
'command': '/usr/bin/php /opt/nextcloud/occ files:scan-app-data', 'command': '/opt/nextcloud_rescan',
'when': 'yearly', 'when': 'Sun 00:00:00',
'user': 'www-data', 'user': 'www-data',
}, },
'nextcloud-scan-files': {
'command': '/usr/bin/php /opt/nextcloud/occ files:scan --all',
'when': 'weekly',
'user': 'www-data',
'after': {
'nextcloud-scan-app-data.service',
},
},
'nextcloud-generate-all-previews': {
'command': '/bin/bash -c ' + quote('php /opt/nextcloud/occ preview:generate-all --workers="$(nproc)" --no-interaction -vvv'),
'when': 'monthly',
'user': 'www-data',
'after': {
'nextcloud-scan-files.service',
},
},
'nextcloud-generate-new-previews': {
'command': '/usr/bin/php /opt/nextcloud/occ preview:pre-generate --no-interaction -vvv',
'when': '*:0/5', # every 5 minutes
'user': 'www-data',
'after': {
'nextcloud-generate-all-previews.service',
},
},
}, },
} }
@ -158,18 +134,10 @@ def config(metadata):
'127.0.0.1', '127.0.0.1',
metadata.get('nextcloud/hostname'), metadata.get('nextcloud/hostname'),
], ],
'enabledPreviewProviders': [
'OC\\Preview\\Image',
'OC\\Preview\\Movie',
'OC\\Preview\\HEIC',
],
'preview_max_x': 1920,
'preview_max_y': 1920,
'preview_max_scale_factor': 4,
'log_type': 'syslog', 'log_type': 'syslog',
'syslog_tag': 'nextcloud', 'syslog_tag': 'nextcloud',
'logfile': '', 'logfile': '',
'loglevel': 2, 'loglevel': 3,
'default_phone_region': 'DE', 'default_phone_region': 'DE',
'versions_retention_obligation': 'auto, 90', 'versions_retention_obligation': 'auto, 90',
'simpleSignUpLink.shown': False, 'simpleSignUpLink.shown': False,

View file

@ -25,9 +25,9 @@ defaults = {
}, },
}, },
'telegraf': { 'telegraf': {
'config': {
'inputs': { 'inputs': {
'postfix': { 'postfix': [{}],
'default': {},
}, },
}, },
}, },

View file

@ -98,17 +98,17 @@ def zfs(metadata):
@metadata_reactor.provides( @metadata_reactor.provides(
'telegraf/inputs/postgresql/default', 'telegraf/config/inputs/postgresql',
) )
def telegraf(metadata): def telegraf(metadata):
return { return {
'telegraf': { 'telegraf': {
'config': {
'inputs': { 'inputs': {
'postgresql': { 'postgresql': [{
'default': {
'address': f'postgres://root:{root_password}@localhost:5432/postgres', 'address': f'postgres://root:{root_password}@localhost:5432/postgres',
'databases': sorted(list(node.metadata.get('postgresql/databases').keys())), 'databases': sorted(list(node.metadata.get('postgresql/databases').keys())),
}, }],
}, },
}, },
}, },

View file

@ -1,4 +1,8 @@
files = { files = {
'/etc/modules-load.d/pppoe.conf': {
'content': 'pppoe\npppox\nppp_generic',
'mode': '0644',
},
'/etc/ppp/peers/isp': { '/etc/ppp/peers/isp': {
'content_type': 'mako', 'content_type': 'mako',
'mode': '0644', 'mode': '0644',

View file

@ -4,11 +4,6 @@ defaults = {
'pppoe': {}, 'pppoe': {},
}, },
}, },
'modules-load': {
'pppoe',
'pppox',
'ppp_generic',
},
'nftables': { 'nftables': {
'nat': { 'nat': {
'oifname ppp0 masquerade', 'oifname ppp0 masquerade',

View file

@ -18,7 +18,7 @@ defaults = {
'sources': { 'sources': {
'proxmox-ve': { 'proxmox-ve': {
'options': { 'options': {
'Architectures': 'amd64', 'aarch': 'amd64',
}, },
'urls': { 'urls': {
'http://download.proxmox.com/debian/pve', 'http://download.proxmox.com/debian/pve',

View file

@ -8,14 +8,16 @@ defaults = {
@metadata_reactor.provides( @metadata_reactor.provides(
'telegraf/agent', 'telegraf/config/agent',
) )
def telegraf(metadata): def telegraf(metadata):
return { return {
'telegraf': { 'telegraf': {
'config': {
'agent': { 'agent': {
'flush_interval': '30s', 'flush_interval': '30s',
'interval': '1m', 'interval': '30s',
},
}, },
}, },
} }

View file

@ -9,7 +9,6 @@ directories = {
}, },
'/var/lib/redis': { '/var/lib/redis': {
'owner': 'redis', 'owner': 'redis',
'group': 'redis',
'mode': '0750', 'mode': '0750',
'needs': [ 'needs': [
'pkg_apt:redis-server', 'pkg_apt:redis-server',

View file

@ -7,16 +7,18 @@ $config['enable_installer'] = true;
/* Local configuration for Roundcube Webmail */ /* Local configuration for Roundcube Webmail */
$config['db_dsnw'] = '${database['provider']}://${database['user']}:${database['password']}@${database['host']}/${database['name']}'; $config['db_dsnw'] = '${database['provider']}://${database['user']}:${database['password']}@${database['host']}/${database['name']}';
$config['imap_host'] = 'ssl://${imap_host}'; $config['imap_host'] = 'localhost';
$config['imap_port'] = 993; $config['smtp_host'] = 'tls://localhost';
#$config['imap_debug'] = true;
$config['smtp_host'] = 'tls://${imap_host}';
$config['smtp_port'] = 587;
$config['smtp_user'] = '%u'; $config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p'; $config['smtp_pass'] = '%p';
#$config['smtp_debug'] = true;
$config['support_url'] = ''; $config['support_url'] = '';
$config['des_key'] = '${des_key}'; $config['des_key'] = '${des_key}';
$config['product_name'] = '${product_name}'; $config['product_name'] = '${product_name}';
$config['plugins'] = array(${', '.join(f'"{plugin}"' for plugin in plugins)}); $config['plugins'] = array(${', '.join(f'"{plugin}"' for plugin in plugins)});
$config['language'] = 'de_DE'; $config['language'] = 'de_DE';
$config['smtp_conn_options'] = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
);

View file

@ -61,7 +61,6 @@ files['/opt/roundcube/config/config.inc.php'] = {
'des_key': node.metadata.get('roundcube/des_key'), 'des_key': node.metadata.get('roundcube/des_key'),
'database': node.metadata.get('roundcube/database'), 'database': node.metadata.get('roundcube/database'),
'plugins': node.metadata.get('roundcube/plugins'), 'plugins': node.metadata.get('roundcube/plugins'),
'imap_host': node.metadata.get('mailserver/hostname'),
}, },
'needs': [ 'needs': [
'action:chown_roundcube', 'action:chown_roundcube',

File diff suppressed because it is too large Load diff

View file

@ -1,11 +0,0 @@
files = {
# https://mikrotik.com/download/tools
'/usr/share/snmp/mibs/MIKROTIK-MIB.txt': {
'source': 'mikrotik.mib',
'content_type': 'binary',
'mode': '0644',
'needed_by': {
'svc_systemd:telegraf.service',
},
},
}

View file

@ -1,444 +0,0 @@
input_defaults = {
"agents": [
f"udp://{routeros_node.hostname}:161"
for routeros_node in repo.nodes_in_group("routeros")
],
"agent_host_tag": "source",
"version": 2,
"community": "public",
"max_repetitions": 5, # supposedly less spiky loads
"tags": {
"operating_system": "routeros",
},
}
defaults = {
'apt': {
'packages': {
'snmp': {},
'snmp-mibs-downloader': {},
},
},
"telegraf": {
"processors": {
"enum": {
"mikrotik_host_mapping":{
# - measurements get switch ip as agent_host tag
# - wie define a value mapping ip -> node name
# - agent_host gets translated and written into host tag
"tagpass": {
"operating_system": ["routeros"],
},
"mapping": [
{
"tags": ["source"],
"dest": "host",
"default": "unknown",
"value_mappings": {
routeros_node.hostname: routeros_node.name
for routeros_node in repo.nodes_in_group("routeros")
},
},
],
},
},
},
"inputs": {
"snmp": {
"mikrotik_switches_fast": {
"interval": "2m",
"collection_jitter": "20s",
**input_defaults,
"table": [
# CPU load (HR-MIB)
{
"name": "mikrotik_cpu",
"oid": "HOST-RESOURCES-MIB::hrProcessorTable",
"field": [
{
"name": "frw_id",
"oid": "HOST-RESOURCES-MIB::hrProcessorFrwID",
"is_tag": True,
},
{
"name": "load",
"oid": "HOST-RESOURCES-MIB::hrProcessorLoad",
},
],
},
# Storage (HR-MIB)
{
"name": "mikrotik_storage",
"oid": "HOST-RESOURCES-MIB::hrStorageTable",
"field": [
{
"name": "index",
"oid": "HOST-RESOURCES-MIB::hrStorageIndex",
"is_tag": True,
},
{
"name": "type",
"oid": "HOST-RESOURCES-MIB::hrStorageType",
"is_tag": True,
},
{
"name": "descr",
"oid": "HOST-RESOURCES-MIB::hrStorageDescr",
"is_tag": True,
},
{
"name": "alloc_unit",
"oid": "HOST-RESOURCES-MIB::hrStorageAllocationUnits",
},
{
"name": "size",
"oid": "HOST-RESOURCES-MIB::hrStorageSize",
},
{
"name": "used",
"oid": "HOST-RESOURCES-MIB::hrStorageUsed",
},
{
"name": "alloc_failures",
"oid": "HOST-RESOURCES-MIB::hrStorageAllocationFailures",
},
],
},
# MikroTik Health (table)
{
"name": "mikrotik_health",
"oid": "MIKROTIK-MIB::mtxrGaugeTable",
"field": [
{
"name": "sensor",
"oid": "MIKROTIK-MIB::mtxrGaugeName",
"is_tag": True,
},
{
"name": "value",
"oid": "MIKROTIK-MIB::mtxrGaugeValue",
},
{
"name": "unit",
"oid": "MIKROTIK-MIB::mtxrGaugeUnit",
"is_tag": True,
},
],
},
],
},
"mikrotik_switches_slow": {
"interval": "7m",
"collection_jitter": "2m",
**input_defaults,
"table": [
# Interface statistics (standard IF-MIB)
{
"name": "mikrotik_interface_generic",
"oid": "IF-MIB::ifTable",
"field": [
# 6: ethernetCsmacd (physischer Ethernet-Port)
# 24: softwareLoopback
# 53: propVirtual (oft VLANs bei MikroTik)
# 131: tunnel
# 135: l2vlan
# 161: ieee8023adLag (Bonding/LACP)
# 209: bridge
{
"name": "ifType",
"oid": "IF-MIB::ifType",
"is_tag": True,
},
# Labels (optional but recommended)
{
"name": "ifName",
"oid": "IF-MIB::ifName",
"is_tag": True,
},
{
"name": "ifAlias",
"oid": "IF-MIB::ifAlias",
"is_tag": True,
},
# Bytes (64-bit)
{
"name": "in_octets",
"oid": "IF-MIB::ifHCInOctets",
},
{
"name": "out_octets",
"oid": "IF-MIB::ifHCOutOctets",
},
# Packets (64-bit unicast)
{
"name": "in_ucast_pkts",
"oid": "IF-MIB::ifHCInUcastPkts",
},
{
"name": "out_ucast_pkts",
"oid": "IF-MIB::ifHCOutUcastPkts",
},
{
"name": "in_mcast_pkts",
"oid": "IF-MIB::ifHCInMulticastPkts",
},
{
"name": "in_bcast_pkts",
"oid": "IF-MIB::ifHCInBroadcastPkts",
},
{
"name": "out_mcast_pkts",
"oid": "IF-MIB::ifHCOutMulticastPkts",
},
{
"name": "out_bcast_pkts",
"oid": "IF-MIB::ifHCOutBroadcastPkts",
},
# Drops / Errors
{
"name": "in_discards",
"oid": "IF-MIB::ifInDiscards",
},
{
"name": "out_discards",
"oid": "IF-MIB::ifOutDiscards",
},
{
"name": "in_errors",
"oid": "IF-MIB::ifInErrors",
},
{
"name": "out_errors",
"oid": "IF-MIB::ifOutErrors",
},
],
},
# Interface PoE
{
"name": "mikrotik_poe",
"oid": "MIKROTIK-MIB::mtxrPOETable",
"field": [
{
"name": "ifName",
"oid": "IF-MIB::ifName",
"is_tag": True,
},
{
"name": "ifAlias",
"oid": "IF-MIB::ifAlias",
"is_tag": True,
},
{
"name": "ifindex",
"oid": "MIKROTIK-MIB::mtxrPOEInterfaceIndex",
"is_tag": True,
},
{
"name": "status",
"oid": "MIKROTIK-MIB::mtxrPOEStatus",
},
{
"name": "voltage",
"oid": "MIKROTIK-MIB::mtxrPOEVoltage",
},
{
"name": "current",
"oid": "MIKROTIK-MIB::mtxrPOECurrent",
},
{
"name": "power",
"oid": "MIKROTIK-MIB::mtxrPOEPower",
},
],
},
],
},
"mikrotik_switches_very_slow": {
"interval": "20m",
"collection_jitter": "5m",
**input_defaults,
"table": [
# Interface statistics (MikroTik-specific mib)
{
"name": "mikrotik_interface_detailed",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsTable",
"field": [
# Join key / label (usually identical to IF-MIB ifName)
{
"name": "ifName",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsName",
"is_tag": True,
},
# join IF-MIB for better labels
{
"name": "ifAlias",
"oid": "IF-MIB::ifAlias",
"is_tag": True,
},
# =========================
# Physical layer (L1/L2)
# =========================
# CRC/FCS errors → very often cabling, connectors, SFPs, signal quality (EMI)
{
"name": "rx_fcs_errors",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxFCSError",
},
# Alignment errors → typically duplex mismatch or PHY problems
{
"name": "rx_align_errors",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxAlignError",
},
# Code errors → PHY encoding errors (signal/SFP/PHY)
{
"name": "rx_code_errors",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxCodeError",
},
# Carrier errors → carrier lost (copper issues, autoneg, PHY instability)
{
"name": "rx_carrier_errors",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxCarrierError",
},
# Jabber → extremely long invalid frames (faulty NIC/PHY, very severe)
{
"name": "rx_jabber",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxJabber",
},
# ==================================
# Length / framing anomalies (diagnostic)
# ==================================
# Frames shorter than minimum (noise, collisions, broken sender)
{
"name": "rx_too_short",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxTooShort",
},
# Frames longer than allowed (MTU mismatch, framing errors)
{
"name": "rx_too_long",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxTooLong",
},
# Fragments (often collision-related or duplex mismatch)
{
"name": "rx_fragment",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxFragment",
},
# Generic length errors
{
"name": "rx_length_errors",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxLengthError",
},
# ==================
# Drops (real packet loss)
# ==================
# RX drops (queue/ASIC/policy/overload) → highly alert-worthy
{
"name": "rx_drop",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxDrop",
},
# TX drops (buffer/queue exhaustion, scheduling, ASIC limits)
{
"name": "tx_drop",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsTxDrop",
},
# =========================================
# Duplex / collision indicators
# (should be zero on full-duplex links)
# =========================================
# Total collisions (relevant only for half-duplex or misconfigurations)
{
"name": "tx_collisions",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsTxCollision",
},
# Late collisions → almost always duplex mismatch / bad autoneg
{
"name": "tx_late_collisions",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsTxLateCollision",
},
# Aggregate collision counter (context)
{
"name": "tx_total_collisions",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsTxTotalCollision",
},
# Excessive collisions → persistent duplex problems
{
"name": "tx_excessive_collisions",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsTxExcessiveCollision",
},
# ==================
# Flow control (diagnostic)
# ==================
# Pause frames received (peer throttling you)
{
"name": "rx_pause",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsRxPause",
},
# Pause frames sent (you throttling the peer)
{
"name": "tx_pause",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsTxPause",
},
# Pause frames actually honored
{
"name": "tx_pause_honored",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsTxPauseHonored",
},
# ==========
# Stability
# ==========
# Link-down events (loose cables, bad SFPs, PoE power drops, reboots)
{
"name": "link_downs",
"oid": "MIKROTIK-MIB::mtxrInterfaceStatsLinkDowns",
},
],
},
],
},
},
},
},
}
# @metadata_reactor.provides(
# 'telegraf/processors/enum',
# )
# def tag_important_ports(metadata):
# # We want a graph, only for important ports. We deem ports important, if they have untagged vlans configured.
# return {
# "telegraf": {
# "processors": {
# "enum": {
# f"mikrotik_port_mapping_{routeros_node.name}":{
# "tagpass": {
# "agent_host": [routeros_node.hostname],
# },
# "mapping": [
# {
# "tag": "ifName",
# "dest": "is_infra",
# "default": "false",
# "value_mappings": {
# port_name: "true"
# for port_name, port_conf in repo.libs.mikrotik.get_netbox_config_for(routeros_node)['interfaces'].items()
# if port_conf['mode'] == "tagged-all" or port_conf['tagged_vlans']
# if port_conf['type'] != "lag"
# },
# },
# ],
# }
# for routeros_node in repo.nodes_in_group("switches-mikrotik")
# },
# },
# },
# }

View file

@ -27,15 +27,15 @@ routeros['/system/identity'] = {
# for topic in LOGGING_TOPICS: # for topic in LOGGING_TOPICS:
# routeros[f'/system/logging?action=memory&topics={topic}'] = {} # routeros[f'/system/logging?action=memory&topics={topic}'] = {}
routeros['/snmp'] = { # routeros['/snmp'] = {
'enabled': True, # 'enabled': True,
} # }
routeros['/snmp/community?name=public'] = { # routeros['/snmp/community?name=public'] = {
'addresses': '0.0.0.0/0', # 'addresses': '0.0.0.0/0',
'disabled': False, # 'disabled': False,
'read-access': True, # 'read-access': True,
'write-access': False, # 'write-access': False,
} # }
routeros['/system/clock'] = { routeros['/system/clock'] = {
'time-zone-autodetect': False, 'time-zone-autodetect': False,
@ -55,7 +55,7 @@ for vlan_name, vlan_id in node.metadata.get('routeros/vlans').items():
'vlan-id': vlan_id, 'vlan-id': vlan_id,
'interface': 'bridge', 'interface': 'bridge',
'tags': { 'tags': {
'routeros-vlans', 'routeros-vlan',
}, },
} }
@ -68,33 +68,10 @@ for vlan_name, vlan_id in node.metadata.get('routeros/vlans').items():
'routeros-vlan-ports', 'routeros-vlan-ports',
}, },
'needs': { 'needs': {
'tag:routeros-vlans', 'tag:routeros-vlan',
}, },
} }
for port_name, port_conf in node.metadata.get('routeros/ports').items():
untagged_vlan = node.metadata.get('routeros/vlan_groups')[port_conf.get('vlan_group')]['untagged']
routeros[f'/interface/bridge/port?interface={port_name}'] = {
'disabled': False,
'bridge': 'bridge',
'pvid': node.metadata.get('routeros/vlans')[untagged_vlan],
'tags': {
'routeros-ports'
},
'needs': {
'tag:routeros-vlan-ports',
},
}
routeros[f'/interface?name={port_name}'] = {
'_comment': port_conf.get('description', ''),
}
if comment := port_conf.get('comment', None):
routeros[f'/interface/bridge/port?interface={port_name}']['_comment'] = comment
routeros[f'/interface?name={port_name}']['_comment'] = comment
# create IPs # create IPs
for ip, ip_conf in node.metadata.get('routeros/ips').items(): for ip, ip_conf in node.metadata.get('routeros/ips').items():
routeros[f'/ip/address?address={ip}'] = { routeros[f'/ip/address?address={ip}'] = {
@ -103,8 +80,7 @@ for ip, ip_conf in node.metadata.get('routeros/ips').items():
'routeros-ip', 'routeros-ip',
}, },
'needs': { 'needs': {
'tag:routeros-vlans', 'tag:routeros-vlan',
'tag:routeros-ports'
}, },
} }
@ -114,8 +90,7 @@ routeros['/interface/bridge?name=bridge'] = {
'priority': node.metadata.get('routeros/bridge_priority'), 'priority': node.metadata.get('routeros/bridge_priority'),
'protocol-mode': 'rstp', 'protocol-mode': 'rstp',
'needs': { 'needs': {
'tag:routeros-vlans', 'tag:routeros-vlan',
'tag:routeros-ports',
'tag:routeros-vlan-ports', 'tag:routeros-vlan-ports',
'tag:routeros-ip', 'tag:routeros-ip',
}, },
@ -127,7 +102,7 @@ routeros['/interface/vlan'] = {
'id-by': 'name', 'id-by': 'name',
}, },
'needed_by': { 'needed_by': {
'tag:routeros-vlans', 'tag:routeros-vlan',
} }
} }
@ -139,6 +114,6 @@ routeros['/interface/bridge/vlan'] = {
}, },
}, },
'needed_by': { 'needed_by': {
'tag:routeros-vlans', 'tag:routeros-vlan',
} }
} }

View file

@ -11,22 +11,24 @@ defaults = {
}, },
'smartctl': {}, 'smartctl': {},
'telegraf': { 'telegraf': {
'config': {
'inputs': { 'inputs': {
'exec': { 'exec': {
'smartctl_power_mode': { h({
'commands': [ 'commands': [
f'sudo /usr/local/share/telegraf/smartctl_power_mode', f'sudo /usr/local/share/telegraf/smartctl_power_mode',
], ],
'data_format': 'influx', 'data_format': 'influx',
'interval': '20s', 'interval': '20s',
}, }),
'smartctl_errors': { h({
'commands': [ 'commands': [
f'sudo /usr/local/share/telegraf/smartctl_errors', f'sudo /usr/local/share/telegraf/smartctl_errors',
], ],
'data_format': 'influx', 'data_format': 'influx',
'interval': '6h', 'interval': '6h',
} })
},
}, },
}, },
}, },

View file

@ -10,11 +10,7 @@ directories = {
'purge': True, 'purge': True,
'mode': '0755', 'mode': '0755',
'skip': dont_touch_sshd, 'skip': dont_touch_sshd,
}, }
'/etc/ssh/ssh_config.d': {
'mode': '0755',
'skip': dont_touch_sshd,
},
} }
files = { files = {

View file

@ -19,7 +19,7 @@ def users(metadata):
'allow_users': set( 'allow_users': set(
name name
for name, conf in metadata.get('users').items() for name, conf in metadata.get('users').items()
if conf.get('authorized_keys', []) or conf.get('authorized_users', {}) if conf.get('authorized_keys', []) or conf.get('authorized_users', [])
), ),
}, },
} }

54
bundles/steam/items.py Normal file
View file

@ -0,0 +1,54 @@
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',
}
}

52
bundles/steam/metadata.py Normal file
View file

@ -0,0 +1,52 @@
defaults = {
'apt': {
'packages': {
'lib32gcc-s1': {},
'unzip': {},
},
},
'steam': {
'games': {
'left4dead2': 222860,
},
},
'zfs': {
'datasets': {
'tank/steam': {
'mountpoint': '/opt/steam',
'backup': False,
},
},
},
}
@metadata_reactor.provides(
'systemd/units',
)
def initial_unit(metadata):
return {
'systemd': {
'units': {
'steam-update.service': {
'Unit': {
'Description': 'steam: install and update games',
'After': 'network-online.target',
},
'Service': {
'Type': 'oneshot',
'User': 'steam',
'Group': 'steam',
'WorkingDirectory': '/opt/steam',
'ExecStart': {
f'/opt/steam/steam/steamcmd.sh +force_install_dir /opt/steam/{game} +login anonymous +app_update {id} validate +quit'
for game, id in metadata.get('steam/games').items()
}
},
'Install': {
'WantedBy': {'multi-user.target'},
},
},
},
},
}

View file

@ -1,7 +1,6 @@
directories = { directories = {
'/etc/sudoers.d': { '/etc/sudoers.d': {
'mode': '0750', 'purge': True,
#'purge': True, # FIXME: purge after managed sudoers are ready
}, },
} }

View file

@ -10,9 +10,6 @@ defaults = {
'resolvconf': { 'resolvconf': {
'installed': False, 'installed': False,
}, },
'netplan.io': {
'installed': False,
},
}, },
}, },
} }

View file

@ -1,5 +1,5 @@
defaults = { defaults = {
'systemd-swap': 2*(2**30), # 2GiB 'systemd-swap': 2*10**9,
'systemd': { 'systemd': {
'units': { 'units': {
'swapfile.swap': { 'swapfile.swap': {

View file

@ -1,8 +1,5 @@
from bundlewrap.utils.dicts import merge_dict from bundlewrap.utils.dicts import merge_dict
files = {}
svc_systemd = {}
directories = { directories = {
'/usr/local/lib/systemd/system': { '/usr/local/lib/systemd/system': {
'purge': True, 'purge': True,
@ -45,9 +42,6 @@ for name, unit in node.metadata.get('systemd/units').items():
else: else:
raise Exception(f'unknown type {extension}') raise Exception(f'unknown type {extension}')
for attribute in ['needs', 'needed_by', 'triggers', 'triggered_by']:
if attribute in unit:
dependencies.setdefault(attribute, []).extend(unit.pop(attribute))
files[path] = { files[path] = {
'content': repo.libs.systemd.generate_unitfile(unit), 'content': repo.libs.systemd.generate_unitfile(unit),

View file

@ -15,7 +15,7 @@ defaults = {
@metadata_reactor.provides( @metadata_reactor.provides(
'telegraf/inputs/exec', 'telegraf/config/inputs/exec',
) )
def telegraf(metadata): def telegraf(metadata):
return { return {
@ -23,11 +23,11 @@ def telegraf(metadata):
'config': { 'config': {
'inputs': { 'inputs': {
'exec': { 'exec': {
'tasmota_charge': { repo.libs.hashable.hashable({
'commands': ["/usr/local/share/telegraf/tasmota_charge"], 'commands': ["/usr/local/share/telegraf/tasmota_charge"],
'name_override': "tasmota_charge", 'name_override': "tasmota_charge",
'data_format': "influx", 'data_format': "influx",
}, }),
}, },
}, },
}, },

View file

@ -1,53 +1,19 @@
import tomlkit import tomlkit
import json
from bundlewrap.metadata import MetadataJSONEncoder
def inner_dict_to_list(dict_of_dicts):
"""
Example:
{
'cpu': {
'default': {'something': True},
'another': {'something': False},
},
}
becomes
{
'cpu': [
{'something': True},
{'something': False},
],
}
"""
return {
key: [value for _, value in sorted(dicts.items())]
for key, dicts in sorted(dict_of_dicts.items())
}
files = { files = {
"/etc/telegraf/telegraf.conf": { '/etc/telegraf/telegraf.conf': {
'owner': 'telegraf', 'content': tomlkit.dumps(
'group': 'telegraf', json.loads(json.dumps(
'mode': '0440', node.metadata.get('telegraf/config'),
'needs': [ cls=MetadataJSONEncoder,
"pkg_apt:telegraf", )),
sort_keys=True,
),
'triggers': [
'svc_systemd:telegraf:restart',
], ],
'content': tomlkit.dumps({
'agent': node.metadata.get('telegraf/agent'),
'inputs': inner_dict_to_list(node.metadata.get('telegraf/inputs')),
'processors': inner_dict_to_list(node.metadata.get('telegraf/processors')),
'outputs': inner_dict_to_list(node.metadata.get('telegraf/outputs')),
}),
'triggers': {
'svc_systemd:telegraf.service:restart',
},
},
'/etc/default/telegraf': {
'content': 'TELEGRAF_OPTS="--strict-env-handling"\n',
'mode': '0644',
'triggers': {
'svc_systemd:telegraf.service:restart',
},
}, },
'/usr/local/share/telegraf/procio': { '/usr/local/share/telegraf/procio': {
'content_type': 'download', 'content_type': 'download',
@ -61,26 +27,9 @@ files = {
}, },
} }
actions = { svc_systemd['telegraf'] = {
'telegraf-test-config': {
'command': "sudo -u telegraf bash -c 'telegraf config check --config /etc/telegraf/telegraf.conf --strict-env-handling'",
'triggered': True,
'needs': [ 'needs': [
'bundle:sudo',
'file:/etc/telegraf/telegraf.conf', 'file:/etc/telegraf/telegraf.conf',
'pkg_apt:telegraf', 'pkg_apt:telegraf',
], ],
},
}
svc_systemd = {
'telegraf.service': {
'needs': ['pkg_apt:telegraf'],
'preceded_by': {
'action:telegraf-test-config',
},
'needs': {
'action:telegraf-test-config',
},
},
} }

View file

@ -7,6 +7,8 @@ defaults = {
# needed by crystal plugins: # needed by crystal plugins:
'libgc-dev': {}, 'libgc-dev': {},
'libevent-dev': {}, 'libevent-dev': {},
# crystal based (procio, pressure_stall):
'libpcre3': {},
}, },
'sources': { 'sources': {
'influxdata': { 'influxdata': {
@ -23,29 +25,26 @@ defaults = {
}, },
}, },
'telegraf': { 'telegraf': {
'config': {
'agent': { 'agent': {
'hostname': node.name, 'hostname': node.name,
'collection_jitter': '20s', 'collection_jitter': '0s',
'flush_interval': '20s', 'flush_interval': '15s',
'flush_jitter': '5s', 'flush_jitter': '0s',
'interval': '2m', 'interval': '15s',
'metric_batch_size': 1000, 'metric_batch_size': 1000,
'metric_buffer_limit': 10000, 'metric_buffer_limit': 10000,
'omit_hostname': False, 'omit_hostname': False,
'round_interval': True, 'round_interval': True,
'skip_processors_after_aggregators': True,
}, },
'inputs': { 'inputs': {
'cpu': { 'cpu': {h({
'default': {
'collect_cpu_time': False, 'collect_cpu_time': False,
'percpu': True, 'percpu': True,
'report_active': False, 'report_active': False,
'totalcpu': True, 'totalcpu': True,
}, })},
}, 'disk': {h({
'disk': {
'default': {
'ignore_fs': [ 'ignore_fs': [
'tmpfs', 'tmpfs',
'devtmpfs', 'devtmpfs',
@ -55,60 +54,42 @@ defaults = {
'aufs', 'aufs',
'squashfs', 'squashfs',
], ],
} })},
}, 'procstat': {h({
'procstat': {
'default': {
'interval': '60s', 'interval': '60s',
'pattern': '.', 'pattern': '.',
'fieldinclude': [ 'fieldinclude': [
'cpu_usage', 'cpu_usage',
'memory_rss', 'memory_rss',
], ],
}, })},
}, 'diskio': {h({
'diskio': {
'default': {
'device_tags': ["ID_PART_ENTRY_NUMBER"], 'device_tags': ["ID_PART_ENTRY_NUMBER"],
} })},
}, 'kernel': {h({})},
'kernel': { 'mem': {h({})},
'default': {}, 'processes': {h({})},
}, 'swap': {h({})},
'mem': { 'system': {h({})},
'default': {}, 'net': {h({})},
},
'processes': {
'default': {},
},
'swap': {
'default': {},
},
'system': {
'default': {},
},
'net': {
'default': {},
},
'exec': { 'exec': {
# h({ h({
# 'commands': [ 'commands': [
# f'sudo /usr/local/share/telegraf/procio', f'sudo /usr/local/share/telegraf/procio',
# ], ],
# 'data_format': 'influx', 'data_format': 'influx',
# 'interval': '20s', 'interval': '20s',
# }), }),
'pressure_stall': { h({
'commands': [ 'commands': [
f'/usr/local/share/telegraf/pressure_stall', f'/usr/local/share/telegraf/pressure_stall',
], ],
'data_format': 'influx', 'data_format': 'influx',
'interval': '10s', 'interval': '10s',
}),
}, },
}, },
}, },
'processors': {},
'outputs': {},
}, },
'grafana_rows': { 'grafana_rows': {
'cpu', 'cpu',
@ -126,42 +107,22 @@ defaults = {
@metadata_reactor.provides( @metadata_reactor.provides(
'telegraf/outputs/influxdb_v2/default', 'telegraf/config/outputs/influxdb_v2',
) )
def influxdb(metadata): def influxdb(metadata):
influxdb_metadata = repo.get_node(metadata.get('telegraf/influxdb_node')).metadata.get('influxdb') influxdb_metadata = repo.get_node(metadata.get('telegraf/influxdb_node')).metadata.get('influxdb')
return { return {
'telegraf': { 'telegraf': {
'config': {
'outputs': { 'outputs': {
'influxdb_v2': { 'influxdb_v2': [{
'default': {
'urls': [f"http://{influxdb_metadata['hostname']}:{influxdb_metadata['port']}"], 'urls': [f"http://{influxdb_metadata['hostname']}:{influxdb_metadata['port']}"],
'token': str(influxdb_metadata['writeonly_token']), 'token': str(influxdb_metadata['writeonly_token']),
'organization': influxdb_metadata['org'], 'organization': influxdb_metadata['org'],
'bucket': influxdb_metadata['bucket'], 'bucket': influxdb_metadata['bucket'],
}, }]
}, },
}, },
}, },
} }
# crystal based (procio, pressure_stall):
@metadata_reactor.provides(
'apt/packages/libpcre2-8-0',
'apt/packages/libpcre3',
)
def libpcre(metadata):
if node.os == 'debian' and node.os_version >= (13,):
libpcre_package = 'libpcre2-8-0'
else:
libpcre_package = 'libpcre3'
return {
'apt': {
'packages': {
libpcre_package: {},
},
},
}

View file

@ -4,7 +4,7 @@ defaults = {
'users': { 'users': {
'root': { 'root': {
'home': '/root', 'home': '/root',
'password': repo.vault.password_for(f'{node.name} user root', length=24), 'password': repo.vault.password_for(f'{node.name} user root'),
}, },
}, },
} }
@ -20,15 +20,11 @@ def authorized_users(metadata):
users[name] = { users[name] = {
'authorized_keys': set(), 'authorized_keys': set(),
} }
for authorized_user, options in config.get('authorized_users', {}).items(): for authorized_user in config.get('authorized_users', set()):
authorized_user_name, authorized_user_node = authorized_user.split('@') authorized_user_name, authorized_user_node = authorized_user.split('@')
authorized_user_public_key = repo.get_node(authorized_user_node).metadata.get(f'users/{authorized_user_name}/pubkey') users[name]['authorized_keys'].add(
repo.get_node(authorized_user_node).metadata.get(f'users/{authorized_user_name}/pubkey')
for command in options.get('commands', []): )
users[name]['authorized_keys'].add(f'command="{command}" ' + authorized_user_public_key)
else:
users[name]['authorized_keys'].add(authorized_user_public_key)
return { return {
'users': users, 'users': users,
} }

View file

@ -44,7 +44,6 @@ defaults = {
@metadata_reactor.provides( @metadata_reactor.provides(
'wol-sleeper/mac',
'wol-sleeper/wake_command', 'wol-sleeper/wake_command',
) )
def wake_command(metadata): def wake_command(metadata):
@ -54,8 +53,7 @@ def wake_command(metadata):
return { return {
'wol-sleeper': { 'wol-sleeper': {
'mac': mac, 'wake_command': f"ssh -o StrictHostKeyChecking=no wol@{waker_hostname} 'wakeonlan {mac} && while ! ping {ip} -c1 -W3; do true; done'",
'wake_command': f"ssh -o StrictHostKeyChecking=no wol@{waker_hostname} '/usr/bin/wakeonlan {mac}' && while ! ping {ip} -c1 -W3; do true; done",
}, },
} }

View file

@ -6,25 +6,17 @@ defaults = {
}, },
} }
@metadata_reactor.provides( @metadata_reactor.provides(
'users/wol/authorized_users', 'users/wol',
) )
def user(metadata): def user(metadata):
return { return {
'users': { 'users': {
'wol': { 'wol': {
'authorized_users': { 'authorized_users': {
f'root@{ssh_client.name}': { f'root@{node.name}'
'commands': { for node in repo.nodes
'/usr/bin/wakeonlan ' + sleeper.metadata.get('wol-sleeper/mac') if node.dummy == False and node.has_bundle('ssh')
for sleeper in repo.nodes
if sleeper.has_bundle('wol-sleeper')
and sleeper.metadata.get('wol-sleeper/waker') == node.name
}
}
for ssh_client in repo.nodes
if ssh_client.dummy == False and ssh_client.has_bundle('ssh')
}, },
}, },
}, },

View file

@ -59,9 +59,9 @@ defaults = {
}, },
}, },
'telegraf': { 'telegraf': {
'config': {
'inputs': { 'inputs': {
'zfs': { 'zfs': [{}],
'default': {},
}, },
}, },
}, },

View file

@ -8,14 +8,14 @@ KHyP5XgRU/pIOyOo3g6+qIkhgynHVYIBuPbFQGEbOuUg7noAwTC9B9pYXSRFq9wk
T/q8rqOBiyO9SWB9gMiem8HNAzUo5TbVp9xPv2pl3mNXwe5te92pjlWdktOsBZuy T/q8rqOBiyO9SWB9gMiem8HNAzUo5TbVp9xPv2pl3mNXwe5te92pjlWdktOsBZuy
TfTgoj3y0HUY48He/z85aJ5j7gX5PU/6arxdABEBAAG0UGRldmVsOmxhbmd1YWdl TfTgoj3y0HUY48He/z85aJ5j7gX5PU/6arxdABEBAAG0UGRldmVsOmxhbmd1YWdl
czpjcnlzdGFsIE9CUyBQcm9qZWN0IDxkZXZlbDpsYW5ndWFnZXM6Y3J5c3RhbEBi czpjcnlzdGFsIE9CUyBQcm9qZWN0IDxkZXZlbDpsYW5ndWFnZXM6Y3J5c3RhbEBi
dWlsZC5vcGVuc3VzZS5vcmc+iQE+BBMBCAAoBQJodLPOAhsDBQkMCLQ6BgsJCAcD dWlsZC5vcGVuc3VzZS5vcmc+iQE+BBMBCAAoBQJkq9RAAhsDBQkIP9SsBgsJCAcD
AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDkVq5yhW0Udi/iB/9pzVWeChRvk7+bC2p3 AgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDkVq5yhW0UdsH4CACAMuwfsUTlUVmdMBw5
QXjc+KRmkev7yC3QglBX/17qDG+nW/z1SptFpIUKMllH/xu0GXIWOW/rxshRdKRK wktrrdwfwN6TiG5tPDjzTcMQNL+RSCh1gNRvaJjNHAy9sAsruGwTyX76K1p942EG
422wnT7KA2AqxArsHfvu0/nGBXAI1DnHuwP0j6xNmmw+uob2nWiUZZNgKydxcGSF F99DrYd/PMBK4oOWe7HHouYIMrLqZFT38shv/tbyJvUfxqfMHSPQJSFPVGtInn3h
fgRfIJcsHBKweasy9G/Fpdur/BFSBNQ8BP6CnB9qx0Z1LgQ6bQQNY1LKH4EzmiNA iKtDeIc88Hl+dsmBhWxDdaoHTGKgIcQTLN1OaX6SsT6WuMo7B4kPxHerwFp/n5bO
rBowUcuVjUzXUW8rc0Old/ffymH3TBM9xQXnsGVZb5+E6NKpcdt0lnWkrtHQK3RX hqyLLkTY0oxJpZlzCj2tYDytHhjkPnYtcPpQ8LnQpGKogUxYDYZ+o4zYvIcT/J5+
ohNmaLwMQe/wMzWN3u/5XshQD8mMQjxEg4QSt2gAEXJdIzI+VgLrGqcfbrk/qhVM cLx1xpf4fI7ZoE+dpIpAGKzN8MoQQ+fjgSheXar35p+8lOKrvrk7MmbQJlBQO+rM
D+c+iEYEExECAAYFAmCKr5QACgkQOzARt2udZSNdFQCgtpRzGoKr9VWnhv+/k4pk IHdJiEYEExECAAYFAmCKr5QACgkQOzARt2udZSNdFQCgtpRzGoKr9VWnhv+/k4pk
Cmp9fycAn0pdJ2xIEsqxOjPBFVDh7Sahecuq Cmp9fycAn0pdJ2xIEsqxOjPBFVDh7Sahecuq
=v2my =yIwD
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,186 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
ugNdPbTx5TQVD48tsKiSIOPP1g==
=UqDn
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,186 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
JTBXMe98MDmvUA==
=OEKh
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -9,16 +9,16 @@ Dts4PNx4Wr9CktHIvbypT4Lk2oJEPWjcCJQHqpPQZXbnclXRlK5Ea0NVpaQdGK+v
JS4HGxFFjSkvTKAZYgwOk93qlpFeDML3TuSgWxuw4NIDitvewudnaWzfl9tDIoVS JS4HGxFFjSkvTKAZYgwOk93qlpFeDML3TuSgWxuw4NIDitvewudnaWzfl9tDIoVS
Bb16nwJ8bMDzovC/RBE14rRKYtMLmBsRzGYHWd0NnX+FitAS9uURHuFxghv9GFPh Bb16nwJ8bMDzovC/RBE14rRKYtMLmBsRzGYHWd0NnX+FitAS9uURHuFxghv9GFPh
eTaXvc4glM94HBUAEQEAAbQmR3JhZmFuYSBMYWJzIDxlbmdpbmVlcmluZ0BncmFm eTaXvc4glM94HBUAEQEAAbQmR3JhZmFuYSBMYWJzIDxlbmdpbmVlcmluZ0BncmFm
YW5hLmNvbT6JAdQEEwEKAD4CGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQS1 YW5hLmNvbT6JAdQEEwEKAD4WIQS1Oud7rbYwpoMEYAWWP6J3EEWFRQUCZOeGaQIb
Oud7rbYwpoMEYAWWP6J3EEWFRQUCaKhvPQUJB4NP1AAKCRCWP6J3EEWFRUjOC/9Y AwUJA8JnAAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCWP6J3EEWFRUiADACa
dWOWJLJVKzLx8uv5YVzebyw15HevhKahbznJX5fHnE8irjkiPFltVEZ4T37s5afR i+xytv2keEFJWjXNnFAx6/obnHRcXOI3w6nH/zL8gNI7YN5jcdQT2NYvKVYTb3fW
GBEJnR1UFd80s7jzwbuoZh/zEB3jN8q50g64AznuzDa0PWKzaY7Tgkssx3+hs6TS GuMsjHWgat5Gq3AtJrOKABpZ6qeYNPk0Axn/dKtOTwXjZ4pKX3bbUYvVfs0fCEZv
vIwV4z8T7f56lDudeHxHXx+htRnZ3ebKNPCJS7+G12GF6W3C3znpdjgvhVUB0uxd B0HHIj2wI9kgMpoTrkj22LE8layZTPOoQ+3/FbLzS8hN3CYZj25mHN7bpZq8EbV3
+42V0fRqk2GLNZeKS9988fi5dYRAy9Ozwced7ByCFjde9FBgUtrH3mG1/ibzLEh0 8FW9EU0HM0tg6CvoxkRiVqAuAC0KnVIZAdhD4dlYKuncq64nMvT1A5wxSYbnE+uf
4k02nYjc8mrH32t4UCWpxQEJ1vZA2vT2HN3/cH/4uyFdyU6OHkMyMbz6lmeXe71d mnWQQhhS6BOwRqN054yw1FrWNDFsvnOSHmr8dIiriv+aZYvx5JQFJ7oZP3LwdYyg
F5hOB4+/RP6Ndyj7ViRNDbm70NRBaFne/+YOJvmMfJTCh7YbF5qEn1ihGkJJ0ohE ocQcAJA8HFTIk3P6uJiIF/zdDzocgdKs+IYDoId0hxX7sGCvqdrsveq8n3m7uQiN
u2IB+EGEhyiDm8SIsj1uMw7n17iIPNtbsU5GgnmLtfguP/WbwKV2UeuxTpiOeYb6 7FvSiV0eXIdV4F7340kc8EKiYwpuYSaZX0UWKLenzlUvD+W4pZCWtoXzPsW7PKUt
blDwRlh48uHMlA5HBW+487Jktw3iPj1IKhdtAC9CU3xAvzDcseMbgmM6Xj2bSQG5 q1xdW0+NY+AGLCvSJCc5F4S5kFCObfBAYBbldjwwJFocdq/YOvvWYTPyV7kJeJS5
AY0EZOeGaQEMALNIFUricEIwtZiX7vSDjwxobbqPKqzdek8x3ud0CyYlrbGHy0k+ AY0EZOeGaQEMALNIFUricEIwtZiX7vSDjwxobbqPKqzdek8x3ud0CyYlrbGHy0k+
FDEXstjJQQ1s9rjJSu3sv5wyg9GDAUH3nzO976n/ZZvKPti3p2XU2UFx5gYkaaFV FDEXstjJQQ1s9rjJSu3sv5wyg9GDAUH3nzO976n/ZZvKPti3p2XU2UFx5gYkaaFV
D56yYxqGY0YU5ft6BG+RUz3iEPg3UBUzt0sCIYnG9+CsDqGOnRYIIa46fu2/H9Vu D56yYxqGY0YU5ft6BG+RUz3iEPg3UBUzt0sCIYnG9+CsDqGOnRYIIa46fu2/H9Vu
@ -27,15 +27,15 @@ D56yYxqGY0YU5ft6BG+RUz3iEPg3UBUzt0sCIYnG9+CsDqGOnRYIIa46fu2/H9Vu
3zht8luFOYpJr2lVzp7n3NwB4zW08RptTzTgFAaW/NH2JjYI+rDvQm4jNs08Dtsp 3zht8luFOYpJr2lVzp7n3NwB4zW08RptTzTgFAaW/NH2JjYI+rDvQm4jNs08Dtsp
nm4OQvBA9Df/6qwMEOZ9i10ixqk+55UpQFJ3nf4uKlSUM7bKXXVcD/odq804Y/K4 nm4OQvBA9Df/6qwMEOZ9i10ixqk+55UpQFJ3nf4uKlSUM7bKXXVcD/odq804Y/K4
y3csE059YVIyaPexEvYSYlHE2odJWRg2Q1VehmrOSC8Qps3xpU7dTHXD74ZpaYbr y3csE059YVIyaPexEvYSYlHE2odJWRg2Q1VehmrOSC8Qps3xpU7dTHXD74ZpaYbr
haViRS5v/lCsiwARAQABiQG8BBgBCgAmAhsMFiEEtTrne622MKaDBGAFlj+idxBF haViRS5v/lCsiwARAQABiQG8BBgBCgAmFiEEtTrne622MKaDBGAFlj+idxBFhUUF
hUUFAmiobzkFCQeDT9AACgkQlj+idxBFhUVsmQwA0PA/zd7NqtnZ/Z8857gp2Wq2 AmTnhmkCGwwFCQPCZwAACgkQlj+idxBFhUUNbQv8DCcfi3GbWfvp9pfY0EJuoFJX
/e4EX8nRjsW2ZlrZfbU5oMQv9OZZ4z1UjIKEUV+TnCwXEKXTMJomdekQSSayVVx/ LNgci7z7smXq7aqDp2huYQ+MulnPAydjRCVW2fkHItF2Ks6l+2/8t5Xz0eesGxST
u5w+0YM8gRuQGrG8hW0GRR8sHIeuwBFlyQrlwxUwXvDOPDYyieETjaQqMucupIKo xTyR31ARENMXaq78Lq+itZ+usOSDNuwJcEmJM6CceNMLs4uFkX2GRYhchkry7P0C
IPm3CjFySvfizvSWUVSWBnGmQfpv6OiGYawvwfewcQHUdLMgWN3lYlzGQJL4+OMm lkLxUTiB43ooi+CqILtlNxH7kM1O4Ncs6UGZMXf2IiG9s3JDCsYVPkC5QDMOPkTy
7XcB8VNTa586Q00fmjDfktHYvGpmhqr3gsd4gS3AjTk0zI65qXBRJkdqVnwUrMUD 2ZriF56uPerlJveF0dC61RZ6RlM3iSJ9Fwvea0Oy4rwkCcs5SHuwoDTFyxiyz0QC
8TcxXYNXf90mhR0NWkLmp6kBYiW8+QY6ndMmRVpodg1A87qgMYaZUAAlxCS4XKTU 9iqi3fG3iSbLvY9UtJ6X+BtDqdXLAT9Pq527mukPP3LwpEqFVyNQKnGLdLOu2YXc
r+/YMDYOWgLN6i4UeYG/3/hsnAEHm5ITojfh6cLfdlhjohFTnD0IYw3AsNJXRzKB TWWWseSQkHRzBmjD18KTD74mg4aXxEabyT4snrXpi5+UGLT4KXGV5syQO6Lc0OGw
1g5FTBKLLLIdXgS/3rWV1qjAd3drQVIMCku6HKl/vT4ftrBHeSyV7eLwOYbe3/bw 9O/0qAIU+YW7ojbKv8fr+NB31TGhGYWASjYlN1NvPotRAK6339O0/Rqr9xGgy3AY
8VMx+lmMheD8/qJMia1om0iBBRSXRjY//f+Lllqm SR+ic2Y610IM7xccKuTVAW9UofKQwJZChqae9VVZ
=TH3J =J9CI
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,99 +1,29 @@
-----BEGIN PGP PUBLIC KEY BLOCK----- -----BEGIN PGP PUBLIC KEY BLOCK-----
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 =YwXc
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
AHMnjg==
=UiOS
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

View file

@ -8,21 +8,16 @@ from(bucket: "${bucket}")
|> filter(fn: (r) => exists r["${exist}"]) // WTF |> filter(fn: (r) => exists r["${exist}"]) // WTF
% endfor % endfor
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) // aggregate early for best performance |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) // aggregate early for best performance
% if over is not None: % if minimum:
|> filter(fn: (r) => r._value > ${over}) |> filter(fn: (r) => r._value > ${minimum})
% endif % endif
% if function == 'derivative': % if function == 'derivative':
|> derivative(nonNegative: true) |> derivative(nonNegative: true)
% elif function == 'difference':
|> difference(nonNegative: true)
% endif % endif
% if boolean_to_int: % if boolean_to_int:
|> map(fn: (r) => ({r with _value: if r._value == true then 1 else 0 })) |> map(fn: (r) => ({r with _value: if r._value == true then 1 else 0 }))
% endif % endif
% if negative: % if negative:
|> map(fn: (r) => ({r with _value: r._value * - 1.0})) |> map(fn: (r) => ({r with _value: r._value * - 1.0}))
% endif
% if multiply is not None:
|> map(fn: (r) => ({r with _value: r._value * ${multiply}}))
% endif % endif
|> yield(name: "mean") |> yield(name: "mean")

View file

@ -51,7 +51,7 @@
}, },
'min': 0, 'min': 0,
'soft_max': 3, 'soft_max': 3,
'display_name': '${__field.labels.resource}', 'display_name': '__field.labels.resource',
'unit': 'percent', 'unit': 'percent',
'tooltip': 'multi', 'tooltip': 'multi',
'legend': { 'legend': {

View file

@ -16,7 +16,7 @@
}, },
}, },
'unit': 'Bps', 'unit': 'Bps',
'display_name': '${__field.labels.name}', 'display_name': '__field.labels.name',
'tooltip': 'multi', 'tooltip': 'multi',
}, },
'write': { 'write': {
@ -36,7 +36,7 @@
}, },
}, },
'unit': 'Bps', 'unit': 'Bps',
'display_name': '${__field.labels.name}', 'display_name': '__field.labels.name',
'tooltip': 'multi', 'tooltip': 'multi',
}, },
} }

View file

@ -17,7 +17,7 @@
}, },
'tooltip': 'multi', 'tooltip': 'multi',
'unit': 'MHz', 'unit': 'MHz',
'display_name': '${__field.labels.cpu}', 'display_name': '__field.labels.cpu',
'min': 0, 'min': 0,
}, },
# 'temperature': { # 'temperature': {
@ -57,6 +57,6 @@
}, },
'tooltip': 'multi', 'tooltip': 'multi',
'unit': 'degrees', 'unit': 'degrees',
'display_name': '${__field.labels.chip}', 'display_name': '__field.labels.chip',
}, },
} }

View file

@ -13,7 +13,7 @@
}, },
}, },
'unit': 'Bps', 'unit': 'Bps',
'display_name': '${__field.labels.interface}', 'display_name': '__field.labels.interface',
'tooltip': 'multi', 'tooltip': 'multi',
}, },
'out': { 'out': {
@ -30,7 +30,7 @@
}, },
}, },
'unit': 'Bps', 'unit': 'Bps',
'display_name': '${__field.labels.interface}', 'display_name': '__field.labels.interface',
'tooltip': 'multi', 'tooltip': 'multi',
}, },
} }

View file

@ -12,7 +12,7 @@
'function': 'max', 'function': 'max',
}, },
}, },
'display_name': '${__field.labels.queue}' 'display_name': '__field.labels.queue'
}, },
'size': { 'size': {
'stacked': True, 'stacked': True,
@ -27,7 +27,7 @@
'function': 'max', 'function': 'max',
}, },
}, },
'display_name': '${__field.labels.queue}' 'display_name': '__field.labels.queue'
}, },
'age': { 'age': {
'stacked': True, 'stacked': True,
@ -42,6 +42,6 @@
'function': 'max', 'function': 'max',
}, },
}, },
'display_name': '${__field.labels.queue}' 'display_name': '__field.labels.queue'
}, },
} }

View file

@ -9,11 +9,11 @@
'cpu_usage', 'cpu_usage',
], ],
}, },
'over': 0.2, 'minimum': 0.2,
}, },
}, },
'unit': 'percent', 'unit': 'percent',
'display_name': '${__field.labels.process_name}', 'display_name': '__field.labels.process_name',
'legend': { 'legend': {
'displayMode': 'table', 'displayMode': 'table',
'placement': 'right', 'placement': 'right',
@ -32,11 +32,11 @@
'memory_rss', 'memory_rss',
], ],
}, },
'over': 10*(10**6), 'minimum': 10*(10**6),
}, },
}, },
'unit': 'bytes', 'unit': 'bytes',
'display_name': '${__field.labels.process_name}', 'display_name': '__field.labels.process_name',
'legend': { 'legend': {
'displayMode': 'table', 'displayMode': 'table',
'placement': 'right', 'placement': 'right',

View file

@ -10,11 +10,11 @@
], ],
}, },
'function': 'derivative', 'function': 'derivative',
'over': 1024, 'minimum': 1024,
}, },
}, },
'unit': 'bytes', 'unit': 'bytes',
'display_name': '${__field.labels.comm}', 'display_name': '__field.labels.comm',
'legend': { 'legend': {
'displayMode': 'table', 'displayMode': 'table',
'placement': 'right', 'placement': 'right',
@ -34,11 +34,11 @@
], ],
}, },
'function': 'derivative', 'function': 'derivative',
'over': 1, 'minimum': 1,
}, },
}, },
'unit': 'bytes', 'unit': 'bytes',
'display_name': '${__field.labels.comm}', 'display_name': '__field.labels.comm',
'legend': { 'legend': {
'displayMode': 'table', 'displayMode': 'table',
'placement': 'right', 'placement': 'right',

View file

@ -1,97 +0,0 @@
{
'critical': {
'stacked': True,
'queries': {
'generic': {
'filters': {
'_measurement': 'mikrotik_interface_generic',
'_field': [
'in_errors',
'out_errors',
],
'operating_system': 'routeros',
},
'function': 'difference',
'over': 0,
},
'mikrotik': {
'filters': {
'_measurement': 'mikrotik_interface_detailed',
'_field': [
'rx_fcs_errors',
'rx_align_errors',
'rx_code_errors',
'rx_carrier_errors',
'rx_jabber',
'rx_fragment',
'rx_length_errors',
'tx_late_collisions',
'tx_excessive_collisions',
'link_downs',
],
'operating_system': 'routeros',
},
'function': 'difference',
'over': 0,
},
},
'min': 0,
'unit': 'cps',
'tooltip': 'multi',
'display_name': '${__field.name} ${__field.labels.ifName} ${__field.labels.ifAlias}',
'legend': {
'displayMode': 'table',
'placement': 'right',
'calcs': [
'max',
],
},
},
'warning': {
'stacked': True,
'queries': {
'generic': {
'filters': {
'_measurement': 'mikrotik_interface_generic',
'_field': [
'in_discards',
'out_discards',
],
'operating_system': 'routeros',
},
'function': 'difference',
'over': 0,
},
'mikrotik': {
'filters': {
'_measurement': 'mikrotik_interface_detailed',
'_field': [
'rx_too_short',
'rx_too_long',
'rx_drop',
'tx_drop',
'rx_pause',
'tx_pause',
'tx_pause_honored',
'tx_collisions',
'tx_total_collisions',
],
'operating_system': 'routeros',
},
'function': 'difference',
'over': 0,
},
},
'min': 0,
'unit': 'cps',
'tooltip': 'multi',
'display_name': '${__field.name} ${__field.labels.ifName} ${__field.labels.ifAlias}',
'legend': {
'displayMode': 'table',
'placement': 'right',
'calcs': [
'max',
],
},
},
}

View file

@ -1,107 +0,0 @@
{
'temperature': {
'stacked': False,
'queries': {
'temp': {
'filters': {
'_measurement': 'mikrotik_health',
'sensor': [
'temperature',
'cpu-temperature',
'switch-temperature',
'board-temperature1',
'sfp-temperature',
],
'_field': [
'value',
],
'operating_system': 'routeros',
},
},
},
'min': 0,
'unit': 'celsius',
'tooltip': 'multi',
'display_name': '${__field.labels.sensor}',
'legend': {
'displayMode': 'hidden',
},
},
'fan': {
'stacked': False,
'queries': {
'temp': {
'filters': {
'_measurement': 'mikrotik_health',
'sensor': [
'fan1-speed',
'fan2-speed',
],
'_field': [
'value',
],
'operating_system': 'routeros',
},
},
},
'min': 0,
'unit': 'rpm',
'tooltip': 'multi',
'display_name': '${__field.labels.sensor}',
'legend': {
'displayMode': 'hidden',
},
},
'psu_current': {
'stacked': False,
'queries': {
'temp': {
'filters': {
'_measurement': 'mikrotik_health',
'sensor': [
'psu1-current',
'psu2-current',
],
'_field': [
'value',
],
'operating_system': 'routeros',
},
'multiply': 0.1,
},
},
'min': 0,
'unit': 'ampere',
'tooltip': 'multi',
'display_name': '${__field.labels.sensor}',
'legend': {
'displayMode': 'hidden',
},
},
'psu_voltage': {
'stacked': False,
'queries': {
'temp': {
'filters': {
'_measurement': 'hw',
'sensor': [
'psu1-voltage',
'psu2-voltage',
],
'_field': [
'value',
],
'operating_system': 'routeros',
},
'multiply': 0.1,
},
},
'min': 0,
'unit': 'volt',
'tooltip': 'multi',
'display_name': '${__field.labels.sensor}',
'legend': {
'displayMode': 'hidden',
},
},
}

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