Merge pull request 'api/tasks: prefix impure actions with run' (#4239) from api-cleanup into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4239
This commit is contained in:
hsjobeki
2025-07-07 11:28:07 +00:00
20 changed files with 69 additions and 56 deletions

View File

@@ -75,7 +75,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
} }
setInstalling(true); setInstalling(true);
await callApi("install_machine", { await callApi("run_machine_install", {
opts: { opts: {
machine: { machine: {
name: name, name: name,
@@ -163,7 +163,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
} }
await callApi( await callApi(
"deploy_machine", "run_machine_deploy",
{ {
machine: { machine: {
name: name, name: name,

View File

@@ -23,7 +23,7 @@ const EditClanForm = (props: EditClanFormProps) => {
const handleSubmit: SubmitHandler<GeneralData> = async (values, event) => { const handleSubmit: SubmitHandler<GeneralData> = async (values, event) => {
await toast.promise( await toast.promise(
(async () => { (async () => {
await callApi("update_clan_meta", { await callApi("set_clan_details", {
options: { options: {
flake: { identifier: props.directory }, flake: { identifier: props.directory },
meta: values, meta: values,

View File

@@ -157,7 +157,7 @@ export const Flash = () => {
console.log("Confirmed flash:", values); console.log("Confirmed flash:", values);
try { try {
await toast.promise( await toast.promise(
callApi("flash_machine", { callApi("run_machine_flash", {
machine: { machine: {
name: values.machine.devicePath, name: values.machine.devicePath,
flake: { flake: {

View File

@@ -120,7 +120,7 @@ export function InstallMachine(props: InstallMachineProps) {
throw new Error("No target host found for the machine"); throw new Error("No target host found for the machine");
} }
const installPromise = callApi("install_machine", { const installPromise = callApi("run_machine_install", {
opts: { opts: {
machine: { machine: {
name: props.name, name: props.name,

View File

@@ -149,7 +149,7 @@ export function MachineForm(props: MachineFormProps) {
setIsUpdating(true); setIsUpdating(true);
const r = await callApi( const r = await callApi(
"deploy_machine", "run_machine_deploy",
{ {
machine: { machine: {
name: machine, name: machine,

View File

@@ -23,7 +23,7 @@ const EditClanForm = (props: EditClanFormProps) => {
const handleSubmit: SubmitHandler<GeneralData> = async (values, event) => { const handleSubmit: SubmitHandler<GeneralData> = async (values, event) => {
await toast.promise( await toast.promise(
(async () => { (async () => {
await callApi("update_clan_meta", { await callApi("set_clan_details", {
options: { options: {
flake: { identifier: props.directory }, flake: { identifier: props.directory },
meta: values, meta: values,

View File

@@ -37,7 +37,7 @@ class Disk:
# TODO: unify this with machine install # TODO: unify this with machine install
@API.register @API.register
def flash_machine( def run_machine_flash(
machine: Machine, machine: Machine,
*, *,
mode: str, mode: str,

View File

@@ -11,7 +11,7 @@ from clan_lib.machines.machines import Machine
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from .flash import Disk, SystemConfig, flash_machine from .flash import Disk, SystemConfig, run_machine_flash
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -84,7 +84,7 @@ def flash_command(args: argparse.Namespace) -> None:
if ask != "y": if ask != "y":
return return
flash_machine( run_machine_flash(
machine, machine,
mode=opts.mode, mode=opts.mode,
disks=opts.disks, disks=opts.disks,

View File

@@ -4,7 +4,7 @@ import sys
from pathlib import Path from pathlib import Path
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
from clan_lib.machines.install import BuildOn, InstallOptions, install_machine from clan_lib.machines.install import BuildOn, InstallOptions, run_machine_install
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.ssh.remote import Remote from clan_lib.ssh.remote import Remote
@@ -65,7 +65,7 @@ def install_command(args: argparse.Namespace) -> None:
if ask != "y": if ask != "y":
return None return None
return install_machine( return run_machine_install(
InstallOptions( InstallOptions(
machine=machine, machine=machine,
kexec=args.kexec, kexec=args.kexec,

View File

@@ -9,7 +9,7 @@ from clan_lib.machines.actions import list_machines
from clan_lib.machines.list import instantiate_inventory_to_machines from clan_lib.machines.list import instantiate_inventory_to_machines
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.machines.suggestions import validate_machine_names from clan_lib.machines.suggestions import validate_machine_names
from clan_lib.machines.update import deploy_machine from clan_lib.machines.update import run_machine_deploy
from clan_lib.nix import nix_config from clan_lib.nix import nix_config
from clan_lib.ssh.remote import Remote from clan_lib.ssh.remote import Remote
@@ -144,7 +144,7 @@ def update_command(args: argparse.Namespace) -> None:
tid=machine.name, tid=machine.name,
async_ctx=AsyncContext(prefix=machine.name), async_ctx=AsyncContext(prefix=machine.name),
), ),
deploy_machine, run_machine_deploy,
machine=machine, machine=machine,
target_host=target_host, target_host=target_host,
build_host=machine.build_host(), build_host=machine.build_host(),

View File

@@ -13,7 +13,6 @@ from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import IO, Any from typing import IO, Any
from clan_lib.api import API
from clan_lib.cmd import Log, RunOpts, run from clan_lib.cmd import Log, RunOpts, run
from clan_lib.dirs import user_config_dir from clan_lib.dirs import user_config_dir
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
@@ -398,7 +397,6 @@ def default_admin_private_key_path() -> Path:
return user_config_dir() / "sops" / "age" / "keys.txt" return user_config_dir() / "sops" / "age" / "keys.txt"
@API.register
def maybe_get_admin_public_keys() -> list[SopsKey] | None: def maybe_get_admin_public_keys() -> list[SopsKey] | None:
keyring = SopsKey.collect_public_keys() keyring = SopsKey.collect_public_keys()

View File

@@ -98,7 +98,7 @@ def find_reachable_host(deploy_info: DeployInfo) -> Remote | None:
return deploy_info.addrs[0] return deploy_info.addrs[0]
for addr in deploy_info.addrs: for addr in deploy_info.addrs:
if addr.is_ssh_reachable(): if addr.check_machine_ssh_reachable():
return addr return addr
return None return None

View File

@@ -12,6 +12,7 @@ from clan_lib.errors import ClanError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# TODO: Unify with "create clan" should be done automatically
@API.register @API.register
def create_secrets_user( def create_secrets_user(
flake_dir: Path, user: str | None = None, force: bool = False flake_dir: Path, user: str | None = None, force: bool = False

View File

@@ -14,7 +14,7 @@ class UpdateOptions:
@API.register @API.register
def update_clan_meta(options: UpdateOptions) -> InventorySnapshot: def set_clan_details(options: UpdateOptions) -> InventorySnapshot:
inventory_store = InventoryStore(options.flake) inventory_store = InventoryStore(options.flake)
inventory = inventory_store.read() inventory = inventory_store.read()
set_value_by_path(inventory, "meta", options.meta) set_value_by_path(inventory, "meta", options.meta)

View File

@@ -17,7 +17,7 @@ def example_function() -> None:
"""Example function for creating logs.""" """Example function for creating logs."""
def deploy_machine() -> None: def run_machine_deploy() -> None:
"""Function for deploying machines.""" """Function for deploying machines."""
@@ -41,7 +41,7 @@ def main() -> None:
for repo in repos: for repo in repos:
for machine in machines: for machine in machines:
log_manager.create_log_file( log_manager.create_log_file(
deploy_machine, run_machine_deploy,
f"deploy_{machine}", f"deploy_{machine}",
["clans", repo, "machines", machine], ["clans", repo, "machines", machine],
) )

View File

@@ -17,7 +17,7 @@ from clan_lib.log_manager import (
# Test functions for log creation # Test functions for log creation
def deploy_machine() -> None: def run_machine_deploy() -> None:
"""Test function for deploying machines.""" """Test function for deploying machines."""
@@ -194,13 +194,13 @@ class TestLogFileCreation:
for repo in repos: for repo in repos:
for machine in machines: for machine in machines:
log_file = configured_log_manager.create_log_file( log_file = configured_log_manager.create_log_file(
deploy_machine, run_machine_deploy,
f"deploy_{machine}", f"deploy_{machine}",
["clans", repo, "machines", machine], ["clans", repo, "machines", machine],
) )
assert log_file.op_key == f"deploy_{machine}" assert log_file.op_key == f"deploy_{machine}"
assert log_file.func_name == "deploy_machine" assert log_file.func_name == "run_machine_deploy"
assert log_file.get_file_path().exists() assert log_file.get_file_path().exists()
# Check the group structure includes URL encoding for dynamic parts # Check the group structure includes URL encoding for dynamic parts
@@ -241,7 +241,7 @@ class TestFilterFunction:
"""Test that empty filter returns top-level groups.""" """Test that empty filter returns top-level groups."""
# Create some log files first # Create some log files first
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, "test_op", ["clans", "repo1", "machines", "machine1"] run_machine_deploy, "test_op", ["clans", "repo1", "machines", "machine1"]
) )
top_level = configured_log_manager.filter([]) top_level = configured_log_manager.filter([])
@@ -258,7 +258,7 @@ class TestFilterFunction:
for repo in repos: for repo in repos:
for machine in machines: for machine in machines:
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, run_machine_deploy,
f"deploy_{machine}", f"deploy_{machine}",
["clans", repo, "machines", machine], ["clans", repo, "machines", machine],
) )
@@ -281,7 +281,7 @@ class TestFilterFunction:
"""Test filtering with specific date.""" """Test filtering with specific date."""
# Create log file # Create log file
log_file = configured_log_manager.create_log_file( log_file = configured_log_manager.create_log_file(
deploy_machine, "test_op", ["clans", "repo1", "machines", "machine1"] run_machine_deploy, "test_op", ["clans", "repo1", "machines", "machine1"]
) )
# Filter with the specific date # Filter with the specific date
@@ -308,14 +308,16 @@ class TestGetLogFile:
"""Test getting log file by operation key.""" """Test getting log file by operation key."""
# Create log file # Create log file
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, "deploy_wintux", ["clans", "repo1", "machines", "wintux"] run_machine_deploy,
"deploy_wintux",
["clans", "repo1", "machines", "wintux"],
) )
# Find it by op_key # Find it by op_key
found_log_file = configured_log_manager.get_log_file("deploy_wintux") found_log_file = configured_log_manager.get_log_file("deploy_wintux")
assert found_log_file is not None assert found_log_file is not None
assert found_log_file.op_key == "deploy_wintux" assert found_log_file.op_key == "deploy_wintux"
assert found_log_file.func_name == "deploy_machine" assert found_log_file.func_name == "run_machine_deploy"
def test_get_log_file_with_selector( def test_get_log_file_with_selector(
self, configured_log_manager: LogManager self, configured_log_manager: LogManager
@@ -323,10 +325,14 @@ class TestGetLogFile:
"""Test getting log file with specific selector like example_usage.py.""" """Test getting log file with specific selector like example_usage.py."""
# Create log files in different locations # Create log files in different locations
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, "deploy_wintux", ["clans", "repo1", "machines", "wintux"] run_machine_deploy,
"deploy_wintux",
["clans", "repo1", "machines", "wintux"],
) )
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, "deploy_wintux", ["clans", "repo2", "machines", "wintux"] run_machine_deploy,
"deploy_wintux",
["clans", "repo2", "machines", "wintux"],
) )
# Find specific one using selector # Find specific one using selector
@@ -341,7 +347,7 @@ class TestGetLogFile:
"""Test getting log file with specific date.""" """Test getting log file with specific date."""
# Create log file # Create log file
log_file = configured_log_manager.create_log_file( log_file = configured_log_manager.create_log_file(
deploy_machine, "deploy_demo", ["clans", "repo1", "machines", "demo"] run_machine_deploy, "deploy_demo", ["clans", "repo1", "machines", "demo"]
) )
# Find it by op_key and date # Find it by op_key and date
@@ -378,10 +384,10 @@ class TestListLogDays:
"""Test listing log days when logs exist.""" """Test listing log days when logs exist."""
# Create log files # Create log files
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, "op1", ["clans", "repo1", "machines", "machine1"] run_machine_deploy, "op1", ["clans", "repo1", "machines", "machine1"]
) )
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, "op2", ["clans", "repo2", "machines", "machine2"] run_machine_deploy, "op2", ["clans", "repo2", "machines", "machine2"]
) )
days = configured_log_manager.list_log_days() days = configured_log_manager.list_log_days()
@@ -406,7 +412,7 @@ class TestApiCompatibility:
for repo in repos: for repo in repos:
for machine in machines: for machine in machines:
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, run_machine_deploy,
f"deploy_{machine}", f"deploy_{machine}",
["clans", repo, "machines", machine], ["clans", repo, "machines", machine],
) )
@@ -741,19 +747,19 @@ class TestLogFileSorting:
# This simulates the realistic scenario where the same operation runs on different machines # This simulates the realistic scenario where the same operation runs on different machines
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, run_machine_deploy,
"deploy_operation", "deploy_operation",
["clans", "repo1", "machines", "machine1"], ["clans", "repo1", "machines", "machine1"],
) )
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, run_machine_deploy,
"deploy_operation", "deploy_operation",
["clans", "repo1", "machines", "machine2"], ["clans", "repo1", "machines", "machine2"],
) )
configured_log_manager.create_log_file( configured_log_manager.create_log_file(
deploy_machine, run_machine_deploy,
"deploy_operation", "deploy_operation",
["clans", "repo2", "machines", "machine1"], ["clans", "repo2", "machines", "machine1"],
) )
@@ -819,7 +825,7 @@ class TestURLEncoding:
# Create log file with special characters # Create log file with special characters
log_file = configured_log_manager.create_log_file( log_file = configured_log_manager.create_log_file(
deploy_machine, run_machine_deploy,
"deploy_special", "deploy_special",
["clans", special_repo, "machines", special_machine], ["clans", special_repo, "machines", special_machine],
) )

View File

@@ -39,7 +39,7 @@ class InstallOptions:
@API.register @API.register
def install_machine(opts: InstallOptions, target_host: Remote) -> None: def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
machine = opts.machine machine = opts.machine
machine.debug(f"installing {machine.name}") machine.debug(f"installing {machine.name}")

View File

@@ -103,7 +103,7 @@ def upload_sources(machine: Machine, ssh: Remote) -> str:
@API.register @API.register
def deploy_machine( def run_machine_deploy(
machine: Machine, target_host: Remote, build_host: Remote | None machine: Machine, target_host: Remote, build_host: Remote | None
) -> None: ) -> None:
with ExitStack() as stack: with ExitStack() as stack:

View File

@@ -12,7 +12,6 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from shlex import quote from shlex import quote
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Literal
from clan_lib.api import API from clan_lib.api import API
from clan_lib.cmd import ClanCmdError, ClanCmdTimeoutError, CmdOut, RunOpts, run from clan_lib.cmd import ClanCmdError, ClanCmdTimeoutError, CmdOut, RunOpts, run
@@ -74,9 +73,9 @@ class Remote:
private_key=private_key if private_key is not None else self.private_key, private_key=private_key if private_key is not None else self.private_key,
password=password if password is not None else self.password, password=password if password is not None else self.password,
forward_agent=self.forward_agent, forward_agent=self.forward_agent,
host_key_check=host_key_check host_key_check=(
if host_key_check is not None host_key_check if host_key_check is not None else self.host_key_check
else self.host_key_check, ),
verbose_ssh=self.verbose_ssh, verbose_ssh=self.verbose_ssh,
ssh_options=self.ssh_options, ssh_options=self.ssh_options,
tor_socks=tor_socks if tor_socks is not None else self.tor_socks, tor_socks=tor_socks if tor_socks is not None else self.tor_socks,
@@ -425,8 +424,8 @@ class Remote:
self.check_sshpass_errorcode(res) self.check_sshpass_errorcode(res)
def is_ssh_reachable(self) -> bool: def check_machine_ssh_reachable(self) -> bool:
return is_ssh_reachable(self) return check_machine_ssh_reachable(self).ok
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -435,10 +434,16 @@ class ConnectionOptions:
retries: int = 10 retries: int = 10
@dataclass
class CheckResult:
ok: bool
reason: str | None = None
@API.register @API.register
def can_ssh_login( def check_machine_ssh_login(
remote: Remote, opts: ConnectionOptions | None = None remote: Remote, opts: ConnectionOptions | None = None
) -> Literal["Online", "Offline"]: ) -> CheckResult:
if opts is None: if opts is None:
opts = ConnectionOptions() opts = ConnectionOptions()
@@ -449,7 +454,7 @@ def can_ssh_login(
["true"], ["true"],
RunOpts(timeout=opts.timeout, needs_user_terminal=True), RunOpts(timeout=opts.timeout, needs_user_terminal=True),
) )
return "Online" return CheckResult(True)
except ClanCmdTimeoutError: except ClanCmdTimeoutError:
pass pass
except ClanCmdError as e: except ClanCmdError as e:
@@ -458,11 +463,13 @@ def can_ssh_login(
else: else:
time.sleep(opts.timeout) time.sleep(opts.timeout)
return "Offline" return CheckResult(False, f"failed after {opts.retries} attempts")
@API.register @API.register
def is_ssh_reachable(remote: Remote, opts: ConnectionOptions | None = None) -> bool: def check_machine_ssh_reachable(
remote: Remote, opts: ConnectionOptions | None = None
) -> CheckResult:
if opts is None: if opts is None:
opts = ConnectionOptions() opts = ConnectionOptions()
@@ -472,10 +479,10 @@ def is_ssh_reachable(remote: Remote, opts: ConnectionOptions | None = None) -> b
sock.settimeout(opts.timeout) sock.settimeout(opts.timeout)
try: try:
sock.connect((remote.address, remote.port or 22)) sock.connect((remote.address, remote.port or 22))
return True return CheckResult(True)
except (TimeoutError, OSError): except (TimeoutError, OSError):
pass pass
else: else:
time.sleep(opts.timeout) time.sleep(opts.timeout)
return False return CheckResult(False, f"failed after {opts.retries} attempts")

View File

@@ -33,7 +33,7 @@ from clan_lib.nix_models.clan import (
) )
from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy
from clan_lib.persist.util import set_value_by_path from clan_lib.persist.util import set_value_by_path
from clan_lib.ssh.remote import Remote, can_ssh_login from clan_lib.ssh.remote import Remote, check_machine_ssh_login
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -190,8 +190,9 @@ def test_clan_create_api(
target_host = machine.target_host().override( target_host = machine.target_host().override(
private_key=private_key, host_key_check="none" private_key=private_key, host_key_check="none"
) )
result = can_ssh_login(target_host) assert check_machine_ssh_login(target_host).ok, (
assert result == "Online", f"Machine {machine.name} is not online" f"Machine {machine.name} is not online"
)
ssh_keys = [ ssh_keys = [
SSHKeyPair( SSHKeyPair(