UI: Init iwd service for single wifi
This commit is contained in:
@@ -6,12 +6,12 @@ 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,
|
||||
|
||||
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],
|
||||
)
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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