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
|
||||
|
||||
# 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 .clan import show, update
|
||||
|
||||
# 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 (
|
||||
backups,
|
||||
clan,
|
||||
facts,
|
||||
flash,
|
||||
history,
|
||||
machines,
|
||||
secrets,
|
||||
state,
|
||||
vars,
|
||||
@@ -29,7 +27,9 @@ from .clan_uri import FlakeId
|
||||
from .custom_logger import setup_logging
|
||||
from .dirs import get_clan_flake_toplevel_or_env
|
||||
from .errors import ClanCmdError, ClanError
|
||||
from .facts import cli as facts
|
||||
from .hyperlink import help_hyperlink
|
||||
from .machines import cli as machines
|
||||
from .profiler import profile
|
||||
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,
|
||||
public_facts_store: FactStoreBase,
|
||||
tmpdir: Path,
|
||||
prompt: Callable[[str], str],
|
||||
prompt: Callable[[str, str], str],
|
||||
) -> bool:
|
||||
service_dir = tmpdir / service
|
||||
# check if all secrets exist and generate them if at least one is missing
|
||||
@@ -93,7 +93,9 @@ def generate_service_facts(
|
||||
else:
|
||||
generator = machine.facts_data[service]["generator"]["finalScript"]
|
||||
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
|
||||
if sys.platform == "linux":
|
||||
cmd = bubblewrap_cmd(generator, facts_dir, secrets_dir)
|
||||
@@ -137,7 +139,7 @@ def generate_service_facts(
|
||||
return True
|
||||
|
||||
|
||||
def prompt_func(text: str) -> str:
|
||||
def prompt_func(service: str, text: str) -> str:
|
||||
print(f"{text}: ")
|
||||
return read_multiline_input()
|
||||
|
||||
@@ -147,7 +149,7 @@ def _generate_facts_for_machine(
|
||||
service: str | None,
|
||||
regenerate: bool,
|
||||
tmpdir: Path,
|
||||
prompt: Callable[[str], str] = prompt_func,
|
||||
prompt: Callable[[str, str], str] = prompt_func,
|
||||
) -> bool:
|
||||
local_temp = tmpdir / machine.name
|
||||
local_temp.mkdir()
|
||||
@@ -189,7 +191,7 @@ def generate_facts(
|
||||
machines: list[Machine],
|
||||
service: str | None,
|
||||
regenerate: bool,
|
||||
prompt: Callable[[str], str] = prompt_func,
|
||||
prompt: Callable[[str, str], str] = prompt_func,
|
||||
) -> bool:
|
||||
was_regenerated = False
|
||||
with TemporaryDirectory() as tmp:
|
||||
|
||||
@@ -25,6 +25,8 @@ from clan_cli.nix import nix_eval
|
||||
from .classes import (
|
||||
AdminConfig,
|
||||
Inventory,
|
||||
IwdConfig,
|
||||
IwdConfigNetwork,
|
||||
# Machine classes
|
||||
Machine,
|
||||
MachineDeploy,
|
||||
@@ -40,6 +42,10 @@ from .classes import (
|
||||
ServiceBorgbackupRole,
|
||||
ServiceBorgbackupRoleClient,
|
||||
ServiceBorgbackupRoleServer,
|
||||
# IWD
|
||||
ServiceIwd,
|
||||
ServiceIwdRole,
|
||||
ServiceIwdRoleDefault,
|
||||
ServiceMeta,
|
||||
# Single Disk service
|
||||
ServiceSingleDisk,
|
||||
@@ -73,6 +79,12 @@ __all__ = [
|
||||
"ServiceAdminRole",
|
||||
"ServiceAdminRoleDefault",
|
||||
"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")
|
||||
|
||||
|
||||
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="")
|
||||
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
|
||||
return 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)"
|
||||
raise ClanError(msg)
|
||||
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)"
|
||||
raise ClanError(msg)
|
||||
return key
|
||||
|
||||
|
||||
def default_sops_key_path() -> Path:
|
||||
@@ -99,15 +106,30 @@ def default_sops_key_path() -> Path:
|
||||
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")
|
||||
if key:
|
||||
return ensure_user_or_machine(flake_dir, get_public_key(key))
|
||||
return get_public_key(key)
|
||||
path = default_sops_key_path()
|
||||
if path.exists():
|
||||
return ensure_user_or_machine(flake_dir, get_public_key(path.read_text()))
|
||||
msg = "No sops key found. Please generate one with 'clan secrets key generate'."
|
||||
raise ClanError(msg)
|
||||
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'."
|
||||
raise ClanError(msg)
|
||||
return ensure_user_or_machine(flake_dir, pub_key)
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BackButton } from "@/src/components/BackButton";
|
||||
import { FileInput } from "@/src/components/FileInput";
|
||||
import { SelectInput } from "@/src/components/SelectInput";
|
||||
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 { createQuery } from "@tanstack/solid-query";
|
||||
import { createSignal, For, Show, Switch, Match } from "solid-js";
|
||||
@@ -608,8 +608,75 @@ export const MachineDetails = () => {
|
||||
when={query.data}
|
||||
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>
|
||||
</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