Merge pull request 'API: remove all python dataclasses for clanModules (services)' (#2080) from hsjobeki/clan-core:hsjobeki-main into main

This commit is contained in:
clan-bot
2024-09-12 16:37:51 +00:00
18 changed files with 289 additions and 600 deletions

View File

@@ -1,62 +1,37 @@
from clan_cli.errors import ClanError # @API.register
from clan_cli.inventory import ( # def set_admin_service(
AdminConfig, # base_url: str,
ServiceAdmin, # allowed_keys: dict[str, str],
ServiceAdminRole, # instance_name: str = "admin",
ServiceAdminRoleDefault, # extra_machines: list[str] | None = None,
ServiceMeta, # ) -> None:
load_inventory_eval, # """
save_inventory, # Set the admin service of a clan
) # Every machine is by default part of the admin service via the 'all' tag
# """
# if extra_machines is None:
# extra_machines = []
# inventory = load_inventory_eval(base_url)
from . import API # if not allowed_keys:
# msg = "At least one key must be provided to ensure access"
# raise ClanError(msg)
# instance = ServiceAdmin(
# meta=ServiceMeta(name=instance_name),
# roles=ServiceAdminRole(
# default=ServiceAdminRoleDefault(
# machines=extra_machines,
# tags=["all"],
# )
# ),
# config=AdminConfig(allowedKeys=allowed_keys),
# )
@API.register # inventory.services.admin[instance_name] = instance
def get_admin_service(base_url: str) -> ServiceAdmin | None:
"""
Return the admin service of a clan.
There is only one admin service. This might be changed in the future # save_inventory(
""" # inventory,
inventory = load_inventory_eval(base_url) # base_url,
return inventory.services.admin.get("admin") # f"Set admin service: '{instance_name}'",
# )
@API.register
def set_admin_service(
base_url: str,
allowed_keys: dict[str, str],
instance_name: str = "admin",
extra_machines: list[str] | None = None,
) -> None:
"""
Set the admin service of a clan
Every machine is by default part of the admin service via the 'all' tag
"""
if extra_machines is None:
extra_machines = []
inventory = load_inventory_eval(base_url)
if not allowed_keys:
msg = "At least one key must be provided to ensure access"
raise ClanError(msg)
instance = ServiceAdmin(
meta=ServiceMeta(name=instance_name),
roles=ServiceAdminRole(
default=ServiceAdminRoleDefault(
machines=extra_machines,
tags=["all"],
)
),
config=AdminConfig(allowedKeys=allowed_keys),
)
inventory.services.admin[instance_name] = instance
save_inventory(
inventory,
base_url,
f"Set admin service: '{instance_name}'",
)

View File

@@ -1,67 +1,34 @@
from clan_cli.inventory import (
ServiceMeta,
ServiceSingleDisk,
ServiceSingleDiskRole,
ServiceSingleDiskRoleDefault,
SingleDiskConfig,
load_inventory_eval,
load_inventory_json,
save_inventory,
)
from . import API
def get_instance_name(machine_name: str) -> str: def get_instance_name(machine_name: str) -> str:
return f"{machine_name}-single-disk" return f"{machine_name}-single-disk"
@API.register # @API.register
def set_single_disk_uuid( # def set_single_disk_uuid(
base_path: str, # base_path: str,
machine_name: str, # machine_name: str,
disk_uuid: str, # disk_uuid: str,
) -> None: # ) -> None:
""" # """
Set the disk UUID of single disk machine # Set the disk UUID of single disk machine
""" # """
inventory = load_inventory_json(base_path) # inventory = load_inventory_json(base_path)
instance_name = get_instance_name(machine_name) # instance_name = get_instance_name(machine_name)
single_disk_config: ServiceSingleDisk = ServiceSingleDisk( # single_disk_config: ServiceSingleDisk = ServiceSingleDisk(
meta=ServiceMeta(name=instance_name), # meta=ServiceMeta(name=instance_name),
roles=ServiceSingleDiskRole( # roles=ServiceSingleDiskRole(
default=ServiceSingleDiskRoleDefault( # default=ServiceSingleDiskRoleDefault(
config=SingleDiskConfig(device=f"/dev/disk/by-id/{disk_uuid}"), # config=SingleDiskConfig(device=f"/dev/disk/by-id/{disk_uuid}"),
machines=[machine_name], # machines=[machine_name],
) # )
), # ),
) # )
inventory.services.single_disk[instance_name] = single_disk_config # inventory.services.single_disk[instance_name] = single_disk_config
save_inventory( # save_inventory(
inventory, # inventory,
base_path, # base_path,
f"Set disk UUID: '{disk_uuid}' on machine: '{machine_name}'", # f"Set disk UUID: '{disk_uuid}' on machine: '{machine_name}'",
) # )
@API.register
def get_single_disk_uuid(
base_path: str,
machine_name: str,
) -> str | None:
"""
Get the disk UUID of single disk machine
"""
inventory = load_inventory_eval(base_path)
instance_name = get_instance_name(machine_name)
single_disk_config: ServiceSingleDisk = inventory.services.single_disk[
instance_name
]
return single_disk_config.roles.default.config.device

View File

@@ -1,109 +1,67 @@
from dataclasses import dataclass 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: def instance_name(machine_name: str) -> str:
return f"{machine_name}_wifi_0_" return f"{machine_name}_wifi_0_"
@API.register
def get_iwd_service(base_url: str, machine_name: str) -> ServiceIwd:
"""
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)
service_config = inventory.services.iwd.get(instance_name(machine_name))
if service_config:
return service_config
# Empty service
return ServiceIwd(
meta=ServiceMeta(name="wifi_0"),
roles=ServiceIwdRole(default=ServiceIwdRoleDefault(machines=[machine_name])),
config=IwdConfig(networks={}),
)
@dataclass @dataclass
class NetworkConfig: class NetworkConfig:
ssid: str ssid: str
password: str password: str
@API.register # @API.register
def set_iwd_service_for_machine( # def set_iwd_service_for_machine(
base_url: str, machine_name: str, networks: dict[str, NetworkConfig] # base_url: str, machine_name: str, networks: dict[str, NetworkConfig]
) -> None: # ) -> None:
""" # """
Set the admin service of a clan # Set the admin service of a clan
Every machine is by default part of the admin service via the 'all' tag # Every machine is by default part of the admin service via the 'all' tag
""" # """
_instance_name = instance_name(machine_name) # _instance_name = instance_name(machine_name)
inventory = load_inventory_eval(base_url) # inventory = load_inventory_eval(base_url)
instance = ServiceIwd( # instance = ServiceIwd(
meta=ServiceMeta(name="wifi_0"), # meta=ServiceMeta(name="wifi_0"),
roles=ServiceIwdRole( # roles=ServiceIwdRole(
default=ServiceIwdRoleDefault( # default=ServiceIwdRoleDefault(
machines=[machine_name], # machines=[machine_name],
) # )
), # ),
config=IwdConfig( # config=IwdConfig(
networks={k: IwdConfigNetwork(v.ssid) for k, v in networks.items()} # networks={k: IwdConfigNetwork(v.ssid) for k, v in networks.items()}
), # ),
) # )
inventory.services.iwd[_instance_name] = instance # inventory.services.iwd[_instance_name] = instance
save_inventory( # save_inventory(
inventory, # inventory,
base_url, # base_url,
f"Set iwd service: '{_instance_name}'", # f"Set iwd service: '{_instance_name}'",
) # )
pubkey = maybe_get_public_key() # pubkey = maybe_get_public_key()
if not pubkey: # if not pubkey:
# TODO: do this automatically # # TODO: do this automatically
# pubkey = generate_key() # # pubkey = generate_key()
raise ClanError(msg="No public key found. Please initialize your key.") # raise ClanError(msg="No public key found. Please initialize your key.")
registered_key = maybe_get_user_or_machine(Path(base_url), pubkey) # registered_key = maybe_get_user_or_machine(Path(base_url), pubkey)
if not registered_key: # if not registered_key:
# TODO: do this automatically # # TODO: do this automatically
# username = os.getlogin() # # username = os.getlogin()
# add_user(Path(base_url), username, pubkey, force=False) # # add_user(Path(base_url), username, pubkey, force=False)
raise ClanError(msg="Your public key is not registered for use with this clan.") # 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()} # password_dict = {f"iwd.{net.ssid}": net.password for net in networks.values()}
for net in networks.values(): # for net in networks.values():
generate_facts( # generate_facts(
service=f"iwd.{net.ssid}", # service=f"iwd.{net.ssid}",
machines=[Machine(machine_name, FlakeId(base_url))], # machines=[Machine(machine_name, FlakeId(base_url))],
regenerate=True, # regenerate=True,
# Just returns the password # # Just returns the password
prompt=lambda service, _msg: password_dict[service], # prompt=lambda service, _msg: password_dict[service],
) # )

View File

@@ -23,35 +23,13 @@ from clan_cli.git import commit_file
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
from .classes import ( from .classes import (
AdminConfig,
Inventory, Inventory,
IwdConfig,
IwdConfigNetwork,
# Machine classes # Machine classes
Machine, Machine,
MachineDeploy, MachineDeploy,
# General classes # General classes
Meta, Meta,
Service, Service,
# Admin service
ServiceAdmin,
ServiceAdminRole,
ServiceAdminRoleDefault,
# Borgbackup service
ServiceBorgbackup,
ServiceBorgbackupRole,
ServiceBorgbackupRoleClient,
ServiceBorgbackupRoleServer,
# IWD
ServiceIwd,
ServiceIwdRole,
ServiceIwdRoleDefault,
ServiceMeta,
# Single Disk service
ServiceSingleDisk,
ServiceSingleDiskRole,
ServiceSingleDiskRoleDefault,
SingleDiskConfig,
) )
# Re export classes here # Re export classes here
@@ -64,27 +42,6 @@ __all__ = [
"Meta", "Meta",
"Inventory", "Inventory",
"MachineDeploy", "MachineDeploy",
"ServiceBorgbackup",
"ServiceMeta",
"ServiceBorgbackupRole",
"ServiceBorgbackupRoleClient",
"ServiceBorgbackupRoleServer",
# Single Disk service
"ServiceSingleDisk",
"ServiceSingleDiskRole",
"ServiceSingleDiskRoleDefault",
"SingleDiskConfig",
# Admin service
"ServiceAdmin",
"ServiceAdminRole",
"ServiceAdminRoleDefault",
"AdminConfig",
# IWD service,
"ServiceIwd",
"ServiceIwdRole",
"ServiceIwdRoleDefault",
"IwdConfig",
"IwdConfigNetwork",
] ]

View File

@@ -30,234 +30,7 @@ class Meta:
icon: None | str = field(default = None) icon: None | str = field(default = None)
@dataclass Service = dict[str, Any]
class AdminConfig:
allowedKeys: dict[str, str] = field(default_factory = dict)
@dataclass
class ServiceAdminMachine:
config: AdminConfig = field(default_factory = AdminConfig)
imports: list[str] = field(default_factory = list)
@dataclass
class ServiceMeta:
name: str
description: None | str = field(default = None)
icon: None | str = field(default = None)
@dataclass
class ServiceAdminRoleDefault:
config: AdminConfig = field(default_factory = AdminConfig)
imports: list[str] = field(default_factory = list)
machines: list[str] = field(default_factory = list)
tags: list[str] = field(default_factory = list)
@dataclass
class ServiceAdminRole:
default: ServiceAdminRoleDefault
@dataclass
class ServiceAdmin:
meta: ServiceMeta
roles: ServiceAdminRole
config: AdminConfig = field(default_factory = AdminConfig)
machines: dict[str, ServiceAdminMachine] = field(default_factory = dict)
@dataclass
class BorgbackupConfigDestination:
name: str
repo: str
@dataclass
class BorgbackupConfig:
destinations: dict[str, BorgbackupConfigDestination] = field(default_factory = dict)
exclude: list[str] = field(default_factory = list)
@dataclass
class ServiceBorgbackupMachine:
config: BorgbackupConfig = field(default_factory = BorgbackupConfig)
imports: list[str] = field(default_factory = list)
@dataclass
class ServiceBorgbackupRoleClient:
config: BorgbackupConfig = field(default_factory = BorgbackupConfig)
imports: list[str] = field(default_factory = list)
machines: list[str] = field(default_factory = list)
tags: list[str] = field(default_factory = list)
@dataclass
class ServiceBorgbackupRoleServer:
config: BorgbackupConfig = field(default_factory = BorgbackupConfig)
imports: list[str] = field(default_factory = list)
machines: list[str] = field(default_factory = list)
tags: list[str] = field(default_factory = list)
@dataclass
class ServiceBorgbackupRole:
client: ServiceBorgbackupRoleClient
server: ServiceBorgbackupRoleServer
@dataclass
class ServiceBorgbackup:
meta: ServiceMeta
roles: ServiceBorgbackupRole
config: BorgbackupConfig = field(default_factory = BorgbackupConfig)
machines: dict[str, ServiceBorgbackupMachine] = field(default_factory = dict)
@dataclass
class IwdConfigNetwork:
ssid: str
@dataclass
class IwdConfig:
networks: dict[str, IwdConfigNetwork] = field(default_factory = dict)
@dataclass
class ServiceIwdMachine:
config: IwdConfig = field(default_factory = IwdConfig)
imports: list[str] = field(default_factory = list)
@dataclass
class ServiceIwdRoleDefault:
config: IwdConfig = field(default_factory = IwdConfig)
imports: list[str] = field(default_factory = list)
machines: list[str] = field(default_factory = list)
tags: list[str] = field(default_factory = list)
@dataclass
class ServiceIwdRole:
default: ServiceIwdRoleDefault
@dataclass
class ServiceIwd:
meta: ServiceMeta
roles: ServiceIwdRole
config: IwdConfig = field(default_factory = IwdConfig)
machines: dict[str, ServiceIwdMachine] = field(default_factory = dict)
@dataclass
class PackagesConfig:
packages: list[str] = field(default_factory = list)
@dataclass
class ServicePackageMachine:
config: PackagesConfig = field(default_factory = PackagesConfig)
imports: list[str] = field(default_factory = list)
@dataclass
class ServicePackageRoleDefault:
config: PackagesConfig = field(default_factory = PackagesConfig)
imports: list[str] = field(default_factory = list)
machines: list[str] = field(default_factory = list)
tags: list[str] = field(default_factory = list)
@dataclass
class ServicePackageRole:
default: ServicePackageRoleDefault
@dataclass
class ServicePackage:
meta: ServiceMeta
roles: ServicePackageRole
config: PackagesConfig = field(default_factory = PackagesConfig)
machines: dict[str, ServicePackageMachine] = field(default_factory = dict)
@dataclass
class SingleDiskConfig:
device: None | str = field(default = None)
@dataclass
class ServiceSingleDiskMachine:
config: SingleDiskConfig = field(default_factory = SingleDiskConfig)
imports: list[str] = field(default_factory = list)
@dataclass
class ServiceSingleDiskRoleDefault:
config: SingleDiskConfig = field(default_factory = SingleDiskConfig)
imports: list[str] = field(default_factory = list)
machines: list[str] = field(default_factory = list)
tags: list[str] = field(default_factory = list)
@dataclass
class ServiceSingleDiskRole:
default: ServiceSingleDiskRoleDefault
@dataclass
class ServiceSingleDisk:
meta: ServiceMeta
roles: ServiceSingleDiskRole
config: SingleDiskConfig = field(default_factory = SingleDiskConfig)
machines: dict[str, ServiceSingleDiskMachine] = field(default_factory = dict)
@dataclass
class StateVersionConfig:
pass
@dataclass
class ServiceStateVersionMachine:
config: StateVersionConfig = field(default_factory = StateVersionConfig)
imports: list[str] = field(default_factory = list)
@dataclass
class ServiceStateVersionRoleDefault:
config: StateVersionConfig = field(default_factory = StateVersionConfig)
imports: list[str] = field(default_factory = list)
machines: list[str] = field(default_factory = list)
tags: list[str] = field(default_factory = list)
@dataclass
class ServiceStateVersionRole:
default: ServiceStateVersionRoleDefault
@dataclass
class ServiceStateVersion:
meta: ServiceMeta
roles: ServiceStateVersionRole
config: StateVersionConfig = field(default_factory = StateVersionConfig)
machines: dict[str, ServiceStateVersionMachine] = field(default_factory = dict)
@dataclass
class Service:
admin: dict[str, ServiceAdmin] = field(default_factory = dict)
borgbackup: dict[str, ServiceBorgbackup] = field(default_factory = dict)
iwd: dict[str, ServiceIwd] = field(default_factory = dict)
packages: dict[str, ServicePackage] = field(default_factory = dict)
single_disk: dict[str, ServiceSingleDisk] = field(default_factory = dict, metadata = {"alias": "single-disk"})
state_version: dict[str, ServiceStateVersion] = field(default_factory = dict, metadata = {"alias": "state-version"})
@dataclass @dataclass
class Inventory: class Inventory:

View File

@@ -63,7 +63,7 @@ let
ln -sf ${nixpkgs'} $out/clan_cli/nixpkgs ln -sf ${nixpkgs'} $out/clan_cli/nixpkgs
cp -r ${../../templates} $out/clan_cli/templates cp -r ${../../templates} $out/clan_cli/templates
${classgen}/bin/classgen ${inventory-schema}/schema.json $out/clan_cli/inventory/classes.py ${classgen}/bin/classgen ${inventory-schema}/schema.json $out/clan_cli/inventory/classes.py --stop-at "Service"
''; '';
# Create a custom nixpkgs for use within the project # Create a custom nixpkgs for use within the project

View File

@@ -73,7 +73,7 @@
]; ];
installPhase = '' installPhase = ''
${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json ./clan_cli/inventory/classes.py ${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json ./clan_cli/inventory/classes.py --stop-at "Service"
python docs.py reference python docs.py reference
mkdir -p $out mkdir -p $out
@@ -93,10 +93,12 @@
]; ];
installPhase = '' installPhase = ''
${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json ./clan_cli/inventory/classes.py ${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json ./clan_cli/inventory/classes.py --stop-at "Service"
mkdir -p $out mkdir -p $out
python api.py > $out/API.json python api.py > $out/API.json
${self'.packages.json2ts}/bin/json2ts --input $out/API.json > $out/API.ts ${self'.packages.json2ts}/bin/json2ts --input $out/API.json > $out/API.ts
${self'.packages.json2ts}/bin/json2ts --input ${self'.packages.inventory-schema}/schema.json > $out/Inventory.ts
cp ${self'.packages.inventory-schema}/schema.json $out/inventory-schema.json
''; '';
}; };
json2ts = pkgs.buildNpmPackage { json2ts = pkgs.buildNpmPackage {
@@ -122,7 +124,7 @@
classFile = "classes.py"; classFile = "classes.py";
}; };
installPhase = '' installPhase = ''
${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json b_classes.py ${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json b_classes.py --stop-at "Service"
file1=$classFile file1=$classFile
file2=b_classes.py file2=b_classes.py

View File

@@ -45,6 +45,6 @@ mkShell {
# Generate classes.py from inventory schema # Generate classes.py from inventory schema
# This file is in .gitignore # This file is in .gitignore
${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json $PKG_ROOT/clan_cli/inventory/classes.py ${self'.packages.classgen}/bin/classgen ${self'.packages.inventory-schema}/schema.json $PKG_ROOT/clan_cli/inventory/classes.py --stop-at "Service"
''; '';
} }

View File

@@ -7,18 +7,6 @@ import pytest
# Functions to test # Functions to test
from clan_cli.api import dataclass_to_dict, from_dict from clan_cli.api import dataclass_to_dict, from_dict
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.inventory import (
Inventory,
Machine,
MachineDeploy,
Meta,
Service,
ServiceBorgbackup,
ServiceBorgbackupRole,
ServiceBorgbackupRoleClient,
ServiceBorgbackupRoleServer,
ServiceMeta,
)
from clan_cli.machines import machines from clan_cli.machines import machines
@@ -172,43 +160,6 @@ def test_list() -> None:
assert result == [Name("John"), Name("Sarah")] assert result == [Name("John"), Name("Sarah")]
def test_deserialize_extensive_inventory() -> None:
# TODO: Make this an abstract test, so it doesn't break the test if the inventory changes
data = {
"meta": {"name": "superclan", "description": "nice clan"},
"services": {
"borgbackup": {
"instance1": {
"meta": {
"name": "borg1",
},
"roles": {
"client": {},
"server": {},
},
}
},
},
"machines": {"foo": {"name": "foo", "deploy": {}}},
}
expected = Inventory(
meta=Meta(name="superclan", description="nice clan"),
services=Service(
borgbackup={
"instance1": ServiceBorgbackup(
meta=ServiceMeta(name="borg1"),
roles=ServiceBorgbackupRole(
client=ServiceBorgbackupRoleClient(),
server=ServiceBorgbackupRoleServer(),
),
)
}
),
machines={"foo": Machine(deploy=MachineDeploy(), name="foo")},
)
assert from_dict(Inventory, data) == expected
def test_alias_field() -> None: def test_alias_field() -> None:
@dataclass @dataclass
class Person: class Person:

View File

@@ -7,11 +7,6 @@ from clan_cli.clan_uri import FlakeId
from clan_cli.inventory import ( from clan_cli.inventory import (
Machine, Machine,
MachineDeploy, MachineDeploy,
ServiceBorgbackup,
ServiceBorgbackupRole,
ServiceBorgbackupRoleClient,
ServiceBorgbackupRoleServer,
ServiceMeta,
load_inventory_json, load_inventory_json,
save_inventory, save_inventory,
) )
@@ -67,18 +62,16 @@ def test_add_module_to_inventory(
inventory = load_inventory_json(base_path) inventory = load_inventory_json(base_path)
inventory.services.borgbackup = { inventory.services = {
"borg1": ServiceBorgbackup( "borgbackup": {
meta=ServiceMeta(name="borg1"), "borg1": {
roles=ServiceBorgbackupRole( "meta": {"name": "borg1"},
client=ServiceBorgbackupRoleClient( "roles": {
machines=["machine1"], "client": {"machines": ["machine1"]},
), "server": {"machines": ["machine1"]},
server=ServiceBorgbackupRoleServer( },
machines=["machine1"], }
), }
),
)
} }
save_inventory(inventory, base_path, "Add borgbackup service") save_inventory(inventory, base_path, "Add borgbackup service")

View File

@@ -45,6 +45,7 @@ def map_json_type(
known_classes = set() known_classes = set()
root_class = "Inventory" root_class = "Inventory"
stop_at = None
def field_def_from_default_type( def field_def_from_default_type(
@@ -198,6 +199,9 @@ def generate_dataclass(schema: dict[str, Any], class_name: str = root_class) ->
required_fields = [] required_fields = []
fields_with_default = [] fields_with_default = []
nested_classes: list[str] = [] nested_classes: list[str] = []
if stop_at and class_name == stop_at:
# Skip generating classes below the stop_at property
return f"{class_name} = dict[str, Any]"
for prop, prop_info in properties.items(): for prop, prop_info in properties.items():
field_name = prop.replace("-", "_") field_name = prop.replace("-", "_")
@@ -272,9 +276,6 @@ def generate_dataclass(schema: dict[str, Any], class_name: str = root_class) ->
field_meta = f"""{{"alias": "{prop}"}}""" field_meta = f"""{{"alias": "{prop}"}}"""
finalize_field = partial(get_field_def, field_name, field_meta) finalize_field = partial(get_field_def, field_name, field_meta)
# if class_name == "DyndnsConfig":
# if class_name == "ServiceDyndnMachine":
# breakpoint()
if "default" in prop_info or field_name not in prop_info.get("required", []): if "default" in prop_info or field_name not in prop_info.get("required", []):
if "default" in prop_info: if "default" in prop_info:
@@ -334,6 +335,10 @@ def generate_dataclass(schema: dict[str, Any], class_name: str = root_class) ->
def run_gen(args: argparse.Namespace) -> None: def run_gen(args: argparse.Namespace) -> None:
print(f"Converting {args.input} to {args.output}") print(f"Converting {args.input} to {args.output}")
if args.stop_at:
global stop_at
stop_at = args.stop_at
dataclass_code = "" dataclass_code = ""
with args.input.open() as f: with args.input.open() as f:
schema = json.load(f) schema = json.load(f)
@@ -358,6 +363,12 @@ def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("input", help="Input JSON schema file", type=Path) parser.add_argument("input", help="Input JSON schema file", type=Path)
parser.add_argument("output", help="Output Python file", type=Path) parser.add_argument("output", help="Output Python file", type=Path)
parser.add_argument(
"--stop-at",
type=str,
help="Property name to stop generating classes for. Other classes below that property will be generated",
default=None,
)
parser.set_defaults(func=run_gen) parser.set_defaults(func=run_gen)
args = parser.parse_args() args = parser.parse_args()

View File

@@ -1,3 +1,3 @@
api app/api
.vite .vite

View File

@@ -0,0 +1,31 @@
import { get_inventory } from "./inventory";
export const instance_name = (machine_name: string) =>
`${machine_name}-single-disk` as const;
export async function set_single_disk_id(
base_path: string,
machine_name: string,
disk_id: string,
) {
const inventory = await get_inventory(base_path);
if (!inventory.services) {
return new Error("No services found in inventory");
}
if (!inventory.services["single-disk"]) {
inventory.services["single-disk"] = {};
}
inventory.services["single-disk"][instance_name(machine_name)] = {
meta: {
name: instance_name(machine_name),
},
roles: {
default: {
machines: [machine_name],
config: {
device: `/dev/disk/by-id/${disk_id}`,
},
},
},
};
}

View File

@@ -1,11 +1,19 @@
import schema from "@/api/API.json" assert { type: "json" }; import schema from "@/api/API.json" assert { type: "json" };
import { API } from "@/api/API"; import { API } from "@/api/API";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { Schema as Inventory } from "@/api/Inventory";
export type OperationNames = keyof API; export type OperationNames = keyof API;
export type OperationArgs<T extends OperationNames> = API[T]["arguments"]; export type OperationArgs<T extends OperationNames> = API[T]["arguments"];
export type OperationResponse<T extends OperationNames> = API[T]["return"]; export type OperationResponse<T extends OperationNames> = API[T]["return"];
export type Services = NonNullable<Inventory["services"]>;
export type ServiceNames = keyof Services;
export type ClanService<T extends ServiceNames> = Services[T];
export type ClanServiceInstance<T extends ServiceNames> = NonNullable<
Services[T]
>[string];
export type SuccessQuery<T extends OperationNames> = Extract< export type SuccessQuery<T extends OperationNames> = Extract<
OperationResponse<T>, OperationResponse<T>,
{ status: "success" } { status: "success" }

View File

@@ -0,0 +1,40 @@
import { callApi, ClanService, ServiceNames, Services } from ".";
import { Schema as Inventory } from "@/api/Inventory";
export async function get_inventory(base_path: string) {
const r = await callApi("get_inventory", {
base_path,
});
if (r.status == "error") {
throw new Error("Failed to get inventory");
}
const inventory: Inventory = r.data;
return inventory;
}
export const single_instance_name = <T extends keyof Services>(
machine_name: string,
service_name: T,
) => `${machine_name}_${service_name}_0` as const;
function get_service<T extends ServiceNames>(base_path: string, service: T) {
return callApi("get_inventory", { base_path }).then((r) => {
if (r.status == "error") {
return null;
}
const inventory: Inventory = r.data;
const serviceInstance = inventory.services?.[service];
return serviceInstance;
});
}
export async function get_single_service<T extends keyof Services>(
base_path: string,
machine_name: string,
service_name: T,
) {
const instance_key = single_instance_name(machine_name, service_name);
const service = await get_service(base_path, "admin");
return service?.[instance_key];
}

View File

@@ -0,0 +1,18 @@
import { callApi } from ".";
import { Schema as Inventory } from "@/api/Inventory";
export const instance_name = (machine_name: string) =>
`${machine_name}_wifi_0` as const;
export async function get_iwd_service(base_path: string, machine_name: string) {
const r = await callApi("get_inventory", {
base_path,
});
if (r.status == "error") {
return null;
}
const inventory: Inventory = r.data;
const instance_key = instance_name(machine_name);
return inventory.services?.iwd?.[instance_key] || null;
}

View File

@@ -1,4 +1,9 @@
import { callApi, SuccessQuery } from "@/src/api"; import {
callApi,
ClanService,
ClanServiceInstance,
SuccessQuery,
} from "@/src/api";
import { BackButton } from "@/src/components/BackButton"; import { BackButton } from "@/src/components/BackButton";
import { useParams } from "@solidjs/router"; import { useParams } from "@solidjs/router";
import { import {
@@ -19,6 +24,7 @@ import {
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/components/TextInput";
import toast from "solid-toast"; import toast from "solid-toast";
import { get_single_service } from "@/src/api/inventory";
interface AdminModuleFormProps { interface AdminModuleFormProps {
admin: AdminData; admin: AdminData;
@@ -184,19 +190,20 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
const handleSubmit = async (values: AdminSettings) => { const handleSubmit = async (values: AdminSettings) => {
console.log("submitting", values, getValues(formStore)); console.log("submitting", values, getValues(formStore));
const r = await callApi("set_admin_service", { // const r = await callApi("set_admin_service", {
base_url: props.base_url, // base_url: props.base_url,
allowed_keys: values.allowedKeys.reduce( // allowed_keys: values.allowedKeys.reduce(
(acc, curr) => ({ ...acc, [curr.name]: curr.value }), // (acc, curr) => ({ ...acc, [curr.name]: curr.value }),
{}, // {}
), // ),
}); // });
if (r.status === "success") { // if (r.status === "success") {
toast.success("Successfully updated admin settings"); // toast.success("Successfully updated admin settings");
} // }
if (r.status === "error") { // if (r.status === "error") {
toast.error(`Failed to update admin settings: ${r.errors[0].message}`); // toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
} toast.error(`Failed to update admin settings: feature disabled`);
// }
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [props.base_url, "get_admin_service"], queryKey: [props.base_url, "get_admin_service"],
}); });
@@ -329,7 +336,7 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
}; };
type GeneralData = SuccessQuery<"show_clan_meta">["data"]; type GeneralData = SuccessQuery<"show_clan_meta">["data"];
type AdminData = SuccessQuery<"get_admin_service">["data"]; type AdminData = ClanServiceInstance<"admin">;
export const ClanDetails = () => { export const ClanDetails = () => {
const params = useParams(); const params = useParams();
@@ -347,11 +354,9 @@ export const ClanDetails = () => {
const adminQuery = createQuery(() => ({ const adminQuery = createQuery(() => ({
queryKey: [clan_dir, "get_admin_service"], queryKey: [clan_dir, "get_admin_service"],
queryFn: async () => { queryFn: async () => {
const result = await callApi("get_admin_service", { const result = await get_single_service(clan_dir, "", "admin");
base_url: clan_dir, if (!result) throw new Error("Failed to fetch data");
}); return result || null;
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data || null;
}, },
})); }));

View File

@@ -1,4 +1,12 @@
import { callApi, SuccessData, SuccessQuery } from "@/src/api"; import {
callApi,
ClanService,
Services,
SuccessData,
SuccessQuery,
} from "@/src/api";
import { set_single_disk_id } from "@/src/api/disk";
import { get_iwd_service } from "@/src/api/wifi";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
import { BackButton } from "@/src/components/BackButton"; import { BackButton } from "@/src/components/BackButton";
import { FileInput } from "@/src/components/FileInput"; import { FileInput } from "@/src/components/FileInput";
@@ -117,17 +125,12 @@ const InstallMachine = (props: InstallMachineProps) => {
return; return;
} }
const r = await callApi("set_single_disk_uuid", { const r = await set_single_disk_id(curr_uri, props.name, disk_id);
base_path: curr_uri, if (!r) {
machine_name: props.name,
disk_uuid: disk_id,
});
if (r.status === "error") {
toast.error("Failed to set disk");
}
if (r.status === "success") {
toast.success("Disk set successfully"); toast.success("Disk set successfully");
setConfirmDisk(true); setConfirmDisk(true);
} else {
toast.error("Failed to set disk");
} }
}; };
@@ -600,7 +603,7 @@ const MachineForm = (props: MachineDetailsProps) => {
); );
}; };
type WifiData = SuccessData<"get_iwd_service">; type WifiData = ClanService<"iwd">;
export const MachineDetails = () => { export const MachineDetails = () => {
const params = useParams(); const params = useParams();
@@ -629,12 +632,9 @@ export const MachineDetails = () => {
queryFn: async () => { queryFn: async () => {
const curr = activeURI(); const curr = activeURI();
if (curr) { if (curr) {
const result = await callApi("get_iwd_service", { const result = await get_iwd_service(curr, params.id);
base_url: curr, if (!result) throw new Error("Failed to fetch data");
machine_name: params.id, return Object.entries(result?.config?.networks || {}).map(
});
if (result.status === "error") throw new Error("Failed to fetch data");
return Object.entries(result.data?.config?.networks || {}).map(
([name, value]) => ({ name, ssid: value.ssid }), ([name, value]) => ({ name, ssid: value.ssid }),
); );
} }
@@ -728,17 +728,17 @@ function WifiModule(props: MachineWifiProps) {
); );
console.log("submitting", values, networks); console.log("submitting", values, networks);
const r = await callApi("set_iwd_service_for_machine", { // const r = await callApi("set_iwd_service_for_machine", {
base_url: props.base_url, // base_url: props.base_url,
machine_name: props.machine_name, // machine_name: props.machine_name,
networks: networks, // networks: networks,
}); // });
if (r.status === "error") { // if (r.status === "error") {
toast.error("Failed to set wifi"); toast.error("Failed to set wifi. Feature disabled temporarily");
} // }
if (r.status === "success") { // if (r.status === "success") {
toast.success("Wifi set successfully"); // toast.success("Wifi set successfully");
} // }
}; };
return ( return (