Mount IMAP data in zfs volume, which should be easily backed
up by TrueNAS. Also enable full text search
This commit is contained in:
parent
8f6c428305
commit
fa8320b805
10 changed files with 249 additions and 154 deletions
10
nix/modules/nixos/infrastructure/nixos-install/default.nix
Normal file
10
nix/modules/nixos/infrastructure/nixos-install/default.nix
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{ lib, ... }:
|
||||||
|
{
|
||||||
|
options.khscodes.infrastructure.nixos-install = {
|
||||||
|
preScript = lib.mkOption {
|
||||||
|
type = lib.types.anything;
|
||||||
|
default = "";
|
||||||
|
description = "Script to run before running nixos-anywhere.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -27,7 +27,8 @@ in
|
||||||
{
|
{
|
||||||
systemd.services."read-vault-auth-from-userdata" = {
|
systemd.services."read-vault-auth-from-userdata" = {
|
||||||
enable = true;
|
enable = true;
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "vault-agent-openbao.service" ];
|
||||||
|
before = [ "vault-agent-openbao.service" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
after = [ "network-online.target" ];
|
after = [ "network-online.target" ];
|
||||||
environment = {
|
environment = {
|
||||||
|
|
|
@ -19,12 +19,12 @@ let
|
||||||
restartUnits =
|
restartUnits =
|
||||||
svcs:
|
svcs:
|
||||||
lib.strings.concatStringsSep "\n" (
|
lib.strings.concatStringsSep "\n" (
|
||||||
lib.lists.map (svc: "systemctl restart ${lib.escapeShellArg svc}") svcs
|
lib.lists.map (svc: "systemctl restart ${lib.escapeShellArg svc} || true") svcs
|
||||||
);
|
);
|
||||||
reloadOrRestartUnits =
|
reloadOrRestartUnits =
|
||||||
svcs:
|
svcs:
|
||||||
lib.strings.concatStringsSep "\n" (
|
lib.strings.concatStringsSep "\n" (
|
||||||
lib.lists.map (svc: "systemctl reload-or-restart ${lib.escapeShellArg svc}") svcs
|
lib.lists.map (svc: "systemctl reload-or-restart ${lib.escapeShellArg svc} || true") svcs
|
||||||
);
|
);
|
||||||
mapTemplate =
|
mapTemplate =
|
||||||
template:
|
template:
|
||||||
|
|
|
@ -19,35 +19,6 @@ let
|
||||||
top = lib.lists.takeEnd 2 split;
|
top = lib.lists.takeEnd 2 split;
|
||||||
in
|
in
|
||||||
lib.strings.concatStringsSep "." top;
|
lib.strings.concatStringsSep "." top;
|
||||||
serviceFromFqdn =
|
|
||||||
fqdn:
|
|
||||||
let
|
|
||||||
split = lib.strings.splitString "." fqdn;
|
|
||||||
in
|
|
||||||
assert
|
|
||||||
lib.strings.hasPrefix "_" (builtins.elemAt split 0)
|
|
||||||
&& lib.strings.hasPrefix "_" (builtins.elemAt split 1);
|
|
||||||
builtins.elemAt split 0;
|
|
||||||
protocolFromFqdn =
|
|
||||||
fqdn:
|
|
||||||
let
|
|
||||||
split = lib.strings.splitString "." fqdn;
|
|
||||||
in
|
|
||||||
assert
|
|
||||||
lib.strings.hasPrefix "_" (builtins.elemAt split 0)
|
|
||||||
&& lib.strings.hasPrefix "_" (builtins.elemAt split 1);
|
|
||||||
builtins.elemAt split 1;
|
|
||||||
nameFromFqdn =
|
|
||||||
fqdn:
|
|
||||||
let
|
|
||||||
split = lib.strings.splitString "." fqdn;
|
|
||||||
in
|
|
||||||
assert
|
|
||||||
lib.strings.hasPrefix "_" (builtins.elemAt split 0)
|
|
||||||
&& lib.strings.hasPrefix "_" (builtins.elemAt split 1);
|
|
||||||
lib.strings.concatStringsSep "." (
|
|
||||||
lib.lists.removePrefix [ (builtins.elemAt split 0) (builtins.elemAt split 1) ] split
|
|
||||||
);
|
|
||||||
dnsARecordModule = lib.khscodes.mkSubmodule {
|
dnsARecordModule = lib.khscodes.mkSubmodule {
|
||||||
description = "Module for defining dns A/AAAA record";
|
description = "Module for defining dns A/AAAA record";
|
||||||
options = {
|
options = {
|
||||||
|
|
|
@ -28,4 +28,7 @@
|
||||||
"secrets.kaareskovgaard.net" = {
|
"secrets.kaareskovgaard.net" = {
|
||||||
"VAULT_TOKEN" = "Initial root token";
|
"VAULT_TOKEN" = "Initial root token";
|
||||||
};
|
};
|
||||||
|
"mx.kaareskovgaard.net" = {
|
||||||
|
"MX_KAARESKOVGAARD_NET_ZROOT_ENCRYPTION_KEY" = "ZROOT_ENCRYPTION_KEY";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,17 @@ pkgs.writeShellApplication {
|
||||||
# Build the configuration to ensure it doesn't fail when trying to install it on the host
|
# Build the configuration to ensure it doesn't fail when trying to install it on the host
|
||||||
nix build --no-link '${inputs.self}#nixosConfigurations."'"$hostname"'".config.system.build.toplevel'
|
nix build --no-link '${inputs.self}#nixosConfigurations."'"$hostname"'".config.system.build.toplevel'
|
||||||
fi
|
fi
|
||||||
baseAttr='${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.infrastructure.provisioning'
|
baseAttr='${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.infrastructure'
|
||||||
config="$(nix build --no-link --print-out-paths "''${baseAttr}.preConfig")"
|
config="$(nix build --no-link --print-out-paths "''${baseAttr}.provisioning.preConfig")"
|
||||||
username="$(nix eval --raw "''${baseAttr}.preImageUsername")"
|
preScript="$(nix eval --raw "''${baseAttr}.nixos-install.preScript")"
|
||||||
|
username="$(nix eval --raw "''${baseAttr}.provisioning.preImageUsername")"
|
||||||
if [[ "$config" == "null" ]]; then
|
if [[ "$config" == "null" ]]; then
|
||||||
echo "No preprovisioning needed"
|
echo "No preprovisioning needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo -n "tempkey" | ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "$username@$host" -- "cat >/tmp/tempkey"
|
|
||||||
nixos-anywhere --flake "${inputs.self}#$hostname" --target-host "$username@$host"
|
INSTALL_ARGS=()
|
||||||
|
eval "$preScript"
|
||||||
|
nixos-anywhere --flake "${inputs.self}#$hostname" --target-host "$username@$host" "''${INSTALL_ARGS[@]}"
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
{
|
{
|
||||||
|
lib,
|
||||||
inputs,
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
locationFromDatacenter =
|
||||||
|
datacenter:
|
||||||
|
let
|
||||||
|
split = lib.strings.splitString "-" datacenter;
|
||||||
|
in
|
||||||
|
assert (lib.lists.length split) == 2;
|
||||||
|
lib.lists.head split;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
"${inputs.self}/nix/profiles/nixos/hetzner-server.nix"
|
"${inputs.self}/nix/profiles/nixos/hetzner-server.nix"
|
||||||
|
./disko.nix
|
||||||
./mailserver
|
./mailserver
|
||||||
];
|
];
|
||||||
khscodes = {
|
khscodes = {
|
||||||
|
@ -15,6 +26,21 @@
|
||||||
server_type = "cax11";
|
server_type = "cax11";
|
||||||
};
|
};
|
||||||
provisioning.pre.modules = [
|
provisioning.pre.modules = [
|
||||||
|
(
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
resource.hcloud_volume.zroot-disk1 = {
|
||||||
|
name = "mx.kaareskovgaard.net-zroot-disk1";
|
||||||
|
size = 100;
|
||||||
|
location = locationFromDatacenter config.khscodes.hcloud.server.compute.datacenter;
|
||||||
|
};
|
||||||
|
resource.hcloud_volume_attachment.zroot-disk1 = {
|
||||||
|
volume_id = "\${ resource.hcloud_volume.zroot-disk1.id }";
|
||||||
|
server_id = config.khscodes.hcloud.output.server.compute.id;
|
||||||
|
automount = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
(
|
(
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
{ pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
diskName = "nixos";
|
diskName = "nixos";
|
||||||
espSize = "500M";
|
espSize = "500M";
|
||||||
|
@ -7,66 +12,121 @@ let
|
||||||
volumeGroupName = "mainpool";
|
volumeGroupName = "mainpool";
|
||||||
rootLvName = "root";
|
rootLvName = "root";
|
||||||
zrootKey = "/run/secret/zroot.key";
|
zrootKey = "/run/secret/zroot.key";
|
||||||
zfsLoadKeyScript = pkgs.writeShellApplication {
|
# Don't ask me why this changes when there's more than one volume attached.
|
||||||
name = "load-zfs-key";
|
nixosDisk = "/dev/sdb";
|
||||||
runtimeInputs = [ pkgs.zfs ];
|
zrootDisk1Disk = "/dev/sda";
|
||||||
|
vmailUser = config.mailserver.vmailUserName;
|
||||||
|
vmailGroup = config.mailserver.vmailGroupName;
|
||||||
|
|
||||||
|
downloadZrootKey = pkgs.writeShellApplication {
|
||||||
|
name = "zfs-download-zroot-key";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.openbao
|
||||||
|
pkgs.zfs
|
||||||
|
pkgs.uutils-coreutils-noprefix
|
||||||
|
pkgs.jq
|
||||||
|
];
|
||||||
text = ''
|
text = ''
|
||||||
if ! zfs load-key -L ${zrootKey} zroot; then
|
if [[ "$(zfs list -j -o keystatus zroot/mailserver | jq --raw-output '.datasets."zroot/mailserver".properties.keystatus.value')" == "available" ]]; then
|
||||||
echo -n "tempkey" /tmp/tempkey
|
>&2 echo "Key already loaded, exiting"
|
||||||
zfs load-key -L /temp/tempkey zroot
|
exit 0
|
||||||
zfs change-key -o keylocation=${zrootKey} zroot
|
|
||||||
fi
|
fi
|
||||||
|
# The vault cli insists on needing a token helper, disable it
|
||||||
|
HOME="$(mktemp -d)"
|
||||||
|
export HOME
|
||||||
|
trap 'rm -rf $HOME' EXIT
|
||||||
|
echo 'token_helper = "/bin/true"' > "$HOME/.vault"
|
||||||
|
role_id="$(cat /var/lib/vault-agent/role-id)"
|
||||||
|
secret_id="$(cat /var/lib/vault-agent/secret-id)"
|
||||||
|
VAULT_TOKEN="$(bao write -field=token auth/approle/login "role_id=$role_id" "secret_id=$secret_id")"
|
||||||
|
export VAULT_TOKEN
|
||||||
|
|
||||||
|
encryption_key="$(bao kv get -mount=opentofu -field=MX_KAARESKOVGAARD_NET_ZROOT_ENCRYPTION_KEY mx.kaareskovgaard.net)"
|
||||||
|
rm -rf "$HOME"
|
||||||
|
|
||||||
|
echo "$encryption_key" | zfs load-key -L file:///dev/stdin zroot/mailserver
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
systemd.services = {
|
||||||
|
dovecot2 = {
|
||||||
|
unitConfig.RequiresMountsFor = [
|
||||||
|
"/var/mailserver/vmail"
|
||||||
|
"/var/mailserver/indices"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
khscodes.infrastructure.nixos-install.preScript = ''
|
||||||
|
encryption_key="$(bao kv get -mount=opentofu -field=MX_KAARESKOVGAARD_NET_ZROOT_ENCRYPTION_KEY mx.kaareskovgaard.net)"
|
||||||
|
tmpfile="$(mktemp)"
|
||||||
|
touch "$tmpfile"
|
||||||
|
chmod 0600 "$tmpfile"
|
||||||
|
trap "rm -f $tmpfile" EXIT
|
||||||
|
echo "$encryption_key" > "$tmpfile"
|
||||||
|
INSTALL_ARGS+=("--disk-encryption-keys")
|
||||||
|
INSTALL_ARGS+=("/run/secret/zroot.key")
|
||||||
|
INSTALL_ARGS+=("$tmpfile")
|
||||||
|
'';
|
||||||
|
systemd.services.zfs-download-zroot-key = {
|
||||||
|
after = [
|
||||||
|
"network-online.target"
|
||||||
|
"zfs-import-zroot.service"
|
||||||
|
"read-vault-auth-from-userdata.service"
|
||||||
|
];
|
||||||
|
wants = [
|
||||||
|
"network-online.target"
|
||||||
|
"zfs-import-zroot.service"
|
||||||
|
"read-vault-auth-from-userdata.service"
|
||||||
|
];
|
||||||
|
wantedBy = [ "zfs-mount.target" ];
|
||||||
|
before = [ "zfs-mount.target" ];
|
||||||
|
environment = {
|
||||||
|
BAO_ADDR = config.khscodes.services.vault-agent.vault.address;
|
||||||
|
};
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
ExecStart = lib.getExe downloadZrootKey;
|
||||||
|
};
|
||||||
|
};
|
||||||
khscodes.services.vault-agent.templates = [
|
khscodes.services.vault-agent.templates = [
|
||||||
{
|
{
|
||||||
contents = ''
|
contents = ''
|
||||||
{{- with pkiCert "mx.kaareskovgaard.net/data/zroot_encryption" -}}
|
{{- with secret "opentofu/data/mx.kaareskovgaard.net" -}}
|
||||||
{{ .Data.data.key }}
|
{{ .Data.data.MX_KAARESKOVGAARD_NET_ZROOT_ENCRYPTION_KEY }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
'';
|
'';
|
||||||
destination = zrootKey;
|
destination = zrootKey;
|
||||||
owner = "root";
|
owner = "root";
|
||||||
group = "root";
|
group = "root";
|
||||||
perms = "0600";
|
perms = "0600";
|
||||||
exec = lib.getExe zfsLoadKeyScript;
|
exec = ''
|
||||||
|
chown ${lib.escapeShellArg vmailUser}:${lib.escapeShellArg vmailGroup} /var/mailserver/vmail
|
||||||
|
chmod 2770 /var/mailserver/vmail
|
||||||
|
chown ${lib.escapeShellArg vmailUser}:${lib.escapeShellArg vmailGroup} /var/mailserver/indices
|
||||||
|
chmod 0700 /var/mailserver/indices
|
||||||
|
'';
|
||||||
restartUnits = [
|
restartUnits = [
|
||||||
|
"zfs-mount.service"
|
||||||
"postfix.service"
|
"postfix.service"
|
||||||
"dovecot2.service"
|
"dovecot2.service"
|
||||||
"rspamd.service"
|
"rspamd.service"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
khscodes.infrastructure.provisioning.pre.modules = [
|
mailserver.mailDirectory = "/var/mailserver/vmail";
|
||||||
{
|
mailserver.indexDir = "/var/mailserver/indices";
|
||||||
resource.random_password.zroot_encryption_key = {
|
|
||||||
length = 48;
|
|
||||||
numeric = true;
|
|
||||||
lower = true;
|
|
||||||
upper = true;
|
|
||||||
special = false;
|
|
||||||
};
|
|
||||||
resource.vault_kv_secret_v2.test = {
|
|
||||||
mount = "mx.kaareskovgaard.net";
|
|
||||||
name = "zroot_encryption";
|
|
||||||
data_json = ''
|
|
||||||
{
|
|
||||||
"key": ''${ jsonencode(resource.random_password.zroot_encryption_key.result) }
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
khscodes.infrastructure.vault-server-approle.policy = {
|
khscodes.infrastructure.vault-server-approle.policy = {
|
||||||
"mx.kaareskovgaard.net/data/zroot_encryption" = {
|
"opentofu/data/mx.kaareskovgaard.net" = {
|
||||||
capabilities = [ "read" ];
|
capabilities = [ "read" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
disko.devices.disk = {
|
networking.hostId = "9af535e4";
|
||||||
|
disko.devices = {
|
||||||
|
disk = {
|
||||||
"${diskName}" = {
|
"${diskName}" = {
|
||||||
device = "/dev/sda";
|
device = nixosDisk;
|
||||||
type = "disk";
|
type = "disk";
|
||||||
content = {
|
content = {
|
||||||
type = "gpt";
|
type = "gpt";
|
||||||
|
@ -91,8 +151,8 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
zroot1 = {
|
zroot-disk1 = {
|
||||||
device = "/dev/sdb";
|
device = zrootDisk1Disk;
|
||||||
type = "disk";
|
type = "disk";
|
||||||
content = {
|
content = {
|
||||||
type = "gpt";
|
type = "gpt";
|
||||||
|
@ -108,7 +168,7 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
devices.lvm_vg = {
|
lvm_vg = {
|
||||||
"${volumeGroupName}" = {
|
"${volumeGroupName}" = {
|
||||||
type = "lvm_vg";
|
type = "lvm_vg";
|
||||||
lvs = {
|
lvs = {
|
||||||
|
@ -141,7 +201,7 @@ in
|
||||||
options = {
|
options = {
|
||||||
encryption = "aes-256-gcm";
|
encryption = "aes-256-gcm";
|
||||||
keyformat = "passphrase";
|
keyformat = "passphrase";
|
||||||
keylocation = "file:///tmp/tempkey";
|
keylocation = "file:///run/secret/zroot.key";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"mailserver/vmail" = {
|
"mailserver/vmail" = {
|
||||||
|
@ -158,11 +218,12 @@ in
|
||||||
type = "topology";
|
type = "topology";
|
||||||
vdev = [
|
vdev = [
|
||||||
{
|
{
|
||||||
members = [ "zroot1" ];
|
members = [ "zroot-disk1" ];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,11 @@ in
|
||||||
fqdn = config.khscodes.networking.fqdn;
|
fqdn = config.khscodes.networking.fqdn;
|
||||||
useUTF8FolderNames = true;
|
useUTF8FolderNames = true;
|
||||||
certificateScheme = "acme";
|
certificateScheme = "acme";
|
||||||
|
fullTextSearch = {
|
||||||
|
enable = true;
|
||||||
|
autoIndex = true;
|
||||||
|
enforced = "body";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
services.fail2ban.jails = {
|
services.fail2ban.jails = {
|
||||||
postfix = {
|
postfix = {
|
||||||
|
|
|
@ -54,6 +54,8 @@ pub enum Endpoint {
|
||||||
Unifi,
|
Unifi,
|
||||||
#[value(name = "vault")]
|
#[value(name = "vault")]
|
||||||
Vault,
|
Vault,
|
||||||
|
#[value(name = "mx.kaareskovgaard.net")]
|
||||||
|
MxKaareSkovgaardNet,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Endpoint {
|
impl Endpoint {
|
||||||
|
@ -83,6 +85,10 @@ impl Endpoint {
|
||||||
let data = VaultData::read_from_bao()?;
|
let data = VaultData::read_from_bao()?;
|
||||||
Ok(data.into())
|
Ok(data.into())
|
||||||
}
|
}
|
||||||
|
Self::MxKaareSkovgaardNet => {
|
||||||
|
let data = MxKaareSkovgaardNetData::read_from_bao()?;
|
||||||
|
Ok(data.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,6 +153,13 @@ entry_definition!(
|
||||||
);
|
);
|
||||||
entry_definition!(VaultDataConfig, VaultData, "vault", &["VAULT_TOKEN"]);
|
entry_definition!(VaultDataConfig, VaultData, "vault", &["VAULT_TOKEN"]);
|
||||||
|
|
||||||
|
entry_definition!(
|
||||||
|
MxKaareSkovgaardNetDataConfig,
|
||||||
|
MxKaareSkovgaardNetData,
|
||||||
|
"mx.kaareskovgaard.net",
|
||||||
|
&["MX_KAARESKOVGAARD_NET_ZROOT_ENCRYPTION_KEY"]
|
||||||
|
);
|
||||||
|
|
||||||
fn transfer() -> anyhow::Result<()> {
|
fn transfer() -> anyhow::Result<()> {
|
||||||
let openstack = OpenstackData::try_new_from_env()?;
|
let openstack = OpenstackData::try_new_from_env()?;
|
||||||
let cloudflare = CloudflareData::try_new_from_env()?;
|
let cloudflare = CloudflareData::try_new_from_env()?;
|
||||||
|
@ -154,6 +167,7 @@ fn transfer() -> anyhow::Result<()> {
|
||||||
let hcloud = HcloudData::try_new_from_env()?;
|
let hcloud = HcloudData::try_new_from_env()?;
|
||||||
let unifi = UnifiData::try_new_from_env()?;
|
let unifi = UnifiData::try_new_from_env()?;
|
||||||
let vault = VaultData::try_new_from_env()?;
|
let vault = VaultData::try_new_from_env()?;
|
||||||
|
let mx_kaareskovgaard_net = MxKaareSkovgaardNetData::try_new_from_env()?;
|
||||||
|
|
||||||
write_kv_data(openstack)?;
|
write_kv_data(openstack)?;
|
||||||
write_kv_data(cloudflare)?;
|
write_kv_data(cloudflare)?;
|
||||||
|
@ -161,6 +175,7 @@ fn transfer() -> anyhow::Result<()> {
|
||||||
write_kv_data(hcloud)?;
|
write_kv_data(hcloud)?;
|
||||||
write_kv_data(unifi)?;
|
write_kv_data(unifi)?;
|
||||||
write_kv_data(vault)?;
|
write_kv_data(vault)?;
|
||||||
|
write_kv_data(mx_kaareskovgaard_net)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue