mirror of
https://github.com/nix-community/nixos-anywhere.git
synced 2024-10-26 16:30:39 +03:00
Merge pull request #17 from numtide/reboot
This commit is contained in:
commit
b79af208e9
48
README.md
48
README.md
@ -28,3 +28,51 @@ Afterwards you can just run:
|
||||
|
||||
The parameter passed to `--flake` should point to your nixos configuration
|
||||
exposed in your flake (`nixosConfigurations.your-system` in the example above).
|
||||
|
||||
`nixos-remote --help`
|
||||
``` shell
|
||||
Usage: nixos-remote [options] ssh-host
|
||||
|
||||
Options:
|
||||
|
||||
* -f, --flake flake
|
||||
set the flake to install the system from
|
||||
* -s, --store-paths
|
||||
set the store paths to the disko-script and nixos-system directly
|
||||
if this is give, flake is not needed
|
||||
* --no-ssh-copy
|
||||
skip copying ssh-keys to target system
|
||||
* --kexec url
|
||||
use another kexec tarball to bootstrap NixOS
|
||||
* --stop-after-disko
|
||||
exit after disko formating, you can then proceed to install manually or some other way
|
||||
* --no-reboot
|
||||
do not reboot after installation
|
||||
* --extra-files files
|
||||
files to copy into the new nixos installation
|
||||
* --debug
|
||||
enable debug output
|
||||
```
|
||||
|
||||
## Using your own kexec image
|
||||
|
||||
By default `nixos-remote` will download the kexec image from [here](https://github.com/nix-community/nixos-images#kexec-tarballs).
|
||||
It is also possible to provide your own by providing a file to `--kexec`. The image will than uploaded prior to executing.
|
||||
|
||||
``` shell
|
||||
nixos-remote \
|
||||
--kexec "$(nix build --print-out-paths github:nix-community/nixos-images#packages.x86_64-linux.kexec-installer-nixos-unstable)/nixos-kexec-installer-x86_64-linux.tar.gz" \
|
||||
--flake 'github:your-user/your-repo#your-system' \
|
||||
root@yourip
|
||||
```
|
||||
|
||||
`--kexec` can be useful for example for aarch64-linux, where there is no
|
||||
pre-build image. The following example assumes that your local machine can
|
||||
build for aarch64-linux either natively or through a remote builder
|
||||
|
||||
``` shell
|
||||
nixos-remote \
|
||||
--kexec "$(nix build --print-out-paths github:nix-community/nixos-images#packages.aarch64-linux.kexec-installer-nixos-unstable)/nixos-kexec-installer-aarch64-linux.tar.gz" \
|
||||
--flake 'your-flake#your-system' \
|
||||
root@yourip
|
||||
```
|
||||
|
14
flake.nix
14
flake.nix
@ -29,15 +29,15 @@
|
||||
checks.x86_64-linux =
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
inputs = {
|
||||
inherit pkgs;
|
||||
inherit (disko.nixosModules) disko;
|
||||
kexec-installer = "${nixos-images.packages.${pkgs.system}.kexec-installer-nixos-unstable}/nixos-kexec-installer-${pkgs.stdenv.hostPlatform.system}.tar.gz";
|
||||
};
|
||||
in
|
||||
{
|
||||
from-nixos = import ./tests/from-nixos.nix {
|
||||
inherit pkgs;
|
||||
disko = disko.nixosModules.disko;
|
||||
kexec-installer = "${nixos-images.packages.${pkgs.system}.kexec-installer-nixos-unstable}/nixos-kexec-installer-${pkgs.stdenv.hostPlatform.system}.tar.gz";
|
||||
makeTest = import (pkgs.path + "/nixos/tests/make-test-python.nix");
|
||||
eval-config = import (pkgs.path + "/nixos/lib/eval-config.nix");
|
||||
};
|
||||
from-nixos = import ./tests/from-nixos.nix inputs;
|
||||
from-nixos-with-sudo = import ./tests/from-nixos-with-sudo.nix inputs;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
140
nixos-remote
140
nixos-remote
@ -18,6 +18,8 @@ Options:
|
||||
use another kexec tarball to bootstrap NixOS
|
||||
* --stop-after-disko
|
||||
exit after disko formating, you can then proceed to install manually or some other way
|
||||
* --no-reboot
|
||||
do not reboot after installation
|
||||
* --extra-files files
|
||||
files to copy into the new nixos installation
|
||||
* --debug
|
||||
@ -31,7 +33,8 @@ abort() {
|
||||
}
|
||||
|
||||
kexec_url=https://github.com/nix-community/nixos-images/releases/download/nixos-22.11/nixos-kexec-installer-x86_64-linux.tar.gz
|
||||
debug=n
|
||||
enable_debug=""
|
||||
maybereboot="reboot"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@ -40,8 +43,8 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
;;
|
||||
-s | --store-paths)
|
||||
disko_script=$2
|
||||
nixos_system=$3
|
||||
disko_script=$(readlink -f "$2")
|
||||
nixos_system=$(readlink -f "$3")
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
@ -57,7 +60,7 @@ while [[ $# -gt 0 ]]; do
|
||||
no_ssh_copy=y
|
||||
;;
|
||||
--debug)
|
||||
debug=y
|
||||
enable_debug="-x"
|
||||
set -x
|
||||
;;
|
||||
--extra-files)
|
||||
@ -67,6 +70,9 @@ while [[ $# -gt 0 ]]; do
|
||||
--stop-after-disko)
|
||||
stop_after_disko=y
|
||||
;;
|
||||
--no-reboot)
|
||||
maybereboot=""
|
||||
;;
|
||||
*)
|
||||
if [[ -z ${ssh_connection:-} ]]; then
|
||||
ssh_connection="$1"
|
||||
@ -126,75 +132,73 @@ else
|
||||
abort "flake must be set"
|
||||
fi
|
||||
|
||||
|
||||
# wait for machine to become reachable (possibly forever)
|
||||
if [[ ${no_ssh_copy-n} != "y" ]]; then
|
||||
until ssh-copy-id -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "$ssh_connection"; do sleep 5; done
|
||||
else
|
||||
until ssh_ -o ConnectTimeout=10 -- exit 0; do sleep 5; done
|
||||
fi
|
||||
|
||||
# first check if the remote system is kexec booted
|
||||
if ssh_ -- test -e /etc/is_kexec; then
|
||||
is_kexec=y
|
||||
fi
|
||||
|
||||
if [[ ${is_kexec-n} != "y" ]]; then
|
||||
# TODO we probably need an architecture detection here
|
||||
ssh_ << SSH
|
||||
set -efu
|
||||
if [ "$debug" = "y" ]; then
|
||||
set -x
|
||||
fi
|
||||
os=\$(uname)
|
||||
if [[ "\$os" != "Linux" ]]; then
|
||||
echo "This script requires Linux as the operating system, but got \${os}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v tar >/dev/null 2>&1; then
|
||||
echo "no tar command found, but required to unpack kexec tarball" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf /root/kexec
|
||||
mkdir -p /root/kexec
|
||||
SSH
|
||||
|
||||
if [[ -f "$kexec_url" ]]; then
|
||||
ssh_ 'tar -C /root/kexec -xvzf-' < "$kexec_url"
|
||||
else
|
||||
ssh_ << SSH
|
||||
set -eu -o pipefail
|
||||
if [ "$debug" = "y" ]; then
|
||||
set -x
|
||||
fi
|
||||
fetch(){
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl --fail -Ss -L "\$1"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget "\$1" -O-
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
curl --fail -Ss -L "\$1"
|
||||
else
|
||||
echo "no downloader (curl or wget) found, bailing out" >&2
|
||||
exit 1
|
||||
fi
|
||||
# TODO we probably need an architecture detection here
|
||||
# TODO if we have specified a user here but we are already booted into the
|
||||
# installer, than the user might not work anymore
|
||||
until facts=$(ssh_ -o ConnectTimeout=10 -- <<SSH
|
||||
set -efu ${enable_debug}
|
||||
has(){
|
||||
command -v tar >/dev/null && echo "y" || echo "n"
|
||||
}
|
||||
fetch "$kexec_url" | tar -C /root/kexec -xvzf-
|
||||
cat <<FACTS
|
||||
is_os=\$(uname)
|
||||
is_kexec=\$(if test -f /etc/is_kexec; then echo "y"; else echo "n"; fi)
|
||||
has_tar=\$(has tar)
|
||||
has_sudo=\$(has sudo)
|
||||
has_wget=\$(has wget)
|
||||
has_curl=\$(has curl)
|
||||
FACTS
|
||||
SSH
|
||||
); do
|
||||
sleep 5
|
||||
done
|
||||
# make facts available in script
|
||||
# shellcheck disable=SC2046
|
||||
export $(echo "$facts" | xargs | grep -E '^(has|is)_[a-z0-9_]+=\S+')
|
||||
|
||||
if [[ ${has_tar-n} == "n" ]]; then
|
||||
abort "no tar command found, but required to unpack kexec tarball"
|
||||
fi
|
||||
maybesudo=""
|
||||
if [[ ${has_sudo-n} == "y" ]]; then
|
||||
maybesudo="sudo"
|
||||
fi
|
||||
if [[ ${is_os-n} != "Linux" ]]; then
|
||||
abort "This script requires Linux as the operating system, but got $is_os"
|
||||
fi
|
||||
|
||||
ssh_ << SSH
|
||||
export TMPDIR=/root/kexec
|
||||
setsid /root/kexec/kexec/run
|
||||
if [[ ${is_kexec-n} != "y" ]] && [[ ${no_ssh_copy-n} != "y" ]]; then
|
||||
ssh-copy-id -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "$ssh_connection"
|
||||
fi
|
||||
|
||||
if [[ ${is_kexec-n} == "n" ]]; then
|
||||
ssh_ << SSH
|
||||
set -efu ${enable_debug}
|
||||
"${maybesudo}" rm -rf /root/kexec
|
||||
"${maybesudo}" mkdir -p /root/kexec
|
||||
SSH
|
||||
|
||||
if [[ -f "$kexec_url" ]]; then
|
||||
ssh_ "${maybesudo} tar -C /root/kexec -xvzf-" < "$kexec_url"
|
||||
elif [[ ${has_curl-n} == "y" ]]; then
|
||||
ssh_ "curl --fail -Ss -L '${kexec_url}' | ${maybesudo} tar -C /root/kexec -xvzf-"
|
||||
elif [[ ${has_wget-n} == "y" ]]; then
|
||||
ssh_ "wget '${kexec_url}' -O- | ${maybesudo} tar -C /root/kexec -xvzf-"
|
||||
else
|
||||
curl --fail -Ss -L "${kexec_url}" | ssh_ "${maybesudo} tar -C /root/kexec -xvzf-"
|
||||
fi
|
||||
|
||||
ssh_ << SSH
|
||||
TMPDIR=/root/kexec setsid ${maybesudo} /root/kexec/kexec/run
|
||||
SSH
|
||||
|
||||
# wait for machine to become unreachable
|
||||
while timeout_ssh_ -- exit 0; do sleep 1; done
|
||||
|
||||
# After kexec we explicitly set the user to root@
|
||||
ssh_connection="root@${ssh_connection#*@}"
|
||||
|
||||
# watiting for machine to become available again
|
||||
until ssh_ -o ConnectTimeout=10 -- exit 0; do sleep 5; done
|
||||
fi
|
||||
@ -213,11 +217,9 @@ if [[ -n ${extra_files:-} ]]; then
|
||||
fi
|
||||
rsync -vrlF -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" "$extra_files" "${ssh_connection}:/mnt/"
|
||||
fi
|
||||
ssh_ << SSH
|
||||
set -efu
|
||||
if [ "$debug" = "y" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
ssh_ <<SSH
|
||||
set -efu ${enable_debug}
|
||||
nixos-install --no-root-passwd --no-channel-copy --system "$nixos_system"
|
||||
reboot
|
||||
${maybereboot}
|
||||
SSH
|
||||
|
@ -7,6 +7,7 @@
|
||||
, coreutils
|
||||
, shellcheck
|
||||
, rsync
|
||||
, curl
|
||||
}:
|
||||
let
|
||||
runtimeDeps = [
|
||||
@ -15,6 +16,7 @@ let
|
||||
rsync
|
||||
nix
|
||||
coreutils
|
||||
curl # when uploading tarballs
|
||||
];
|
||||
in
|
||||
stdenvNoCC.mkDerivation {
|
||||
|
21
tests/from-nixos-with-sudo.nix
Normal file
21
tests/from-nixos-with-sudo.nix
Normal file
@ -0,0 +1,21 @@
|
||||
(import ./lib/test-base.nix) {
|
||||
name = "nixos-remote";
|
||||
nodes = {
|
||||
installer = ./modules/installer.nix;
|
||||
installed = ./modules/installed.nix;
|
||||
};
|
||||
testScript = ''
|
||||
start_all()
|
||||
installer.succeed("""
|
||||
eval $(ssh-agent)
|
||||
ssh-add /etc/sshKey
|
||||
${../nixos-remote} \
|
||||
--no-ssh-copy-id \
|
||||
--debug \
|
||||
--kexec /etc/nixos-remote/kexec-installer \
|
||||
--stop-after-disko \
|
||||
--store-paths /etc/nixos-remote/disko /etc/nixos-remote/system-to-install \
|
||||
nixos@installed >&2
|
||||
""")
|
||||
'';
|
||||
}
|
@ -1,97 +1,8 @@
|
||||
{ pkgs ? import <nixpkgs> {}
|
||||
, makeTest ? import <nixpkgs/nixos/tests/make-test-python.nix>
|
||||
, eval-config ? import <nixpkgs/nixos/lib/eval-config.nix>
|
||||
, disko ? "${builtins.fetchTarball "https://github.com/nix-community/disko/archive/master.tar.gz"}/module.nix"
|
||||
, kexec-installer ? builtins.fetchurl "https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-kexec-installer-${pkgs.stdenv.hostPlatform.system}.tar.gz"
|
||||
, ... }:
|
||||
let
|
||||
systemToInstall = { modulesPath, ... }: {
|
||||
imports = [
|
||||
disko
|
||||
(modulesPath + "/testing/test-instrumentation.nix")
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
(modulesPath + "/profiles/minimal.nix")
|
||||
];
|
||||
networking.hostName = "nixos-remote";
|
||||
documentation.enable = false;
|
||||
hardware.enableAllFirmware = false;
|
||||
networking.hostId = "8425e349"; # from profiles/base.nix, needed for zfs
|
||||
boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms
|
||||
boot.loader.grub.devices = [ "/dev/vda" ];
|
||||
disko.devices = {
|
||||
disk = {
|
||||
vda = {
|
||||
device = "/dev/vda";
|
||||
type = "disk";
|
||||
content = {
|
||||
type = "table";
|
||||
format = "gpt";
|
||||
partitions = [
|
||||
{
|
||||
name = "boot";
|
||||
type = "partition";
|
||||
start = "0";
|
||||
end = "1M";
|
||||
part-type = "primary";
|
||||
flags = ["bios_grub"];
|
||||
}
|
||||
{
|
||||
type = "partition";
|
||||
name = "ESP";
|
||||
start = "1MiB";
|
||||
end = "100MiB";
|
||||
bootable = true;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "root";
|
||||
type = "partition";
|
||||
start = "100MiB";
|
||||
end = "100%";
|
||||
part-type = "primary";
|
||||
bootable = true;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
evaledSystem = eval-config {
|
||||
modules = [ systemToInstall ];
|
||||
system = "x86_64-linux";
|
||||
};
|
||||
in
|
||||
makeTest {
|
||||
(import ./lib/test-base.nix) {
|
||||
name = "nixos-remote";
|
||||
nodes = {
|
||||
installer = {
|
||||
documentation.enable = false;
|
||||
environment.etc.sshKey = {
|
||||
source = ./ssh-keys/ssh;
|
||||
mode = "0600";
|
||||
};
|
||||
programs.ssh.startAgent = true;
|
||||
system.extraDependencies = [
|
||||
evaledSystem.config.system.build.disko
|
||||
evaledSystem.config.system.build.toplevel
|
||||
];
|
||||
};
|
||||
installed = {
|
||||
virtualisation.memorySize = 4096;
|
||||
documentation.enable = false;
|
||||
services.openssh.enable = true;
|
||||
users.users.root.openssh.authorizedKeys.keyFiles = [ ./ssh-keys/ssh.pub ];
|
||||
};
|
||||
installer = ./modules/installer.nix;
|
||||
installed = ./modules/installed.nix;
|
||||
};
|
||||
testScript = ''
|
||||
def create_test_machine(oldmachine=None, args={}): # taken from <nixpkgs/nixos/tests/installer.nix>
|
||||
@ -103,9 +14,7 @@ makeTest {
|
||||
} | args)
|
||||
driver.machines.append(machine)
|
||||
return machine
|
||||
|
||||
start_all()
|
||||
installed.wait_for_unit("sshd.service")
|
||||
installer.succeed("mkdir -p /tmp/extra-files/var/lib/secrets")
|
||||
installer.succeed("echo value > /tmp/extra-files/var/lib/secrets/key")
|
||||
installer.succeed("""
|
||||
@ -114,9 +23,9 @@ makeTest {
|
||||
${../nixos-remote} \
|
||||
--no-ssh-copy-id \
|
||||
--debug \
|
||||
--kexec ${kexec-installer} \
|
||||
--kexec /etc/nixos-remote/kexec-installer \
|
||||
--extra-files /tmp/extra-files \
|
||||
--store-paths ${toString evaledSystem.config.system.build.disko} ${toString evaledSystem.config.system.build.toplevel} \
|
||||
--store-paths /etc/nixos-remote/disko /etc/nixos-remote/system-to-install \
|
||||
root@installed >&2
|
||||
""")
|
||||
installed.shutdown()
|
||||
@ -127,7 +36,4 @@ makeTest {
|
||||
content = new_machine.succeed("cat /var/lib/secrets/key").strip()
|
||||
assert "value" == content, f"secret does not have expected value: {content}"
|
||||
'';
|
||||
} {
|
||||
pkgs = pkgs;
|
||||
system = pkgs.system;
|
||||
}
|
||||
|
14
tests/lib/test-base.nix
Normal file
14
tests/lib/test-base.nix
Normal file
@ -0,0 +1,14 @@
|
||||
test:
|
||||
{ pkgs ? import <nixpkgs> {}, ... } @ args:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
nixos-lib = import (pkgs.path + "/nixos/lib") {};
|
||||
in
|
||||
(nixos-lib.runTest {
|
||||
hostPkgs = pkgs;
|
||||
# speed-up evaluation
|
||||
defaults.documentation.enable = lib.mkDefault false;
|
||||
# to accept external dependencies such as disko
|
||||
node.specialArgs.inputs = args;
|
||||
imports = [ test ];
|
||||
}).config.result
|
12
tests/modules/installed.nix
Normal file
12
tests/modules/installed.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
virtualisation.memorySize = 4096;
|
||||
services.openssh.enable = true;
|
||||
users.users.root.openssh.authorizedKeys.keyFiles = [ ./ssh-keys/ssh.pub ];
|
||||
users.users.nixos = {
|
||||
isNormalUser = true;
|
||||
openssh.authorizedKeys.keyFiles = [ ./ssh-keys/ssh.pub ];
|
||||
extraGroups = [ "wheel" ];
|
||||
};
|
||||
security.sudo.enable = true;
|
||||
security.sudo.wheelNeedsPassword = false;
|
||||
}
|
19
tests/modules/installer.nix
Normal file
19
tests/modules/installer.nix
Normal file
@ -0,0 +1,19 @@
|
||||
{ config, lib, pkgs, inputs, ... }:
|
||||
let
|
||||
disko = inputs.disko; #or "${builtins.fetchTarball "https://github.com/nix-community/disko/archive/master.tar.gz"}/module.nix";
|
||||
kexec-installer = inputs.kexec-installer; # or builtins.fetchurl "https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-kexec-installer-${pkgs.stdenv.hostPlatform.system}.tar.gz";
|
||||
system-to-install = pkgs.nixos [
|
||||
./system-to-install.nix
|
||||
disko
|
||||
];
|
||||
in
|
||||
{
|
||||
programs.ssh.startAgent = true;
|
||||
|
||||
environment.etc = {
|
||||
sshKey = { source = ./ssh-keys/ssh; mode = "0600"; };
|
||||
"nixos-remote/disko".source = system-to-install.config.system.build.disko;
|
||||
"nixos-remote/system-to-install".source = system-to-install.config.system.build.toplevel;
|
||||
"nixos-remote/kexec-installer".source = kexec-installer;
|
||||
};
|
||||
}
|
60
tests/modules/system-to-install.nix
Normal file
60
tests/modules/system-to-install.nix
Normal file
@ -0,0 +1,60 @@
|
||||
{modulesPath, self, ...}: {
|
||||
imports = [
|
||||
(modulesPath + "/testing/test-instrumentation.nix")
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
(modulesPath + "/profiles/minimal.nix")
|
||||
];
|
||||
networking.hostName = "nixos-remote";
|
||||
documentation.enable = false;
|
||||
hardware.enableAllFirmware = false;
|
||||
networking.hostId = "8425e349"; # from profiles/base.nix, needed for zfs
|
||||
boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms
|
||||
boot.loader.grub.devices = ["/dev/vda"];
|
||||
disko.devices = {
|
||||
disk = {
|
||||
vda = {
|
||||
device = "/dev/vda";
|
||||
type = "disk";
|
||||
content = {
|
||||
type = "table";
|
||||
format = "gpt";
|
||||
partitions = [
|
||||
{
|
||||
name = "boot";
|
||||
type = "partition";
|
||||
start = "0";
|
||||
end = "1M";
|
||||
part-type = "primary";
|
||||
flags = ["bios_grub"];
|
||||
}
|
||||
{
|
||||
type = "partition";
|
||||
name = "ESP";
|
||||
start = "1MiB";
|
||||
end = "100MiB";
|
||||
bootable = true;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "root";
|
||||
type = "partition";
|
||||
start = "100MiB";
|
||||
end = "100%";
|
||||
part-type = "primary";
|
||||
bootable = true;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user