From e8d936b2970fceb0a43a8bf51880648229f3ba1d Mon Sep 17 00:00:00 2001 From: jpattWPC Date: Thu, 7 Sep 2023 15:52:18 -0500 Subject: [PATCH] Version Upgrade - Add current VM state to VM list - Add option for reset button to be enabled in INI file - Disable Connect button when VM is in a transition state --- dist/vdiclient.json | 2 +- vdiclient.ini.example | 2 + vdiclient.py | 122 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/dist/vdiclient.json b/dist/vdiclient.json index ec150cf..95aeef9 100644 --- a/dist/vdiclient.json +++ b/dist/vdiclient.json @@ -1,6 +1,6 @@ { "upgrade_guid" : "46cbad92-353e-4b28-9bee-83950991dad8", - "version" : "1.1.03", + "version" : "1.2.01", "product_name" : "VDI Client", "manufacturer" : "Josh Patten", "name" : "VDI Client", diff --git a/vdiclient.ini.example b/vdiclient.ini.example index 8d3fa27..ba209f6 100644 --- a/vdiclient.ini.example +++ b/vdiclient.ini.example @@ -15,6 +15,8 @@ fullscreen = True inidebug = False # Select which guest types to display. Acceptable values: both, lxc, qemu guest_type = both +# Show VM option for resetting VM +#show_reset = True [Authentication] # This is the authentication backend that will be used to authenticate diff --git a/vdiclient.py b/vdiclient.py index a336298..b20b282 100644 --- a/vdiclient.py +++ b/vdiclient.py @@ -34,6 +34,8 @@ class G: verify_ssl = True icon = None inidebug = False + show_reset = False + show_hibernate = False addl_params = None theme = 'LightBlue' guest_type = 'both' @@ -96,6 +98,8 @@ def loadconfig(config_location = None): G.inidebug = config['General'].getboolean('inidebug') if 'guest_type' in config['General']: G.guest_type = config['General']['guest_type'] + if 'show_reset' in config['General']: + G.show_reset = config['General'].getboolean('show_reset') if not 'Authentication' in config: win_popup_button(f'Unable to read supplied configuration:\nNo `Authentication` section defined!', 'OK') return False @@ -132,11 +136,14 @@ def loadconfig(config_location = None): def win_popup(message): layout = [ - [sg.Text(message)] + [sg.Text(message, key='-TXT-')] ] - window = sg.Window('Message', layout, return_keyboard_events=True, no_titlebar=True, keep_on_top=True, finalize=True) + window = sg.Window('Message', layout, return_keyboard_events=True, no_titlebar=True, keep_on_top=True, finalize=True, ) window.bring_to_front() _, _ = window.read(timeout=10) # Fixes a black screen bug + window['-TXT-'].update(message) + sleep(.15) + window['-TXT-'].update(message) return window def win_popup_button(message, button): @@ -208,8 +215,36 @@ def setvmlayout(vms): layoutcolumn = [] for vm in vms: if not vm["status"] == "unknown": + print(vm) + vmkeyname = f'-VM|{vm["vmid"]}-' connkeyname = f'-CONN|{vm["vmid"]}-' - layoutcolumn.append([sg.Text(vm['name'], font=["Helvetica", 14], size=(22*G.scaling, 1*G.scaling)), sg.Button('Connect', font=["Helvetica", 14], key=connkeyname)]) + resetkeyname = f'-RESET|{vm["vmid"]}-' + hiberkeyname = f'-HIBER|{vm["vmid"]}-' + state = 'stopped' + connbutton = sg.Button('Connect', font=["Helvetica", 14], key=connkeyname) + if vm['status'] == 'running': + if 'lock' in vm: + state = vm['lock'] + if state in ('suspending', 'suspended'): + if state == 'suspended': + state = 'starting' + connbutton = sg.Button('Connect', font=["Helvetica", 14], key=connkeyname, disabled=True) + else: + state = vm['status'] + tmplayout = [ + sg.Text(vm['name'], font=["Helvetica", 14], size=(22*G.scaling, 1*G.scaling)), + sg.Text(f"State: {state}", font=["Helvetica", 0], size=(22*G.scaling, 1*G.scaling), key=vmkeyname), + connbutton + ] + if G.show_reset: + tmplayout.append( + sg.Button('Reset', font=["Helvetica", 14], key=resetkeyname) + ) + if G.show_hibernate: + tmplayout.append( + sg.Button('Hibernate', font=["Helvetica", 14], key=hiberkeyname) + ) + layoutcolumn.append(tmplayout) layoutcolumn.append([sg.HorizontalSeparator()]) if len(vms) > 5: # We need a scrollbar layout.append([sg.Column(layoutcolumn, scrollable = True, size = [450*G.scaling, None] )]) @@ -231,14 +266,56 @@ def iniwin(inistring): iniwindow.close() return True -def vmaction(vmnode, vmid, vmtype): +def vmaction(vmnode, vmid, vmtype, action='connect'): status = False if vmtype == 'qemu': vmstatus = G.proxmox.nodes(vmnode).qemu(str(vmid)).status.get('current') else: # Not sure this is even a thing, but here it is... vmstatus = G.proxmox.nodes(vmnode).lxc(str(vmid)).status.get('current') + if action == 'reload': + stoppop = win_popup(f'Stopping {vmstatus["name"]}...') + sleep(.1) + try: + if vmtype == 'qemu': + jobid = G.proxmox.nodes(vmnode).qemu(str(vmid)).status.stop.post(timeout=28) + else: # Not sure this is even a thing, but here it is... + jobid = G.proxmox.nodes(vmnode).lxc(str(vmid)).status.stop.post(timeout=28) + except proxmoxer.core.ResourceException as e: + stoppop.close() + win_popup_button(f"Unable to stop VM, please provide your system administrator with the following error:\n {e!r}", 'OK') + return False + running = True + i = 0 + while running and i < 30: + try: + jobstatus = G.proxmox.nodes(vmnode).tasks(jobid).status.get() + except Exception: + # We ran into a query issue here, going to skip this round and try again + jobstatus = {} + if 'exitstatus' in jobstatus: + stoppop.close() + stoppop = None + if jobstatus['exitstatus'] != 'OK': + win_popup_button('Unable to stop VM, please contact your system administrator for assistance', 'OK') + return False + else: + running = False + status = True + sleep(1) + i += 1 + if not status: + if stoppop: + stoppop.close() + return status + status = False + if vmtype == 'qemu': + vmstatus = G.proxmox.nodes(vmnode).qemu(str(vmid)).status.get('current') + else: # Not sure this is even a thing, but here it is... + vmstatus = G.proxmox.nodes(vmnode).lxc(str(vmid)).status.get('current') + sleep(.2) if vmstatus['status'] != 'running': startpop = win_popup(f'Starting {vmstatus["name"]}...') + sleep(.1) try: if vmtype == 'qemu': jobid = G.proxmox.nodes(vmnode).qemu(str(vmid)).status.start.post(timeout=28) @@ -271,6 +348,8 @@ def vmaction(vmnode, vmid, vmtype): if startpop: startpop.close() return status + if action == 'reload': + return try: if vmtype == 'qemu': spiceconfig = G.proxmox.nodes(vmnode).qemu(str(vmid)).spiceproxy.post() @@ -433,18 +512,39 @@ def showvms(): window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, no_titlebar=G.kiosk) timer = datetime.now() while True: - if (datetime.now() - timer).total_seconds() > 10: + if (datetime.now() - timer).total_seconds() > 5: timer = datetime.now() newvmlist = getvms(listonly = True) if vmlist != newvmlist: vmlist = newvmlist.copy() - layout = setvmlayout(getvms()) + vms = getvms() + layout = setvmlayout(vms) window.close() if G.icon: window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, no_titlebar=G.kiosk, icon=G.icon) else: window = sg.Window(G.title, layout, return_keyboard_events=True,finalize=True, resizable=False, no_titlebar=G.kiosk) window.bring_to_front() + else: # Refresh existing vm status + newvms = getvms() + for vm in newvms: + vmkeyname = f'-VM|{vm["vmid"]}-' + connkeyname = f'-CONN|{vm["vmid"]}-' + state = 'stopped' + if vm['status'] == 'running': + if 'lock' in vm: + state = vm['lock'] + if state in ('suspending', 'suspended'): + window[connkeyname].update(disabled=True) + if state == 'suspended': + state = 'starting' + else: + state = vm['status'] + window[connkeyname].update(disabled=False) + else: + window[connkeyname].update(disabled=False) + window[vmkeyname].update(f"State: {state}") + event, values = window.read(timeout = 1000) if event in ('Logout', None): window.close() @@ -459,6 +559,16 @@ def showvms(): vmaction(vm['node'], vmid, vm['type']) if not found: win_popup_button(f'VM {vm["name"]} no longer availble, please contact your system administrator', 'OK') + elif event.startswith('-RESET'): + eventparams = event.split('|') + vmid = eventparams[1][:-1] + found = False + for vm in vms: + if str(vm['vmid']) == vmid: + found = True + vmaction(vm['node'], vmid, vm['type'], action='reload') + if not found: + win_popup_button(f'VM {vm["name"]} no longer availble, please contact your system administrator', 'OK') return True def main():