feat: adguard home
feat: implement extra module containers feat: final smb confi feat: final nextcloud confi feat: rework networks ip feat: move acme and ddclient to server
This commit is contained in:
parent
ffd869b97d
commit
e469eab2b8
|
@ -1056,11 +1056,11 @@
|
|||
"pre-commit-hooks": "pre-commit-hooks_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1704938286,
|
||||
"narHash": "sha256-/uv+N2v5ixqYz7SG8R5GWOTdrNKboHEp85BR5Jdz6qE=",
|
||||
"lastModified": 1704999567,
|
||||
"narHash": "sha256-Whj1PFPomS/f97OD30CRrETTH/dmnUJdjEevDLJG4MM=",
|
||||
"owner": "oddlama",
|
||||
"repo": "nixos-extra-modules",
|
||||
"rev": "c55f465ba1f369852ab4122a9fa42c85b4a571de",
|
||||
"rev": "4744a2844cd74ca9b122fbaaae5ae97159c0d30e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -165,7 +165,6 @@
|
|||
modules = [
|
||||
./nix/installer-configuration.nix
|
||||
./modules/config/ssh.nix
|
||||
{system.stateVersion = stateVersion;}
|
||||
];
|
||||
format =
|
||||
{
|
||||
|
|
|
@ -17,3 +17,7 @@ system = "x86_64-linux"
|
|||
[maddy]
|
||||
type = "nixos"
|
||||
system = "x86_64-linux"
|
||||
|
||||
[elisabeth]
|
||||
type = "nixos"
|
||||
system = "x86_64-linux"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
{inputs, ...}: {
|
||||
imports = [
|
||||
inputs.nixos-hardware.nixosModules.common-gpu-nvidia-nonprime
|
||||
inputs.nixos-hardware.nixosModules.common-cpu-intel
|
||||
inputs.nixos-hardware.nixosModules.common-pc
|
||||
inputs.nixos-hardware.nixosModules.common-pc
|
||||
inputs.nixos-hardware.nixosModules.common-pc-hdd
|
||||
inputs.nixos-hardware.nixosModules.common-pc-ssd
|
||||
|
@ -12,7 +14,6 @@
|
|||
../../modules/optional/xserver.nix
|
||||
../../modules/optional/secureboot.nix
|
||||
|
||||
../../modules/hardware/intel.nix
|
||||
../../modules/hardware/nintendo.nix
|
||||
../../modules/hardware/nvidia.nix
|
||||
../../modules/hardware/physical.nix
|
||||
|
|
|
@ -43,4 +43,5 @@
|
|||
fileSystems."/state".neededForBoot = true;
|
||||
fileSystems."/persist".neededForBoot = true;
|
||||
fileSystems."/panzer/state".neededForBoot = true;
|
||||
boot.initrd.systemd.services."zfs-import-panzer".after = ["cryptsetup.target"];
|
||||
}
|
||||
|
|
|
@ -4,19 +4,12 @@
|
|||
};
|
||||
systemd.network.networks = {
|
||||
"01-lan1" = {
|
||||
address = ["192.168.178.30/24"];
|
||||
gateway = ["192.168.178.1"];
|
||||
DHCP = "yes";
|
||||
matchConfig.MACAddress = config.secrets.secrets.local.networking.lan1.mac;
|
||||
dns = ["192.168.178.2"];
|
||||
networkConfig = {
|
||||
IPv6PrivacyExtensions = "yes";
|
||||
MulticastDNS = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
networking.extraHosts = ''
|
||||
192.168.178.32 pgrossmann.org
|
||||
192.168.178.32 nc.pgrossmann.org
|
||||
'';
|
||||
}
|
||||
|
|
34
hosts/elisabeth/default.nix
Normal file
34
hosts/elisabeth/default.nix
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
inputs,
|
||||
minimal,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
imports =
|
||||
[
|
||||
inputs.nixos-hardware.nixosModules.common-pc
|
||||
inputs.nixos-hardware.nixosModules.common-pc-ssd
|
||||
inputs.nixos-hardware.nixosModules.common-pc-hdd
|
||||
inputs.nixos-hardware.nixosModules.common-cpu-amd
|
||||
inputs.nixos-hardware.nixosModules.common-cpu-amd-pstate
|
||||
|
||||
../../modules/config
|
||||
../../modules/optional/initrd-ssh.nix
|
||||
|
||||
../../modules/hardware/physical.nix
|
||||
../../modules/hardware/zfs.nix
|
||||
|
||||
../../modules/services/acme.nix
|
||||
../../modules/services/ddclient.nix
|
||||
|
||||
./net.nix
|
||||
./fs.nix
|
||||
]
|
||||
++ lib.lists.optionals (!minimal) [
|
||||
./guests.nix
|
||||
];
|
||||
services.xserver = {
|
||||
layout = "de";
|
||||
xkbVariant = "bone";
|
||||
};
|
||||
}
|
128
hosts/elisabeth/fs.nix
Normal file
128
hosts/elisabeth/fs.nix
Normal file
|
@ -0,0 +1,128 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
disko.devices = {
|
||||
disk = {
|
||||
internal-ssd = {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${config.secrets.secrets.local.disko.nvme}";
|
||||
content = with lib.disko.gpt; {
|
||||
type = "table";
|
||||
format = "gpt";
|
||||
partitions = [
|
||||
(partEfi "boot" "0%" "1GiB")
|
||||
(partLuksZfs "ssd" "rpool" "1GiB" "100%")
|
||||
];
|
||||
};
|
||||
};
|
||||
"4TB-hdd-1" = {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${config.secrets.secrets.local.disko."4TB-1"}";
|
||||
content = lib.disko.content.luksZfs "hdd-4TB-1" "renaultft";
|
||||
};
|
||||
"4TB-hdd-2" = {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${config.secrets.secrets.local.disko."4TB-2"}";
|
||||
content = lib.disko.content.luksZfs "hdd-4TB-2" "renaultft";
|
||||
};
|
||||
"4TB-hdd-3" = {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${config.secrets.secrets.local.disko."4TB-3"}";
|
||||
content = lib.disko.content.luksZfs "hdd-4TB-3" "renaultft";
|
||||
};
|
||||
"8TB-hdd-1" = {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${config.secrets.secrets.local.disko."8TB-1"}";
|
||||
content = lib.disko.content.luksZfs "hdd-8TB-1" "panzer";
|
||||
};
|
||||
"8TB-hdd-2" = {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${config.secrets.secrets.local.disko."8TB-2"}";
|
||||
content = lib.disko.content.luksZfs "hdd-8TB-2" "panzer";
|
||||
};
|
||||
"8TB-hdd-3" = {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${config.secrets.secrets.local.disko."8TB-3"}";
|
||||
content = lib.disko.content.luksZfs "hdd-8TB-3" "panzer";
|
||||
};
|
||||
};
|
||||
|
||||
zpool = with lib.disko.zfs; {
|
||||
rpool = mkZpool {datasets = impermanenceZfsDatasets;};
|
||||
panzer = mkZpool {
|
||||
datasets = {
|
||||
"safe/guests" = unmountable;
|
||||
};
|
||||
};
|
||||
renaultft = mkZpool {
|
||||
datasets = {
|
||||
"safe/guests" = unmountable;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.zrepl = {
|
||||
enable = true;
|
||||
settings = {
|
||||
global = {
|
||||
logging = [
|
||||
{
|
||||
type = "syslog";
|
||||
level = "info";
|
||||
format = "human";
|
||||
}
|
||||
];
|
||||
# TODO Monitoring
|
||||
};
|
||||
jobs = [
|
||||
#{
|
||||
# type = "push";
|
||||
# name = "push-to-remote";
|
||||
#}
|
||||
{
|
||||
type = "snap";
|
||||
name = "mach-schnipp-schusss";
|
||||
filesystems = {
|
||||
"panzer/safe<" = true;
|
||||
"rpool/local/state<" = true;
|
||||
"rpool/safe<" = true;
|
||||
"renaultft/safe<" = true;
|
||||
};
|
||||
snapshotting = {
|
||||
type = "periodic";
|
||||
prefix = "zrepl-";
|
||||
interval = "10m";
|
||||
timestamp_format = "iso-8601";
|
||||
};
|
||||
pruning = {
|
||||
keep = [
|
||||
{
|
||||
type = "regex";
|
||||
regex = "^zrepl-.*$";
|
||||
negate = true;
|
||||
}
|
||||
{
|
||||
type = "grid";
|
||||
grid = lib.concatStringsSep " | " [
|
||||
"1x1d(keep=all)"
|
||||
"142x1h(keep=2)"
|
||||
"90x1d(keep=2)"
|
||||
"500x7d"
|
||||
];
|
||||
regex = "^zrepl-.*$";
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
fileSystems."/state".neededForBoot = true;
|
||||
fileSystems."/persist".neededForBoot = true;
|
||||
boot.initrd.systemd.services."zfs-import-panzer".after = ["cryptsetup.target"];
|
||||
boot.initrd.systemd.services."zfs-import-renaultft".after = ["cryptsetup.target"];
|
||||
}
|
130
hosts/elisabeth/guests.nix
Normal file
130
hosts/elisabeth/guests.nix
Normal file
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
config,
|
||||
stateVersion,
|
||||
inputs,
|
||||
lib,
|
||||
minimal,
|
||||
nodes,
|
||||
...
|
||||
}: let
|
||||
adguardhomedomain = "adguardhome.${config.secrets.secrets.global.domains.web}";
|
||||
nextclouddomain = "nc.${config.secrets.secrets.global.domains.web}";
|
||||
in {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedSetup = true;
|
||||
upstreams.adguardhome = {
|
||||
servers."TODO:3000" = {};
|
||||
|
||||
extraConfig = ''
|
||||
zone adguardhome 64k ;
|
||||
keepalive 5 ;
|
||||
'';
|
||||
};
|
||||
virtualHosts.${adguardhomedomain} = {
|
||||
forceSSL = true;
|
||||
useACMEHost = "web";
|
||||
locations."/" = {
|
||||
proxyPass = "http://adguardhome";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
extraConfig = ''
|
||||
allow 192.168.178.0/24;
|
||||
deny all;
|
||||
'';
|
||||
};
|
||||
upstreams.nextcloud = {
|
||||
servers."TODO:80" = {};
|
||||
|
||||
extraConfig = ''
|
||||
zone nextcloud 64k ;
|
||||
keepalive 5 ;
|
||||
'';
|
||||
};
|
||||
virtualHosts.${nextclouddomain} = {
|
||||
forceSSL = true;
|
||||
useACMEHost = "web";
|
||||
locations."/".proxyPass = "http://nextcloud";
|
||||
extraConfig = ''
|
||||
client_max_body_size 4G ;
|
||||
'';
|
||||
};
|
||||
};
|
||||
guests = let
|
||||
mkGuest = guestName: {
|
||||
enablePanzer ? false,
|
||||
enableRenaultFT ? false,
|
||||
...
|
||||
}: {
|
||||
autostart = true;
|
||||
zfs."/state" = {
|
||||
pool = "rpool";
|
||||
dataset = "local/guests/${guestName}";
|
||||
};
|
||||
zfs."/persist" = {
|
||||
pool = "rpool";
|
||||
dataset = "safe/guests/${guestName}";
|
||||
};
|
||||
zfs."/panzer" = lib.mkIf enablePanzer {
|
||||
pool = "panzer";
|
||||
dataset = "safe/guests/${guestName}";
|
||||
};
|
||||
zfs."/renaultft" = lib.mkIf enableRenaultFT {
|
||||
pool = "renaultft";
|
||||
dataset = "safe/guests/${guestName}";
|
||||
};
|
||||
modules = [
|
||||
../../modules/config
|
||||
../../modules/services/${guestName}.nix
|
||||
{
|
||||
node.secretsDir = ./secrets/${guestName};
|
||||
systemd.network.networks."10-${config.guests.${guestName}.networking.mainLinkName}" = {
|
||||
DHCP = lib.mkForce "no";
|
||||
address = [(lib.net.cidr.host config.secrets.secrets.global.net.ips.${config.guests.${guestName}.nodeName} config.secrets.secrets.global.net.privateSubnet)];
|
||||
gateway = [(lib.net.cidr.host 1 config.secrets.secrets.global.net.privateSubnet)];
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
#deadnix: skip
|
||||
mkMicrovm = guestName: cfg: {
|
||||
${guestName} =
|
||||
mkGuest guestName cfg
|
||||
// {
|
||||
backend = "microvm";
|
||||
microvm = {
|
||||
system = "x86_64-linux";
|
||||
macvtap = "lan";
|
||||
baseMac = config.repo.secrets.local.networking.interfaces.lan.mac;
|
||||
};
|
||||
extraSpecialArgs = {
|
||||
inherit (inputs.self) nodes;
|
||||
inherit (inputs.self.pkgs.x86_64-linux) lib;
|
||||
inherit inputs minimal stateVersion;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkContainer = guestName: cfg: {
|
||||
${guestName} =
|
||||
mkGuest guestName cfg
|
||||
// {
|
||||
backend = "container";
|
||||
container.macvlan = "lan";
|
||||
extraSpecialArgs = {
|
||||
inherit lib nodes inputs minimal stateVersion;
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{}
|
||||
// mkContainer "adguardhome" {}
|
||||
// mkContainer "nextcloud" {
|
||||
enablePanzer = true;
|
||||
}
|
||||
// mkContainer "samba" {
|
||||
enablePanzer = true;
|
||||
enableRenaultFT = true;
|
||||
};
|
||||
}
|
42
hosts/elisabeth/net.nix
Normal file
42
hosts/elisabeth/net.nix
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
networking = {
|
||||
inherit (config.secrets.secrets.local.networking) hostId;
|
||||
};
|
||||
systemd.network.networks = {
|
||||
"lan01" = {
|
||||
address = [(lib.net.cidr.host config.secrets.secrets.global.net.ips.${config.node.name} config.secrets.secrets.global.net.privateSubnet)];
|
||||
gateway = [(lib.net.cidr.host 1 config.secrets.secrets.global.net.privateSubnet)];
|
||||
#matchConfig.MACAddress = config.secrets.secrets.local.networking.interfaces.lan01.mac;
|
||||
matchConfig.Name = "lan";
|
||||
networkConfig = {
|
||||
IPv6PrivacyExtensions = "yes";
|
||||
MulticastDNS = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
boot.initrd.systemd.network = {
|
||||
enable = true;
|
||||
networks = {
|
||||
# redo the network cause the livesystem has macvlans
|
||||
"lan01" = {
|
||||
address = [(lib.net.cidr.host config.secrets.secrets.global.net.ips.${config.node.name} config.secrets.secrets.global.net.privateSubnet)];
|
||||
gateway = [(lib.net.cidr.host 1 config.secrets.secrets.global.net.privateSubnet)];
|
||||
matchConfig.MACAddress = config.secrets.secrets.local.networking.interfaces.lan01.mac;
|
||||
networkConfig = {
|
||||
IPv6PrivacyExtensions = "yes";
|
||||
MulticastDNS = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
# To be able to ping containers from the host, it is necessary
|
||||
# to create a macvlan on the host on the VLAN 1 network.
|
||||
networking.macvlans.lan = {
|
||||
interface = "lan01";
|
||||
mode = "bridge";
|
||||
};
|
||||
}
|
20
hosts/elisabeth/secrets/generated/dhparams.pem.age
Normal file
20
hosts/elisabeth/secrets/generated/dhparams.pem.age
Normal file
|
@ -0,0 +1,20 @@
|
|||
age-encryption.org/v1
|
||||
-> X25519 WretELIMVw/omsoHEMGR7PsFsfiUEfyUmlKMzmrw+wA
|
||||
IW+zJKWSMfZiKs1LQwuAtej7ZDEvDt5oY+wfWpZoB1c
|
||||
-> piv-p256 XTQkUA A5MNklHowU6rYbcJBT/+dW0v9Gex5IJ1sC5ksuRsfu1k
|
||||
VPN/pCvMXi6Uc1uk6yuySK/e8bSjJ66zm4W62leQpBk
|
||||
-> piv-p256 ZFgiIw Ah5jjfu6nrqXrW7YqfIEKWF3PrLOmEEM5LhRvi5EJVmE
|
||||
MaVt5imJLBgM3NEw7tc18g9jMwPRl9c5RgCFzDIl8hk
|
||||
-> piv-p256 5vmPtQ AqViuuU1xW/ngBTWFMjZax9SaQyZ/COo0fHNOwq/8Hkb
|
||||
MDD3bD8PMS3AWPougqz/BXGGZGGnFPafZ0dc7Xqa0VM
|
||||
-> piv-p256 ZFgiIw AuTg62739Zom64yEb4FZfA5lyeW9YP9h+3iDQJcQZSuM
|
||||
TtwsPfCJi6bYH8tpPSdf9ZQlpXUC6t/AT1wM2aCXcNM
|
||||
-> "-grease
|
||||
n7GU3iZJjAz/ul8nNXzXYtrR
|
||||
--- mvuAEeT2IOYZKF9u/htBSJSAxKuzLjx4hR65yyHzPK4
|
||||
fœ<šJ{ïAzß(Ôz(Z|òxÏ0þhÍiaJd*T,o‡Ì«“A?¯ôò,ùÒ>²?»òe§.*Ð'Lû„ Æ7Õ®®B±÷ˆ®Â=<3D>ÈQ&†’<>c?*Wã¢#"ðÛù$Ee~?íû1‡í²Aý…ÿ±°aŸñÓ1Elƶx<C2B6>ÄñƼñ\<5C>Ëfµ'ê¼Xb"K‘ÞñØÜf
oÄhy|I¨5¦hÐÏÍÕî%?CKÒ<0C>R‰…È5÷ŽC! T7¯ÏÒððñ½'=ʉzÝIøSΰÖ&AÖ&l´²®¤†ïþv$JYŸíR<C3AD>LšDVòO"üô±Ùåў¥µLSˆ|€¾øˆ\St,éÅŽÇ€w°Ïɬ<C389>,]>¼Qï8uLä~L&ûhÎ ÑÅ`špÙXÐ)Çål²–
|
||||
ã`ãÓ¸L‘/Rþ åÖ<ºZ3qW
|
||||
î®\n]$çlÂÊ>Á<>Døj˜a5ÔÂ@=ñÒ5À|‰‰@Rõü- a½ÊÖµë<C2B5><C3AB>Xàs•³^žŠÆ´—`ašì«b^ƒ0qræˆÙ™F¿%ÖC–åë¼Ykª¯à&%<25><>ʪ7V™þUj2¯Ü.¥õ<C2A5>KÞ'«c!†ôÈ°+AÝ
è[ ¸<>ö¥r©—¼&òjÉ[<06>†G<E280A0>t2D}ÿ»‚³-ÙHî Þàú€Å¾4ø0奣ýÊå&WÖ,“;}¢Ø’À5ðš¤÷
|
||||
‰Zmìô^á[=çLËa(
|
||||
4=NŽ8ø¹=ð<>ƒ¢ü=‘åKQl“ð^ôm¶¢Aæ´?¸ì*°¾$í+‚Vº<56>Ç—%UZ"á2²™Bá7‘’ë‹ÈbÅ>ÆMÛÊé(ø§û·ÿbÇAV^Ù¾a1bæg£ûA³ù,:±¢‚ÅíÖöRÙ.RöZ6M„ãšö!
BÏ·þQ¶³El
|
||||
”‰‰ïÓ¹~qö=ê³ üJ<C3BC>R{Œ#¾Ø×fªwQC>®cOºf<>°q¬ªÊä.eY@®Ú_붹»<cï.G˜â®ßÓù9E¬~´ë£¬xPÿ
|
BIN
hosts/elisabeth/secrets/generated/initrd_host_ed25519_key.age
Normal file
BIN
hosts/elisabeth/secrets/generated/initrd_host_ed25519_key.age
Normal file
Binary file not shown.
BIN
hosts/elisabeth/secrets/nextcloud/generated/ncpasswd.age
Normal file
BIN
hosts/elisabeth/secrets/nextcloud/generated/ncpasswd.age
Normal file
Binary file not shown.
BIN
hosts/elisabeth/secrets/secrets.nix.age
Normal file
BIN
hosts/elisabeth/secrets/secrets.nix.age
Normal file
Binary file not shown.
|
@ -19,7 +19,6 @@
|
|||
|
||||
../../modules/hardware/bluetooth.nix
|
||||
../../modules/hardware/laptop.nix
|
||||
../../modules/hardware/intel.nix
|
||||
../../modules/hardware/physical.nix
|
||||
../../modules/hardware/pipewire.nix
|
||||
../../modules/hardware/yubikey.nix
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
type = "table";
|
||||
format = "gpt";
|
||||
partitions = [
|
||||
(partEfiBoot "boot" "0%" "260MB")
|
||||
(partEfi "boot" "0%" "260MB")
|
||||
{
|
||||
name = "rpool";
|
||||
content = {
|
||||
|
@ -25,7 +25,7 @@
|
|||
};
|
||||
};
|
||||
zpool = with lib.disko.zfs; {
|
||||
rpool = defaultZpoolOptions // {datasets = defaultZfsDatasets;};
|
||||
rpool = mkZpool {datasets = impermanenceZfsDatasets;};
|
||||
};
|
||||
};
|
||||
fileSystems."/state".neededForBoot = true;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
format = "gpt";
|
||||
partitions = [
|
||||
(partGrub "grub" "0%" "1MiB")
|
||||
(partEfiBoot "bios" "1MiB" "512MiB")
|
||||
(partEfi "bios" "1MiB" "512MiB")
|
||||
(partLuksZfs "rpool" "rpool" "512MiB" "100%")
|
||||
#(lib.attrsets.recursiveUpdate (partLuksZfs "rpool" "rpool" "17GiB" "100%") {content.extraFormatArgs = ["--pbkdf pbkdf2"];})
|
||||
];
|
||||
|
@ -22,7 +22,7 @@
|
|||
};
|
||||
|
||||
zpool = with lib.disko.zfs; {
|
||||
rpool = defaultZpoolOptions // {datasets = defaultZfsDatasets;};
|
||||
rpool = mkZpool {datasets = impermanenceZfsDatasets;};
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
../../modules/hardware/bluetooth.nix
|
||||
../../modules/hardware/laptop.nix
|
||||
../../modules/hardware/intel.nix
|
||||
../../modules/hardware/nvidia.nix
|
||||
../../modules/hardware/physical.nix
|
||||
../../modules/hardware/pipewire.nix
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
type = "table";
|
||||
format = "gpt";
|
||||
partitions = [
|
||||
(partEfiBoot "boot" "0%" "1GiB")
|
||||
(partEfi "boot" "0%" "1GiB")
|
||||
(partSwap "swap" "1GiB" "17GiB")
|
||||
(partLuksZfs "rpool" "rpool" "17GiB" "100%")
|
||||
];
|
||||
|
@ -20,7 +20,7 @@
|
|||
};
|
||||
};
|
||||
zpool = with lib.disko.zfs; {
|
||||
rpool = defaultZpoolOptions // {datasets = defaultZfsDatasets;};
|
||||
rpool = mkZpool {datasets = impermanenceZfsDatasets;};
|
||||
};
|
||||
};
|
||||
fileSystems."/state".neededForBoot = true;
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
../../modules/config
|
||||
../../modules/optional/initrd-ssh.nix
|
||||
|
||||
../../modules/hardware/intel.nix
|
||||
../../modules/hardware/physical.nix
|
||||
../../modules/hardware/zfs.nix
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
type = "table";
|
||||
format = "gpt";
|
||||
partitions = [
|
||||
(partEfiBoot "boot" "0%" "1GiB")
|
||||
(partEfi "boot" "0%" "1GiB")
|
||||
(partSwap "swap" "1GiB" "17GiB")
|
||||
(lib.attrsets.recursiveUpdate (partLuksZfs "rpool" "rpool" "17GiB" "100%") {content.extraFormatArgs = ["--pbkdf pbkdf2"];})
|
||||
];
|
||||
|
@ -43,10 +43,10 @@
|
|||
};
|
||||
|
||||
zpool = with lib.disko.zfs; {
|
||||
rpool = defaultZpoolOptions // {datasets = defaultZfsDatasets;};
|
||||
rpool = mkZpool {datasets = impermanenceZfsDatasets;};
|
||||
panzer =
|
||||
defaultZpoolOptions
|
||||
// {
|
||||
mkZpool
|
||||
{
|
||||
datasets = {
|
||||
"local" = unmountable;
|
||||
"local/state" = filesystem "/panzer/state";
|
||||
|
|
16
hosts/testienix/secrets/generated/ncpasswd.age
Normal file
16
hosts/testienix/secrets/generated/ncpasswd.age
Normal file
|
@ -0,0 +1,16 @@
|
|||
age-encryption.org/v1
|
||||
-> X25519 +i63saSU8RBHO56nE65z4pFN72weFIH0MO2B6kKFX1o
|
||||
O03ucWspS7ERnPwqPVVDLpokcR+VDGfeema+7VCdUcE
|
||||
-> piv-p256 XTQkUA A222BiQ7aVaSdbpTgH0zop6Yc7iD3o9p+DpBT/53cfsI
|
||||
NiOx77wz7D8tIWOitsVynStIGjUlDaXfYdjAvjvpV68
|
||||
-> piv-p256 ZFgiIw AgR85lUO7c+rwARcAkHBzoWONva7zDwCZ8hGNhP5FNXb
|
||||
iIWz22A6YIzLlEbOhA6AwIVS5B4mOOLUMUvjel/QPPM
|
||||
-> piv-p256 5vmPtQ Ao50NLd25O1sdk96G8a3acSjhOwfq+DbHiVl6q/E+2+3
|
||||
4R9ScWJsjyLxUqVTaKfzmsMvbZQH8shiqPbIGshbNpA
|
||||
-> piv-p256 ZFgiIw A90xpxtG8MMDmsQpgx5fRavYIrmlv0rkcjev3LZYKRnS
|
||||
2Wc3c2LPcWcRfL5+yH/GNWwblkofSrY/Bj7AxuOPX8g
|
||||
-> `tr=>/j-grease Fr#5$ANy D0UHo: aD
|
||||
F8V7YAtFk4XQjKdsN/pwtYnH
|
||||
--- wCUeLyGHUi/Qwc7INkFXilCQz/N5rRgVtZ3TQu6jfgU
|
||||
Lnţ62ú6:©†F¬Ö•tW<74>őô†Rm®ŠÓňęĂň\ÂmbiÝ‘°ˇÁŞPśµç‡ĹWŽEd!ő™[/B
|
||||
k«żÜ÷4`hC¨Lł˝řĚęS
|
|
@ -1,4 +1,3 @@
|
|||
inputs: [
|
||||
(import ./containers.nix inputs)
|
||||
(import ./misc.nix inputs)
|
||||
]
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
{
|
||||
inputs,
|
||||
lib,
|
||||
stateVersion,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}: {
|
||||
system.stateVersion = stateVersion;
|
||||
|
||||
age.rekey = {
|
||||
inherit
|
||||
(inputs.self.secretsConfig)
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
powerManagement.cpuFreqGovernor = "powersave";
|
||||
hardware.cpu.intel.updateMicrocode = true;
|
||||
}
|
65
modules/services/adguardhome.nix
Normal file
65
modules/services/adguardhome.nix
Normal file
|
@ -0,0 +1,65 @@
|
|||
{config, ...}: {
|
||||
services.adguardhome = {
|
||||
enable = true;
|
||||
mutableSettings = false;
|
||||
openFirewall = true; # opens webinterface firewall
|
||||
settings = {
|
||||
bind_port = 3000;
|
||||
bind_host = "0.0.0.0";
|
||||
dns = {
|
||||
bind_hosts = ["TODO"];
|
||||
anonymize_client_ip = true;
|
||||
upstream_dns = [
|
||||
"1.0.0.1"
|
||||
"2606:4700:4700::1111"
|
||||
"8.8.8.8"
|
||||
"2001:4860:4860::8844"
|
||||
];
|
||||
bootstrap_dns = [
|
||||
"1.0.0.1"
|
||||
"2606:4700:4700::1111"
|
||||
"8.8.8.8"
|
||||
"2001:4860:4860::8844"
|
||||
];
|
||||
};
|
||||
user_rules = ''
|
||||
||${config.secrets.secrets.global.domains.web}^$dnsrewrite=TODO
|
||||
'';
|
||||
dhcp.enabled = false;
|
||||
ratelimit = 60;
|
||||
users = [
|
||||
{
|
||||
name = "patrick";
|
||||
password = "$2b$05$Dapc2LWUfebNOgIeBcaf2OVhW7uKmthmp9Ptykn96Iw1UE5pt2U72";
|
||||
}
|
||||
];
|
||||
filters = [
|
||||
{
|
||||
name = "AdGuard DNS filter";
|
||||
url = "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
name = "AdaAway Default Blocklist";
|
||||
url = "https://adaway.org/hosts.txt";
|
||||
enabled = true;
|
||||
}
|
||||
{
|
||||
name = "OISD (Big)";
|
||||
url = "https://big.oisd.nl";
|
||||
enabled = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
networking.firewall = {
|
||||
allowedTCPPorts = [53];
|
||||
allowedUDPPorts = [53];
|
||||
};
|
||||
environment.persistence."/persist".directories = [
|
||||
{
|
||||
directory = "/var/lib/private/AdGuardHome";
|
||||
mode = "0700";
|
||||
}
|
||||
];
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
utils,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
inherit
|
||||
(lib)
|
||||
mapAttrs'
|
||||
concatStrings
|
||||
nameValuePair
|
||||
mapAttrsToList
|
||||
flip
|
||||
types
|
||||
mkOption
|
||||
mkEnableOption
|
||||
mdDoc
|
||||
mkIf
|
||||
disko
|
||||
makeBinPath
|
||||
escapeShellArg
|
||||
mkMerge
|
||||
;
|
||||
in {
|
||||
options.containers = mkOption {
|
||||
type = types.attrsOf (types.submodule (
|
||||
{name, ...}: {
|
||||
options = {
|
||||
zfs = {
|
||||
enable = mkEnableOption (mdDoc "persistent data on separate zfs dataset");
|
||||
|
||||
pool = mkOption {
|
||||
type = types.str;
|
||||
description = mdDoc "The host's zfs pool on which the dataset resides";
|
||||
};
|
||||
|
||||
dataset = mkOption {
|
||||
type = types.str;
|
||||
default = "safe/containers/${name}";
|
||||
description = mdDoc "The host's dataset that should be used for this containers persistent data (will automatically be created)";
|
||||
};
|
||||
|
||||
mountpoint = mkOption {
|
||||
type = types.str;
|
||||
description = mdDoc "The host's mountpoint for the containers dataset";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
));
|
||||
};
|
||||
config.system.activationScripts = let
|
||||
mkDir = paths: (concatStrings (flip map paths (path: ''
|
||||
[[ -d "${path}" ]] || ${pkgs.coreutils}/bin/mkdir -p "${path}"
|
||||
'')));
|
||||
in
|
||||
flip mapAttrs' config.containers (
|
||||
name: value:
|
||||
nameValuePair "mkContainerFolder-${name}" (mkDir (mapAttrsToList (_: x: x.hostPath) value.bindMounts))
|
||||
);
|
||||
config.disko = mkMerge (flip mapAttrsToList config.containers
|
||||
(
|
||||
_: cfg: {
|
||||
devices.zpool = mkIf cfg.zfs.enable {
|
||||
${cfg.zfs.pool}.datasets."${cfg.zfs.dataset}" =
|
||||
disko.zfs.filesystem cfg.zfs.mountpoint;
|
||||
};
|
||||
|
||||
# Ensure that the zfs dataset exists before it is mounted.
|
||||
}
|
||||
));
|
||||
config.systemd = mkMerge (flip mapAttrsToList config.containers
|
||||
(
|
||||
name: cfg: {
|
||||
services = let
|
||||
fsMountUnit = "${utils.escapeSystemdPath cfg.zfs.mountpoint}.mount";
|
||||
in
|
||||
mkIf cfg.zfs.enable {
|
||||
# Ensure that the zfs dataset exists before it is mounted.
|
||||
"zfs-ensure-${utils.escapeSystemdPath cfg.zfs.mountpoint}" = {
|
||||
wantedBy = [fsMountUnit];
|
||||
before = [fsMountUnit];
|
||||
after = [
|
||||
"zfs-import-${utils.escapeSystemdPath cfg.zfs.pool}.service"
|
||||
"zfs-mount.target"
|
||||
];
|
||||
unitConfig.DefaultDependencies = "no";
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = let
|
||||
poolDataset = "${cfg.zfs.pool}/${cfg.zfs.dataset}";
|
||||
diskoDataset = config.disko.devices.zpool.${cfg.zfs.pool}.datasets.${cfg.zfs.dataset};
|
||||
in ''
|
||||
export PATH=${makeBinPath [pkgs.zfs]}":$PATH"
|
||||
if ! zfs list -H -o type ${escapeShellArg poolDataset} &>/dev/null ; then
|
||||
${diskoDataset._create}
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
# Ensure that the zfs dataset has the correct permissions when mounted
|
||||
"zfs-chown-${utils.escapeSystemdPath cfg.zfs.mountpoint}" = {
|
||||
after = [fsMountUnit];
|
||||
unitConfig.DefaultDependencies = "no";
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = ''
|
||||
chmod 755 ${escapeShellArg cfg.zfs.mountpoint}
|
||||
'';
|
||||
};
|
||||
|
||||
"container@${name}" = {
|
||||
requires = [fsMountUnit "zfs-chown-${utils.escapeSystemdPath cfg.zfs.mountpoint}.service"];
|
||||
after = [fsMountUnit "zfs-chown-${utils.escapeSystemdPath cfg.zfs.mountpoint}.service"];
|
||||
};
|
||||
};
|
||||
}
|
||||
));
|
||||
}
|
|
@ -5,11 +5,11 @@
|
|||
};
|
||||
services.ddclient = {
|
||||
enable = true;
|
||||
zone = config.secrets.secrets.global.domains.mail;
|
||||
zone = config.secrets.secrets.global.domains.web;
|
||||
protocol = "Cloudflare";
|
||||
username = "token";
|
||||
use = "web, web='https://cloudflare.com/cdn-cgi/trace', web-skip='ip='";
|
||||
passwordFile = config.age.secrets.cloudflare_token_dns.path;
|
||||
domains = [config.secrets.secrets.global.domains.mail];
|
||||
domains = [config.secrets.secrets.global.domains.web];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,122 +1,92 @@
|
|||
{
|
||||
lib,
|
||||
stateVersion,
|
||||
pkgs,
|
||||
config,
|
||||
#deadnix: skip
|
||||
pkgs, # not unused needed for the usage of attrs later to contains pkgs
|
||||
...
|
||||
} @ attrs: let
|
||||
hostName = "nc.${config.secrets.secrets.global.domains.mail}";
|
||||
}: let
|
||||
hostName = "nc.${config.secrets.secrets.global.domains.web}";
|
||||
in {
|
||||
imports = [./containers.nix ./ddclient.nix ./acme.nix];
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedSetup = true;
|
||||
upstreams.nextcloud = {
|
||||
servers."192.168.178.33:80" = {};
|
||||
|
||||
extraConfig = ''
|
||||
zone nextcloud 64k ;
|
||||
keepalive 5 ;
|
||||
'';
|
||||
};
|
||||
virtualHosts.${hostName} = {
|
||||
forceSSL = true;
|
||||
useACMEHost = "mail";
|
||||
locations."/".proxyPass = "http://nextcloud";
|
||||
extraConfig = ''
|
||||
client_max_body_size 4G ;
|
||||
'';
|
||||
systemd.network.networks = {
|
||||
"TODO" = {
|
||||
address = ["192.168.178.33/24"];
|
||||
gateway = ["192.168.178.1"];
|
||||
matchConfig.Name = "lan01*";
|
||||
dns = ["192.168.178.2"];
|
||||
networkConfig = {
|
||||
IPv6PrivacyExtensions = "yes";
|
||||
MulticastDNS = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
containers.nextcloud = lib.containers.mkConfig "nextcloud" attrs {
|
||||
zfs = {
|
||||
enable = true;
|
||||
pool = "panzer";
|
||||
environment.persistence."/persist".directories = [
|
||||
{
|
||||
directory = "/var/lib/postgresql/";
|
||||
user = "postgres";
|
||||
group = "postgres";
|
||||
mode = "750";
|
||||
}
|
||||
];
|
||||
environment.persistence."/panzer".directories = [
|
||||
{
|
||||
directory = config.services.nextcloud.home;
|
||||
user = "nextcloud";
|
||||
group = "nextcloud";
|
||||
mode = "750";
|
||||
}
|
||||
];
|
||||
age.secrets.ncpasswd = {
|
||||
generator.script = "alnum";
|
||||
mode = "440";
|
||||
owner = "nextcloud";
|
||||
};
|
||||
services.postgresql.package = pkgs.postgresql_16;
|
||||
services.nginx.virtualHosts.${hostName}.extraConfig = ''
|
||||
allow TODO;
|
||||
deny all;
|
||||
'';
|
||||
|
||||
services.nextcloud = {
|
||||
inherit hostName;
|
||||
enable = true;
|
||||
package = pkgs.nextcloud28;
|
||||
configureRedis = true;
|
||||
config.adminpassFile = config.age.secrets.ncpasswd.path; # Kinda ok just remember to instanly change after first setup
|
||||
config.adminuser = "admin";
|
||||
extraApps = with config.services.nextcloud.package.packages.apps; {
|
||||
inherit contacts calendar tasks notes maps phonetrack;
|
||||
};
|
||||
maxUploadSize = "4G";
|
||||
extraAppsEnable = true;
|
||||
database.createLocally = true;
|
||||
phpOptions."opcache.interned_strings_buffer" = "32";
|
||||
extraOptions = {
|
||||
default_phone_region = "DE";
|
||||
trusted_proxies = ["TODO"];
|
||||
overwriteprotocol = "https";
|
||||
enabledPreviewProviders = [
|
||||
"OC\\Preview\\BMP"
|
||||
"OC\\Preview\\GIF"
|
||||
"OC\\Preview\\JPEG"
|
||||
"OC\\Preview\\Krita"
|
||||
"OC\\Preview\\MarkDown"
|
||||
"OC\\Preview\\MP3"
|
||||
"OC\\Preview\\OpenDocument"
|
||||
"OC\\Preview\\PNG"
|
||||
"OC\\Preview\\TXT"
|
||||
"OC\\Preview\\XBitmap"
|
||||
"OC\\Preview\\HEIC"
|
||||
];
|
||||
};
|
||||
config = {
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
#TODO enable recommended nginx setup
|
||||
systemd.network.networks = {
|
||||
"lan01" = {
|
||||
address = ["192.168.178.33/24"];
|
||||
gateway = ["192.168.178.1"];
|
||||
matchConfig.Name = "lan01*";
|
||||
dns = ["192.168.178.2"];
|
||||
networkConfig = {
|
||||
IPv6PrivacyExtensions = "yes";
|
||||
MulticastDNS = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
environment.persistence."/persist".directories = [
|
||||
{
|
||||
directory = config.services.nextcloud.home;
|
||||
user = "nextcloud";
|
||||
group = "nextcloud";
|
||||
mode = "750";
|
||||
}
|
||||
];
|
||||
services.nextcloud = {
|
||||
inherit hostName;
|
||||
enable = true;
|
||||
package = pkgs.nextcloud28;
|
||||
configureRedis = true;
|
||||
config.adminpassFile = "${pkgs.writeText "adminpass" "test123"}"; # DON'T DO THIS IN PRODUCTION - the password file will be world-readable in the Nix Store!
|
||||
config.adminuser = "admin";
|
||||
extraApps = with config.services.nextcloud.package.packages.apps; {
|
||||
inherit contacts calendar tasks notes maps;
|
||||
};
|
||||
maxUploadSize = "2G";
|
||||
extraAppsEnable = true;
|
||||
database.createLocally = true;
|
||||
phpOptions."opcache.interned_strings_buffer" = "32";
|
||||
extraOptions = {
|
||||
default_phone_region = "DE";
|
||||
trusted_proxies = ["192.168.178.32"];
|
||||
overwriteprotocol = "https";
|
||||
enabledPreviewProviders = [
|
||||
"OC\\Preview\\BMP"
|
||||
"OC\\Preview\\GIF"
|
||||
"OC\\Preview\\JPEG"
|
||||
"OC\\Preview\\Krita"
|
||||
"OC\\Preview\\MarkDown"
|
||||
"OC\\Preview\\MP3"
|
||||
"OC\\Preview\\OpenDocument"
|
||||
"OC\\Preview\\PNG"
|
||||
"OC\\Preview\\TXT"
|
||||
"OC\\Preview\\XBitmap"
|
||||
"OC\\Preview\\HEIC"
|
||||
];
|
||||
};
|
||||
config = {
|
||||
dbtype = "pgsql";
|
||||
};
|
||||
};
|
||||
|
||||
system.stateVersion = stateVersion;
|
||||
|
||||
networking = {
|
||||
firewall = {
|
||||
enable = true;
|
||||
allowedTCPPorts = [80];
|
||||
};
|
||||
# Use systemd-resolved inside the container
|
||||
useHostResolvConf = lib.mkForce false;
|
||||
};
|
||||
|
||||
services.resolved.enable = true;
|
||||
dbtype = "pgsql";
|
||||
};
|
||||
};
|
||||
}
|
||||
#wireguard
|
||||
#samba/printer finding
|
||||
#vaultwarden
|
||||
#maddy
|
||||
#kanidm
|
||||
#remote backups
|
||||
#immich
|
||||
|
||||
networking = {
|
||||
firewall.allowedTCPPorts = [80];
|
||||
# Use systemd-resolved inside the container
|
||||
useHostResolvConf = lib.mkForce false;
|
||||
};
|
||||
|
||||
services.resolved.enable = true;
|
||||
}
|
||||
|
|
|
@ -3,17 +3,16 @@
|
|||
lib,
|
||||
...
|
||||
}: {
|
||||
services.samba-wsdd.enable = true; # make shares visible for windows 10 clients
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
5357 # wsdd
|
||||
];
|
||||
networking.firewall.allowedUDPPorts = [
|
||||
3702 # wsdd
|
||||
];
|
||||
services.samba-wsdd = {
|
||||
enable = true; # make shares visible for windows 10 clients
|
||||
openFirewall = true;
|
||||
};
|
||||
services.samba = {
|
||||
enable = true;
|
||||
securityType = "user";
|
||||
openFirewall = true;
|
||||
enableWinbindd = false;
|
||||
enableNmbd = false;
|
||||
extraConfig = lib.concatLines [
|
||||
''
|
||||
logging = systemd
|
||||
|
@ -56,10 +55,12 @@
|
|||
name,
|
||||
user ? "smb",
|
||||
group ? "smb",
|
||||
persistRoot ? "/panzer",
|
||||
}: cfg: {
|
||||
"${name}" =
|
||||
{
|
||||
"path" = "/media/smb/${name}";
|
||||
"#persistRoot" = persistRoot;
|
||||
"read only" = "no";
|
||||
"guest ok" = "no";
|
||||
"create mask" = "0640";
|
||||
|
@ -101,7 +102,10 @@
|
|||
user = "family";
|
||||
group = "family";
|
||||
} {})
|
||||
((mkShare {name = "media";})
|
||||
(mkShare {
|
||||
name = "media";
|
||||
persistRoot = "/renaultft";
|
||||
}
|
||||
{
|
||||
"read only" = "yes";
|
||||
"write list" = "@family";
|
||||
|
@ -149,10 +153,14 @@
|
|||
}));
|
||||
};
|
||||
|
||||
environment.persistence."/panzer/persist".directories = lib.flip lib.mapAttrsToList config.services.samba.shares (_: v: {
|
||||
directory = "${v.path}";
|
||||
user = "${v."force user"}";
|
||||
group = "${v."force group"}";
|
||||
mode = "0770";
|
||||
});
|
||||
environment.persistence = lib.mkMerge (lib.flip lib.mapAttrsToList config.services.samba.shares (_: v: {
|
||||
${v."#persistRoot"}.directories = [
|
||||
{
|
||||
directory = "${v.path}";
|
||||
user = "${v."force user"}";
|
||||
group = "${v."force group"}";
|
||||
mode = "0770";
|
||||
}
|
||||
];
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -48,13 +48,18 @@ inputs: let
|
|||
nixosConfigurations = flip mapAttrs nixosHosts (mkHost {minimal = false;});
|
||||
minimalConfigurations = flip mapAttrs nixosHosts (mkHost {minimal = true;});
|
||||
|
||||
# True NixOS nodes can define additional microvms (guest nodes) that are built
|
||||
# together with the true host. We collect all defined microvm nodes
|
||||
# from each node here to allow accessing any node via the unified attribute `nodes`.
|
||||
# True NixOS nodes can define additional guest nodes that are built
|
||||
# together with it. We collect all defined guests from each node here
|
||||
# to allow accessing any node via the unified attribute `nodes`.
|
||||
guestConfigurations = flip concatMapAttrs self.nixosConfigurations (_: node:
|
||||
mapAttrs'
|
||||
(vm: _: nameValuePair vm {inherit (node.config.containers.${vm}) config;})
|
||||
(node.config.containers or {}));
|
||||
flip mapAttrs' (node.config.guests or {}) (
|
||||
guestName: guestDef:
|
||||
nameValuePair guestDef.nodeName (
|
||||
if guestDef.backend == "microvm"
|
||||
then node.config.microvm.vms.${guestName}.config
|
||||
else node.config.containers.${guestName}.nixosConfiguration
|
||||
)
|
||||
));
|
||||
in {
|
||||
inherit
|
||||
hosts
|
||||
|
|
Binary file not shown.
Loading…
Reference in a new issue