Merge pull request 'clanCore: init machine_id_v3' (#2085) from Qubasa/clan-core:machine_id_v3 into main
This commit is contained in:
3
clanModules/disk-id/README.md
Normal file
3
clanModules/disk-id/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
description = "Generates a uuid for use in disk device naming"
|
||||
---
|
||||
26
clanModules/disk-id/default.nix
Normal file
26
clanModules/disk-id/default.nix
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
{
|
||||
|
||||
config = {
|
||||
clan.core.vars.generators.disk-id = {
|
||||
files.diskId.secret = false;
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.bash
|
||||
];
|
||||
script = ''
|
||||
uuid=$(bash ${../uuid4.sh})
|
||||
|
||||
# Remove the hyphens from the UUID
|
||||
uuid_no_hyphens=$(echo -n "$uuid" | tr -d '-')
|
||||
|
||||
echo -n "$uuid_no_hyphens" > "$out/diskId"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
1
clanModules/disk-id/roles/default.nix
Normal file
1
clanModules/disk-id/roles/default.nix
Normal file
@@ -0,0 +1 @@
|
||||
{ }
|
||||
@@ -5,6 +5,8 @@
|
||||
borgbackup = ./borgbackup;
|
||||
borgbackup-static = ./borgbackup-static;
|
||||
deltachat = ./deltachat;
|
||||
machine-id = ./machine-id;
|
||||
disk-id = ./disk-id;
|
||||
dyndns = ./dyndns;
|
||||
ergochat = ./ergochat;
|
||||
garage = ./garage;
|
||||
|
||||
3
clanModules/machine-id/README.md
Normal file
3
clanModules/machine-id/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
description = "Sets the /etc/machine-id and exposes it as a nix option"
|
||||
---
|
||||
45
clanModules/machine-id/default.nix
Normal file
45
clanModules/machine-id/default.nix
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
var = config.clan.core.vars.generators.machine-id.files.machineId or { };
|
||||
in
|
||||
{
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf ((var.machineId.value or null) != null) {
|
||||
assertions = [
|
||||
{
|
||||
assertion = lib.stringLength var.machineId.value == 32;
|
||||
message = "machineId must be exactly 32 characters long.";
|
||||
}
|
||||
];
|
||||
boot.kernelParams = [
|
||||
''systemd.machine_id=${var.machineId.value}''
|
||||
];
|
||||
environment.etc."machine-id" = {
|
||||
text = var.machineId.value;
|
||||
};
|
||||
})
|
||||
{
|
||||
clan.core.vars.generators.machine-id = {
|
||||
files.machineId.secret = false;
|
||||
runtimeInputs = [
|
||||
pkgs.coreutils
|
||||
pkgs.bash
|
||||
];
|
||||
script = ''
|
||||
uuid=$(bash ${../uuid4.sh})
|
||||
|
||||
# Remove the hyphens from the UUID
|
||||
uuid_no_hyphens=$(echo -n "$uuid" | tr -d '-')
|
||||
|
||||
echo -n "$uuid_no_hyphens" > "$out/machineId"
|
||||
'';
|
||||
};
|
||||
}
|
||||
];
|
||||
}
|
||||
1
clanModules/machine-id/roles/default.nix
Normal file
1
clanModules/machine-id/roles/default.nix
Normal file
@@ -0,0 +1 @@
|
||||
{ }
|
||||
20
clanModules/uuid4.sh
Normal file
20
clanModules/uuid4.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Read 16 bytes from /dev/urandom
|
||||
uuid=$(dd if=/dev/urandom bs=1 count=16 2>/dev/null | od -An -tx1 | tr -d ' \n')
|
||||
|
||||
# Break the UUID into pieces and apply the required modifications
|
||||
byte6=${uuid:12:2}
|
||||
byte8=${uuid:16:2}
|
||||
|
||||
# Construct the correct version and variant
|
||||
hex_byte6=$(printf "%x" $((0x$byte6 & 0x0F | 0x40)))
|
||||
hex_byte8=$(printf "%x" $((0x$byte8 & 0x3F | 0x80)))
|
||||
|
||||
# Rebuild the UUID with the correct fields
|
||||
uuid_v4="${uuid:0:12}${hex_byte6}${uuid:14:2}${hex_byte8}${uuid:18:14}"
|
||||
|
||||
# Format the UUID correctly 8-4-4-4-12
|
||||
uuid_formatted="${uuid_v4:0:8}-${uuid_v4:8:4}-${uuid_v4:12:4}-${uuid_v4:16:4}-${uuid_v4:20:12}"
|
||||
|
||||
echo -n "$uuid_formatted"
|
||||
@@ -60,6 +60,7 @@ nav:
|
||||
- reference/clanModules/borgbackup.md
|
||||
- reference/clanModules/deltachat.md
|
||||
- reference/clanModules/dyndns.md
|
||||
- reference/clanModules/disk-id.md
|
||||
- reference/clanModules/ergochat.md
|
||||
- reference/clanModules/garage.md
|
||||
- reference/clanModules/golem-provider.md
|
||||
@@ -69,6 +70,7 @@ nav:
|
||||
- reference/clanModules/localbackup.md
|
||||
- reference/clanModules/localsend.md
|
||||
- reference/clanModules/matrix-synapse.md
|
||||
- reference/clanModules/machine-id.md
|
||||
- reference/clanModules/moonlight.md
|
||||
- reference/clanModules/mumble.md
|
||||
- reference/clanModules/nginx.md
|
||||
|
||||
@@ -14,183 +14,202 @@ lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
|
||||
|
||||
=== "**Single Disk**"
|
||||
Below is the configuration for `disko.nix`
|
||||
```nix hl_lines="14 40"
|
||||
{ lib, ... }:
|
||||
let
|
||||
mirrorBoot = idx: {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${idx}";
|
||||
content = {
|
||||
```nix hl_lines="16 47"
|
||||
{ lib, clan-core, ... }:
|
||||
let
|
||||
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
|
||||
mirrorBoot = idx: {
|
||||
# suffix is to prevent disk name collisions
|
||||
name = idx + suffix;
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${idx}";
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
"${config.networking.hostName}-boot" = {
|
||||
size = "1M";
|
||||
type = "EF02"; # for grub MBR
|
||||
priority = 1;
|
||||
"boot" = {
|
||||
size = "1M";
|
||||
type = "EF02"; # for grub MBR
|
||||
priority = 1;
|
||||
};
|
||||
"ESP" = lib.mkIf (idx == "nvme-eui.002538b931b59865") {
|
||||
size = "1G";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "nofail" ];
|
||||
};
|
||||
"${config.networking.hostName}-ESP" = lib.mkIf (idx == "nvme-eui.002538b931b59865") {
|
||||
size = "1G";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "nofail" ];
|
||||
};
|
||||
};
|
||||
"${config.networking.hostName}-root" = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "zfs";
|
||||
pool = "zroot";
|
||||
};
|
||||
};
|
||||
"root" = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "zfs";
|
||||
pool = "zroot";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
clan-core.clanModules.disk-id
|
||||
];
|
||||
|
||||
disko.devices = {
|
||||
disk = {
|
||||
x = mirrorBoot "nvme-eui.002538b931b59865";
|
||||
};
|
||||
zpool = {
|
||||
zroot = {
|
||||
config = {
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
|
||||
disko.devices = {
|
||||
disk = {
|
||||
x = mirrorBoot "nvme-eui.002538b931b59865";
|
||||
};
|
||||
zpool = {
|
||||
zroot = {
|
||||
type = "zpool";
|
||||
rootFsOptions = {
|
||||
compression = "lz4";
|
||||
acltype = "posixacl";
|
||||
xattr = "sa";
|
||||
"com.sun:auto-snapshot" = "true";
|
||||
mountpoint = "none";
|
||||
compression = "lz4";
|
||||
acltype = "posixacl";
|
||||
xattr = "sa";
|
||||
"com.sun:auto-snapshot" = "true";
|
||||
mountpoint = "none";
|
||||
};
|
||||
datasets = {
|
||||
"root" = {
|
||||
type = "zfs_fs";
|
||||
options = {
|
||||
mountpoint = "none";
|
||||
encryption = "aes-256-gcm";
|
||||
keyformat = "passphrase";
|
||||
keylocation = "file:///tmp/secret.key";
|
||||
};
|
||||
"root" = {
|
||||
type = "zfs_fs";
|
||||
options = {
|
||||
mountpoint = "none";
|
||||
encryption = "aes-256-gcm";
|
||||
keyformat = "passphrase";
|
||||
keylocation = "file:///tmp/secret.key";
|
||||
};
|
||||
"root/nixos" = {
|
||||
type = "zfs_fs";
|
||||
options.mountpoint = "/";
|
||||
mountpoint = "/";
|
||||
};
|
||||
"root/home" = {
|
||||
type = "zfs_fs";
|
||||
options.mountpoint = "/home";
|
||||
mountpoint = "/home";
|
||||
};
|
||||
"root/tmp" = {
|
||||
type = "zfs_fs";
|
||||
mountpoint = "/tmp";
|
||||
options = {
|
||||
mountpoint = "/tmp";
|
||||
sync = "disabled";
|
||||
};
|
||||
};
|
||||
"root/nixos" = {
|
||||
type = "zfs_fs";
|
||||
options.mountpoint = "/";
|
||||
mountpoint = "/";
|
||||
};
|
||||
"root/home" = {
|
||||
type = "zfs_fs";
|
||||
options.mountpoint = "/home";
|
||||
mountpoint = "/home";
|
||||
};
|
||||
"root/tmp" = {
|
||||
type = "zfs_fs";
|
||||
mountpoint = "/tmp";
|
||||
options = {
|
||||
mountpoint = "/tmp";
|
||||
sync = "disabled";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
=== "**Raid 1**"
|
||||
Below is the configuration for `disko.nix`
|
||||
```nix hl_lines="14 40 41"
|
||||
{ lib, ... }:
|
||||
let
|
||||
mirrorBoot = idx: {
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${idx}";
|
||||
content = {
|
||||
```nix hl_lines="16 47 48"
|
||||
{ lib, clan-core, ... }:
|
||||
let
|
||||
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
|
||||
mirrorBoot = idx: {
|
||||
# suffix is to prevent disk name collisions
|
||||
name = idx + suffix;
|
||||
type = "disk";
|
||||
device = "/dev/disk/by-id/${idx}";
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
boot = {
|
||||
size = "1M";
|
||||
type = "EF02"; # for grub MBR
|
||||
priority = 1;
|
||||
"boot" = {
|
||||
size = "1M";
|
||||
type = "EF02"; # for grub MBR
|
||||
priority = 1;
|
||||
};
|
||||
"ESP" = lib.mkIf (idx == "nvme-eui.002538b931b59865") {
|
||||
size = "1G";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "nofail" ];
|
||||
};
|
||||
ESP = lib.mkIf (idx == "nvme-eui.002538b931b59865") {
|
||||
size = "1G";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "nofail" ];
|
||||
};
|
||||
};
|
||||
zfs = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "zfs";
|
||||
pool = "zroot";
|
||||
};
|
||||
};
|
||||
"root" = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "zfs";
|
||||
pool = "zroot";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
clan-core.clanModules.disk-id
|
||||
];
|
||||
|
||||
disko.devices = {
|
||||
disk = {
|
||||
x = mirrorBoot "nvme-eui.002538b931b59865";
|
||||
y = mirrorBoot "myOtherDrive"
|
||||
};
|
||||
zpool = {
|
||||
zroot = {
|
||||
config = {
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
|
||||
disko.devices = {
|
||||
disk = {
|
||||
x = mirrorBoot "nvme-eui.002538b931b59865";
|
||||
y = mirrorBoot "my-other-disk";
|
||||
};
|
||||
zpool = {
|
||||
zroot = {
|
||||
type = "zpool";
|
||||
rootFsOptions = {
|
||||
compression = "lz4";
|
||||
acltype = "posixacl";
|
||||
xattr = "sa";
|
||||
"com.sun:auto-snapshot" = "true";
|
||||
mountpoint = "none";
|
||||
compression = "lz4";
|
||||
acltype = "posixacl";
|
||||
xattr = "sa";
|
||||
"com.sun:auto-snapshot" = "true";
|
||||
mountpoint = "none";
|
||||
};
|
||||
datasets = {
|
||||
"root" = {
|
||||
type = "zfs_fs";
|
||||
options = {
|
||||
mountpoint = "none";
|
||||
encryption = "aes-256-gcm";
|
||||
keyformat = "passphrase";
|
||||
keylocation = "file:///tmp/secret.key";
|
||||
};
|
||||
"root" = {
|
||||
type = "zfs_fs";
|
||||
options = {
|
||||
mountpoint = "none";
|
||||
encryption = "aes-256-gcm";
|
||||
keyformat = "passphrase";
|
||||
keylocation = "file:///tmp/secret.key";
|
||||
};
|
||||
"root/nixos" = {
|
||||
type = "zfs_fs";
|
||||
options.mountpoint = "/";
|
||||
mountpoint = "/";
|
||||
};
|
||||
"root/home" = {
|
||||
type = "zfs_fs";
|
||||
options.mountpoint = "/home";
|
||||
mountpoint = "/home";
|
||||
};
|
||||
"root/tmp" = {
|
||||
type = "zfs_fs";
|
||||
mountpoint = "/tmp";
|
||||
options = {
|
||||
mountpoint = "/tmp";
|
||||
sync = "disabled";
|
||||
};
|
||||
};
|
||||
"root/nixos" = {
|
||||
type = "zfs_fs";
|
||||
options.mountpoint = "/";
|
||||
mountpoint = "/";
|
||||
};
|
||||
"root/home" = {
|
||||
type = "zfs_fs";
|
||||
options.mountpoint = "/home";
|
||||
mountpoint = "/home";
|
||||
};
|
||||
"root/tmp" = {
|
||||
type = "zfs_fs";
|
||||
mountpoint = "/tmp";
|
||||
options = {
|
||||
mountpoint = "/tmp";
|
||||
sync = "disabled";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Below is the configuration for `initrd.nix`.
|
||||
|
||||
@@ -25,6 +25,9 @@ let
|
||||
evaled = lib.evalModules {
|
||||
modules = [
|
||||
baseModule
|
||||
{
|
||||
clan.core.clanDir = ./.;
|
||||
}
|
||||
clan-core.nixosModules.clanCore
|
||||
] ++ (map (name: clanModules.${name}) modulenames);
|
||||
};
|
||||
|
||||
@@ -23,27 +23,27 @@ let
|
||||
description = ''
|
||||
List of imported '.nix' files.
|
||||
|
||||
Each filename must be a string and is interpreted relative to the 'directory' passed to buildClan.
|
||||
The import only happens if the machine is part of the service or role.
|
||||
Each filename must be a string and is interpreted relative to the 'directory' passed to buildClan.
|
||||
The import only happens if the machine is part of the service or role.
|
||||
|
||||
## Example
|
||||
## Example
|
||||
|
||||
To import the `special.nix` file
|
||||
To import the `special.nix` file
|
||||
|
||||
```
|
||||
. Clan Directory
|
||||
├── flake.nix
|
||||
...
|
||||
└── modules
|
||||
├── special.nix
|
||||
└── ...
|
||||
```
|
||||
```
|
||||
. Clan Directory
|
||||
├── flake.nix
|
||||
...
|
||||
└── modules
|
||||
├── special.nix
|
||||
└── ...
|
||||
```
|
||||
|
||||
```nix
|
||||
{
|
||||
imports = [ "modules/special.nix" ];
|
||||
}
|
||||
```
|
||||
```nix
|
||||
{
|
||||
imports = [ "modules/special.nix" ];
|
||||
}
|
||||
```
|
||||
|
||||
'';
|
||||
default = [ ];
|
||||
|
||||
4
pkgs/clan-cli/clan_cli/inventory/update.sh
Executable file
4
pkgs/clan-cli/clan_cli/inventory/update.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
jsonSchema=$(nix build .#inventory-schema --print-out-paths)/schema.json
|
||||
nix run .#classgen "$jsonSchema" "$PKG_ROOT/clan_cli/inventory/classes.py"
|
||||
@@ -1,54 +1,54 @@
|
||||
{ self, lib, ... }:
|
||||
{
|
||||
self,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
|
||||
flashInstallerModule =
|
||||
{ config, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./iwd.nix
|
||||
self.nixosModules.installer
|
||||
# Allow to download pre-build binaries from our nix caches
|
||||
self.clanModules.trusted-nix-caches
|
||||
];
|
||||
|
||||
system.stateVersion = config.system.nixos.version;
|
||||
nixpkgs.pkgs = self.inputs.nixpkgs.legacyPackages.x86_64-linux;
|
||||
}
|
||||
// flashDiskoConfig;
|
||||
|
||||
# Important: The partition names need to be different to the clan install
|
||||
flashDiskoConfig = {
|
||||
boot.loader.grub.efiSupport = lib.mkDefault true;
|
||||
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
|
||||
disko.devices = {
|
||||
disk = {
|
||||
main = {
|
||||
type = "disk";
|
||||
device = lib.mkDefault "/dev/null";
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
installer-boot = {
|
||||
size = "1M";
|
||||
type = "EF02"; # for grub MBR
|
||||
priority = 1;
|
||||
};
|
||||
installer-ESP = {
|
||||
size = "512M";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
boot.loader.grub.efiSupport = lib.mkDefault true;
|
||||
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
|
||||
disko.devices = {
|
||||
disk = {
|
||||
"main" = {
|
||||
type = "disk";
|
||||
device = lib.mkDefault "/dev/null";
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
"boot" = {
|
||||
size = "1M";
|
||||
type = "EF02"; # for grub MBR
|
||||
priority = 1;
|
||||
};
|
||||
};
|
||||
installer-root = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/";
|
||||
"ESP" = {
|
||||
size = "512M";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
};
|
||||
};
|
||||
"root" = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -56,7 +56,7 @@ let
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
clan = {
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
{ lib, ... }:
|
||||
{ lib, clan-core, ... }:
|
||||
|
||||
let
|
||||
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
clan-core.clanModules.disk-id
|
||||
];
|
||||
|
||||
boot.loader.grub.efiSupport = lib.mkDefault true;
|
||||
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
|
||||
disko.devices = {
|
||||
disk = {
|
||||
main = {
|
||||
"main" = {
|
||||
# suffix is to prevent disk name collisions
|
||||
name = "main-" + suffix;
|
||||
type = "disk";
|
||||
# Set the following in flake.nix for each maschine:
|
||||
# device = <uuid>;
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
{ lib, ... }:
|
||||
{ lib, clan-core, ... }:
|
||||
|
||||
let
|
||||
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
clan-core.clanModules.disk-id
|
||||
];
|
||||
|
||||
# TO NOT EDIT THIS FILE AFTER INSTALLATION of a machine
|
||||
# Otherwise your system might not boot because of missing partitions / filesystems
|
||||
boot.loader.grub.efiSupport = lib.mkDefault true;
|
||||
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
|
||||
disko.devices = {
|
||||
disk = {
|
||||
main = {
|
||||
"main" = {
|
||||
# suffix is to prevent disk name collisions
|
||||
name = "main-" + suffix;
|
||||
type = "disk";
|
||||
# Set the following in flake.nix for each maschine:
|
||||
# device = <uuid>;
|
||||
|
||||
Reference in New Issue
Block a user