Merge pull request 'move ssh cli to cli submodule' (#113) from Mic92-mic92 into main

This commit is contained in:
clan-bot
2023-08-09 13:55:00 +00:00
9 changed files with 57 additions and 55 deletions

View File

@@ -1,8 +1,9 @@
import argparse import argparse
import sys import sys
from . import admin, secrets, ssh from . import admin, secrets
from .errors import ClanError from .errors import ClanError
from .ssh import cli as ssh_cli
has_argcomplete = True has_argcomplete = True
try: try:
@@ -27,7 +28,7 @@ def main() -> None:
# warn(f"The config command does not work in the nix sandbox: {e}") # warn(f"The config command does not work in the nix sandbox: {e}")
parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine") parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine")
ssh.register_parser(parser_ssh) ssh_cli.register_parser(parser_ssh)
parser_secrets = subparsers.add_parser("secrets", help="manage secrets") parser_secrets = subparsers.add_parser("secrets", help="manage secrets")
secrets.register_parser(parser_secrets) secrets.register_parser(parser_secrets)

View File

@@ -144,7 +144,7 @@ class HostKeyCheck(Enum):
NONE = 2 NONE = 2
class DeployHost: class Host:
def __init__( def __init__(
self, self,
host: str, host: str,
@@ -158,7 +158,7 @@ class DeployHost:
verbose_ssh: bool = False, verbose_ssh: bool = False,
) -> None: ) -> None:
""" """
Creates a DeployHost Creates a Host
@host the hostname to connect to via ssh @host the hostname to connect to via ssh
@port the port to connect to via ssh @port the port to connect to via ssh
@forward_agent: wheter to forward ssh agent @forward_agent: wheter to forward ssh agent
@@ -495,7 +495,7 @@ T = TypeVar("T")
class HostResult(Generic[T]): class HostResult(Generic[T]):
def __init__(self, host: DeployHost, result: Union[T, Exception]) -> None: def __init__(self, host: Host, result: Union[T, Exception]) -> None:
self.host = host self.host = host
self._result = result self._result = result
@@ -518,12 +518,12 @@ class HostResult(Generic[T]):
return self._result return self._result
DeployResults = List[HostResult[subprocess.CompletedProcess[str]]] Results = List[HostResult[subprocess.CompletedProcess[str]]]
def _worker( def _worker(
func: Callable[[DeployHost], T], func: Callable[[Host], T],
host: DeployHost, host: Host,
results: List[HostResult[T]], results: List[HostResult[T]],
idx: int, idx: int,
) -> None: ) -> None:
@@ -534,15 +534,15 @@ def _worker(
results[idx] = HostResult(host, e) results[idx] = HostResult(host, e)
class DeployGroup: class Group:
def __init__(self, hosts: List[DeployHost]) -> None: def __init__(self, hosts: List[Host]) -> None:
self.hosts = hosts self.hosts = hosts
def _run_local( def _run_local(
self, self,
cmd: Union[str, List[str]], cmd: Union[str, List[str]],
host: DeployHost, host: Host,
results: DeployResults, results: Results,
stdout: FILE = None, stdout: FILE = None,
stderr: FILE = None, stderr: FILE = None,
extra_env: Dict[str, str] = {}, extra_env: Dict[str, str] = {},
@@ -569,8 +569,8 @@ class DeployGroup:
def _run_remote( def _run_remote(
self, self,
cmd: Union[str, List[str]], cmd: Union[str, List[str]],
host: DeployHost, host: Host,
results: DeployResults, results: Results,
stdout: FILE = None, stdout: FILE = None,
stderr: FILE = None, stderr: FILE = None,
extra_env: Dict[str, str] = {}, extra_env: Dict[str, str] = {},
@@ -621,8 +621,8 @@ class DeployGroup:
check: bool = True, check: bool = True,
verbose_ssh: bool = False, verbose_ssh: bool = False,
timeout: float = math.inf, timeout: float = math.inf,
) -> DeployResults: ) -> Results:
results: DeployResults = [] results: Results = []
threads = [] threads = []
for host in self.hosts: for host in self.hosts:
fn = self._run_local if local else self._run_remote fn = self._run_local if local else self._run_remote
@@ -662,7 +662,7 @@ class DeployGroup:
check: bool = True, check: bool = True,
verbose_ssh: bool = False, verbose_ssh: bool = False,
timeout: float = math.inf, timeout: float = math.inf,
) -> DeployResults: ) -> Results:
""" """
Command to run on the remote host via ssh Command to run on the remote host via ssh
@stdout if not None stdout of the command will be redirected to this file i.e. stdout=subprocss.PIPE @stdout if not None stdout of the command will be redirected to this file i.e. stdout=subprocss.PIPE
@@ -671,7 +671,7 @@ class DeployGroup:
@verbose_ssh: Enables verbose logging on ssh connections @verbose_ssh: Enables verbose logging on ssh connections
@timeout: Timeout in seconds for the command to complete @timeout: Timeout in seconds for the command to complete
@return a lists of tuples containing DeployNode and the result of the command for this DeployNode @return a lists of tuples containing Host and the result of the command for this Host
""" """
return self._run( return self._run(
cmd, cmd,
@@ -693,7 +693,7 @@ class DeployGroup:
cwd: Union[None, str, Path] = None, cwd: Union[None, str, Path] = None,
check: bool = True, check: bool = True,
timeout: float = math.inf, timeout: float = math.inf,
) -> DeployResults: ) -> Results:
""" """
Command to run locally for each host in the group in parallel Command to run locally for each host in the group in parallel
@cmd the commmand to run @cmd the commmand to run
@@ -703,7 +703,7 @@ class DeployGroup:
@extra_env environment variables to override whe running the command @extra_env environment variables to override whe running the command
@timeout: Timeout in seconds for the command to complete @timeout: Timeout in seconds for the command to complete
@return a lists of tuples containing DeployNode and the result of the command for this DeployNode @return a lists of tuples containing Host and the result of the command for this Host
""" """
return self._run( return self._run(
cmd, cmd,
@@ -717,7 +717,7 @@ class DeployGroup:
) )
def run_function( def run_function(
self, func: Callable[[DeployHost], T], check: bool = True self, func: Callable[[Host], T], check: bool = True
) -> List[HostResult[T]]: ) -> List[HostResult[T]]:
""" """
Function to run for each host in the group in parallel Function to run for each host in the group in parallel
@@ -745,9 +745,9 @@ class DeployGroup:
self._reraise_errors(results) self._reraise_errors(results)
return results return results
def filter(self, pred: Callable[[DeployHost], bool]) -> "DeployGroup": def filter(self, pred: Callable[[Host], bool]) -> "Group":
"""Return a new DeployGroup with the results filtered by the predicate""" """Return a new Group with the results filtered by the predicate"""
return DeployGroup(list(filter(pred, self.hosts))) return Group(list(filter(pred, self.hosts)))
@overload @overload

View File

@@ -3,7 +3,7 @@ import json
import subprocess import subprocess
from typing import Optional from typing import Optional
from .nix import nix_shell from ..nix import nix_shell
def ssh( def ssh(

View File

@@ -24,7 +24,7 @@ KEYS = [
@pytest.fixture @pytest.fixture
def test_keys() -> list[KeyPair]: def age_keys() -> list[KeyPair]:
""" """
Root directory of the tests Root directory of the tests
""" """

View File

@@ -3,4 +3,4 @@ import sys
sys.path.append(os.path.join(os.path.dirname(__file__), "helpers")) sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
pytest_plugins = ["temporary_dir", "clan_flake", "root", "test_keys"] pytest_plugins = ["temporary_dir", "clan_flake", "root", "age_keys"]

View File

@@ -6,21 +6,21 @@ from environment import mock_env
from secret_cli import SecretCli from secret_cli import SecretCli
if TYPE_CHECKING: if TYPE_CHECKING:
from test_keys import KeyPair from age_keys import KeyPair
def test_import_sops( def test_import_sops(
test_root: Path, test_root: Path,
clan_flake: Path, clan_flake: Path,
capsys: pytest.CaptureFixture, capsys: pytest.CaptureFixture,
test_keys: list["KeyPair"], age_keys: list["KeyPair"],
) -> None: ) -> None:
cli = SecretCli() cli = SecretCli()
with mock_env(SOPS_AGE_KEY=test_keys[1].privkey): with mock_env(SOPS_AGE_KEY=age_keys[1].privkey):
cli.run(["machines", "add", "machine1", test_keys[0].pubkey]) cli.run(["machines", "add", "machine1", age_keys[0].pubkey])
cli.run(["users", "add", "user1", test_keys[1].pubkey]) cli.run(["users", "add", "user1", age_keys[1].pubkey])
cli.run(["users", "add", "user2", test_keys[2].pubkey]) cli.run(["users", "add", "user2", age_keys[2].pubkey])
cli.run(["groups", "add-user", "group1", "user1"]) cli.run(["groups", "add-user", "group1", "user1"])
cli.run(["groups", "add-user", "group1", "user2"]) cli.run(["groups", "add-user", "group1", "user2"])

View File

@@ -9,22 +9,22 @@ from secret_cli import SecretCli
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
if TYPE_CHECKING: if TYPE_CHECKING:
from test_keys import KeyPair from age_keys import KeyPair
def _test_identities( def _test_identities(
what: str, what: str,
clan_flake: Path, clan_flake: Path,
capsys: pytest.CaptureFixture, capsys: pytest.CaptureFixture,
test_keys: list["KeyPair"], age_keys: list["KeyPair"],
) -> None: ) -> None:
cli = SecretCli() cli = SecretCli()
sops_folder = clan_flake / "sops" sops_folder = clan_flake / "sops"
cli.run([what, "add", "foo", test_keys[0].pubkey]) cli.run([what, "add", "foo", age_keys[0].pubkey])
assert (sops_folder / what / "foo" / "key.json").exists() assert (sops_folder / what / "foo" / "key.json").exists()
with pytest.raises(ClanError): with pytest.raises(ClanError):
cli.run([what, "add", "foo", test_keys[0].pubkey]) cli.run([what, "add", "foo", age_keys[0].pubkey])
cli.run( cli.run(
[ [
@@ -32,7 +32,7 @@ def _test_identities(
"add", "add",
"-f", "-f",
"foo", "foo",
test_keys[0].privkey, age_keys[0].privkey,
] ]
) )
capsys.readouterr() # empty the buffer capsys.readouterr() # empty the buffer
@@ -54,19 +54,19 @@ def _test_identities(
def test_users( def test_users(
clan_flake: Path, capsys: pytest.CaptureFixture, test_keys: list["KeyPair"] clan_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
) -> None: ) -> None:
_test_identities("users", clan_flake, capsys, test_keys) _test_identities("users", clan_flake, capsys, age_keys)
def test_machines( def test_machines(
clan_flake: Path, capsys: pytest.CaptureFixture, test_keys: list["KeyPair"] clan_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
) -> None: ) -> None:
_test_identities("machines", clan_flake, capsys, test_keys) _test_identities("machines", clan_flake, capsys, age_keys)
def test_groups( def test_groups(
clan_flake: Path, capsys: pytest.CaptureFixture, test_keys: list["KeyPair"] clan_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
) -> None: ) -> None:
cli = SecretCli() cli = SecretCli()
capsys.readouterr() # empty the buffer capsys.readouterr() # empty the buffer
@@ -77,13 +77,13 @@ def test_groups(
cli.run(["groups", "add-machine", "group1", "machine1"]) cli.run(["groups", "add-machine", "group1", "machine1"])
with pytest.raises(ClanError): # user does not exist yet with pytest.raises(ClanError): # user does not exist yet
cli.run(["groups", "add-user", "groupb1", "user1"]) cli.run(["groups", "add-user", "groupb1", "user1"])
cli.run(["machines", "add", "machine1", test_keys[0].pubkey]) cli.run(["machines", "add", "machine1", age_keys[0].pubkey])
cli.run(["groups", "add-machine", "group1", "machine1"]) cli.run(["groups", "add-machine", "group1", "machine1"])
# Should this fail? # Should this fail?
cli.run(["groups", "add-machine", "group1", "machine1"]) cli.run(["groups", "add-machine", "group1", "machine1"])
cli.run(["users", "add", "user1", test_keys[0].pubkey]) cli.run(["users", "add", "user1", age_keys[0].pubkey])
cli.run(["groups", "add-user", "group1", "user1"]) cli.run(["groups", "add-user", "group1", "user1"])
capsys.readouterr() # empty the buffer capsys.readouterr() # empty the buffer
@@ -99,7 +99,7 @@ def test_groups(
def test_secrets( def test_secrets(
clan_flake: Path, capsys: pytest.CaptureFixture, test_keys: list["KeyPair"] clan_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
) -> None: ) -> None:
cli = SecretCli() cli = SecretCli()
capsys.readouterr() # empty the buffer capsys.readouterr() # empty the buffer
@@ -125,18 +125,18 @@ def test_secrets(
cli.run(["list"]) cli.run(["list"])
assert capsys.readouterr().out == "key\n" assert capsys.readouterr().out == "key\n"
cli.run(["machines", "add", "machine1", test_keys[0].pubkey]) cli.run(["machines", "add", "machine1", age_keys[0].pubkey])
cli.run(["machines", "add-secret", "machine1", "key"]) cli.run(["machines", "add-secret", "machine1", "key"])
with mock_env(SOPS_AGE_KEY=test_keys[0].privkey, SOPS_AGE_KEY_FILE=""): with mock_env(SOPS_AGE_KEY=age_keys[0].privkey, SOPS_AGE_KEY_FILE=""):
capsys.readouterr() capsys.readouterr()
cli.run(["get", "key"]) cli.run(["get", "key"])
assert capsys.readouterr().out == "foo" assert capsys.readouterr().out == "foo"
cli.run(["machines", "remove-secret", "machine1", "key"]) cli.run(["machines", "remove-secret", "machine1", "key"])
cli.run(["users", "add", "user1", test_keys[1].pubkey]) cli.run(["users", "add", "user1", age_keys[1].pubkey])
cli.run(["users", "add-secret", "user1", "key"]) cli.run(["users", "add-secret", "user1", "key"])
with mock_env(SOPS_AGE_KEY=test_keys[1].privkey, SOPS_AGE_KEY_FILE=""): with mock_env(SOPS_AGE_KEY=age_keys[1].privkey, SOPS_AGE_KEY_FILE=""):
capsys.readouterr() capsys.readouterr()
cli.run(["get", "key"]) cli.run(["get", "key"])
assert capsys.readouterr().out == "foo" assert capsys.readouterr().out == "foo"
@@ -151,7 +151,7 @@ def test_secrets(
capsys.readouterr() # empty the buffer capsys.readouterr() # empty the buffer
cli.run(["set", "--group", "admin-group", "key2"]) cli.run(["set", "--group", "admin-group", "key2"])
with mock_env(SOPS_AGE_KEY=test_keys[1].privkey, SOPS_AGE_KEY_FILE=""): with mock_env(SOPS_AGE_KEY=age_keys[1].privkey, SOPS_AGE_KEY_FILE=""):
capsys.readouterr() capsys.readouterr()
cli.run(["get", "key"]) cli.run(["get", "key"])
assert capsys.readouterr().out == "foo" assert capsys.readouterr().out == "foo"

View File

@@ -6,7 +6,8 @@ import pytest_subprocess.fake_process
from environment import mock_env from environment import mock_env
from pytest_subprocess import utils from pytest_subprocess import utils
import clan_cli.ssh import clan_cli
from clan_cli.ssh import cli
def test_no_args( def test_no_args(
@@ -40,7 +41,7 @@ def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None:
fp.any(), fp.any(),
] ]
fp.register(cmd) fp.register(cmd)
clan_cli.ssh.ssh( cli.ssh(
host=host, host=host,
user=user, user=user,
) )
@@ -64,7 +65,7 @@ def test_ssh_with_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None:
fp.any(), fp.any(),
] ]
fp.register(cmd) fp.register(cmd)
clan_cli.ssh.ssh( cli.ssh(
host=host, host=host,
user=user, user=user,
password="XXX", password="XXX",
@@ -75,5 +76,5 @@ def test_ssh_with_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None:
def test_qrcode_scan(fp: pytest_subprocess.fake_process.FakeProcess) -> None: def test_qrcode_scan(fp: pytest_subprocess.fake_process.FakeProcess) -> None:
cmd: list[Union[str, utils.Any]] = [fp.any()] cmd: list[Union[str, utils.Any]] = [fp.any()]
fp.register(cmd, stdout="https://test.test") fp.register(cmd, stdout="https://test.test")
result = clan_cli.ssh.qrcode_scan("test.png") result = cli.qrcode_scan("test.png")
assert result == "https://test.test" assert result == "https://test.test"