device.py
1 from typing import Any, cast 2 3 from .command import command, CommandError 4 from .help import print_help 5 from ..context import MainContext, DeviceContext 6 from ..exceptions import ( 7 AlreadyOwnADeviceError, 8 DeviceNotFoundError, 9 IncompatibleCPUSocketError, 10 NotEnoughRAMSlotsError, 11 IncompatibleRAMTypesError, 12 IncompatibleDriverInterfaceError, 13 DeviceIsStarterDeviceError, 14 ) 15 from ..models import Device, ResourceUsage, DeviceHardware, InventoryElement, HardwareConfig 16 from ..util import is_uuid 17 18 19 def get_device(context: MainContext, name_or_uuid: str, devices: list[Device] | None = None) -> Device: 20 if is_uuid(name_or_uuid): 21 try: 22 return Device.get_device(context.client, name_or_uuid) 23 except DeviceNotFoundError: 24 raise CommandError(f"There is no device with the uuid '{name_or_uuid}'.") 25 else: 26 found_devices: list[Device] = [] 27 for device in devices or Device.list_devices(context.client): 28 if device.name == name_or_uuid: 29 found_devices.append(device) 30 if not found_devices: 31 raise CommandError(f"There is no device with the name '{name_or_uuid}'.") 32 if len(found_devices) > 1: 33 raise CommandError( 34 f"There is more than one device with the name '{name_or_uuid}'. You need to specify its UUID." 35 ) 36 return found_devices[0] 37 38 39 @command("device", [MainContext, DeviceContext]) 40 def handle_device(context: MainContext, args: list[str]) -> None: 41 """ 42 Manage your devices 43 """ 44 45 if args: 46 raise CommandError("Unknown subcommand.") 47 print_help(context, handle_device) 48 49 50 @handle_device.subcommand("list") 51 def handle_device_list(context: MainContext, args: list[str]) -> None: 52 """ 53 List your devices 54 """ 55 56 if len(args) != 0: 57 raise CommandError("usage: device list") 58 59 devices: list[Device] = Device.list_devices(context.client) 60 if not devices: 61 print("You don't have any devices.") 62 else: 63 print("Your devices:") 64 for device in devices: 65 print(f" - [{['off', 'on'][device.powered_on]}] {device.name} (UUID: {device.uuid})") 66 67 68 @handle_device.subcommand("create") 69 def handle_device_create(context: MainContext, args: list[str]) -> None: 70 """ 71 Create your starter device 72 """ 73 74 if len(args) != 0: 75 raise CommandError("usage: device create") 76 77 try: 78 device: Device = Device.starter_device(context.client) 79 except AlreadyOwnADeviceError: 80 raise CommandError("You already own a device.") 81 82 print("Your device has been created!") 83 print(f"Hostname: {device.name} (UUID: {device.uuid})") 84 85 86 @handle_device.subcommand("build") 87 def handle_device_build(context: MainContext, args: list[str]) -> None: 88 """ 89 Build a new device 90 """ 91 92 if len(args) < 5: 93 raise CommandError("usage: device build <mainboard> <cpu> <gpu> <ram> [<ram>...] <disk> [<disk>...]") 94 95 hardware: HardwareConfig = context.client.get_hardware_config() 96 mainboard, cpu, gpu, *ram_and_disk = args 97 ram: list[str] = [] 98 disk: list[str] = [] 99 100 for e in hardware.mainboard: 101 if e.replace(" ", "") == mainboard: 102 mainboard = e 103 break 104 else: 105 print(f"'{mainboard}' is no mainboard.") 106 return 107 108 for e in hardware.cpu: 109 if e.replace(" ", "") == cpu: 110 cpu = e 111 break 112 else: 113 print(f"'{cpu}' is no cpu.") 114 return 115 116 for e in hardware.gpu: 117 if e.replace(" ", "") == gpu: 118 gpu = e 119 break 120 else: 121 print(f"'{gpu}' is no gpu.") 122 return 123 124 for element in ram_and_disk: 125 for e in hardware.ram: 126 if e.replace(" ", "") == element: 127 ram.append(e) 128 break 129 else: 130 for e in hardware.disk: 131 if e.replace(" ", "") == element: 132 disk.append(e) 133 break 134 else: 135 print(f"'{element}' is neither ram nor disk.") 136 return 137 138 if not ram: 139 raise CommandError("You have to chose at least one ram.") 140 if not disk: 141 raise CommandError("You have to chose at least one hard drive.") 142 143 inventory: list[str] = [e.name for e in InventoryElement.list_inventory(context.client)] 144 inventory_complete = True 145 for element in [mainboard, cpu, gpu] + ram + disk: 146 if element in inventory: 147 inventory.remove(element) 148 else: 149 print(f"'{element}' could not be found in your inventory.") 150 inventory_complete = False 151 if not inventory_complete: 152 return 153 154 try: 155 device: Device = Device.build(context.client, mainboard, cpu, gpu, ram, disk) 156 except IncompatibleCPUSocketError: 157 raise CommandError("The mainboard socket is not compatible with the cpu.") 158 except NotEnoughRAMSlotsError: 159 raise CommandError("The mainboard has not enough ram slots.") 160 except IncompatibleRAMTypesError: 161 raise CommandError("A ram type is incompatible with the mainboard.") 162 except IncompatibleDriverInterfaceError: 163 raise CommandError("The drive interface is not compatible with the mainboard.") 164 else: 165 print("Your device has been created!") 166 print(f"Hostname: {device.name} (UUID: {device.uuid})") 167 168 169 @handle_device.subcommand("boot", aliases=["start"]) 170 def handle_device_boot(context: MainContext, args: list[str]) -> None: 171 """ 172 Boot a device 173 """ 174 175 if len(args) != 1: 176 raise CommandError("usage: device boot <name|uuid>") 177 178 device: Device = get_device(context, args[0]) 179 if device.powered_on: 180 raise CommandError("This device is already powered on.") 181 182 device.power() 183 184 185 @handle_device.subcommand("shutdown", aliases=["poweroff", "halt"]) 186 def handle_device_shutdown(context: MainContext, args: list[str]) -> None: 187 """ 188 Shut down a device 189 """ 190 191 if len(args) != 1: 192 raise CommandError("usage: device shutdown <name|uuid>") 193 194 device: Device = get_device(context, args[0]) 195 if not device.powered_on: 196 raise CommandError("This device is not powered on.") 197 198 device.power() 199 if isinstance(context, DeviceContext) and context.host.uuid == device.uuid: 200 context.close() 201 202 203 @command("shutdown", [DeviceContext], ["poweroff", "halt"]) 204 def handle_shutdown(context: DeviceContext, _: Any) -> None: 205 """Shutdown this device""" 206 207 device: Device = context.host 208 if not device.powered_on: 209 raise CommandError("This device is not powered on.") 210 211 device.power() 212 context.close() 213 214 215 @handle_device.subcommand("connect") 216 def handle_device_connect(context: MainContext, args: list[str]) -> None: 217 """ 218 Connect to one of your devices 219 """ 220 221 if len(args) != 1: 222 raise CommandError("usage: device connect <name|uuid>") 223 224 device: Device = get_device(context, args[0]) 225 if not device.powered_on: 226 if not context.confirm("This device is not powered on. Do you want to start it now?"): 227 return 228 device.power() 229 230 context.open(DeviceContext(context.root_context, cast(str, context.session_token), device)) 231 232 233 @handle_device.subcommand("delete") 234 def handle_device_delete(context: MainContext, args: list[str]) -> None: 235 """ 236 Delete a device 237 """ 238 239 if len(args) != 1: 240 raise CommandError("usage: device delete <name|uuid>") 241 242 device: Device = get_device(context, args[0]) 243 if not context.confirm(f"Are you sure you want to delete the device '{device.name}' including all its files?"): 244 return 245 246 try: 247 device.delete() 248 except DeviceIsStarterDeviceError: 249 raise CommandError("You cannot delete your starter device.") 250 251 print("Device has been deleted.") 252 253 254 @handle_device_boot.completer() 255 @handle_device_shutdown.completer() 256 @handle_device_connect.completer() 257 @handle_device_delete.completer() 258 def complete_device(context: MainContext, args: list[str]) -> list[str]: 259 if len(args) == 1: 260 device_names: list[str] = [device.name for device in Device.list_devices(context.client)] 261 return [name for name in device_names if device_names.count(name) == 1] 262 return [] 263 264 265 @handle_device_build.completer() 266 def complete_build(context: MainContext, args: list[str]) -> list[str]: 267 if len(args) == 1: 268 return [name.replace(" ", "") for name in list(context.client.get_hardware_config().mainboard)] 269 if len(args) == 2: 270 return [name.replace(" ", "") for name in list(context.client.get_hardware_config().cpu)] 271 if len(args) == 3: 272 return [name.replace(" ", "") for name in list(context.client.get_hardware_config().gpu)] 273 if len(args) == 4: 274 return [name.replace(" ", "") for name in list(context.client.get_hardware_config().ram)] 275 if len(args) >= 5: 276 hardware: HardwareConfig = context.client.get_hardware_config() 277 return [name.replace(" ", "") for name in list(hardware.ram) + list(hardware.disk)] 278 return [] 279 280 281 @command("hostname", [DeviceContext]) 282 def handle_hostname(context: DeviceContext, args: list[str]) -> None: 283 """ 284 Show or modify the name of the device 285 """ 286 287 if args: 288 name: str = " ".join(args) 289 if not name: 290 raise CommandError("The name must not be empty.") 291 if len(name) > 15: 292 raise CommandError("The name cannot be longer than 15 characters.") 293 context.host.change_name(name) 294 else: 295 print(context.host.name) 296 297 298 @command("top", [DeviceContext]) 299 def handle_top(context: DeviceContext, _: Any) -> None: 300 """ 301 Display the current resource usage of this device 302 """ 303 304 print(f"Resource usage of '{context.host.name}':") 305 print() 306 resource_usage: ResourceUsage = context.host.get_resource_usage() 307 hardware: dict[str, DeviceHardware] = {dh.hardware_type: dh for dh in context.host.get_hardware()} 308 309 print(f" Mainboard: {hardware['mainboard'].hardware_element}") 310 print() 311 312 print(f" CPU: {hardware['cpu'].hardware_element}") 313 print(f" => Usage: {resource_usage.cpu * 100:.1f}%") 314 print() 315 316 print(f" RAM: {hardware['ram'].hardware_element}") 317 print(f" => Usage: {resource_usage.ram * 100:.1f}%") 318 print() 319 320 if "gpu" in hardware: 321 print(f" GPU: {hardware['gpu'].hardware_element}") 322 print(f" => Usage: {resource_usage.gpu * 100:.1f}%") 323 print() 324 325 print(f" Disk: {hardware['disk'].hardware_element}") 326 print(f" => Usage: {resource_usage.disk * 100:.1f}%") 327 print() 328 329 print(" Network:") 330 print(f" => Usage: {resource_usage.network * 100:.1f}%")