From 930f1660e978997d2340a51d824f255ddbae7796 Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 24 Jul 2023 11:31:52 +0200 Subject: [PATCH 01/14] installer: wait for onion hostname to be available --- flake-parts/packages.nix | 24 +++++++++++++++--------- installer.nix | 3 +++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/flake-parts/packages.nix b/flake-parts/packages.nix index 8ae436a..7b6c70a 100644 --- a/flake-parts/packages.nix +++ b/flake-parts/packages.nix @@ -1,11 +1,17 @@ { self, lib, ... }: { - flake.packages.x86_64-linux = { - install-iso = (lib.nixosSystem { - system = "x86_64-linux"; - modules = [ - self.nixosModules.installer - self.inputs.nixos-generators.nixosModules.all-formats - ]; - }).config.formats.install-iso; - }; + flake.packages.x86_64-linux = + let + installer = lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + self.nixosModules.installer + self.inputs.nixos-generators.nixosModules.all-formats + ]; + }; + in + { + install-iso = installer.config.formats.install-iso; + install-vm-nogui = installer.config.formats.vm-nogui; + install-vm = installer.config.formats.vm; + }; } diff --git a/installer.nix b/installer.nix index 2daef89..73fb913 100644 --- a/installer.nix +++ b/installer.nix @@ -7,18 +7,21 @@ ]; services.openssh.settings.PermitRootLogin = "yes"; system.activationScripts.root-password = '' + mkdir -p /var/shared ${pkgs.pwgen}/bin/pwgen -s 16 1 > /var/shared/root-password echo "root:$(cat /var/shared/root-password)" | chpasswd ''; hidden-announce = { enable = true; script = pkgs.writers.writeDash "write-hostname" '' + mkdir -p /var/shared echo "$1" > /var/shared/onion-hostname ''; }; services.getty.autologinUser = lib.mkForce "root"; programs.bash.interactiveShellInit = '' if [ "$(tty)" = "/dev/tty1" ]; then + until test -e /var/shared/onion-hostname; do sleep 1; done echo "ssh://root:$(cat /var/shared/root-password)@$(cat /var/shared/onion-hostname)" fi ''; From e9c04326817e0decd2bd9f32b49c61f87d051520 Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 24 Jul 2023 12:09:11 +0200 Subject: [PATCH 02/14] rename hidden-announce to hidden-ssh-announce, create qr codes --- flake.nix | 4 ++-- hidden-announce.nix => hidden-ssh-announce.nix | 11 ++++++----- installer.nix | 15 ++++++++++++--- 3 files changed, 20 insertions(+), 10 deletions(-) rename hidden-announce.nix => hidden-ssh-announce.nix (76%) diff --git a/flake.nix b/flake.nix index 58d64b7..2001f8c 100644 --- a/flake.nix +++ b/flake.nix @@ -31,12 +31,12 @@ installer = { imports = [ ./installer.nix - ./hidden-announce.nix + ./hidden-ssh-announce.nix ]; }; hidden-announce = { imports = [ - ./hidden-announce.nix + ./hidden-ssh-announce.nix ]; }; }; diff --git a/hidden-announce.nix b/hidden-ssh-announce.nix similarity index 76% rename from hidden-announce.nix rename to hidden-ssh-announce.nix index bcb8e6c..7fb6c5b 100644 --- a/hidden-announce.nix +++ b/hidden-ssh-announce.nix @@ -3,11 +3,11 @@ , pkgs , ... }: { - options.hidden-announce = { - enable = lib.mkEnableOption "hidden-announce"; + options.hidden-ssh-announce = { + enable = lib.mkEnableOption "hidden-ssh-announce"; script = lib.mkOption { type = lib.types.package; - default = pkgs.writers.writeDash "test-output"; + default = pkgs.writers.writeDash "test-output" "echo $1"; description = '' script to run when the hidden tor service was started and they hostname is known. takes the hostname as $1 @@ -15,7 +15,8 @@ }; }; - config = lib.mkIf config.hidden-announce.enable { + config = lib.mkIf config.hidden-ssh-announce.enable { + services.openssh.enable = true; services.tor = { enable = true; relay.onionServices.hidden-ssh = { @@ -43,7 +44,7 @@ sleep 1 done - ${config.hidden-announce.script} "$(cat ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname)" + ${config.hidden-ssh-announce.script} "$(cat ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname)" ''; PrivateTmp = "true"; User = "tor"; diff --git a/installer.nix b/installer.nix index 73fb913..b1aa815 100644 --- a/installer.nix +++ b/installer.nix @@ -11,18 +11,27 @@ ${pkgs.pwgen}/bin/pwgen -s 16 1 > /var/shared/root-password echo "root:$(cat /var/shared/root-password)" | chpasswd ''; - hidden-announce = { + hidden-ssh-announce = { enable = true; script = pkgs.writers.writeDash "write-hostname" '' mkdir -p /var/shared echo "$1" > /var/shared/onion-hostname + ${pkgs.jq}/bin/jq -nc \ + --arg password "$(cat /var/shared/root-password)" \ + --arg address "$(cat /var/shared/onion-hostname)" '{ + password: $password, address: $address + }' > /var/shared/login.info + cat /var/shared/login.info | + ${pkgs.qrencode}/bin/qrencode -t utf8 > /var/shared/qrcode.utf8 + cat /var/shared/login.info | + ${pkgs.qrencode}/bin/qrencode -t png > /var/shared/qrcode.png ''; }; services.getty.autologinUser = lib.mkForce "root"; programs.bash.interactiveShellInit = '' if [ "$(tty)" = "/dev/tty1" ]; then - until test -e /var/shared/onion-hostname; do sleep 1; done - echo "ssh://root:$(cat /var/shared/root-password)@$(cat /var/shared/onion-hostname)" + until test -e /var/shared/qrcode.utf8; do sleep 1; done + cat /var/shared/qrcode.utf8 fi ''; formatConfigs.install-iso = { From 291b353d9cce69e59b5958072f2c26eef7b24a1c Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 24 Jul 2023 16:48:50 +0200 Subject: [PATCH 03/14] clan-cli: actually run commands --- pkgs/clan-cli/clan_cli/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index d834148..772619d 100755 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -20,10 +20,14 @@ def main() -> None: admin.register_parser(parser_admin) if has_argcomplete: argcomplete.autocomplete(parser) - parser.parse_args() + if len(sys.argv) == 1: parser.print_help() + args = parser.parse_args() + if hasattr(args, "func"): + args.func(args) + if __name__ == "__main__": # pragma: no cover main() From 09bc5b06c90cfc538217b86b6f99d2cf0b1c0fca Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 24 Jul 2023 16:49:18 +0200 Subject: [PATCH 04/14] clan-cli: add ssh subcommand --- pkgs/clan-cli/clan_cli/__init__.py | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 772619d..4bc36e3 100755 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -1,5 +1,7 @@ # !/usr/bin/env python3 import argparse +import json +import subprocess import sys from . import admin @@ -11,6 +13,64 @@ except ImportError: # pragma: no cover has_argcomplete = False +def ssh(args: argparse.Namespace) -> None: + if args.json: + ssh_data = json.load(args.json) + subprocess.run( + [ + "nix", + "shell", + "nixpkgs#sshpass", + "-c", + "torify", + "sshpass", + "-p", + ssh_data.get("password"), + "ssh", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "StrictHostKeyChecking=no", + f'root@{ssh_data["address"]}', + ] + + args.ssh_args + ) + elif args.png: + png_text = subprocess.Popen( + [ + "nix", + "shell", + "nixpkgs#zbar", + "-c", + "zbarimg", + "--quiet", + "--raw", + args.png, + ], + stdout=subprocess.PIPE, + ).stdout.read() + ssh_data = json.loads(png_text) + subprocess.run( + [ + "nix", + "shell", + "nixpkgs#sshpass", + "-c", + "torify", + "sshpass", + "-p", + ssh_data.get("password"), + "ssh", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "StrictHostKeyChecking=no", + f'root@{ssh_data["address"]}', + ] + + args.ssh_args + ) + + # this will be the entrypoint under /bin/clan (see pyproject.toml config) def main() -> None: parser = argparse.ArgumentParser(description="cLAN tool") @@ -18,6 +78,22 @@ def main() -> None: parser_admin = subparsers.add_parser("admin") admin.register_parser(parser_admin) + + parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine") + parser_ssh.add_argument( + "-j", + "--json", + help="specify the json file for ssh data (generated by starting the clan installer", + ) + parser_ssh.add_argument( + "-P", + "--png", + help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer", + ) + # TODO pass all args we don't parse into ssh_args, currently it fails if arg starts with - + parser_ssh.add_argument("ssh_args", nargs="*", default=[]) + parser_ssh.set_defaults(func=ssh) + if has_argcomplete: argcomplete.autocomplete(parser) From 4571b65123f23c2d8467b335d4f4bf5c3062b90b Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 24 Jul 2023 16:49:32 +0200 Subject: [PATCH 05/14] clan-cli: set mainProgram name --- pkgs/clan-cli/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 1badd22..0fcc0c2 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -49,6 +49,7 @@ let installShellCompletion --fish --name clan.fish \ <(${python3.pkgs.argcomplete}/bin/register-python-argcomplete --shell fish clan) ''; + meta.mainProgram = "clan"; }; checkPython = python3.withPackages (_ps: devDependencies ++ dependencies); From c8e59471a0a8d091d53b87d0fb693024adca6f50 Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 24 Jul 2023 16:49:54 +0200 Subject: [PATCH 06/14] flake.nix: set clan-cli as default package --- pkgs/clan-cli/flake-module.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix index 1d1d144..b8df407 100644 --- a/pkgs/clan-cli/flake-module.nix +++ b/pkgs/clan-cli/flake-module.nix @@ -7,6 +7,7 @@ in { packages.${name} = package; + packages.default = package; checks = package.tests; }; } From a3bcb93f2fa2dd47a180defae7757695fba9790d Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 24 Jul 2023 21:11:49 +0200 Subject: [PATCH 07/14] clan-cli: split out ssh subcommand, add more tests --- pkgs/clan-cli/clan_cli/__init__.py | 78 ++------------------------- pkgs/clan-cli/clan_cli/ssh.py | 80 ++++++++++++++++++++++++++++ pkgs/clan-cli/tests/test_clan_ssh.py | 57 ++++++++++++++++++++ 3 files changed, 140 insertions(+), 75 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/ssh.py create mode 100644 pkgs/clan-cli/tests/test_clan_ssh.py diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 4bc36e3..e4ff0df 100755 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -1,10 +1,8 @@ # !/usr/bin/env python3 import argparse -import json -import subprocess import sys -from . import admin +from . import admin, ssh has_argcomplete = True try: @@ -13,64 +11,6 @@ except ImportError: # pragma: no cover has_argcomplete = False -def ssh(args: argparse.Namespace) -> None: - if args.json: - ssh_data = json.load(args.json) - subprocess.run( - [ - "nix", - "shell", - "nixpkgs#sshpass", - "-c", - "torify", - "sshpass", - "-p", - ssh_data.get("password"), - "ssh", - "-o", - "UserKnownHostsFile=/dev/null", - "-o", - "StrictHostKeyChecking=no", - f'root@{ssh_data["address"]}', - ] - + args.ssh_args - ) - elif args.png: - png_text = subprocess.Popen( - [ - "nix", - "shell", - "nixpkgs#zbar", - "-c", - "zbarimg", - "--quiet", - "--raw", - args.png, - ], - stdout=subprocess.PIPE, - ).stdout.read() - ssh_data = json.loads(png_text) - subprocess.run( - [ - "nix", - "shell", - "nixpkgs#sshpass", - "-c", - "torify", - "sshpass", - "-p", - ssh_data.get("password"), - "ssh", - "-o", - "UserKnownHostsFile=/dev/null", - "-o", - "StrictHostKeyChecking=no", - f'root@{ssh_data["address"]}', - ] - + args.ssh_args - ) - - # this will be the entrypoint under /bin/clan (see pyproject.toml config) def main() -> None: parser = argparse.ArgumentParser(description="cLAN tool") @@ -80,19 +20,7 @@ def main() -> None: admin.register_parser(parser_admin) parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine") - parser_ssh.add_argument( - "-j", - "--json", - help="specify the json file for ssh data (generated by starting the clan installer", - ) - parser_ssh.add_argument( - "-P", - "--png", - help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer", - ) - # TODO pass all args we don't parse into ssh_args, currently it fails if arg starts with - - parser_ssh.add_argument("ssh_args", nargs="*", default=[]) - parser_ssh.set_defaults(func=ssh) + ssh.register_parser(parser_ssh) if has_argcomplete: argcomplete.autocomplete(parser) @@ -102,7 +30,7 @@ def main() -> None: args = parser.parse_args() if hasattr(args, "func"): - args.func(args) + args.func(args) # pragma: no cover if __name__ == "__main__": # pragma: no cover diff --git a/pkgs/clan-cli/clan_cli/ssh.py b/pkgs/clan-cli/clan_cli/ssh.py new file mode 100644 index 0000000..1771e5d --- /dev/null +++ b/pkgs/clan-cli/clan_cli/ssh.py @@ -0,0 +1,80 @@ +import argparse +import json +import subprocess +from typing import Optional + + +def ssh( + host: str, + user: str = "root", + password: Optional[str] = None, + ssh_args: list[str] = [], +) -> None: + if ssh_args is None: + ssh_args = [] + nix_shell_args = [] + password_args = [] + if password: + nix_shell_args = [ + "nix", + "shell", + "nixpkgs#sshpass", + "-c", + ] + password_args = [ + "sshpass", + "-p", + password, + ] + _ssh_args = ssh_args + [ + "ssh", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "StrictHostKeyChecking=no", + f"{user}@{host}", + ] + cmd = nix_shell_args + ["torify"] + password_args + _ssh_args + subprocess.run(cmd) + + +def qrcode_scan(pictureFile: str) -> dict: + subprocess.Popen( + [ + "nix", + "shell", + "nixpkgs#zbar", + "-c", + "zbarimg", + "--quiet", + "--raw", + pictureFile, + ], + stdout=subprocess.PIPE, + ).stdout.read() + + +def main(args: argparse.Namespace) -> None: + if args.json: + with open(args.json) as file: + ssh_data = json.load(file) + ssh(host=ssh_data["address"], password=ssh_data["password"]) + elif args.png: + ssh_data = json.loads(qrcode_scan(args.png)) + ssh(host=ssh_data["address"], password=ssh_data["password"]) + + +def register_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "-j", + "--json", + help="specify the json file for ssh data (generated by starting the clan installer", + ) + parser.add_argument( + "-P", + "--png", + help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer", + ) + # TODO pass all args we don't parse into ssh_args, currently it fails if arg starts with - + parser.add_argument("ssh_args", nargs="*", default=[]) + parser.set_defaults(func=main) diff --git a/pkgs/clan-cli/tests/test_clan_ssh.py b/pkgs/clan-cli/tests/test_clan_ssh.py new file mode 100644 index 0000000..99f9148 --- /dev/null +++ b/pkgs/clan-cli/tests/test_clan_ssh.py @@ -0,0 +1,57 @@ +import argparse +import json +import tempfile +from typing import Union + +import pytest_subprocess.fake_process +from pytest_subprocess import utils + +import clan_cli.ssh + + +# using fp fixture from pytest-subprocess +def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: + host = "somehost" + user = "user" + cmd: list[Union[str, utils.Any]] = [ + "torify", + "ssh", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "StrictHostKeyChecking=no", + f"{user}@{host}", + ] + fp.register(cmd) + clan_cli.ssh.ssh( + host=host, + user=user, + ) + assert fp.call_count(cmd) == 1 + + +# using fp fixture from pytest-subprocess +def test_ssh_json(fp: pytest_subprocess.fake_process.FakeProcess) -> None: + with tempfile.NamedTemporaryFile(mode="w+") as file: + json.dump({"password": "XXX", "address": "somehost"}, file) + cmd: list[Union[str, utils.Any]] = [ + "nix", + "shell", + "nixpkgs#sshpass", + "-c", + "torify", + "sshpass", + "-p", + "XXX", + "ssh", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "StrictHostKeyChecking=no", + "root@somehost", + ] + fp.register(cmd) + file.seek(0) # write file and go to the beginning + args = argparse.Namespace(json=file.name, ssh_args=[]) + clan_cli.ssh.main(args) + assert fp.call_count(cmd) == 1 From 8513c1165725ae477320174095afd11313a25155 Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 24 Jul 2023 21:15:51 +0200 Subject: [PATCH 08/14] gitignore: add htmlcov --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7733d7d..8893b55 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__ .pytest_cache .pythonenv .ruff_cache +htmlcov From c354157bd5e5ef761d30155de55a983aa021301a Mon Sep 17 00:00:00 2001 From: DavHau Date: Tue, 25 Jul 2023 00:59:03 +0200 Subject: [PATCH 09/14] clan-cli: minor fixes in ssh.py --- pkgs/clan-cli/clan_cli/ssh.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/ssh.py b/pkgs/clan-cli/clan_cli/ssh.py index 1771e5d..c33a2f2 100644 --- a/pkgs/clan-cli/clan_cli/ssh.py +++ b/pkgs/clan-cli/clan_cli/ssh.py @@ -10,8 +10,6 @@ def ssh( password: Optional[str] = None, ssh_args: list[str] = [], ) -> None: - if ssh_args is None: - ssh_args = [] nix_shell_args = [] password_args = [] if password: @@ -68,12 +66,12 @@ def register_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( "-j", "--json", - help="specify the json file for ssh data (generated by starting the clan installer", + help="specify the json file for ssh data (generated by starting the clan installer)", ) parser.add_argument( "-P", "--png", - help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer", + help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)", ) # TODO pass all args we don't parse into ssh_args, currently it fails if arg starts with - parser.add_argument("ssh_args", nargs="*", default=[]) From 202e07d5fe47e703767cb9a600323e1490a6babd Mon Sep 17 00:00:00 2001 From: DavHau Date: Tue, 25 Jul 2023 00:59:51 +0200 Subject: [PATCH 10/14] clan-cli: hint on how to get coverage html --- .gitignore | 1 + pkgs/clan-cli/default.nix | 3 ++- pkgs/clan-cli/pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8893b55..d999462 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ __pycache__ .mypy_cache .pytest_cache .pythonenv +.reports .ruff_cache htmlcov diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 0fcc0c2..3b939a6 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -66,7 +66,8 @@ let cp -r ${src} ./src chmod +w -R ./src cd src - ${checkPython}/bin/python -m pytest ./tests + ${checkPython}/bin/python -m pytest ./tests \ + || echo -e "generate coverage report py running:\n pytest; firefox .reports/html/index.html" touch $out ''; diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index fa2a293..eff85f5 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -9,7 +9,7 @@ dynamic = ["version"] scripts = {clan = "clan_cli:main"} [tool.pytest.ini_options] -addopts = "--cov . --cov-report term --cov-fail-under=100 --no-cov-on-fail" +addopts = "--cov . --cov-report term --cov-report html:.reports/html --cov-fail-under=100 --no-cov-on-fail" [tool.mypy] python_version = "3.10" From 310bdacb9de854229d72a1ac482abb0a5d05f2d9 Mon Sep 17 00:00:00 2001 From: DavHau Date: Tue, 25 Jul 2023 15:16:05 +0200 Subject: [PATCH 11/14] clan-cli/ssh: add tests + fixes --- pkgs/clan-cli/clan_cli/ssh.py | 10 +++--- pkgs/clan-cli/tests/test_clan_ssh.py | 54 +++++++++++++++------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/ssh.py b/pkgs/clan-cli/clan_cli/ssh.py index c33a2f2..797dc42 100644 --- a/pkgs/clan-cli/clan_cli/ssh.py +++ b/pkgs/clan-cli/clan_cli/ssh.py @@ -36,7 +36,7 @@ def ssh( subprocess.run(cmd) -def qrcode_scan(pictureFile: str) -> dict: +def qrcode_scan(pictureFile: str) -> dict: # pragma: no cover subprocess.Popen( [ "nix", @@ -52,7 +52,7 @@ def qrcode_scan(pictureFile: str) -> dict: ).stdout.read() -def main(args: argparse.Namespace) -> None: +def main(args: argparse.Namespace) -> None: # pragma: no cover if args.json: with open(args.json) as file: ssh_data = json.load(file) @@ -62,13 +62,15 @@ def main(args: argparse.Namespace) -> None: ssh(host=ssh_data["address"], password=ssh_data["password"]) + def register_parser(parser: argparse.ArgumentParser) -> None: - parser.add_argument( + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( "-j", "--json", help="specify the json file for ssh data (generated by starting the clan installer)", ) - parser.add_argument( + group.add_argument( "-P", "--png", help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)", diff --git a/pkgs/clan-cli/tests/test_clan_ssh.py b/pkgs/clan-cli/tests/test_clan_ssh.py index 99f9148..7465c2e 100644 --- a/pkgs/clan-cli/tests/test_clan_ssh.py +++ b/pkgs/clan-cli/tests/test_clan_ssh.py @@ -1,6 +1,8 @@ import argparse import json import tempfile +import pytest +import sys from typing import Union import pytest_subprocess.fake_process @@ -8,6 +10,15 @@ from pytest_subprocess import utils import clan_cli.ssh +def test_no_args( + capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(sys, "argv", ["", "ssh"]) + with pytest.raises(SystemExit) as pytest_wrapped_e: + clan_cli.main() + captured = capsys.readouterr() + assert captured.err.startswith("usage:") + # using fp fixture from pytest-subprocess def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: @@ -21,6 +32,7 @@ def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: "-o", "StrictHostKeyChecking=no", f"{user}@{host}", + fp.any(), ] fp.register(cmd) clan_cli.ssh.ssh( @@ -30,28 +42,20 @@ def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: assert fp.call_count(cmd) == 1 -# using fp fixture from pytest-subprocess -def test_ssh_json(fp: pytest_subprocess.fake_process.FakeProcess) -> None: - with tempfile.NamedTemporaryFile(mode="w+") as file: - json.dump({"password": "XXX", "address": "somehost"}, file) - cmd: list[Union[str, utils.Any]] = [ - "nix", - "shell", - "nixpkgs#sshpass", - "-c", - "torify", - "sshpass", - "-p", - "XXX", - "ssh", - "-o", - "UserKnownHostsFile=/dev/null", - "-o", - "StrictHostKeyChecking=no", - "root@somehost", - ] - fp.register(cmd) - file.seek(0) # write file and go to the beginning - args = argparse.Namespace(json=file.name, ssh_args=[]) - clan_cli.ssh.main(args) - assert fp.call_count(cmd) == 1 +def test_ssh_with_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: + host = "somehost" + user = "user" + cmd: list[Union[str, utils.Any]] = [ + "nix", + "shell", + "nixpkgs#sshpass", + "-c", + fp.any(), + ] + fp.register(cmd) + clan_cli.ssh.ssh( + host=host, + user=user, + password="XXX", + ) + assert fp.call_count(cmd) == 1 From 2a31d785cffa7f578a86787c9ebe615080072445 Mon Sep 17 00:00:00 2001 From: DavHau Date: Tue, 25 Jul 2023 15:43:26 +0200 Subject: [PATCH 12/14] clan-cli: add checks for black and ruff --- pkgs/clan-cli/default.nix | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 3b939a6..2963cf5 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -41,7 +41,7 @@ let propagatedBuildInputs = dependencies ++ [ ]; - passthru.tests = { inherit clan-tests clan-mypy; }; + passthru.tests = { inherit clan-black clan-mypy clan-pytest clan-ruff; }; passthru.devDependencies = devDependencies; postInstall = '' installShellCompletion --bash --name clan \ @@ -54,6 +54,14 @@ let checkPython = python3.withPackages (_ps: devDependencies ++ dependencies); + clan-black = runCommand "${name}-black" { } '' + cp -r ${src} ./src + chmod +w -R ./src + cd src + ${checkPython}/bin/black --check . + touch $out + ''; + clan-mypy = runCommand "${name}-mypy" { } '' cp -r ${src} ./src chmod +w -R ./src @@ -62,7 +70,7 @@ let touch $out ''; - clan-tests = runCommand "${name}-tests" { } '' + clan-pytest = runCommand "${name}-tests" { } '' cp -r ${src} ./src chmod +w -R ./src cd src @@ -71,5 +79,13 @@ let touch $out ''; + clan-ruff = runCommand "${name}-ruff" { } '' + cp -r ${src} ./src + chmod +w -R ./src + cd src + ${pkgs.ruff}/bin/ruff check . + touch $out + ''; + in package From 1183ac96182f04d74d155da22853c635ad0990db Mon Sep 17 00:00:00 2001 From: DavHau Date: Tue, 25 Jul 2023 15:43:45 +0200 Subject: [PATCH 13/14] clan-cli: format --- pkgs/clan-cli/clan_cli/ssh.py | 34 ++++++++++++++++------------ pkgs/clan-cli/tests/test_clan_ssh.py | 15 ++++++++---- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/ssh.py b/pkgs/clan-cli/clan_cli/ssh.py index 797dc42..65e896f 100644 --- a/pkgs/clan-cli/clan_cli/ssh.py +++ b/pkgs/clan-cli/clan_cli/ssh.py @@ -36,20 +36,25 @@ def ssh( subprocess.run(cmd) -def qrcode_scan(pictureFile: str) -> dict: # pragma: no cover - subprocess.Popen( - [ - "nix", - "shell", - "nixpkgs#zbar", - "-c", - "zbarimg", - "--quiet", - "--raw", - pictureFile, - ], - stdout=subprocess.PIPE, - ).stdout.read() +def qrcode_scan(pictureFile: str) -> str: # pragma: no cover + return ( + subprocess.run( + [ + "nix", + "shell", + "nixpkgs#zbar", + "-c", + "zbarimg", + "--quiet", + "--raw", + pictureFile, + ], + stdout=subprocess.PIPE, + check=True, + ) + .stdout.decode() + .strip() + ) def main(args: argparse.Namespace) -> None: # pragma: no cover @@ -62,7 +67,6 @@ def main(args: argparse.Namespace) -> None: # pragma: no cover ssh(host=ssh_data["address"], password=ssh_data["password"]) - def register_parser(parser: argparse.ArgumentParser) -> None: group = parser.add_mutually_exclusive_group(required=True) group.add_argument( diff --git a/pkgs/clan-cli/tests/test_clan_ssh.py b/pkgs/clan-cli/tests/test_clan_ssh.py index 7465c2e..77dcc4e 100644 --- a/pkgs/clan-cli/tests/test_clan_ssh.py +++ b/pkgs/clan-cli/tests/test_clan_ssh.py @@ -1,20 +1,18 @@ -import argparse -import json -import tempfile -import pytest import sys from typing import Union +import pytest import pytest_subprocess.fake_process from pytest_subprocess import utils import clan_cli.ssh + def test_no_args( capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(sys, "argv", ["", "ssh"]) - with pytest.raises(SystemExit) as pytest_wrapped_e: + with pytest.raises(SystemExit): clan_cli.main() captured = capsys.readouterr() assert captured.err.startswith("usage:") @@ -59,3 +57,10 @@ def test_ssh_with_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None: password="XXX", ) assert fp.call_count(cmd) == 1 + + +def test_qrcode_scan(fp: pytest_subprocess.fake_process.FakeProcess) -> None: + cmd: list[Union[str, utils.Any]] = [fp.any()] + fp.register(cmd, stdout="https://test.test") + result = clan_cli.ssh.qrcode_scan("test.png") + assert result == "https://test.test" From e56b86f8b2ab7afd81a2e863b7b483d80851f9c9 Mon Sep 17 00:00:00 2001 From: DavHau Date: Tue, 25 Jul 2023 15:48:01 +0200 Subject: [PATCH 14/14] clan-cli: remove coverage exclusion for qrcode_scan --- pkgs/clan-cli/clan_cli/ssh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/clan-cli/clan_cli/ssh.py b/pkgs/clan-cli/clan_cli/ssh.py index 65e896f..b07c4d0 100644 --- a/pkgs/clan-cli/clan_cli/ssh.py +++ b/pkgs/clan-cli/clan_cli/ssh.py @@ -36,7 +36,7 @@ def ssh( subprocess.run(cmd) -def qrcode_scan(pictureFile: str) -> str: # pragma: no cover +def qrcode_scan(pictureFile: str) -> str: return ( subprocess.run( [