Compare commits

..

14 Commits

12 changed files with 155 additions and 61 deletions

View File

@@ -2,6 +2,7 @@
f011c8d71ba09bd94ab04b8d771858b90a03fbf9 f011c8d71ba09bd94ab04b8d771858b90a03fbf9
3aff25b4486a143cd6282f8845c16216598e1c7e 3aff25b4486a143cd6282f8845c16216598e1c7e
2204b12fadf27886058e6945806ce93a547f5278 2204b12fadf27886058e6945806ce93a547f5278
77236af5896524218605badcd3cdfc2267b213da
# host rename # host rename
cfe4c43887a41e52be4e6472474c0fc3788f86e8 cfe4c43887a41e52be4e6472474c0fc3788f86e8

View File

@@ -22,12 +22,16 @@
]; ];
default = "systemd-boot"; default = "systemd-boot";
}; };
plymouth.enable = lib.mkEnableOption "plymouth boot splash";
}; };
config = lib.mkIf cfg.enable ( config = lib.mkIf cfg.enable (
lib.mkMerge [ lib.mkMerge [
{ {
boot.loader.efi.canTouchEfiVariables = true; boot.loader.efi.canTouchEfiVariables = true;
# request the largest framebuffer uefi offers; plymouth inherits it
boot.loader.systemd-boot.consoleMode = "max";
} }
(lib.mkIf (cfg.mode == "systemd-boot") { (lib.mkIf (cfg.mode == "systemd-boot") {
@@ -41,6 +45,28 @@
pkiBundle = "/var/lib/sbctl"; pkiBundle = "/var/lib/sbctl";
}; };
}) })
(lib.mkIf cfg.plymouth.enable {
# plymouth needs systemd-initrd to render the luks prompt cleanly
boot.initrd.systemd.enable = true;
# host is responsible for early-KMS so plymouth lands on the gpu driver,
# not simpledrm (e.g. hardware.amdgpu.initrd.enable on amd hosts)
boot.plymouth.enable = true;
stylix.targets.plymouth.logoAnimated = false;
boot.kernelParams = [
"quiet"
"splash"
"loglevel=3"
"rd.systemd.show_status=false"
"rd.udev.log_level=3"
"udev.log_priority=3"
"plymouth.force-scale=1"
];
boot.consoleLogLevel = 0;
boot.initrd.verbose = false;
})
] ]
); );
}; };

View File

@@ -71,6 +71,12 @@
]; ];
}; };
# honor persist_mode so electron apps don't re-prompt for screencast every login
systemd.user.services.xdg-desktop-portal-wlr.environment.XDPW_PERSIST_MODE = "permanent";
# enable ozone/wayland for electron apps so idle detection works
environment.sessionVariables.NIXOS_OZONE_WL = "1";
fonts.packages = with pkgs; [ fonts.packages = with pkgs; [
font-awesome font-awesome
nerd-fonts.jetbrains-mono nerd-fonts.jetbrains-mono
@@ -114,7 +120,7 @@
bolt-launcher bolt-launcher
libnotify libnotify
bibata-cursors bibata-cursors
vesktop discord
rocketchat-desktop rocketchat-desktop
telegram-desktop telegram-desktop
slack slack
@@ -256,7 +262,7 @@
# app deep links # app deep links
"x-scheme-handler/tg" = "org.telegram.desktop.desktop"; "x-scheme-handler/tg" = "org.telegram.desktop.desktop";
"x-scheme-handler/discord" = "vesktop.desktop"; "x-scheme-handler/discord" = "discord.desktop";
"x-scheme-handler/slack" = "slack.desktop"; "x-scheme-handler/slack" = "slack.desktop";
}; };
}; };

View File

@@ -59,6 +59,9 @@
boot.initrd.systemd.enable = true; boot.initrd.systemd.enable = true;
# remote unlock may take a while; don't let device units give up
boot.initrd.systemd.settings.Manager.DefaultDeviceTimeoutSec = "infinity";
boot.initrd.network = { boot.initrd.network = {
enable = true; enable = true;
ssh = { ssh = {

View File

@@ -39,6 +39,7 @@
programs.neovim = { programs.neovim = {
enable = true; enable = true;
sideloadInitLua = true;
vimAlias = true; vimAlias = true;
defaultEditor = true; defaultEditor = true;
package = inputs.neovim-nightly-overlay.packages.${pkgs.stdenv.hostPlatform.system}.default; package = inputs.neovim-nightly-overlay.packages.${pkgs.stdenv.hostPlatform.system}.default;

View File

@@ -43,19 +43,15 @@
config = lib.mkIf cfg.enable ( config = lib.mkIf cfg.enable (
lib.mkMerge [ lib.mkMerge [
{ {
services.qemuGuest.enable = true;
services.spice-vdagentd.enable = lib.mkIf (!cfg.headless) true; services.spice-vdagentd.enable = lib.mkIf (!cfg.headless) true;
boot.kernelParams = lib.mkIf cfg.headless [ "console=ttyS0,115200" ]; boot.kernelParams = lib.mkIf cfg.headless [ "console=ttyS0,115200" ];
# 9p autoloads on first mount
boot.initrd.availableKernelModules = [ boot.initrd.availableKernelModules = [
"9p" "9p"
"9pnet_virtio" "9pnet_virtio"
]; ];
boot.kernelModules = [
"9p"
"9pnet_virtio"
];
networking = { networking = {
useDHCP = true; useDHCP = true;
@@ -68,7 +64,6 @@
curl curl
wget wget
htop htop
sshfs
]; ];
} }

42
flake.lock generated
View File

@@ -273,11 +273,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776661682, "lastModified": 1776777932,
"narHash": "sha256-X32LTSDqUdVqMy85WYdRgyt0I75wc4Lhi9j+lrCDR8w=", "narHash": "sha256-0R3Yow/NzSeVGUke5tL7CCkqmss4Vmi6BbV6idHzq/8=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "4bfce11ea820df0359f73736fd59c7e8f53641a6", "rev": "5d5640599a0050b994330328b9fd45709c909720",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -317,11 +317,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1776643520, "lastModified": 1776729909,
"narHash": "sha256-O6lIvHkvuYNG7WovtyOGu+Z1vYkr/DB2YDF6PchsT/A=", "narHash": "sha256-wGu/N42PJqrj8ju9GoXdppg4rwaKzZqdAjsgxJbCvfY=",
"owner": "nix-community", "owner": "nix-community",
"repo": "neovim-nightly-overlay", "repo": "neovim-nightly-overlay",
"rev": "4a27c9c5416c624fc35ec0607a9f7ddeb59e7c99", "rev": "ff21a18bde28b4c8ca0bc1f9a5b7186a1b89a3d1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -333,11 +333,11 @@
"neovim-src": { "neovim-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1776639951, "lastModified": 1776727374,
"narHash": "sha256-R/STk7D50lC0W6DTu/L+U3IYbd/IJFgikGdwrHbJXU4=", "narHash": "sha256-iP5SviNXW5W+ay4ZmwjDFsfQjfM+fYlUxRlLPHjpwWI=",
"owner": "neovim", "owner": "neovim",
"repo": "neovim", "repo": "neovim",
"rev": "5f6abd34f5c12534df0d79ee3507d40106ad505d", "rev": "901b3f0c394a53961781ebeee682e64ad690a242",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -395,11 +395,11 @@
}, },
"nixpkgs-master": { "nixpkgs-master": {
"locked": { "locked": {
"lastModified": 1776662323, "lastModified": 1776807375,
"narHash": "sha256-Gkexeq/VVRE4kxElVRv+m4mwKFcFhyWrmOmfbCM/KSE=", "narHash": "sha256-LDnHG0T54OEHyRydmGUlAND8ham0KrRNWjgoS+6GUd4=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "83604aae06f90be32437da472e2a8d9ebf7f10ff", "rev": "553ecb1686a2edb75dee44c9f72e1674e6adc26a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -411,11 +411,11 @@
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1776434932, "lastModified": 1776560675,
"narHash": "sha256-gyqXNMgk3sh+ogY5svd2eNLJ6oEwzbAeaoBrrxD0lKk=", "narHash": "sha256-p68udKWWh7+V4ZPpcMDq0gTHWNZJnr4JPI+kHPPE40o=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c7f47036d3df2add644c46d712d14262b7d86c0c", "rev": "e07580dae39738e46609eaab8b154de2488133ce",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -427,11 +427,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1776169885, "lastModified": 1776548001,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -550,11 +550,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776119890, "lastModified": 1776771786,
"narHash": "sha256-Zm6bxLNnEOYuS/SzrAGsYuXSwk3cbkRQZY0fJnk8a5M=", "narHash": "sha256-DRFGPfFV6hbrfO9a1PH1FkCi7qR5FgjSqsQGGvk1rdI=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "d4971dd58c6627bfee52a1ad4237637c0a2fb0cd", "rev": "bef289e2248991f7afeb95965c82fbcd8ff72598",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -13,19 +13,38 @@
documentation.enable = false; documentation.enable = false;
environment.defaultPackages = [ ]; environment.defaultPackages = [ ];
# compressed qcow2, no channel copy # qcow2, no channel copy; post-processed with parallel zstd on qcow2 v3
# (~half the size of zlib v2, faster decompress)
image.modules.qemu = image.modules.qemu =
{ config, modulesPath, ... }: { config, modulesPath, ... }:
{ {
system.build.image = lib.mkForce ( system.build.image = lib.mkForce (
import (modulesPath + "/../lib/make-disk-image.nix") { let
inherit lib config pkgs; rawImage = import (modulesPath + "/../lib/make-disk-image.nix") {
inherit (config.virtualisation) diskSize; inherit lib config pkgs;
inherit (config.virtualisation) diskSize;
inherit (config.image) baseName;
format = "qcow2";
copyChannel = false;
partitionTableType = "legacy";
};
inherit (config.image) baseName; inherit (config.image) baseName;
format = "qcow2-compressed"; in
copyChannel = false; pkgs.runCommand baseName { nativeBuildInputs = [ pkgs.qemu-utils ]; } ''
partitionTableType = "legacy"; mkdir -p $out
} # qemu-img caps -m at 16
cores="''${NIX_BUILD_CORES:-4}"
[ "$cores" -gt 0 ] || cores=4
[ "$cores" -gt 16 ] && cores=16
qemu-img convert \
-f qcow2 \
-O qcow2 \
-c \
-o compression_type=zstd \
-m "$cores" \
${rawImage}/${baseName}.qcow2 \
$out/${baseName}.qcow2
''
); );
}; };
@@ -70,7 +89,7 @@
features.neovim.dotfiles = inputs.nvim; features.neovim.dotfiles = inputs.nvim;
# ensure .config exists with correct ownership before automount # ensure .config exists with correct ownership before automount
systemd.tmpfiles.rules = [ "d /home/matej/.config 0755 matej users -" ]; systemd.tmpfiles.rules = [ "d /home/matej/.config 0700 matej users -" ];
# TODO:(@janezicmatej) replace ssh with virtio-console (hvc0) when qemu 11.0 lands # TODO:(@janezicmatej) replace ssh with virtio-console (hvc0) when qemu 11.0 lands
# https://www.mail-archive.com/qemu-devel@nongnu.org/msg1162844.html # https://www.mail-archive.com/qemu-devel@nongnu.org/msg1162844.html

View File

@@ -10,6 +10,7 @@
inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series
]; ];
features.bootloader.plymouth.enable = true;
features.desktop.bluetooth.enable = true; features.desktop.bluetooth.enable = true;
features.gnupg.yubikey.enable = true; features.gnupg.yubikey.enable = true;
features.udev = { features.udev = {

View File

@@ -6,7 +6,10 @@
{ {
features.nix-settings.towerCache.enable = false; features.nix-settings.towerCache.enable = false;
features.bootloader.mode = "lanzaboote"; features.bootloader = {
mode = "lanzaboote";
plymouth.enable = true;
};
features.desktop.bluetooth.enable = true; features.desktop.bluetooth.enable = true;
features.gnupg.yubikey.enable = true; features.gnupg.yubikey.enable = true;
features.udev = { features.udev = {
@@ -23,6 +26,8 @@
nix.settings.secret-key-files = [ config.sops.secrets.nix-signing-key.path ]; nix.settings.secret-key-files = [ config.sops.secrets.nix-signing-key.path ];
boot.kernelParams = [ "btusb.reset=1" ]; boot.kernelParams = [ "btusb.reset=1" ];
# early kms so plymouth lands on amdgpu, not simpledrm
hardware.amdgpu.initrd.enable = true;
services.udisks2.enable = true; services.udisks2.enable = true;

View File

@@ -2,7 +2,7 @@
let let
inherit (pkgs) stdenv lib; inherit (pkgs) stdenv lib;
version = "2.1.114"; version = "2.1.116";
# upstream ships platform-native binaries as separate npm packages under # upstream ships platform-native binaries as separate npm packages under
# @anthropic-ai/claude-code-<platform>; the wrapper package is just a # @anthropic-ai/claude-code-<platform>; the wrapper package is just a
@@ -10,19 +10,19 @@ let
sources = { sources = {
"x86_64-linux" = { "x86_64-linux" = {
slug = "linux-x64"; slug = "linux-x64";
hash = "sha256-gejcdjRzKnWsvLzxJLfdjr+PeYdOR9tkCOL8owuJuf8="; hash = "sha256-QEjJ4CRk35TubDNW02Dzcu+EMRLLndJUXJeP3BFT3b8=";
}; };
"aarch64-linux" = { "aarch64-linux" = {
slug = "linux-arm64"; slug = "linux-arm64";
hash = "sha256-atThX6FuIJe0t7pQRd76ZIVCPd+AKfkLl1a48eLglQE="; hash = "sha256-/Hqp8GQx8Hub8K4w0Fnx/AksksY61vRC44XxrJVwF5w=";
}; };
"x86_64-darwin" = { "x86_64-darwin" = {
slug = "darwin-x64"; slug = "darwin-x64";
hash = "sha256-1tUHaaE4AI8r7W+vS4wCKTH3OjDOxMRtSzyUt+LHhAs="; hash = "sha256-O3J/ew2fWbUQePs6tHEhK0Q9E3Mx/BDSL7b7NL3FRc8=";
}; };
"aarch64-darwin" = { "aarch64-darwin" = {
slug = "darwin-arm64"; slug = "darwin-arm64";
hash = "sha256-Nx0I2PZgoLeI43c5O4rlyPgEQGDom3RO5pemV9V1vqg="; hash = "sha256-O41sf7b05SJfXVjszMeTp838mja+PgZ+aEKykLsHeNo=";
}; };
}; };

View File

@@ -27,14 +27,32 @@ info() {
# globals for cleanup trap # globals for cleanup trap
CLEANUP_OVERLAY="" CLEANUP_OVERLAY=""
CLEANUP_TMPDIR=""
QEMU_PID="" QEMU_PID=""
VM_READY=false
cleanup() { cleanup() {
[ -n "$QEMU_PID" ] && kill "$QEMU_PID" 2>/dev/null && wait "$QEMU_PID" 2>/dev/null [ -n "$QEMU_PID" ] && kill "$QEMU_PID" 2>/dev/null && wait "$QEMU_PID" 2>/dev/null
[ -n "$CLEANUP_OVERLAY" ] && rm -rf "$CLEANUP_OVERLAY" [ -n "$CLEANUP_OVERLAY" ] && rm -rf "$CLEANUP_OVERLAY"
# preserve tmpdir on abnormal exit so the qemu log survives for inspection
if [ -n "$CLEANUP_TMPDIR" ]; then
if [ "$VM_READY" = true ]; then
rm -rf "$CLEANUP_TMPDIR"
else
echo "qemu log preserved: $CLEANUP_TMPDIR/qemu.log" >&2
fi
fi
return 0 return 0
} }
trap cleanup EXIT trap cleanup EXIT
# returns 0 once the guest's sshd is speaking (first bytes are "SSH-")
awaiting_ssh_banner() {
local port="$1"
local banner
banner=$(timeout 2 bash -c "exec 3<>/dev/tcp/localhost/$port; head -c 4 <&3" 2>/dev/null) || return 1
[ "$banner" = "SSH-" ]
}
usage() { usage() {
cat <<EOF cat <<EOF
Usage: ephvm-run.sh [options] Usage: ephvm-run.sh [options]
@@ -55,6 +73,8 @@ EOF
main() { main() {
setup_colors setup_colors
[ "$EUID" -eq 0 ] && die "ephvm-run.sh must not run as root"
local ssh_port="" memory=4G cpus=2 claude=true disk_size="" serial=false local ssh_port="" memory=4G cpus=2 claude=true disk_size="" serial=false
local -a mounts=() local -a mounts=()
@@ -110,15 +130,13 @@ main() {
CLEANUP_OVERLAY=$(mktemp -d) CLEANUP_OVERLAY=$(mktemp -d)
local overlay="$CLEANUP_OVERLAY/overlay.qcow2" local overlay="$CLEANUP_OVERLAY/overlay.qcow2"
qemu-img create -f qcow2 -b "$(realpath "$image")" -F qcow2 "$overlay" "$disk_size" qemu-img create -f qcow2 -b "$(realpath "$image")" -F qcow2 "$overlay" "$disk_size"
drive_arg="file=$overlay,format=qcow2" drive_arg="if=none,id=hd0,file=$overlay,format=qcow2,cache=writeback,aio=threads,discard=unmap,detect-zeroes=unmap"
else else
drive_arg="file=$image,format=qcow2,snapshot=on" drive_arg="if=none,id=hd0,file=$image,format=qcow2,snapshot=on,cache=writeback,aio=threads,discard=unmap,detect-zeroes=unmap"
fi fi
command -v qemu-system-x86_64 &>/dev/null || die "qemu-system-x86_64 not found" command -v qemu-system-x86_64 &>/dev/null || die "qemu-system-x86_64 not found"
[ -r /dev/kvm ] || die "/dev/kvm not readable; kvm is required"
local accel="tcg"
[ -r /dev/kvm ] && accel="kvm"
# auto-allocate ssh port unless serial mode # auto-allocate ssh port unless serial mode
if [ "$serial" = false ] && [ -z "$ssh_port" ]; then if [ "$serial" = false ] && [ -z "$ssh_port" ]; then
@@ -128,28 +146,33 @@ main() {
done done
fi fi
local nic_arg="user" local nic_arg="user,model=virtio-net-pci"
if [ -n "$ssh_port" ]; then if [ -n "$ssh_port" ]; then
nic_arg="user,hostfwd=tcp::${ssh_port}-:22" nic_arg="user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:${ssh_port}-:22"
fi fi
local -a qemu_args=( local -a qemu_args=(
qemu-system-x86_64 qemu-system-x86_64
-accel "$accel" -accel kvm
-cpu host
-m "$memory" -m "$memory"
-smp "$cpus" -smp "$cpus"
-drive "$drive_arg" -drive "$drive_arg"
-device "virtio-blk-pci,drive=hd0"
-device virtio-rng-pci
-nic "$nic_arg" -nic "$nic_arg"
-nographic -nographic
-sandbox "on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny"
) )
if [ "$accel" != "tcg" ]; then
qemu_args+=(-cpu host)
fi
local fs_id=0 mount_path name tag local fs_id=0 mount_path name tag
for mount_path in "${mounts[@]}"; do for mount_path in "${mounts[@]}"; do
[ -e "$mount_path" ] || die "--mount path does not exist: $mount_path"
mount_path=$(realpath "$mount_path") mount_path=$(realpath "$mount_path")
# qemu parses -virtfs as csv, a comma in the path would inject options
case "$mount_path" in
*,*) die "--mount path may not contain commas: $mount_path" ;;
esac
name=$(basename "$mount_path") name=$(basename "$mount_path")
tag="m_${name:0:29}" tag="m_${name:0:29}"
qemu_args+=( qemu_args+=(
@@ -163,6 +186,9 @@ main() {
mkdir -p "$CLAUDE_CONFIG_DIR" mkdir -p "$CLAUDE_CONFIG_DIR"
local claude_dir local claude_dir
claude_dir=$(realpath "$CLAUDE_CONFIG_DIR") claude_dir=$(realpath "$CLAUDE_CONFIG_DIR")
case "$claude_dir" in
*,*) die "claude config dir may not contain commas: $claude_dir" ;;
esac
qemu_args+=( qemu_args+=(
-virtfs "local,path=$claude_dir,mount_tag=claude,security_model=none,id=fs${fs_id}" -virtfs "local,path=$claude_dir,mount_tag=claude,security_model=none,id=fs${fs_id}"
@@ -171,27 +197,38 @@ main() {
fi fi
info "---" info "---"
info "Accel: $accel" [ -n "$ssh_port" ] && info "SSH: ssh -p $ssh_port matej@localhost"
info "---" info "---"
if [ "$serial" = true ]; then if [ "$serial" = true ]; then
exec "${qemu_args[@]}" exec "${qemu_args[@]}"
fi fi
CLEANUP_TMPDIR=$(mktemp -d)
local qemu_log="$CLEANUP_TMPDIR/qemu.log"
# start qemu in background and auto-ssh # start qemu in background and auto-ssh
"${qemu_args[@]}" &>/dev/null & "${qemu_args[@]}" &>"$qemu_log" &
QEMU_PID=$! QEMU_PID=$!
# throwaway ssh key (vm accepts any key via AuthorizedKeysCommand)
local ssh_key="$CLEANUP_TMPDIR/id_ed25519"
ssh-keygen -t ed25519 -f "$ssh_key" -N "" -q
info "waiting for vm (port $ssh_port)..." info "waiting for vm (port $ssh_port)..."
local attempts=0 local attempts=0
while ! (echo > /dev/tcp/localhost/"$ssh_port") 2>/dev/null; do # poll for the real SSH banner, not TCP accept: qemu's user-mode nic
# accepts host-side the moment qemu starts, well before guest sshd is up
while ! awaiting_ssh_banner "$ssh_port"; do
attempts=$((attempts + 1)) attempts=$((attempts + 1))
[ $attempts -gt 60 ] && die "vm did not become ready in 60s" [ $attempts -gt 120 ] && die "vm did not become ready in 60s"
kill -0 "$QEMU_PID" 2>/dev/null || die "qemu exited unexpectedly" kill -0 "$QEMU_PID" 2>/dev/null || die "qemu exited unexpectedly"
sleep 1 sleep 0.5
done done
VM_READY=true
ssh -p "$ssh_port" -t \ ssh -p "$ssh_port" -t \
-i "$ssh_key" \
-o StrictHostKeyChecking=no \ -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \ -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \ -o LogLevel=ERROR \