Merge pull request 'UI: Init iwd service for single wifi' (#2033) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -6,20 +6,18 @@ from pathlib import Path
|
|||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
# These imports are unused, but necessary for @API.register to run once.
|
# These imports are unused, but necessary for @API.register to run once.
|
||||||
from .api import admin, directory, disk, mdns_discovery, modules
|
from .api import admin, directory, disk, iwd, mdns_discovery, modules
|
||||||
from .arg_actions import AppendOptionAction
|
from .arg_actions import AppendOptionAction
|
||||||
from .clan import show, update
|
from .clan import show, update
|
||||||
|
|
||||||
# API endpoints that are not used in the cli.
|
# API endpoints that are not used in the cli.
|
||||||
__all__ = ["directory", "mdns_discovery", "modules", "update", "disk", "admin"]
|
__all__ = ["directory", "mdns_discovery", "modules", "update", "disk", "admin", "iwd"]
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
backups,
|
backups,
|
||||||
clan,
|
clan,
|
||||||
facts,
|
|
||||||
flash,
|
flash,
|
||||||
history,
|
history,
|
||||||
machines,
|
|
||||||
secrets,
|
secrets,
|
||||||
state,
|
state,
|
||||||
vars,
|
vars,
|
||||||
@@ -29,7 +27,9 @@ from .clan_uri import FlakeId
|
|||||||
from .custom_logger import setup_logging
|
from .custom_logger import setup_logging
|
||||||
from .dirs import get_clan_flake_toplevel_or_env
|
from .dirs import get_clan_flake_toplevel_or_env
|
||||||
from .errors import ClanCmdError, ClanError
|
from .errors import ClanCmdError, ClanError
|
||||||
|
from .facts import cli as facts
|
||||||
from .hyperlink import help_hyperlink
|
from .hyperlink import help_hyperlink
|
||||||
|
from .machines import cli as machines
|
||||||
from .profiler import profile
|
from .profiler import profile
|
||||||
from .ssh import cli as ssh_cli
|
from .ssh import cli as ssh_cli
|
||||||
|
|
||||||
|
|||||||
100
pkgs/clan-cli/clan_cli/api/iwd.py
Normal file
100
pkgs/clan-cli/clan_cli/api/iwd.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_cli.clan_uri import FlakeId
|
||||||
|
from clan_cli.errors import ClanError
|
||||||
|
from clan_cli.facts.generate import generate_facts
|
||||||
|
from clan_cli.inventory import (
|
||||||
|
IwdConfig,
|
||||||
|
IwdConfigNetwork,
|
||||||
|
ServiceIwd,
|
||||||
|
ServiceIwdRole,
|
||||||
|
ServiceIwdRoleDefault,
|
||||||
|
ServiceMeta,
|
||||||
|
load_inventory_eval,
|
||||||
|
save_inventory,
|
||||||
|
)
|
||||||
|
from clan_cli.machines.machines import Machine
|
||||||
|
from clan_cli.secrets.sops import (
|
||||||
|
maybe_get_public_key,
|
||||||
|
maybe_get_user_or_machine,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import API
|
||||||
|
|
||||||
|
|
||||||
|
def instance_name(machine_name: str) -> str:
|
||||||
|
return f"{machine_name}_wifi_0_"
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def get_iwd_service(base_url: str, machine_name: str) -> ServiceIwd | None:
|
||||||
|
"""
|
||||||
|
Return the admin service of a clan.
|
||||||
|
|
||||||
|
There is only one admin service. This might be changed in the future
|
||||||
|
"""
|
||||||
|
inventory = load_inventory_eval(base_url)
|
||||||
|
return inventory.services.iwd.get(instance_name(machine_name))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NetworkConfig:
|
||||||
|
ssid: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def set_iwd_service_for_machine(
|
||||||
|
base_url: str, machine_name: str, networks: dict[str, NetworkConfig]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Set the admin service of a clan
|
||||||
|
Every machine is by default part of the admin service via the 'all' tag
|
||||||
|
"""
|
||||||
|
_instance_name = instance_name(machine_name)
|
||||||
|
|
||||||
|
inventory = load_inventory_eval(base_url)
|
||||||
|
|
||||||
|
instance = ServiceIwd(
|
||||||
|
meta=ServiceMeta(name="wifi_0"),
|
||||||
|
roles=ServiceIwdRole(
|
||||||
|
default=ServiceIwdRoleDefault(
|
||||||
|
machines=[machine_name],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
config=IwdConfig(
|
||||||
|
networks={k: IwdConfigNetwork(v.ssid) for k, v in networks.items()}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
inventory.services.iwd[_instance_name] = instance
|
||||||
|
|
||||||
|
save_inventory(
|
||||||
|
inventory,
|
||||||
|
base_url,
|
||||||
|
f"Set iwd service: '{_instance_name}'",
|
||||||
|
)
|
||||||
|
|
||||||
|
pubkey = maybe_get_public_key()
|
||||||
|
if not pubkey:
|
||||||
|
# TODO: do this automatically
|
||||||
|
# pubkey = generate_key()
|
||||||
|
raise ClanError(msg="No public key found. Please initialize your key.")
|
||||||
|
|
||||||
|
registered_key = maybe_get_user_or_machine(Path(base_url), pubkey)
|
||||||
|
if not registered_key:
|
||||||
|
# TODO: do this automatically
|
||||||
|
# username = os.getlogin()
|
||||||
|
# add_user(Path(base_url), username, pubkey, force=False)
|
||||||
|
raise ClanError(msg="Your public key is not registered for use with this clan.")
|
||||||
|
|
||||||
|
password_dict = {f"iwd.{net.ssid}": net.password for net in networks.values()}
|
||||||
|
for net in networks.values():
|
||||||
|
generate_facts(
|
||||||
|
service=f"iwd.{net.ssid}",
|
||||||
|
machines=[Machine(machine_name, FlakeId(base_url))],
|
||||||
|
regenerate=True,
|
||||||
|
# Just returns the password
|
||||||
|
prompt=lambda service, _msg: password_dict[service],
|
||||||
|
)
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
# !/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from clan_cli.hyperlink import help_hyperlink
|
|
||||||
|
|
||||||
from .check import register_check_parser
|
|
||||||
from .generate import register_generate_parser
|
|
||||||
from .list import register_list_parser
|
|
||||||
from .upload import register_upload_parser
|
|
||||||
|
|
||||||
|
|
||||||
# takes a (sub)parser and configures it
|
|
||||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
|
||||||
subparser = parser.add_subparsers(
|
|
||||||
title="command",
|
|
||||||
description="the command to run",
|
|
||||||
help="the command to run",
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
check_parser = subparser.add_parser(
|
|
||||||
"check",
|
|
||||||
help="check if facts are up to date",
|
|
||||||
epilog=(
|
|
||||||
f"""
|
|
||||||
This subcommand allows checking if all facts are up to date.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan facts check [MACHINE]
|
|
||||||
Will check facts for the specified machine.
|
|
||||||
|
|
||||||
|
|
||||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/getting-started/secrets")}
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_check_parser(check_parser)
|
|
||||||
|
|
||||||
list_parser = subparser.add_parser(
|
|
||||||
"list",
|
|
||||||
help="list all facts",
|
|
||||||
epilog=(
|
|
||||||
f"""
|
|
||||||
This subcommand allows listing all public facts for a specific machine.
|
|
||||||
|
|
||||||
The resulting list will be a json string with the name of the fact as its key
|
|
||||||
and the fact itself as it's value.
|
|
||||||
|
|
||||||
This is how an example output might look like:
|
|
||||||
```
|
|
||||||
\u007b
|
|
||||||
"[FACT_NAME]": "[FACT]"
|
|
||||||
\u007d
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan facts list [MACHINE]
|
|
||||||
Will list facts for the specified machine.
|
|
||||||
|
|
||||||
|
|
||||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/getting-started/secrets")}
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_list_parser(list_parser)
|
|
||||||
|
|
||||||
parser_generate = subparser.add_parser(
|
|
||||||
"generate",
|
|
||||||
help="generate public and secret facts for machines",
|
|
||||||
epilog=(
|
|
||||||
f"""
|
|
||||||
This subcommand allows control of the generation of facts.
|
|
||||||
Often this function will be invoked automatically on deploying machines,
|
|
||||||
but there are situations the user may want to have more granular control,
|
|
||||||
especially for the regeneration of certain services.
|
|
||||||
|
|
||||||
A service is an included clan-module that implements facts generation functionality.
|
|
||||||
For example the zerotier module will generate private and public facts.
|
|
||||||
In this case the public fact will be the resulting zerotier-ip of the machine.
|
|
||||||
The secret fact will be the zerotier-identity-secret, which is used by zerotier
|
|
||||||
to prove the machine has control of the zerotier-ip.
|
|
||||||
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan facts generate
|
|
||||||
Will generate facts for all machines.
|
|
||||||
|
|
||||||
$ clan facts generate [MACHINE]
|
|
||||||
Will generate facts for the specified machine.
|
|
||||||
|
|
||||||
$ clan facts generate [MACHINE] --service [SERVICE]
|
|
||||||
Will generate facts for the specified machine for the specified service.
|
|
||||||
|
|
||||||
$ clan facts generate --service [SERVICE] --regenerate
|
|
||||||
Will regenerate facts, if they are already generated for a specific service.
|
|
||||||
This is especially useful for resetting certain passwords while leaving the rest
|
|
||||||
of the facts for a machine in place.
|
|
||||||
|
|
||||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/getting-started/secrets")}
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_generate_parser(parser_generate)
|
|
||||||
|
|
||||||
parser_upload = subparser.add_parser(
|
|
||||||
"upload",
|
|
||||||
help="upload secrets for machines",
|
|
||||||
epilog=(
|
|
||||||
f"""
|
|
||||||
This subcommand allows uploading secrets to remote machines.
|
|
||||||
|
|
||||||
If using sops as a secret backend it will upload the private key to the machine.
|
|
||||||
If using password store it uploads all the secrets you manage to the machine.
|
|
||||||
|
|
||||||
The default backend is sops.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan facts upload [MACHINE]
|
|
||||||
Will upload secrets to a specific machine.
|
|
||||||
|
|
||||||
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/getting-started/secrets")}
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_upload_parser(parser_upload)
|
|
||||||
|
|||||||
133
pkgs/clan-cli/clan_cli/facts/cli.py
Normal file
133
pkgs/clan-cli/clan_cli/facts/cli.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# !/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from clan_cli.hyperlink import help_hyperlink
|
||||||
|
|
||||||
|
from .check import register_check_parser
|
||||||
|
from .generate import register_generate_parser
|
||||||
|
from .list import register_list_parser
|
||||||
|
from .upload import register_upload_parser
|
||||||
|
|
||||||
|
|
||||||
|
# takes a (sub)parser and configures it
|
||||||
|
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
subparser = parser.add_subparsers(
|
||||||
|
title="command",
|
||||||
|
description="the command to run",
|
||||||
|
help="the command to run",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
check_parser = subparser.add_parser(
|
||||||
|
"check",
|
||||||
|
help="check if facts are up to date",
|
||||||
|
epilog=(
|
||||||
|
f"""
|
||||||
|
This subcommand allows checking if all facts are up to date.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ clan facts check [MACHINE]
|
||||||
|
Will check facts for the specified machine.
|
||||||
|
|
||||||
|
|
||||||
|
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/getting-started/secrets")}
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
)
|
||||||
|
register_check_parser(check_parser)
|
||||||
|
|
||||||
|
list_parser = subparser.add_parser(
|
||||||
|
"list",
|
||||||
|
help="list all facts",
|
||||||
|
epilog=(
|
||||||
|
f"""
|
||||||
|
This subcommand allows listing all public facts for a specific machine.
|
||||||
|
|
||||||
|
The resulting list will be a json string with the name of the fact as its key
|
||||||
|
and the fact itself as it's value.
|
||||||
|
|
||||||
|
This is how an example output might look like:
|
||||||
|
```
|
||||||
|
\u007b
|
||||||
|
"[FACT_NAME]": "[FACT]"
|
||||||
|
\u007d
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ clan facts list [MACHINE]
|
||||||
|
Will list facts for the specified machine.
|
||||||
|
|
||||||
|
|
||||||
|
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/getting-started/secrets")}
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
)
|
||||||
|
register_list_parser(list_parser)
|
||||||
|
|
||||||
|
parser_generate = subparser.add_parser(
|
||||||
|
"generate",
|
||||||
|
help="generate public and secret facts for machines",
|
||||||
|
epilog=(
|
||||||
|
f"""
|
||||||
|
This subcommand allows control of the generation of facts.
|
||||||
|
Often this function will be invoked automatically on deploying machines,
|
||||||
|
but there are situations the user may want to have more granular control,
|
||||||
|
especially for the regeneration of certain services.
|
||||||
|
|
||||||
|
A service is an included clan-module that implements facts generation functionality.
|
||||||
|
For example the zerotier module will generate private and public facts.
|
||||||
|
In this case the public fact will be the resulting zerotier-ip of the machine.
|
||||||
|
The secret fact will be the zerotier-identity-secret, which is used by zerotier
|
||||||
|
to prove the machine has control of the zerotier-ip.
|
||||||
|
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ clan facts generate
|
||||||
|
Will generate facts for all machines.
|
||||||
|
|
||||||
|
$ clan facts generate [MACHINE]
|
||||||
|
Will generate facts for the specified machine.
|
||||||
|
|
||||||
|
$ clan facts generate [MACHINE] --service [SERVICE]
|
||||||
|
Will generate facts for the specified machine for the specified service.
|
||||||
|
|
||||||
|
$ clan facts generate --service [SERVICE] --regenerate
|
||||||
|
Will regenerate facts, if they are already generated for a specific service.
|
||||||
|
This is especially useful for resetting certain passwords while leaving the rest
|
||||||
|
of the facts for a machine in place.
|
||||||
|
|
||||||
|
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/getting-started/secrets")}
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
)
|
||||||
|
register_generate_parser(parser_generate)
|
||||||
|
|
||||||
|
parser_upload = subparser.add_parser(
|
||||||
|
"upload",
|
||||||
|
help="upload secrets for machines",
|
||||||
|
epilog=(
|
||||||
|
f"""
|
||||||
|
This subcommand allows uploading secrets to remote machines.
|
||||||
|
|
||||||
|
If using sops as a secret backend it will upload the private key to the machine.
|
||||||
|
If using password store it uploads all the secrets you manage to the machine.
|
||||||
|
|
||||||
|
The default backend is sops.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ clan facts upload [MACHINE]
|
||||||
|
Will upload secrets to a specific machine.
|
||||||
|
|
||||||
|
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/getting-started/secrets")}
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
)
|
||||||
|
register_upload_parser(parser_upload)
|
||||||
@@ -68,7 +68,7 @@ def generate_service_facts(
|
|||||||
secret_facts_store: SecretStoreBase,
|
secret_facts_store: SecretStoreBase,
|
||||||
public_facts_store: FactStoreBase,
|
public_facts_store: FactStoreBase,
|
||||||
tmpdir: Path,
|
tmpdir: Path,
|
||||||
prompt: Callable[[str], str],
|
prompt: Callable[[str, str], str],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
service_dir = tmpdir / service
|
service_dir = tmpdir / service
|
||||||
# check if all secrets exist and generate them if at least one is missing
|
# check if all secrets exist and generate them if at least one is missing
|
||||||
@@ -93,7 +93,9 @@ def generate_service_facts(
|
|||||||
else:
|
else:
|
||||||
generator = machine.facts_data[service]["generator"]["finalScript"]
|
generator = machine.facts_data[service]["generator"]["finalScript"]
|
||||||
if machine.facts_data[service]["generator"]["prompt"]:
|
if machine.facts_data[service]["generator"]["prompt"]:
|
||||||
prompt_value = prompt(machine.facts_data[service]["generator"]["prompt"])
|
prompt_value = prompt(
|
||||||
|
service, machine.facts_data[service]["generator"]["prompt"]
|
||||||
|
)
|
||||||
env["prompt_value"] = prompt_value
|
env["prompt_value"] = prompt_value
|
||||||
if sys.platform == "linux":
|
if sys.platform == "linux":
|
||||||
cmd = bubblewrap_cmd(generator, facts_dir, secrets_dir)
|
cmd = bubblewrap_cmd(generator, facts_dir, secrets_dir)
|
||||||
@@ -137,7 +139,7 @@ def generate_service_facts(
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def prompt_func(text: str) -> str:
|
def prompt_func(service: str, text: str) -> str:
|
||||||
print(f"{text}: ")
|
print(f"{text}: ")
|
||||||
return read_multiline_input()
|
return read_multiline_input()
|
||||||
|
|
||||||
@@ -147,7 +149,7 @@ def _generate_facts_for_machine(
|
|||||||
service: str | None,
|
service: str | None,
|
||||||
regenerate: bool,
|
regenerate: bool,
|
||||||
tmpdir: Path,
|
tmpdir: Path,
|
||||||
prompt: Callable[[str], str] = prompt_func,
|
prompt: Callable[[str, str], str] = prompt_func,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
local_temp = tmpdir / machine.name
|
local_temp = tmpdir / machine.name
|
||||||
local_temp.mkdir()
|
local_temp.mkdir()
|
||||||
@@ -189,7 +191,7 @@ def generate_facts(
|
|||||||
machines: list[Machine],
|
machines: list[Machine],
|
||||||
service: str | None,
|
service: str | None,
|
||||||
regenerate: bool,
|
regenerate: bool,
|
||||||
prompt: Callable[[str], str] = prompt_func,
|
prompt: Callable[[str, str], str] = prompt_func,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
was_regenerated = False
|
was_regenerated = False
|
||||||
with TemporaryDirectory() as tmp:
|
with TemporaryDirectory() as tmp:
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ from clan_cli.nix import nix_eval
|
|||||||
from .classes import (
|
from .classes import (
|
||||||
AdminConfig,
|
AdminConfig,
|
||||||
Inventory,
|
Inventory,
|
||||||
|
IwdConfig,
|
||||||
|
IwdConfigNetwork,
|
||||||
# Machine classes
|
# Machine classes
|
||||||
Machine,
|
Machine,
|
||||||
MachineDeploy,
|
MachineDeploy,
|
||||||
@@ -40,6 +42,10 @@ from .classes import (
|
|||||||
ServiceBorgbackupRole,
|
ServiceBorgbackupRole,
|
||||||
ServiceBorgbackupRoleClient,
|
ServiceBorgbackupRoleClient,
|
||||||
ServiceBorgbackupRoleServer,
|
ServiceBorgbackupRoleServer,
|
||||||
|
# IWD
|
||||||
|
ServiceIwd,
|
||||||
|
ServiceIwdRole,
|
||||||
|
ServiceIwdRoleDefault,
|
||||||
ServiceMeta,
|
ServiceMeta,
|
||||||
# Single Disk service
|
# Single Disk service
|
||||||
ServiceSingleDisk,
|
ServiceSingleDisk,
|
||||||
@@ -73,6 +79,12 @@ __all__ = [
|
|||||||
"ServiceAdminRole",
|
"ServiceAdminRole",
|
||||||
"ServiceAdminRoleDefault",
|
"ServiceAdminRoleDefault",
|
||||||
"AdminConfig",
|
"AdminConfig",
|
||||||
|
# IWD service,
|
||||||
|
"ServiceIwd",
|
||||||
|
"ServiceIwdRole",
|
||||||
|
"ServiceIwdRoleDefault",
|
||||||
|
"IwdConfig",
|
||||||
|
"IwdConfigNetwork",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
# !/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from .create import register_create_parser
|
|
||||||
from .delete import register_delete_parser
|
|
||||||
from .hardware import register_hw_generate
|
|
||||||
from .install import register_install_parser
|
|
||||||
from .list import register_list_parser
|
|
||||||
from .update import register_update_parser
|
|
||||||
|
|
||||||
|
|
||||||
# takes a (sub)parser and configures it
|
|
||||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
|
||||||
subparser = parser.add_subparsers(
|
|
||||||
title="command",
|
|
||||||
description="the command to run",
|
|
||||||
help="the command to run",
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
update_parser = subparser.add_parser(
|
|
||||||
"update",
|
|
||||||
help="Update a machine",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
This subcommand provides an interface to update machines managed by clan.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan machines update [MACHINES]
|
|
||||||
Will update the specified machine [MACHINE], if [MACHINE] is omitted, the command
|
|
||||||
will attempt to update every configured machine.
|
|
||||||
To exclude machines being updated `clan.deployment.requireExplicitUpdate = true;`
|
|
||||||
can be set in the machine config.
|
|
||||||
|
|
||||||
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_update_parser(update_parser)
|
|
||||||
|
|
||||||
create_parser = subparser.add_parser("create", help="Create a machine")
|
|
||||||
register_create_parser(create_parser)
|
|
||||||
|
|
||||||
delete_parser = subparser.add_parser("delete", help="Delete a machine")
|
|
||||||
register_delete_parser(delete_parser)
|
|
||||||
|
|
||||||
list_parser = subparser.add_parser(
|
|
||||||
"list",
|
|
||||||
help="List machines",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
This subcommand lists all machines managed by this clan.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan machines list
|
|
||||||
Lists all the machines and their descriptions.
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_list_parser(list_parser)
|
|
||||||
|
|
||||||
generate_hw_parser = subparser.add_parser(
|
|
||||||
"hw-generate",
|
|
||||||
help="Generate hardware specifics for a machine",
|
|
||||||
description="""
|
|
||||||
Generates hardware specifics for a machine. Such as the host platform, available kernel modules, etc.
|
|
||||||
|
|
||||||
The target must be a Linux based system reachable via SSH.
|
|
||||||
""",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan machines hw-generate [MACHINE] [TARGET_HOST]
|
|
||||||
Will generate hardware specifics for the the specified `[TARGET_HOST]` and place the result in hardware.nix for the given machine `[MACHINE]`.
|
|
||||||
|
|
||||||
For more detailed information, visit: https://docs.clan.lol/getting-started/configure/#machine-configuration
|
|
||||||
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
)
|
|
||||||
register_hw_generate(generate_hw_parser)
|
|
||||||
|
|
||||||
install_parser = subparser.add_parser(
|
|
||||||
"install",
|
|
||||||
help="Install a machine",
|
|
||||||
description="""
|
|
||||||
Install a configured machine over the network.
|
|
||||||
The target must be a Linux based system reachable via SSH.
|
|
||||||
Installing a machine means overwriting the target's disk.
|
|
||||||
""",
|
|
||||||
epilog=(
|
|
||||||
"""
|
|
||||||
This subcommand provides an interface to install machines managed by clan.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ clan machines install [MACHINE] [TARGET_HOST]
|
|
||||||
Will install the specified machine [MACHINE], to the specified [TARGET_HOST].
|
|
||||||
|
|
||||||
$ clan machines install [MACHINE] --json [JSON]
|
|
||||||
Will install the specified machine [MACHINE] to the host exposed by
|
|
||||||
the deployment information of the [JSON] deployment string.
|
|
||||||
|
|
||||||
For information on how to set up the installer see: https://docs.clan.lol/getting-started/installer/
|
|
||||||
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
register_install_parser(install_parser)
|
|
||||||
|
|||||||
115
pkgs/clan-cli/clan_cli/machines/cli.py
Normal file
115
pkgs/clan-cli/clan_cli/machines/cli.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# !/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from .create import register_create_parser
|
||||||
|
from .delete import register_delete_parser
|
||||||
|
from .hardware import register_hw_generate
|
||||||
|
from .install import register_install_parser
|
||||||
|
from .list import register_list_parser
|
||||||
|
from .update import register_update_parser
|
||||||
|
|
||||||
|
|
||||||
|
# takes a (sub)parser and configures it
|
||||||
|
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
subparser = parser.add_subparsers(
|
||||||
|
title="command",
|
||||||
|
description="the command to run",
|
||||||
|
help="the command to run",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_parser = subparser.add_parser(
|
||||||
|
"update",
|
||||||
|
help="Update a machine",
|
||||||
|
epilog=(
|
||||||
|
"""
|
||||||
|
This subcommand provides an interface to update machines managed by clan.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ clan machines update [MACHINES]
|
||||||
|
Will update the specified machine [MACHINE], if [MACHINE] is omitted, the command
|
||||||
|
will attempt to update every configured machine.
|
||||||
|
To exclude machines being updated `clan.deployment.requireExplicitUpdate = true;`
|
||||||
|
can be set in the machine config.
|
||||||
|
|
||||||
|
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
)
|
||||||
|
register_update_parser(update_parser)
|
||||||
|
|
||||||
|
create_parser = subparser.add_parser("create", help="Create a machine")
|
||||||
|
register_create_parser(create_parser)
|
||||||
|
|
||||||
|
delete_parser = subparser.add_parser("delete", help="Delete a machine")
|
||||||
|
register_delete_parser(delete_parser)
|
||||||
|
|
||||||
|
list_parser = subparser.add_parser(
|
||||||
|
"list",
|
||||||
|
help="List machines",
|
||||||
|
epilog=(
|
||||||
|
"""
|
||||||
|
This subcommand lists all machines managed by this clan.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ clan machines list
|
||||||
|
Lists all the machines and their descriptions.
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
)
|
||||||
|
register_list_parser(list_parser)
|
||||||
|
|
||||||
|
generate_hw_parser = subparser.add_parser(
|
||||||
|
"hw-generate",
|
||||||
|
help="Generate hardware specifics for a machine",
|
||||||
|
description="""
|
||||||
|
Generates hardware specifics for a machine. Such as the host platform, available kernel modules, etc.
|
||||||
|
|
||||||
|
The target must be a Linux based system reachable via SSH.
|
||||||
|
""",
|
||||||
|
epilog=(
|
||||||
|
"""
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ clan machines hw-generate [MACHINE] [TARGET_HOST]
|
||||||
|
Will generate hardware specifics for the the specified `[TARGET_HOST]` and place the result in hardware.nix for the given machine `[MACHINE]`.
|
||||||
|
|
||||||
|
For more detailed information, visit: https://docs.clan.lol/getting-started/configure/#machine-configuration
|
||||||
|
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
register_hw_generate(generate_hw_parser)
|
||||||
|
|
||||||
|
install_parser = subparser.add_parser(
|
||||||
|
"install",
|
||||||
|
help="Install a machine",
|
||||||
|
description="""
|
||||||
|
Install a configured machine over the network.
|
||||||
|
The target must be a Linux based system reachable via SSH.
|
||||||
|
Installing a machine means overwriting the target's disk.
|
||||||
|
""",
|
||||||
|
epilog=(
|
||||||
|
"""
|
||||||
|
This subcommand provides an interface to install machines managed by clan.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ clan machines install [MACHINE] [TARGET_HOST]
|
||||||
|
Will install the specified machine [MACHINE], to the specified [TARGET_HOST].
|
||||||
|
|
||||||
|
$ clan machines install [MACHINE] --json [JSON]
|
||||||
|
Will install the specified machine [MACHINE] to the host exposed by
|
||||||
|
the deployment information of the [JSON] deployment string.
|
||||||
|
|
||||||
|
For information on how to set up the installer see: https://docs.clan.lol/getting-started/installer/
|
||||||
|
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
formatter_class=argparse.RawTextHelpFormatter,
|
||||||
|
)
|
||||||
|
register_install_parser(install_parser)
|
||||||
@@ -75,7 +75,7 @@ def get_user_name(flake_dir: Path, user: str) -> str:
|
|||||||
print(f"{flake_dir / user} already exists")
|
print(f"{flake_dir / user} already exists")
|
||||||
|
|
||||||
|
|
||||||
def ensure_user_or_machine(flake_dir: Path, pub_key: str) -> SopsKey:
|
def maybe_get_user_or_machine(flake_dir: Path, pub_key: str) -> SopsKey | None:
|
||||||
key = SopsKey(pub_key, username="")
|
key = SopsKey(pub_key, username="")
|
||||||
folders = [sops_users_folder(flake_dir), sops_machines_folder(flake_dir)]
|
folders = [sops_users_folder(flake_dir), sops_machines_folder(flake_dir)]
|
||||||
|
|
||||||
@@ -88,8 +88,15 @@ def ensure_user_or_machine(flake_dir: Path, pub_key: str) -> SopsKey:
|
|||||||
key.username = user.name
|
key.username = user.name
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_user_or_machine(flake_dir: Path, pub_key: str) -> SopsKey:
|
||||||
|
key = maybe_get_user_or_machine(flake_dir, pub_key)
|
||||||
|
if not key:
|
||||||
msg = f"Your sops key is not yet added to the repository. Please add it with 'clan secrets users add youruser {pub_key}' (replace youruser with your user name)"
|
msg = f"Your sops key is not yet added to the repository. Please add it with 'clan secrets users add youruser {pub_key}' (replace youruser with your user name)"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
def default_sops_key_path() -> Path:
|
def default_sops_key_path() -> Path:
|
||||||
@@ -99,15 +106,30 @@ def default_sops_key_path() -> Path:
|
|||||||
return user_config_dir() / "sops" / "age" / "keys.txt"
|
return user_config_dir() / "sops" / "age" / "keys.txt"
|
||||||
|
|
||||||
|
|
||||||
def ensure_sops_key(flake_dir: Path) -> SopsKey:
|
def maybe_get_public_key() -> str | None:
|
||||||
key = os.environ.get("SOPS_AGE_KEY")
|
key = os.environ.get("SOPS_AGE_KEY")
|
||||||
if key:
|
if key:
|
||||||
return ensure_user_or_machine(flake_dir, get_public_key(key))
|
return get_public_key(key)
|
||||||
path = default_sops_key_path()
|
path = default_sops_key_path()
|
||||||
if path.exists():
|
if path.exists():
|
||||||
return ensure_user_or_machine(flake_dir, get_public_key(path.read_text()))
|
return get_public_key(path.read_text())
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_get_sops_key(flake_dir: Path) -> SopsKey | None:
|
||||||
|
pub_key = maybe_get_public_key()
|
||||||
|
if pub_key:
|
||||||
|
return maybe_get_user_or_machine(flake_dir, pub_key)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_sops_key(flake_dir: Path) -> SopsKey:
|
||||||
|
pub_key = maybe_get_public_key()
|
||||||
|
if not pub_key:
|
||||||
msg = "No sops key found. Please generate one with 'clan secrets key generate'."
|
msg = "No sops key found. Please generate one with 'clan secrets key generate'."
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
return ensure_user_or_machine(flake_dir, pub_key)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { BackButton } from "@/src/components/BackButton";
|
|||||||
import { FileInput } from "@/src/components/FileInput";
|
import { FileInput } from "@/src/components/FileInput";
|
||||||
import { SelectInput } from "@/src/components/SelectInput";
|
import { SelectInput } from "@/src/components/SelectInput";
|
||||||
import { TextInput } from "@/src/components/TextInput";
|
import { TextInput } from "@/src/components/TextInput";
|
||||||
import { createForm, getValue, reset } from "@modular-forms/solid";
|
import { createForm, FieldValues, getValue, reset } from "@modular-forms/solid";
|
||||||
import { useParams } from "@solidjs/router";
|
import { useParams } from "@solidjs/router";
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { createSignal, For, Show, Switch, Match } from "solid-js";
|
import { createSignal, For, Show, Switch, Match } from "solid-js";
|
||||||
@@ -608,8 +608,75 @@ export const MachineDetails = () => {
|
|||||||
when={query.data}
|
when={query.data}
|
||||||
fallback={<span class="loading loading-lg"></span>}
|
fallback={<span class="loading loading-lg"></span>}
|
||||||
>
|
>
|
||||||
{(data) => <MachineForm initialData={data()} />}
|
{(data) => (
|
||||||
|
<>
|
||||||
|
<MachineForm initialData={data()} />
|
||||||
|
<MachineWifi
|
||||||
|
base_url={activeURI() || ""}
|
||||||
|
machine_name={data().machine.name}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface WifiForm extends FieldValues {
|
||||||
|
ssid: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MachineWifiProps {
|
||||||
|
base_url: string;
|
||||||
|
machine_name: string;
|
||||||
|
}
|
||||||
|
function MachineWifi(props: MachineWifiProps) {
|
||||||
|
const [formStore, { Form, Field }] = createForm<WifiForm>();
|
||||||
|
|
||||||
|
const handleSubmit = async (values: WifiForm) => {
|
||||||
|
console.log("submitting", values);
|
||||||
|
const r = await callApi("set_iwd_service_for_machine", {
|
||||||
|
base_url: props.base_url,
|
||||||
|
machine_name: props.machine_name,
|
||||||
|
networks: {
|
||||||
|
[values.ssid]: { ssid: values.ssid, password: values.password },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>MachineWifi</h1>
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<Field name="ssid">
|
||||||
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
formStore={formStore}
|
||||||
|
inputProps={props}
|
||||||
|
label="Name"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
error={field.error}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="password">
|
||||||
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
formStore={formStore}
|
||||||
|
inputProps={props}
|
||||||
|
label="Password"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
error={field.error}
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<button class="btn" type="submit">
|
||||||
|
<span>Submit</span>
|
||||||
|
</button>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user