virtui-manager 1.3.0__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vmanager/vmcard.py CHANGED
@@ -28,7 +28,7 @@ from .vm_actions import (
28
28
  clone_vm, rename_vm, create_vm_snapshot,
29
29
  restore_vm_snapshot, delete_vm_snapshot,
30
30
  create_external_overlay, commit_disk_changes,
31
- discard_overlay, delete_vm
31
+ discard_overlay, delete_vm, hibernate_vm
32
32
  )
33
33
 
34
34
  from .vm_queries import (
@@ -51,11 +51,12 @@ from .utils import (
51
51
  extract_server_name_from_uri,
52
52
  generate_tooltip_markdown,
53
53
  remote_viewer_cmd,
54
+ check_tmux,
54
55
  )
55
56
  from .constants import (
56
57
  ButtonLabels, TabTitles, StatusText,
57
58
  SparklineLabels, ErrorMessages, DialogMessages, VmAction,
58
- WarningMessages, SuccessMessages
59
+ WarningMessages, SuccessMessages, StaticText, ProgressMessages
59
60
  )
60
61
 
61
62
  class VMCardActions(Static):
@@ -66,12 +67,14 @@ class VMCardActions(Static):
66
67
  def compose(self):
67
68
  self.card.ui["start"] = Button(ButtonLabels.START, id="start", variant="success")
68
69
  self.card.ui["shutdown"] = Button(ButtonLabels.SHUTDOWN, id="shutdown", variant="primary")
70
+ self.card.ui["hibernate"] = Button(ButtonLabels.HIBERNATE_VM, id="hibernate", variant="primary")
69
71
  self.card.ui["stop"] = Button(ButtonLabels.FORCE_OFF, id="stop", variant="error")
70
72
  self.card.ui["pause"] = Button(ButtonLabels.PAUSE, id="pause", variant="primary")
71
73
  self.card.ui["resume"] = Button(ButtonLabels.RESUME, id="resume", variant="success")
72
74
  self.card.ui["configure-button"] = Button(ButtonLabels.CONFIGURE, id="configure-button", variant="primary")
73
75
  self.card.ui["web_console"] = Button(ButtonLabels.WEB_CONSOLE, id="web_console", variant="default")
74
76
  self.card.ui["connect"] = Button(ButtonLabels.CONNECT, id="connect", variant="default")
77
+ self.card.ui["tmux_console"] = Button(ButtonLabels.TEXT_CONSOLE, id="tmux_console", variant="default")
75
78
 
76
79
  self.card.ui["snapshot_take"] = Button(ButtonLabels.SNAPSHOT, id="snapshot_take", variant="primary")
77
80
  self.card.ui["snapshot_restore"] = Button(ButtonLabels.RESTORE_SNAPSHOT, id="snapshot_restore", variant="primary")
@@ -100,16 +103,18 @@ class VMCardActions(Static):
100
103
  yield self.card.ui["pause"]
101
104
  yield self.card.ui["resume"]
102
105
  with Vertical():
103
- yield self.card.ui["configure-button"]
104
106
  yield self.card.ui["web_console"]
105
107
  yield self.card.ui["connect"]
106
- with TabPane(self.card._get_snapshot_tab_title(num_snapshots=0), id="snapshot-tab"):
108
+ if os.environ.get("TMUX") and check_tmux():
109
+ yield self.card.ui["tmux_console"]
110
+ with TabPane(TabTitles.STATE_MANAGEMENT, id="snapshot-tab"):
107
111
  with Horizontal():
108
112
  with Vertical():
109
113
  yield self.card.ui["snapshot_take"]
110
114
  yield self.card.ui["snapshot_restore"]
111
115
  yield self.card.ui["snapshot_delete"]
112
116
  with Vertical():
117
+ yield self.card.ui["hibernate"]
113
118
  yield self.card.ui["create_overlay"]
114
119
  yield self.card.ui["commit_disk"]
115
120
  yield self.card.ui["discard_overlay"]
@@ -122,6 +127,7 @@ class VMCardActions(Static):
122
127
  yield self.card.ui["clone"]
123
128
  yield self.card.ui["migration"]
124
129
  with Vertical():
130
+ yield self.card.ui["configure-button"]
125
131
  yield self.card.ui["xml"]
126
132
  yield Static(classes="button-separator")
127
133
  yield self.card.ui["rename-button"]
@@ -184,21 +190,19 @@ class VMCard(Static):
184
190
  def _get_snapshot_tab_title(self, num_snapshots: int = -1) -> str:
185
191
  """Get snapshot tab title. Pass num_snapshots to avoid blocking libvirt call."""
186
192
  if num_snapshots == -1:
187
- # If no count provided, don't fetch it here to avoid blocking.
188
- # For now, return default if we can't get it cheaply.
189
- return TabTitles.SNAP_OVER_UPDATE # TabTitles.SNAPSHOT + "/" + TabTitles.OVERLAY
193
+ # If no count provided, don't fetch it here to avoid blocking.
194
+ # For now, return default if we can't get it cheaply.
195
+ return TabTitles.SNAP_OVER_UPDATE
190
196
 
191
197
  if self.vm:
192
198
  try:
193
- if num_snapshots == 0:
194
- return TabTitles.SNAPSHOT + "/" + TabTitles.OVERLAY
195
- elif num_snapshots == 1:
196
- return TabTitles.SNAPSHOT + "(" + str(num_snapshots) + ")" + "/" + TabTitles.OVERLAY
197
- elif num_snapshots >= 2:
198
- return TabTitles.SNAPSHOTS + "(" + str(num_snapshots) + ")" "/" + TabTitles.OVERLAY
199
+ if num_snapshots <= 0:
200
+ return TabTitles.STATE_MANAGEMENT
201
+ else:
202
+ return f"{TabTitles.STATE_MANAGEMENT}({num_snapshots})"
199
203
  except libvirt.libvirtError:
200
204
  pass # Domain might be transient or invalid
201
- return TabTitles.SNAPSHOT + "/" + TabTitles.OVERLAY
205
+ return TabTitles.STATE_MANAGEMENT
202
206
 
203
207
  def update_snapshot_tab_title(self, num_snapshots: int = -1) -> None:
204
208
  """Updates the snapshot tab title."""
@@ -248,6 +252,15 @@ class VMCard(Static):
248
252
  status_widget.update(status_text)
249
253
 
250
254
  def compose(self):
255
+ self.ui["btn_quick_start"] = Button("▶", id="start", variant="success", classes="btn-small")
256
+ self.ui["btn_quick_start"].tooltip = StaticText.START_VMS
257
+ self.ui["btn_quick_view"] = Button("👁", id="connect", classes="btn-small")
258
+ self.ui["btn_quick_view"].tooltip = StaticText.REMOTE_VIEWER
259
+ self.ui["btn_quick_resume"] = Button("⏯", id="resume", variant="success", classes="btn-small")
260
+ self.ui["btn_quick_resume"].tooltip = ButtonLabels.RESUME
261
+ self.ui["btn_quick_stop"] = Button("■", id="shutdown", variant="primary", classes="btn-small")
262
+ self.ui["btn_quick_stop"].tooltip = ButtonLabels.SHUTDOWN
263
+
251
264
  self.ui["checkbox"] = Checkbox("", id="vm-select-checkbox", classes="vm-select-checkbox", value=self.is_selected, tooltip="Select VM")
252
265
  self.ui["vmname"] = Static(self._get_vm_display_name(), id="vmname", classes="vmname")
253
266
  self.ui["status"] = Static(f"{self.status}{self.webc_status_indicator}", id="status")
@@ -286,6 +299,12 @@ class VMCard(Static):
286
299
  with Vertical():
287
300
  yield self.ui["vmname"]
288
301
  yield self.ui["status"]
302
+ with Horizontal(classes="quick-actions"):
303
+ with Vertical():
304
+ yield self.ui["btn_quick_resume"]
305
+ yield self.ui["btn_quick_stop"]
306
+ yield self.ui["btn_quick_view"]
307
+ yield self.ui["btn_quick_start"]
289
308
 
290
309
  yield self.ui["sparklines_container"]
291
310
  yield self.ui["collapsible"]
@@ -311,6 +330,8 @@ class VMCard(Static):
311
330
  # Clean up dynamic UI references to avoid memory leaks and stale state
312
331
  keys_to_keep = {
313
332
  "checkbox", "vmname", "status", "collapsible",
333
+ "btn_quick_start", "btn_quick_stop", "btn_quick_view",
334
+ "btn_quick_resume",
314
335
  "sparklines_container",
315
336
  # Resource Sparklines
316
337
  "cpu_label", "cpu_sparkline", "cpu_sparkline_container",
@@ -342,7 +363,7 @@ class VMCard(Static):
342
363
  """Updates the tooltip for the VM name using Markdown."""
343
364
  if not self.display or not self.ui or "vmname" not in self.ui:
344
365
  return
345
-
366
+
346
367
  uuid = self.internal_id
347
368
  if not uuid:
348
369
  return
@@ -388,7 +409,7 @@ class VMCard(Static):
388
409
  if ips:
389
410
  ip_display = ", ".join(ips)
390
411
 
391
- cpu_model_display = f" ({self.cpu_model})" if self.cpu_model else ""
412
+ cpu_model_display = f" {self.cpu_model}" if self.cpu_model else ""
392
413
 
393
414
  tooltip_md = generate_tooltip_markdown(
394
415
  uuid=uuid_display,
@@ -397,7 +418,7 @@ class VMCard(Static):
397
418
  ip=ip_display,
398
419
  boot=self.boot_device or "N/A",
399
420
  cpu=self.cpu,
400
- cpu_model=self.cpu_model or "",
421
+ cpu_model=cpu_model_display or "",
401
422
  memory=self.memory
402
423
  )
403
424
 
@@ -551,19 +572,20 @@ class VMCard(Static):
551
572
  if not self.ui:
552
573
  return
553
574
 
554
- #sparklines = self.ui.get("sparklines_container")
575
+ sparklines = self.ui.get("sparklines_container")
555
576
  collapsible = self.ui.get("collapsible")
556
577
  vmname = self.ui.get("vmname")
557
578
  vmstatus = self.ui.get("status")
558
579
  checkbox = self.ui.get("checkbox")
559
580
 
560
- if value: # if compact view, add hidden class
561
- #if sparklines and sparklines.is_mounted:
562
- # logging.info("DEBUG remove spark")
563
- # sparklines.remove()
581
+ if value:
582
+ if sparklines and sparklines.is_mounted:
583
+ sparklines.display = False
564
584
  if collapsible and collapsible.is_mounted:
565
585
  collapsible.collapsed = True
566
586
  collapsible.remove()
587
+ #if checkbox and checkbox.is_mounted:
588
+ # checkbox.display = False
567
589
  else:
568
590
  try:
569
591
  info_container = self.query_one("#info-container")
@@ -571,13 +593,17 @@ class VMCard(Static):
571
593
  # Check if collapsible is already a child of info_container to avoid double mounting
572
594
  if collapsible not in info_container.children:
573
595
  info_container.mount(collapsible)
596
+ if sparklines:
597
+ sparklines.display = True
598
+ #if checkbox:
599
+ # checkbox.display = True
600
+
574
601
  except NoMatches:
575
602
  # This can happen if the card is not fully initialized or structures changed
576
603
  logging.warning(f"Could not find #info-container on VMCard {self.name} when switching to detailed view.")
577
604
  except Exception as e:
578
605
  # Catch-all for potential mounting errors (e.g. already mounted elsewhere?)
579
- logging.warning(f"Error restoring collapsible in detailed view: {e}")
580
-
606
+ logging.warning(f"Error restoring collapsible in detailed view: {e}")
581
607
 
582
608
  # Ensure sparklines visibility is correct
583
609
  self.watch_stats_view_mode(self.stats_view_mode, self.stats_view_mode)
@@ -586,10 +612,11 @@ class VMCard(Static):
586
612
  if value: # Compact view
587
613
  self.styles.height = 4
588
614
  self.styles.width = 20
589
- if vmname: vmname.styles.content_align = ("left", "middle")
615
+ if vmname:
616
+ vmname.styles.content_align = ("left", "middle")
590
617
  if vmstatus: vmstatus.styles.content_align = ("left", "middle")
591
618
  if checkbox: checkbox.styles.width = "2"
592
- else: # Detailed view
619
+ else: # Detailed view
593
620
  self.styles.height = 14
594
621
  self.styles.width = 41
595
622
  if vmname: vmname.styles.content_align = ("center", "middle")
@@ -626,7 +653,17 @@ class VMCard(Static):
626
653
 
627
654
  def watch_server_border_color(self, old_color: str, new_color: str) -> None:
628
655
  """Called when server_border_color changes."""
629
- self.styles.border = ("solid", new_color)
656
+ if self.is_selected:
657
+ self.styles.border = ("panel", "white")
658
+ else:
659
+ self.styles.border = ("solid", new_color)
660
+
661
+ def on_click(self, event: Click) -> None:
662
+ """Handle click events on the card."""
663
+ if event.button == 3:
664
+ self.is_selected = not self.is_selected
665
+ self.post_message(VMSelectionChanged(vm_uuid=self.raw_uuid, is_selected=self.is_selected))
666
+ event.stop()
630
667
 
631
668
  def on_unmount(self) -> None:
632
669
  """Stop the timer and cancel any running stat workers when the widget is removed."""
@@ -670,11 +707,10 @@ class VMCard(Static):
670
707
 
671
708
  def watch_is_selected(self, old_value: bool, new_value: bool) -> None:
672
709
  """Called when is_selected changes to update the checkbox."""
673
- if not self.ui:
674
- return
675
- checkbox = self.ui.get("checkbox")
676
- if checkbox:
677
- checkbox.value = new_value
710
+ if self.ui:
711
+ checkbox = self.ui.get("checkbox")
712
+ if checkbox:
713
+ checkbox.value = new_value
678
714
 
679
715
  if new_value:
680
716
  self.styles.border = ("panel", "white")
@@ -885,11 +921,20 @@ class VMCard(Static):
885
921
  is_pmsuspended = self.status == StatusText.PMSUSPENDED
886
922
  is_blocked = self.status == StatusText.BLOCKED
887
923
 
924
+ if not self.ui.get("btn_quick_start"):
925
+ return
926
+
927
+ self.ui["btn_quick_start"].display = is_stopped
928
+ self.ui["btn_quick_stop"].display = is_running or is_blocked
929
+ self.ui["btn_quick_view"].display = (is_running or is_paused or is_blocked)
930
+ self.ui["btn_quick_resume"].display = is_paused or is_pmsuspended
931
+
888
932
  if not self.ui.get("rename-button"):
889
933
  return
890
934
 
891
935
  self.ui["start"].display = is_stopped
892
936
  self.ui["shutdown"].display = is_running or is_blocked
937
+ self.ui["hibernate"].display = is_running or is_blocked
893
938
  self.ui["stop"].display = is_running or is_paused or is_pmsuspended or is_blocked
894
939
  self.ui["delete"].display = is_running or is_paused or is_stopped or is_pmsuspended or is_blocked
895
940
  self.ui["clone"].display = is_stopped
@@ -993,7 +1038,7 @@ class VMCard(Static):
993
1038
 
994
1039
  def update_ui():
995
1040
  self._update_slow_buttons(snapshot_summary, has_overlay)
996
-
1041
+
997
1042
  try:
998
1043
  self.app.call_from_thread(update_ui)
999
1044
  except RuntimeError:
@@ -1016,10 +1061,10 @@ class VMCard(Static):
1016
1061
  pane = tabbed_content.get_tab("snapshot-tab")
1017
1062
  if snapshot_count > 0:
1018
1063
  latest = snapshot_summary.get('latest')
1019
- info = f"Latest: {latest['name']} ({latest['time']})" if latest else "Unknown"
1064
+ info = f"{StaticText.LATEST_SNAPSHOT} {latest['name']} ({latest['time']})" if latest else "Unknown"
1020
1065
  pane.tooltip = f"{info}\nTotal: {snapshot_count}"
1021
1066
  else:
1022
- pane.tooltip = "No Snapshots created"
1067
+ pane.tooltip = StaticText.NO_SNAPSHOTS_CREATED
1023
1068
  except Exception:
1024
1069
  pass
1025
1070
 
@@ -1065,12 +1110,14 @@ class VMCard(Static):
1065
1110
 
1066
1111
  button_handlers = {
1067
1112
  "shutdown": self._handle_shutdown_button,
1113
+ "hibernate": self._handle_hibernate_button,
1068
1114
  "stop": self._handle_stop_button,
1069
1115
  "pause": self._handle_pause_button,
1070
1116
  "resume": self._handle_resume_button,
1071
1117
  "xml": self._handle_xml_button,
1072
1118
  "connect": self._handle_connect_button,
1073
1119
  "web_console": self._handle_web_console_button,
1120
+ "tmux_console": self._handle_tmux_console_button,
1074
1121
  "snapshot_take": self._handle_snapshot_take_button,
1075
1122
  "snapshot_restore": self._handle_snapshot_restore_button,
1076
1123
  "snapshot_delete": self._handle_snapshot_delete_button,
@@ -1177,7 +1224,7 @@ class VMCard(Static):
1177
1224
  self.app.vm_service.unsuppress_vm_events(self.internal_id)
1178
1225
 
1179
1226
  self.app.push_screen(
1180
- ConfirmationDialog(f"Are you sure you want to discard changes in '{target_disk}' and revert to its backing file? This action cannot be undone."),
1227
+ ConfirmationDialog(DialogMessages.CONFIRM_DISCARD_CHANGES.format(target_disk=target_disk)),
1181
1228
  on_confirm
1182
1229
  )
1183
1230
 
@@ -1185,7 +1232,7 @@ class VMCard(Static):
1185
1232
  proceed_with_discard(overlay_disks[0])
1186
1233
  else:
1187
1234
  self.app.push_screen(
1188
- SelectDiskModal(overlay_disks, "Select overlay disk to discard:"),
1235
+ SelectDiskModal(overlay_disks, StaticText.SELECT_OVERLAY_DISCARD),
1189
1236
  proceed_with_discard
1190
1237
  )
1191
1238
 
@@ -1211,7 +1258,7 @@ class VMCard(Static):
1211
1258
 
1212
1259
  def on_confirm(confirmed: bool):
1213
1260
  if confirmed:
1214
- progress_modal = ProgressModal(title=f"Committing changes for {self.name}...")
1261
+ progress_modal = ProgressModal(title=ProgressMessages.COMMITTING_CHANGES_FOR.format(name=self.name))
1215
1262
  self.app.push_screen(progress_modal)
1216
1263
 
1217
1264
  def do_commit():
@@ -1230,7 +1277,7 @@ class VMCard(Static):
1230
1277
  self.app.worker_manager.run(do_commit, name=f"commit_{self.name}")
1231
1278
 
1232
1279
  self.app.push_screen(
1233
- ConfirmationDialog(f"Are you sure you want to merge changes from '{target_disk}' into its backing file?"),
1280
+ ConfirmationDialog(DialogMessages.CONFIRM_MERGE_CHANGES.format(target_disk=target_disk)),
1234
1281
  on_confirm
1235
1282
  )
1236
1283
 
@@ -1243,6 +1290,27 @@ class VMCard(Static):
1243
1290
  if self.status in (StatusText.RUNNING, StatusText.PAUSED):
1244
1291
  self.post_message(VmActionRequest(self.internal_id, VmAction.STOP))
1245
1292
 
1293
+ def _handle_hibernate_button(self, event: Button.Pressed) -> None:
1294
+ """Handles the save button press."""
1295
+ logging.info(f"Attempting to save (hibernate) VM: {self.name}")
1296
+
1297
+ def do_save():
1298
+ self.stop_background_activities()
1299
+ self.app.vm_service.suppress_vm_events(self.internal_id)
1300
+ try:
1301
+ hibernate_vm(self.vm)
1302
+ self.app.call_from_thread(self.app.show_success_message, SuccessMessages.VM_SAVED_TEMPLATE.format(vm_name=self.name))
1303
+ self.app.vm_service.invalidate_vm_state_cache(self.internal_id)
1304
+ self.app.call_from_thread(setattr, self, 'status', StatusText.STOPPED)
1305
+ self.app.call_from_thread(self.update_button_layout)
1306
+ except Exception as e:
1307
+ self.app.call_from_thread(self.app.show_error_message, ErrorMessages.ERROR_ON_VM_DURING_ACTION.format(vm_name=self.name, action='save', error=e))
1308
+ finally:
1309
+ self.app.vm_service.unsuppress_vm_events(self.internal_id)
1310
+
1311
+ if self.status in (StatusText.RUNNING, StatusText.PAUSED):
1312
+ self.app.worker_manager.run(do_save, name=f"save_{self.internal_id}")
1313
+
1246
1314
  def stop_background_activities(self):
1247
1315
  """ Stop background activities before action """
1248
1316
  with self._timer_lock:
@@ -1324,7 +1392,7 @@ class VMCard(Static):
1324
1392
  self.app.refresh_vm_list()
1325
1393
  except libvirt.libvirtError as e:
1326
1394
  self.app.show_error_message(ErrorMessages.INVALID_XML_TEMPLATE.format(vm_name=self.name, error=e))
1327
- logging.error(error_msg)
1395
+ logging.error(e)
1328
1396
  else:
1329
1397
  self.app.show_success_message(SuccessMessages.NO_XML_CHANGES)
1330
1398
 
@@ -1430,6 +1498,50 @@ class VMCard(Static):
1430
1498
  else:
1431
1499
  self.app.worker_manager.run(worker, name=f"start_console_{self.vm.name()}")
1432
1500
 
1501
+ def _handle_tmux_console_button(self, event: Button.Pressed) -> None:
1502
+ """Handles the text console button press by opening a new tmux window."""
1503
+ logging.info(f"Attempting to open text console for VM: {self.name}")
1504
+
1505
+ # Check if running in tmux
1506
+ if not os.environ.get("TMUX"):
1507
+ self.app.show_error_message("This feature requires running inside tmux.")
1508
+ return
1509
+
1510
+ try:
1511
+ # Use cached values to avoid libvirt calls where possible
1512
+ uri = self.app.vm_service.get_uri_for_connection(self.conn)
1513
+ if not uri:
1514
+ uri = self.conn.getURI()
1515
+
1516
+ # Get proper domain name
1517
+ _, domain_name = self.app.vm_service.get_vm_identity(self.vm, self.conn)
1518
+
1519
+ # Construct command
1520
+ # tmux new-window -n "Console: <vm_name>" "virsh -c <uri> console <vm_name>; read"
1521
+ help_msg = (
1522
+ "echo '---------------------------------------------------------'; "
1523
+ "echo 'Tmux Navigation Help:'; "
1524
+ "echo ' Ctrl+B N or P - Move to the next or previous window.'; "
1525
+ "echo ' Ctrl+B W - Open a panel to navigate across windows in multiple sessions.'; "
1526
+ "echo ' Ctrl+] - Close the current view.'; "
1527
+ "echo ' Ctrl+B ? - View all keybindings. Press Q to exit.';"
1528
+ "echo '---------------------------------------------------------'; "
1529
+ "echo 'Starting console...'; sleep 1;"
1530
+ )
1531
+ cmd = [
1532
+ "tmux", "new-window",
1533
+ "-n", f"{domain_name}",
1534
+ f"{help_msg} virsh -c {uri} console {domain_name}; echo '\nConsole session ended. Press Enter to close window.'; read"
1535
+ ]
1536
+
1537
+ logging.info(f"Launching tmux console: {' '.join(cmd)}")
1538
+ subprocess.Popen(cmd)
1539
+ self.app.show_quick_message(f"Opened console for {domain_name}")
1540
+
1541
+ except Exception as e:
1542
+ logging.error(f"Failed to open tmux console: {e}")
1543
+ self.app.show_error_message(f"Failed to open console: {e}")
1544
+
1433
1545
  def _handle_snapshot_take_button(self, event: Button.Pressed) -> None:
1434
1546
  """Handles the snapshot take button press."""
1435
1547
  logging.info(f"Attempting to take snapshot for VM: {self.name}")
@@ -1680,10 +1792,10 @@ class VMCard(Static):
1680
1792
  pane = tabbed_content.get_tab("snapshot-tab")
1681
1793
  if snapshot_count > 0:
1682
1794
  latest = snapshot_summary.get('latest')
1683
- info = f"Latest: {latest['name']} ({latest['time']})" if latest else "Unknown"
1795
+ info = f"{StaticText.LATEST_SNAPSHOT} {latest['name']} ({latest['time']})" if latest else "Unknown"
1684
1796
  pane.tooltip = f"{info}\nTotal: {snapshot_count}"
1685
1797
  else:
1686
- pane.tooltip = "No Snapshots created"
1798
+ pane.tooltip = StaticText.NO_SNAPSHOTS_CREATED
1687
1799
  except Exception:
1688
1800
  pass
1689
1801
 
@@ -1922,8 +2034,6 @@ class VMCard(Static):
1922
2034
  finally:
1923
2035
  self.app.vm_service.unsuppress_vm_events(internal_id)
1924
2036
 
1925
- num_snapshots = self.vm.snapshotNum(0)
1926
-
1927
2037
  def on_confirm_rename(confirmed: bool, delete_snapshots=False) -> None:
1928
2038
  if confirmed:
1929
2039
  do_rename()
@@ -1960,10 +2070,10 @@ class VMCard(Static):
1960
2070
  def get_details_worker():
1961
2071
  try:
1962
2072
  result = self.app.vm_service.get_vm_details(
1963
- active_uris,
1964
- uuid,
1965
- domain=vm_obj,
1966
- conn=conn_obj,
2073
+ active_uris,
2074
+ uuid,
2075
+ domain=vm_obj,
2076
+ conn=conn_obj,
1967
2077
  cached_ips=cached_ips
1968
2078
  )
1969
2079
 
@@ -1975,22 +2085,27 @@ class VMCard(Static):
1975
2085
 
1976
2086
  vm_info, domain, conn_for_domain = result
1977
2087
 
1978
- def on_detail_modal_dismissed(res):
2088
+ def on_detail_modal_dismissed(_=None):
1979
2089
  self.post_message(VmCardUpdateRequest(self.internal_id))
1980
2090
  self._perform_tooltip_update()
1981
2091
 
1982
2092
  self.app.push_screen(
1983
- VMDetailModal(vm_name, vm_info, domain, conn_for_domain, self.app.vm_service.invalidate_vm_state_cache),
2093
+ VMDetailModal(
2094
+ vm_name,
2095
+ vm_info,
2096
+ domain,
2097
+ conn_for_domain,
2098
+ self.app.vm_service.invalidate_vm_state_cache),
1984
2099
  on_detail_modal_dismissed
1985
- )
2100
+ )
1986
2101
 
1987
2102
  self.app.call_from_thread(show_details)
1988
2103
 
1989
2104
  except Exception as e:
1990
- def show_error():
2105
+ def show_error(error_instance):
1991
2106
  loading_modal.dismiss()
1992
- self.app.show_error_message(ErrorMessages.ERROR_GETTING_VM_DETAILS_TEMPLATE.format(vm_name=vm_name, error=e))
1993
- self.app.call_from_thread(show_error)
2107
+ self.app.show_error_message(ErrorMessages.ERROR_GETTING_VM_DETAILS_TEMPLATE.format(vm_name=vm_name, error=error_instance))
2108
+ self.app.call_from_thread(show_error, e)
1994
2109
 
1995
2110
  self.app.worker_manager.run(get_details_worker, name=f"get_details_{uuid}")
1996
2111
 
@@ -2003,27 +2118,16 @@ class VMCard(Static):
2003
2118
  self.app.show_error_message(ErrorMessages.SELECT_AT_LEAST_TWO_SERVERS_FOR_MIGRATION)
2004
2119
  return
2005
2120
 
2006
- selected_vm_uuids = self.app.selected_vm_uuids
2121
+ selected_vm_uuids = list(self.app.selected_vm_uuids)
2007
2122
  selected_vms = []
2008
2123
  if selected_vm_uuids:
2124
+ found_domains_dict = self.app.vm_service.find_domains_by_uuids(self.app.active_uris, selected_vm_uuids)
2009
2125
  for uuid in selected_vm_uuids:
2010
- #Use cached domain lookup instead of iterating all URIs
2011
- with self.app.vm_service._cache_lock:
2012
- domain = self.app.vm_service._domain_cache.get(uuid)
2013
-
2126
+ domain = found_domains_dict.get(uuid)
2014
2127
  if domain:
2015
- try:
2016
- # Verify domain is still valid
2017
- domain.info()
2018
- selected_vms.append(domain)
2019
- found_domain = True
2020
- except libvirt.libvirtError:
2021
- found_domain = False
2128
+ selected_vms.append(domain)
2022
2129
  else:
2023
- found_domain = False
2024
- if not vm_info:
2025
- self.app.show_error_message(ErrorMessages.SELECTED_VM_NOT_FOUND_ON_ACTIVE_SERVER_TEMPLATE.format(uuid=uuid))
2026
-
2130
+ self.app.show_error_message(ErrorMessages.SELECTED_VM_NOT_FOUND_ON_ACTIVE_SERVER_TEMPLATE.format(uuid=uuid))
2027
2131
  if not selected_vms:
2028
2132
  selected_vms = [self.vm]
2029
2133
 
@@ -2053,7 +2157,7 @@ class VMCard(Static):
2053
2157
  state, _ = state_tuple
2054
2158
  if state in [libvirt.VIR_DOMAIN_RUNNING, libvirt.VIR_DOMAIN_PAUSED]:
2055
2159
  active_vms.append(vm)
2056
- except:
2160
+ except Exception:
2057
2161
  # Fallback to isActive() if cache lookup fails
2058
2162
  if vm.isActive():
2059
2163
  active_vms.append(vm)
@@ -2118,7 +2222,7 @@ class VMCard(Static):
2118
2222
  try:
2119
2223
  # Use vm_service to get XML (handles caching)
2120
2224
  self.app.vm_service._get_domain_xml(self.vm, internal_id=self.internal_id)
2121
-
2225
+
2122
2226
  # Update tooltip on main thread
2123
2227
  self.app.call_from_thread(self._perform_tooltip_update)
2124
2228
  self.app.call_from_thread(self.app.show_quick_message, f"Info refreshed for {self.name}")
@@ -17,7 +17,7 @@ from urllib.parse import urlparse
17
17
 
18
18
  import libvirt
19
19
 
20
- from .constants import AppInfo
20
+ from .constants import AppInfo, ErrorMessages, SuccessMessages
21
21
  from .events import VmCardUpdateRequest
22
22
  from .config import load_config, get_log_path
23
23
  from .vm_queries import get_vm_graphics_info