Files
clan-core/pkgs/clan-cli/clan_cli/api/disk.py
2024-11-29 21:30:46 +01:00

193 lines
6.4 KiB
Python

import json
import logging
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from uuid import uuid4
from clan_cli.api import API
from clan_cli.dirs import TemplateType, clan_templates
from clan_cli.errors import ClanError
from clan_cli.git import commit_file
from clan_cli.machines.hardware import HardwareConfig, show_machine_hardware_config
log = logging.getLogger(__name__)
def disk_in_facter_report(hw_report: dict) -> bool:
return "hardware" in hw_report and "disk" in hw_report["hardware"]
def get_best_unix_device_name(unix_device_names: list[str]) -> str:
# find the first device name that is disk/by-id
for device_name in unix_device_names:
if "disk/by-id" in device_name:
return device_name
else:
# if no by-id found, use the first device name
return unix_device_names[0]
def hw_main_disk_options(hw_report: dict) -> list[str]:
if not disk_in_facter_report(hw_report):
msg = "hw_report doesnt include 'disk' information"
raise ClanError(msg, description=f"{hw_report.keys()}")
disks = hw_report["hardware"]["disk"]
options: list[str] = []
for disk in disks:
unix_device_names = disk["unix_device_names"]
device_name = get_best_unix_device_name(unix_device_names)
options += [device_name]
return options
@dataclass
class Placeholder:
# Input name for the user
label: str
options: list[str] | None
required: bool
@dataclass
class DiskSchema:
name: str
placeholders: dict[str, Placeholder]
# must be manually kept in sync with the ${clancore}/templates/disks directory
templates: dict[str, dict[str, Callable[[dict[str, Any]], Placeholder]]] = {
"single-disk": {
# Placeholders
"mainDisk": lambda hw_report: Placeholder(
label="Main disk", options=hw_main_disk_options(hw_report), required=True
),
}
}
@API.register
def get_disk_schemas(base_path: Path, machine_name: str) -> dict[str, DiskSchema]:
"""
Get the available disk schemas
"""
disk_templates = clan_templates(TemplateType.DISK)
disk_schemas = {}
hw_report_path = HardwareConfig.NIXOS_FACTER.config_path(base_path, machine_name)
if not hw_report_path.exists():
msg = "Hardware configuration missing"
raise ClanError(msg)
hw_report = {}
with hw_report_path.open("r") as hw_report_file:
hw_report = json.load(hw_report_file)
for disk_template in disk_templates.iterdir():
if disk_template.is_file():
schema_name = disk_template.stem
if schema_name not in templates:
msg = f"Disk schema {schema_name} not found in templates"
raise ClanError(
msg,
description="This is an internal architecture problem. Because disk schemas dont define their own interface",
)
placeholder_getters = templates.get(schema_name)
placeholders = {}
if placeholder_getters:
placeholders = {k: v(hw_report) for k, v in placeholder_getters.items()}
disk_schemas[schema_name] = DiskSchema(
name=schema_name, placeholders=placeholders
)
return disk_schemas
@API.register
def set_machine_disk_schema(
base_path: Path,
machine_name: str,
schema_name: str,
# Placeholders are used to fill in the disk schema
# Use get disk schemas to get the placeholders and their options
placeholders: dict[str, str],
force: bool = False,
) -> None:
"""
Set the disk placeholders of the template
"""
# Assert the hw-config must exist before setting the disk
hw_config = show_machine_hardware_config(base_path, machine_name)
hw_config_path = hw_config.config_path(base_path, machine_name)
if not hw_config_path.exists():
msg = "Hardware configuration must exist before applying disk schema"
raise ClanError(msg)
if hw_config != HardwareConfig.NIXOS_FACTER:
msg = "Hardware configuration must use type FACTER for applying disk schema automatically"
raise ClanError(msg)
disk_schema_path = clan_templates(TemplateType.DISK) / f"{schema_name}.nix"
if not disk_schema_path.exists():
msg = f"Disk schema not found at {disk_schema_path}"
raise ClanError(msg)
# Check that the placeholders are valid
disk_schema = get_disk_schemas(base_path, machine_name)[schema_name]
# check that all required placeholders are present
for placeholder_name, schema_placeholder in disk_schema.placeholders.items():
if schema_placeholder.required and placeholder_name not in placeholders:
msg = f"Required placeholder {placeholder_name} - {schema_placeholder} missing"
raise ClanError(msg)
# For every placeholder check that the value is valid
for placeholder_name, placeholder_value in placeholders.items():
ph = disk_schema.placeholders.get(placeholder_name)
# Unknown placeholder
if not ph:
msg = (
f"Placeholder {placeholder_name} not found in disk schema {schema_name}"
)
raise ClanError(msg)
# Invalid value. Check if the value is one of the provided options
if ph.options and placeholder_value not in ph.options:
msg = (
f"Invalid value {placeholder_value} for placeholder {placeholder_name}"
)
raise ClanError(msg, description=f"Valid options: {ph.options}")
with disk_schema_path.open("r") as disk_template:
config_str = disk_template.read()
for placeholder_name, placeholder_value in placeholders.items():
config_str = config_str.replace(
r"{{" + placeholder_name + r"}}", placeholder_value
)
# Custom replacements
config_str = config_str.replace(r"{{uuid}}", str(uuid4()).replace("-", ""))
# place disko.nix alongside the hw-config
disko_file_path = hw_config_path.parent.joinpath("disko.nix")
if disko_file_path.exists() and not force:
msg = f"Disk schema already exists at {disko_file_path}"
raise ClanError(msg)
with disko_file_path.open("w") as disk_config:
disk_config.write(config_str)
commit_file(
disko_file_path,
base_path,
commit_message=f"Set disk schema of machine: {machine_name} to {schema_name}",
)