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

1#!/usr/bin/env python3 

2""" 

3Configuration dataclasses for server-list. 

4 

5Provides typed configuration classes that represent config.yaml structure. 

6""" 

7 

8from dataclasses import dataclass, field 

9from pathlib import Path 

10 

11 

12@dataclass 

13class StorageConfig: 

14 """Storage device configuration.""" 

15 

16 name: str 

17 model: str 

18 volume: str 

19 

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 ) 

28 

29 

30@dataclass 

31class VmConfig: 

32 """VM configuration.""" 

33 

34 name: str 

35 

36 @classmethod 

37 def parse(cls, data: dict) -> "VmConfig": 

38 """Parse from dictionary.""" 

39 return cls(name=data["name"]) 

40 

41 

42@dataclass 

43class MountConfig: 

44 """Mount point configuration.""" 

45 

46 label: str 

47 path: str 

48 type: str = "filesystem" # "btrfs", "filesystem", or "windows" 

49 

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 ) 

61 

62 

63@dataclass 

64class MachineConfig: 

65 """Machine (server) configuration.""" 

66 

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) 

78 

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 ) 

101 

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 

130 

131 

132@dataclass 

133class WebappConfig: 

134 """Web application configuration.""" 

135 

136 static_dir_path: str 

137 image_dir_path: str 

138 

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 ) 

146 

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 

153 

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 

160 

161 

162@dataclass 

163class DataConfig: 

164 """Data/cache directory configuration.""" 

165 

166 cache: str 

167 

168 @classmethod 

169 def parse(cls, data: dict) -> "DataConfig": 

170 """Parse from dictionary.""" 

171 return cls(cache=data["cache"]) 

172 

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 

179 

180 

181@dataclass 

182class Config: 

183 """Main configuration class representing config.yaml.""" 

184 

185 webapp: WebappConfig 

186 data: DataConfig 

187 machine: list[MachineConfig] 

188 

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 ) 

200 

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 

205 

206 data = my_lib.config.load(config_path, schema_path) 

207 return cls.parse(data) 

208 

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 

215 

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] 

219 

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 

224 

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 }