/ PyCrypCli / PyCrypCli / commands / device.py
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}%")