feat(tools): Enhance guest tools installation and management

- Added new API endpoints for tool catalog and installation management.
- Updated usage documentation to include new tool installation features.
- Implemented asynchronous tool installation with job tracking.
- Introduced integration tests for tool installation in disposable VMs.
- Enhanced dashboard UI to reflect tool installation status and usage instructions.
- Improved error handling and logging for tool installation processes.
- Added support for multiple Python versions and Conda environments.
- Refactored tool management logic to ensure better state handling and job persistence.
This commit is contained in:
2026-09-25 23:52:21 +05:30
parent a41aafce83
commit 3e17e3e72f
16 changed files with 729 additions and 34 deletions
+28 -9
View File
@@ -77,13 +77,20 @@ class ToolService:
self._owner = descriptor
try:
self.store.recover()
self._workers_left = self.settings.tool_workers
self._threads = [threading.Thread(target=self._worker, name=f"uvm-tools-{index}", daemon=True)
for index in range(self.settings.tool_workers)]
for thread in self._threads:
thread.start()
for index in range(self.settings.tool_workers):
thread = threading.Thread(target=self._worker, name=f"uvm-tools-{index}", daemon=True)
self._workers_left += 1
try:
thread.start()
except BaseException:
self._workers_left -= 1
raise
self._threads.append(thread)
except BaseException:
self._release_owner()
self._stop.set()
self.notify()
if not self._workers_left:
self._release_owner()
raise
def _release_owner(self):
@@ -181,8 +188,11 @@ class ToolService:
data = self.store.read(vm_id)
running = self.lifecycle.inspect(vm.id).observed_status == "running"
if refresh and running:
supported = data["supported"]
try:
result = self._execute(vm, self._payload(vm, "probe", all_selections()), 90)
if type(result.get("supported")) is bool:
supported = result["supported"]
if not result["ok"]:
raise UvmError(_text(result.get("error"), 2048))
inventory = self._inventory(result, all_selections())
@@ -192,7 +202,8 @@ class ToolService:
data = self.store.read(vm_id)
except UvmError as error:
with self.store.transaction(vm_id) as current:
current.update(error=_text(error, 2048), agent_ready=not isinstance(error, GuestAgentUnavailableError))
current.update(error=_text(error, 2048), agent_ready=not isinstance(error, GuestAgentUnavailableError),
supported=supported)
data = self.store.read(vm_id)
if not running:
data.update(agent_ready=False, guest_busy=False)
@@ -258,7 +269,7 @@ class ToolService:
def ready(current, data):
observed = {item["key"]: item for item in data["inventory"]}
observed.update({item["key"]: item for item in initial})
data.update(inventory=list(observed.values()), checked_at=time.time(), agent_ready=True,
data.update(inventory=list(observed.values()), agent_ready=True,
guest_busy=False, supported=True, error=None)
self._update(job, ready)
for index, selection in enumerate(selections):
@@ -289,7 +300,7 @@ class ToolService:
inventory[selection.key] = observed
elif selection.key in inventory:
inventory[selection.key].update(installed=False, error="Refresh to verify after the failed installation.")
data.update(inventory=list(inventory.values()), checked_at=time.time())
data.update(inventory=list(inventory.values()))
self._update(job, finish_item)
submitted = False
@@ -317,3 +328,11 @@ class ToolService:
self._update(job, failed)
except NotFoundError:
pass
except Exception:
logging.getLogger(__name__).exception("Unexpected tool installation failure")
try:
self._update(job, lambda current, data: self.store.interrupt(
current, "Unexpected installer failure; check the management server log and refresh before retrying.",
time.time(), outcome_unknown=submitted))
except NotFoundError:
pass