From a80805d69aeacaf10585e1974fe946dcaedca1b7 Mon Sep 17 00:00:00 2001 From: joshpatten Date: Wed, 19 Apr 2023 17:12:16 -0500 Subject: [PATCH 1/5] Fixed virt-manager link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2495fbe..2ce56c8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This project's focus is to create a simple VDI client intended for mass deployme ## Windows Installation -You **MUST** install virt-viewer prior to using PVE VDI client, you may download it from the [official Virtual Machine Manager](https://virt-manager.org/download/) site. +You **MUST** install virt-viewer prior to using PVE VDI client, you may download it from the [official Virtual Machine Manager](https://virt-manager.org/download) site. Please visit the [releases](https://github.com/joshpatten/PVE-VDIClient/releases) section to download a prebuilt MSI package From 8dff999823e6fcc80c0310bc9b051c6d3e8fdd03 Mon Sep 17 00:00:00 2001 From: TechQI Date: Sun, 25 Jun 2023 10:27:47 +0800 Subject: [PATCH 2/5] Change the python3 env path --- vdiclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vdiclient.py b/vdiclient.py index a336298..6563339 100644 --- a/vdiclient.py +++ b/vdiclient.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!env python3 import proxmoxer # pip install proxmoxer import PySimpleGUI as sg # pip install PySimpleGUI gui = 'TK' From e8d936b2970fceb0a43a8bf51880648229f3ba1d Mon Sep 17 00:00:00 2001 From: jpattWPC Date: Thu, 7 Sep 2023 15:52:18 -0500 Subject: [PATCH 3/5] 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(): From e848a938df4318f4b70e7399f8ae3763e4503b7b Mon Sep 17 00:00:00 2001 From: jpattWPC Date: Thu, 14 Sep 2023 20:35:27 -0500 Subject: [PATCH 4/5] Add window sizing option --- dist/vdiclient.json | 2 +- vdiclient.ini.example | 3 +++ vdiclient.py | 14 ++++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/dist/vdiclient.json b/dist/vdiclient.json index 95aeef9..ef55688 100644 --- a/dist/vdiclient.json +++ b/dist/vdiclient.json @@ -1,6 +1,6 @@ { "upgrade_guid" : "46cbad92-353e-4b28-9bee-83950991dad8", - "version" : "1.2.01", + "version" : "1.2.02", "product_name" : "VDI Client", "manufacturer" : "Josh Patten", "name" : "VDI Client", diff --git a/vdiclient.ini.example b/vdiclient.ini.example index ba209f6..8a2ec01 100644 --- a/vdiclient.ini.example +++ b/vdiclient.ini.example @@ -17,6 +17,9 @@ inidebug = False guest_type = both # Show VM option for resetting VM #show_reset = True +# Set Window Dimensions. Only use if window isn't sizing properly +#window_width = 800 +#window_height = 600 [Authentication] # This is the authentication backend that will be used to authenticate diff --git a/vdiclient.py b/vdiclient.py index b20b282..f6d9ce4 100644 --- a/vdiclient.py +++ b/vdiclient.py @@ -39,6 +39,8 @@ class G: addl_params = None theme = 'LightBlue' guest_type = 'both' + width = None + height = None def loadconfig(config_location = None): if config_location: @@ -100,6 +102,10 @@ def loadconfig(config_location = None): G.guest_type = config['General']['guest_type'] if 'show_reset' in config['General']: G.show_reset = config['General'].getboolean('show_reset') + if 'window_width' in config['General']: + G.width = config['General'].getint('window_width') + if 'window_height' in config['General']: + G.height = config['General'].getint('window_height') if not 'Authentication' in config: win_popup_button(f'Unable to read supplied configuration:\nNo `Authentication` section defined!', 'OK') return False @@ -507,9 +513,9 @@ def showvms(): layout = setvmlayout(vms) if G.icon: - window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, no_titlebar=G.kiosk, icon=G.icon) + window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, no_titlebar=G.kiosk, size=(G.width, G.height), icon=G.icon) else: - window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, no_titlebar=G.kiosk) + window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, size=(G.width, G.height), no_titlebar=G.kiosk) timer = datetime.now() while True: if (datetime.now() - timer).total_seconds() > 5: @@ -521,9 +527,9 @@ def showvms(): 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) + window = sg.Window(G.title, layout, return_keyboard_events=True, finalize=True, resizable=False, no_titlebar=G.kiosk, size=(G.width, G.height), icon=G.icon) else: - window = sg.Window(G.title, layout, return_keyboard_events=True,finalize=True, resizable=False, no_titlebar=G.kiosk) + window = sg.Window(G.title, layout, return_keyboard_events=True,finalize=True, resizable=False, no_titlebar=G.kiosk, size=(G.width, G.height)) window.bring_to_front() else: # Refresh existing vm status newvms = getvms() From b32d38338b629e1476f8d2245b1fe17533147fe7 Mon Sep 17 00:00:00 2001 From: jpattWPC Date: Thu, 21 Sep 2023 13:21:10 -0500 Subject: [PATCH 5/5] Add requests checking --- dist/vdiclient.json | 2 +- vdiclient.py | 63 ++++++++++++++++++++++++--------------------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/dist/vdiclient.json b/dist/vdiclient.json index ef55688..cf1dd14 100644 --- a/dist/vdiclient.json +++ b/dist/vdiclient.json @@ -1,6 +1,6 @@ { "upgrade_guid" : "46cbad92-353e-4b28-9bee-83950991dad8", - "version" : "1.2.02", + "version" : "1.2.03", "product_name" : "VDI Client", "manufacturer" : "Josh Patten", "name" : "VDI Client", diff --git a/vdiclient.py b/vdiclient.py index f6d9ce4..e181b97 100644 --- a/vdiclient.py +++ b/vdiclient.py @@ -210,6 +210,9 @@ def getvms(listonly = False): except proxmoxer.core.ResourceException as e: win_popup_button(f"Unable to display list of VMs:\n {e!r}", 'OK') return False + except requests.exceptions.ConnectionError as e: + print(f"Encountered error when querying proxmox: {e!r}") + return False def setvmlayout(vms): layout = [] @@ -221,7 +224,6 @@ 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"]}-' resetkeyname = f'-RESET|{vm["vmid"]}-' @@ -521,35 +523,38 @@ def showvms(): if (datetime.now() - timer).total_seconds() > 5: timer = datetime.now() newvmlist = getvms(listonly = True) - if vmlist != newvmlist: - vmlist = newvmlist.copy() - 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, size=(G.width, G.height), icon=G.icon) - else: - window = sg.Window(G.title, layout, return_keyboard_events=True,finalize=True, resizable=False, no_titlebar=G.kiosk, size=(G.width, G.height)) - 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' + if newvmlist: + if vmlist != newvmlist: + vmlist = newvmlist.copy() + vms = getvms() + if vms: + 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, size=(G.width, G.height), icon=G.icon) else: - state = vm['status'] - window[connkeyname].update(disabled=False) - else: - window[connkeyname].update(disabled=False) - window[vmkeyname].update(f"State: {state}") + window = sg.Window(G.title, layout, return_keyboard_events=True,finalize=True, resizable=False, no_titlebar=G.kiosk, size=(G.width, G.height)) + window.bring_to_front() + else: # Refresh existing vm status + newvms = getvms() + if newvms: + 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):