Coverage for src / server_list / config.py: 69%
118 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-31 11:45 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-31 11:45 +0000
1#!/usr/bin/env python3
2"""
3Configuration dataclasses for server-list.
5Provides typed configuration classes that represent config.yaml structure.
6"""
8from dataclasses import dataclass, field
9from pathlib import Path
12@dataclass
13class StorageConfig:
14 """Storage device configuration."""
16 name: str
17 model: str
18 volume: str
20 @classmethod
21 def parse(cls, data: dict) -> "StorageConfig":
22 """Parse from dictionary."""
23 return cls(
24 name=data["name"],
25 model=data["model"],
26 volume=data["volume"],
27 )
30@dataclass
31class VmConfig:
32 """VM configuration."""
34 name: str
36 @classmethod
37 def parse(cls, data: dict) -> "VmConfig":
38 """Parse from dictionary."""
39 return cls(name=data["name"])
42@dataclass
43class MountConfig:
44 """Mount point configuration."""
46 label: str
47 path: str
48 type: str = "filesystem" # "btrfs", "filesystem", or "windows"
50 @classmethod
51 def parse(cls, data: dict) -> "MountConfig":
52 """Parse from dictionary."""
53 label = data["label"]
54 # If path is not specified, use label as display path
55 path = data.get("path", label)
56 return cls(
57 label=label,
58 path=path,
59 type=data.get("type", "filesystem"),
60 )
63@dataclass
64class MachineConfig:
65 """Machine (server) configuration."""
67 name: str
68 mode: str # Server model name
69 cpu: str
70 ram: str
71 os: str
72 storage: list[StorageConfig] = field(default_factory=list)
73 filesystem: list[str] = field(default_factory=list) # e.g., ["zfs"]
74 esxi: str | None = None
75 ilo: str | None = None
76 vm: list[VmConfig] = field(default_factory=list)
77 mount: list[MountConfig] = field(default_factory=list)
79 @classmethod
80 def parse(cls, data: dict) -> "MachineConfig":
81 """Parse from dictionary."""
82 raw_storage = data.get("storage", [])
83 # Parse storage as array of objects
84 storage = [StorageConfig.parse(s) for s in raw_storage] if raw_storage else []
85 filesystem = data.get("filesystem", [])
86 vm = [VmConfig.parse(v) for v in data.get("vm", [])]
87 mount = [MountConfig.parse(m) for m in data.get("mount", [])]
88 return cls(
89 name=data["name"],
90 mode=data["mode"],
91 cpu=data["cpu"],
92 ram=data["ram"],
93 os=data["os"],
94 storage=storage,
95 filesystem=filesystem,
96 esxi=data.get("esxi"),
97 ilo=data.get("ilo"),
98 vm=vm,
99 mount=mount,
100 )
102 def to_dict(self) -> dict:
103 """Convert to dictionary for API responses."""
104 result: dict = {
105 "name": self.name,
106 "mode": self.mode,
107 "cpu": self.cpu,
108 "ram": self.ram,
109 "os": self.os,
110 }
111 if self.storage: 111 ↛ 116line 111 didn't jump to line 116 because the condition on line 111 was always true
112 result["storage"] = [
113 {"name": s.name, "model": s.model, "volume": s.volume}
114 for s in self.storage
115 ]
116 if self.filesystem: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 result["filesystem"] = self.filesystem
118 if self.esxi: 118 ↛ 120line 118 didn't jump to line 120 because the condition on line 118 was always true
119 result["esxi"] = self.esxi
120 if self.ilo: 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true
121 result["ilo"] = self.ilo
122 if self.vm: 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true
123 result["vm"] = [{"name": v.name} for v in self.vm]
124 if self.mount: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 result["mount"] = [
126 {"label": m.label, "path": m.path, "type": m.type}
127 for m in self.mount
128 ]
129 return result
132@dataclass
133class WebappConfig:
134 """Web application configuration."""
136 static_dir_path: str
137 image_dir_path: str
139 @classmethod
140 def parse(cls, data: dict) -> "WebappConfig":
141 """Parse from dictionary."""
142 return cls(
143 static_dir_path=data["static_dir_path"],
144 image_dir_path=data["image_dir_path"],
145 )
147 def get_static_dir(self, base_dir: Path) -> Path:
148 """Get absolute path to static directory."""
149 path = Path(self.static_dir_path)
150 if not path.is_absolute():
151 path = base_dir / path
152 return path
154 def get_image_dir(self, base_dir: Path) -> Path:
155 """Get absolute path to image directory."""
156 path = Path(self.image_dir_path)
157 if not path.is_absolute():
158 path = base_dir / path
159 return path
162@dataclass
163class DataConfig:
164 """Data/cache directory configuration."""
166 cache: str
168 @classmethod
169 def parse(cls, data: dict) -> "DataConfig":
170 """Parse from dictionary."""
171 return cls(cache=data["cache"])
173 def get_cache_dir(self, base_dir: Path) -> Path:
174 """Get absolute path to cache directory."""
175 path = Path(self.cache)
176 if not path.is_absolute():
177 path = base_dir / path
178 return path
181@dataclass
182class Config:
183 """Main configuration class representing config.yaml."""
185 webapp: WebappConfig
186 data: DataConfig
187 machine: list[MachineConfig]
189 @classmethod
190 def parse(cls, data: dict) -> "Config":
191 """Parse from dictionary (parsed YAML)."""
192 webapp = WebappConfig.parse(data["webapp"])
193 data_config = DataConfig.parse(data["data"])
194 machines = [MachineConfig.parse(m) for m in data["machine"]]
195 return cls(
196 webapp=webapp,
197 data=data_config,
198 machine=machines,
199 )
201 @classmethod
202 def load(cls, config_path: Path, schema_path: Path) -> "Config":
203 """Load config from YAML file with schema validation."""
204 import my_lib.config
206 data = my_lib.config.load(config_path, schema_path)
207 return cls.parse(data)
209 def get_machine_by_name(self, name: str) -> MachineConfig | None:
210 """Find machine by name."""
211 for machine in self.machine:
212 if machine.name == name:
213 return machine
214 return None
216 def get_esxi_hosts(self) -> list[str]:
217 """Get list of ESXi host names."""
218 return [m.name for m in self.machine if m.esxi]
220 def is_esxi_host(self, name: str) -> bool:
221 """Check if a machine is an ESXi host."""
222 machine = self.get_machine_by_name(name)
223 return machine is not None and machine.esxi is not None
225 def to_dict(self) -> dict:
226 """Convert to dictionary for API responses."""
227 return {
228 "webapp": {
229 "static_dir_path": self.webapp.static_dir_path,
230 "image_dir_path": self.webapp.image_dir_path,
231 },
232 "data": {
233 "cache": self.data.cache,
234 },
235 "machine": [m.to_dict() for m in self.machine],
236 }