17 Commits
1.0.4 ... 1.1.0

Author SHA1 Message Date
jpattWPC
a7f15ac8b9 Require 64 bit support 2023-02-01 11:10:50 -06:00
jpattWPC
696b8c7cd2 Remove PySimpleGuiQT support
- Remove PySimpleGUIQt support, as it requires breaking changes for Python 3.11
- Build MSI based off Python 3.11 (significantly smaller due to removing Qt requirement)
- Bump version to 1.1.0
2023-02-01 11:06:05 -06:00
jpattWPC
ef95a822c5 Merge pull request #36 from aacater/main
check if template key exists
2022-10-24 10:40:51 -05:00
Alex Cater
26c6ca6bc8 check if template key exists 2022-10-23 22:09:04 -08:00
jpattWPC
9518f65c05 Fix build environment, version bump 2022-10-19 11:32:23 -05:00
jpattWPC
02df87523d Merge pull request #30 from aacater/main
Option to filter guests by type
2022-10-19 11:19:58 -05:00
aacater
09a8d11c96 option to filter guest types 2022-09-09 17:45:51 -08:00
aacater
d643cf85f4 skip templates 2022-09-09 17:44:28 -08:00
jpattWPC
f9be014ea2 API Token Login
Add full support for passwordless token-only login.
2022-08-19 22:16:08 -05:00
jpattWPC
ad31d37364 Update MSI version 2022-08-19 21:28:27 -05:00
jpattWPC
b11817a997 Custom parameter support
Add support for custom remote-viewer paramters: https://www.mankier.com/1/remote-viewer
2022-08-19 21:12:03 -05:00
jpattWPC
3f3323710e Merge pull request #27 from rayksland/patch-1
Update vdiclient.ini.example
2022-08-16 11:57:13 -05:00
rayksland
e2f0b26e40 Update vdiclient.ini.example
grammatical edit. 'it's' is a contraction of 'it is' whereas 'its' is the possessive form of 'it.'
2022-08-15 12:06:42 -07:00
jpattWPC
c78ef81994 Update release number 2022-07-19 10:39:58 -05:00
jpattWPC
44b0bfae59 Work around http redireect issue 2022-07-19 10:37:26 -05:00
jpattWPC
5db945dced Add fullscreen toggle 2022-07-11 14:07:32 -05:00
jpattWPC
34b4d010f5 Add last ditch config read 2022-07-11 13:54:10 -05:00
6 changed files with 111 additions and 50 deletions

7
dist/createmsi.py vendored
View File

@@ -304,7 +304,8 @@ class PackageGenerator:
}) })
def path_to_id(self, pathname): def path_to_id(self, pathname):
return pathname.replace('\\', '_').replace('/', '_').replace('#', '_').replace('-', '_') return pathname.replace('\\', '_').replace('/', '_').replace('#', '_').replace('-', '_').replace("+", "__")
def create_xml(self, nodes, current_dir, parent_xml_node, staging_dir): def create_xml(self, nodes, current_dir, parent_xml_node, staging_dir):
cur_node = nodes[current_dir] cur_node = nodes[current_dir]
if cur_node.files: if cur_node.files:
@@ -327,7 +328,7 @@ class PackageGenerator:
}) })
self.component_num += 1 self.component_num += 1
for f in cur_node.files: for f in cur_node.files:
file_id = self.path_to_id(os.path.join(current_dir, f)).replace("+", "__") file_id = self.path_to_id(os.path.join(current_dir, f))
ET.SubElement(comp_xml_node, 'File', { ET.SubElement(comp_xml_node, 'File', {
'Id': file_id, 'Id': file_id,
'Name': f, 'Name': f,
@@ -335,7 +336,7 @@ class PackageGenerator:
}) })
for dirname in cur_node.dirs: for dirname in cur_node.dirs:
dir_id = os.path.join(current_dir, dirname).replace('\\', '_').replace('/', '_') dir_id = self.path_to_id(os.path.join(current_dir, dirname))
dir_node = ET.SubElement(parent_xml_node, 'Directory', { dir_node = ET.SubElement(parent_xml_node, 'Directory', {
'Id': dir_id, 'Id': dir_id,
'Name': dirname, 'Name': dirname,

3
dist/vdiclient.json vendored
View File

@@ -1,10 +1,11 @@
{ {
"upgrade_guid" : "46cbad92-353e-4b28-9bee-83950991dad8", "upgrade_guid" : "46cbad92-353e-4b28-9bee-83950991dad8",
"version" : "1.0.4", "version" : "1.1.0",
"product_name" : "VDI Client", "product_name" : "VDI Client",
"manufacturer" : "Josh Patten", "manufacturer" : "Josh Patten",
"name" : "VDI Client", "name" : "VDI Client",
"name_base" : "vdiclient", "name_base" : "vdiclient",
"arch": 64,
"comments" : "This is the Proxmox VDI client. This client interfaces with Proxmox requires that virt-viewer be installed.", "comments" : "This is the Proxmox VDI client. This client interfaces with Proxmox requires that virt-viewer be installed.",
"installdir" : "VDIClient", "installdir" : "VDIClient",
"installscope" : "perMachine", "installscope" : "perMachine",

View File

@@ -1,6 +1,6 @@
@echo off @echo off
pip install pyinstaller pip install pyinstaller
pip install proxmoxer pip install proxmoxer
pip install PySimpleGUIQt pip install PySimpleGUI
pip install requests pip install requests
pip install pywin32 pip install pywin32

View File

@@ -1,6 +1,4 @@
#!/bin/bash #!/bin/bash
pip3 install proxmoxer pip3 install proxmoxer
pip3 install PySimpleGUIQt
# If PySimpleGUIQt fails, VDIClient will fall back to PySimpleGUI
pip3 install PySimpleGUI pip3 install PySimpleGUI
pip3 install requests pip3 install requests

View File

@@ -9,8 +9,12 @@ icon = vdiicon.ico
logo = vdiclient.png logo = vdiclient.png
# Enable Kiosk mode, which does not allow the user to close anything # Enable Kiosk mode, which does not allow the user to close anything
kiosk = False kiosk = False
# Enable/Disable Fullscreen mode (not applicable in Kiosk mode)
fullscreen = True
# Enable displaying SPICE ini file before opening virt-viewer # Enable displaying SPICE ini file before opening virt-viewer
inidebug = False inidebug = False
# Select which guest types to display. Acceptable values: both, lxc, qemu
guest_type = both
[Authentication] [Authentication]
# This is the authentication backend that will be used to authenticate # This is the authentication backend that will be used to authenticate
@@ -19,11 +23,11 @@ auth_backend = pve
auth_totp = false auth_totp = false
# If disabled, TLS certificate will not be checked # If disabled, TLS certificate will not be checked
tls_verify = false tls_verify = false
# User name # User name (if using token)
#user = user #user = user
# API Token Name # API Token Name
#token_name = dvi #token_name = dvi
#API Token Value # API Token Value
#token_value = xxx-x-x-x-xxx #token_value = xxx-x-x-x-xxx
[Hosts] [Hosts]
@@ -32,8 +36,17 @@ tls_verify = false
pve1.example.com = 8006 pve1.example.com = 8006
[SpiceProxyRedirect] [SpiceProxyRedirect]
# The Spice Proxy provided by the Proxmox API may need to have it's host/port rewritten # The Spice Proxy provided by the Proxmox API may need to have its host/port rewritten
# These rewrite rules are written `IP:port = IP:port` # These rewrite rules are written `IP:port = IP:port`
# 1. Use the inidebug and read the current proxy=pve1.example.com:3128 # 1. Use the inidebug and read the current proxy=pve1.example.com:3128
# 2. Add your proxmox ip to the right side e.g. 123.123.123.123:6000 # 2. Add your proxmox ip to the right side e.g. 123.123.123.123:6000
pve1.example.com:3128 = 123.123.123.123:6000 pve1.example.com:3128 = 123.123.123.123:6000
#[AdditionalParameters]
# If you wish to define additional parameters to pass to virt-viewer you may define them here
# More parameter definitions here: https://www.mankier.com/1/remote-viewer
# Some Examples:
# Enable USB passthrough
#enable-usbredir = true
# Enable auto USB device sharing
#enable-usb-autoshare = true

View File

@@ -1,11 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
import proxmoxer # pip install proxmoxer import proxmoxer # pip install proxmoxer
try: import PySimpleGUI as sg # pip install PySimpleGUI
import PySimpleGUIQt as sg # pip install PySimpleGUIQt gui = 'TK'
gui = 'QT'
except ImportError:
import PySimpleGUI as sg # pip install PySimpleGUI
gui = 'TK'
import requests import requests
from configparser import ConfigParser from configparser import ConfigParser
import random import random
@@ -32,10 +28,13 @@ class G:
totp = False totp = False
imagefile = None imagefile = None
kiosk = False kiosk = False
fullscreen = True
verify_ssl = True verify_ssl = True
icon = None icon = None
inidebug = False inidebug = False
addl_params = None
theme = 'LightBlue' theme = 'LightBlue'
guest_type = 'both'
def get_dpi(): def get_dpi():
import ctypes import ctypes
@@ -70,6 +69,9 @@ def loadconfig(config_location = None):
config_location = f'{os.getenv("PROGRAMFILES")}\\VDIClient\\vdiclient.ini' config_location = f'{os.getenv("PROGRAMFILES")}\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location): if not os.path.exists(config_location):
config_location = f'{os.getenv("PROGRAMFILES(x86)")}\\VDIClient\\vdiclient.ini' config_location = f'{os.getenv("PROGRAMFILES(x86)")}\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location):
# Last ditch effort
config_location = 'C:\\Program Files\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location): if not os.path.exists(config_location):
win_popup_button(f'Unable to read supplied configuration from any location!', 'OK') win_popup_button(f'Unable to read supplied configuration from any location!', 'OK')
return False return False
@@ -104,8 +106,12 @@ def loadconfig(config_location = None):
G.imagefile = config['General']['logo'] G.imagefile = config['General']['logo']
if 'kiosk' in config['General']: if 'kiosk' in config['General']:
G.kiosk = config['General'].getboolean('kiosk') G.kiosk = config['General'].getboolean('kiosk')
if 'fullscreen' in config['General']:
G.fullscreen = config['General'].getboolean('fullscreen')
if 'inidebug' in config['General']: if 'inidebug' in config['General']:
G.inidebug = config['General'].getboolean('inidebug') G.inidebug = config['General'].getboolean('inidebug')
if 'guest_type' in config['General']:
G.guest_type = config['General']['guest_type']
if not 'Authentication' in config: if not 'Authentication' in config:
win_popup_button(f'Unable to read supplied configuration:\nNo `Authentication` section defined!', 'OK') win_popup_button(f'Unable to read supplied configuration:\nNo `Authentication` section defined!', 'OK')
return False return False
@@ -134,12 +140,17 @@ def loadconfig(config_location = None):
if 'SpiceProxyRedirect' in config: if 'SpiceProxyRedirect' in config:
for key in config['SpiceProxyRedirect']: for key in config['SpiceProxyRedirect']:
G.spiceproxy_conv[key] = config['SpiceProxyRedirect'][key] G.spiceproxy_conv[key] = config['SpiceProxyRedirect'][key]
if 'AdditionalParameters' in config:
G.addl_params = {}
for key in config['AdditionalParameters']:
G.addl_params[key] = config['AdditionalParameters'][key]
return True return True
def win_popup(message): def win_popup(message):
layout = [[sg.Text(message)]] layout = [[sg.Text(message)]]
window = sg.Window('Message', layout, no_titlebar=True, keep_on_top=True, finalize=True) window = sg.Window('Message', layout, no_titlebar=True, keep_on_top=True, finalize=True)
window.bring_to_front() window.bring_to_front()
_, _ = window.read(timeout=1) # Fixes a black screen bug
return window return window
def win_popup_button(message, button): def win_popup_button(message, button):
@@ -174,9 +185,18 @@ def setmainlayout():
def getvms(): def getvms():
vms = [] vms = []
try:
for vm in G.proxmox.cluster.resources.get(type='vm'): for vm in G.proxmox.cluster.resources.get(type='vm'):
if 'template' in vm and vm['template']:
continue
if G.guest_type == 'both':
vms.append(vm)
elif G.guest_type == vm['type']:
vms.append(vm) vms.append(vm)
return vms return vms
except proxmoxer.core.ResourceException as e:
win_popup_button(f"Unable to display list of VMs:\n {e!r}", 'OK')
return False
def setvmlayout(vms): def setvmlayout(vms):
layout = [] layout = []
@@ -231,7 +251,11 @@ def vmaction(vmnode, vmid, vmtype):
running = False running = False
i = 0 i = 0
while running == False and i < 30: while running == False and i < 30:
try:
jobstatus = G.proxmox.nodes(vmnode).tasks(jobid).status.get() 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: if 'exitstatus' in jobstatus:
startpop.close() startpop.close()
startpop = None startpop = None
@@ -253,7 +277,7 @@ def vmaction(vmnode, vmid, vmtype):
spiceconfig = G.proxmox.nodes(vmnode).lxc(str(vmid)).spiceproxy.post() spiceconfig = G.proxmox.nodes(vmnode).lxc(str(vmid)).spiceproxy.post()
confignode = ConfigParser() confignode = ConfigParser()
confignode['virt-viewer'] = {} confignode['virt-viewer'] = {}
for key,value in spiceconfig.items(): for key, value in spiceconfig.items():
if key == 'proxy': if key == 'proxy':
val = value[7:].lower() val = value[7:].lower()
if val in G.spiceproxy_conv: if val in G.spiceproxy_conv:
@@ -262,6 +286,9 @@ def vmaction(vmnode, vmid, vmtype):
confignode['virt-viewer'][key] = f'{value}' confignode['virt-viewer'][key] = f'{value}'
else: else:
confignode['virt-viewer'][key] = f'{value}' confignode['virt-viewer'][key] = f'{value}'
if G.addl_params:
for key, value in G.addl_params.items():
confignode['virt-viewer'][key] = f'{value}'
inifile = StringIO('') inifile = StringIO('')
confignode.write(inifile) confignode.write(inifile)
inifile.seek(0) inifile.seek(0)
@@ -274,7 +301,7 @@ def vmaction(vmnode, vmid, vmtype):
pcmd.append('--kiosk') pcmd.append('--kiosk')
pcmd.append('--kiosk-quit') pcmd.append('--kiosk-quit')
pcmd.append('on-disconnect') pcmd.append('on-disconnect')
else: elif G.fullscreen:
pcmd.append('--full-screen') pcmd.append('--full-screen')
pcmd.append('-') #We need it to listen on stdin pcmd.append('-') #We need it to listen on stdin
process = subprocess.Popen(pcmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) process = subprocess.Popen(pcmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
@@ -310,7 +337,7 @@ def setcmd():
win_popup_button('Installation of virt-viewer missing, please install using `apt install virt-viewer`', 'OK') win_popup_button('Installation of virt-viewer missing, please install using `apt install virt-viewer`', 'OK')
sys.exit() sys.exit()
def pveauth(username, passwd, totp): def pveauth(username, passwd=None, totp=None):
random.shuffle(G.hostpool) random.shuffle(G.hostpool)
err = None err = None
for hostinfo in G.hostpool: for hostinfo in G.hostpool:
@@ -343,6 +370,19 @@ def pveauth(username, passwd, totp):
def loginwindow(): def loginwindow():
layout = setmainlayout() layout = setmainlayout()
if G.user and G.token_name and G.token_value: # We need to skip the login
popwin = win_popup("Please wait, authenticating...")
connected, authenticated, error = pveauth(G.user)
popwin.close()
if not connected:
win_popup_button(f'Unable to connect to any VDI server, are you connected to the Internet?\nError Info: {error}', 'OK')
return False
elif connected and not authenticated:
win_popup_button('Invalid username and/or password, please try again!', 'OK')
return False
elif connected and authenticated:
return True
else:
if G.icon: if G.icon:
window = sg.Window(G.title, layout, return_keyboard_events=True, resizable=False, no_titlebar=G.kiosk, icon=G.icon) window = sg.Window(G.title, layout, return_keyboard_events=True, resizable=False, no_titlebar=G.kiosk, icon=G.icon)
else: else:
@@ -361,7 +401,7 @@ def loginwindow():
if '-totp-' in values: if '-totp-' in values:
if values['-totp-'] not in (None, ''): if values['-totp-'] not in (None, ''):
totp = values['-totp-'] totp = values['-totp-']
connected, authenticated, error = pveauth(user, passwd, totp) connected, authenticated, error = pveauth(user, passwd=passwd, totp=totp)
popwin.close() popwin.close()
if not connected: if not connected:
win_popup_button(f'Unable to connect to any VDI server, are you connected to the Internet?\nError Info: {error}', 'OK') win_popup_button(f'Unable to connect to any VDI server, are you connected to the Internet?\nError Info: {error}', 'OK')
@@ -374,6 +414,8 @@ def loginwindow():
def showvms(): def showvms():
vms = getvms() vms = getvms()
if vms == False:
return False
if len(vms) < 1: if len(vms) < 1:
win_popup_button('No desktop instances found, please consult with your system administrator', 'OK') win_popup_button('No desktop instances found, please consult with your system administrator', 'OK')
return False return False
@@ -427,12 +469,18 @@ def main():
if not loggedin: if not loggedin:
loggedin = loginwindow() loggedin = loginwindow()
if not loggedin: if not loggedin:
if G.user and G.token_name and G.token_value: # This means if we don't exit we'll be in an infinite loop
return 1
break break
else: else:
vmstat = showvms() vmstat = showvms()
if not vmstat: if not vmstat:
G.proxmox = None G.proxmox = None
loggedin = False loggedin = False
if G.user and G.token_name and G.token_value: # This means if we don't exit we'll be in an infinite loop
return 0
else: else:
return return
sys.exit(main())
if __name__ == '__main__':
sys.exit(main())