5 Commits

Author SHA1 Message Date
jpattWPC
07ea02d246 Add HTTP config download option 2023-10-15 11:39:54 -05:00
jpattWPC
2476778fe6 Update README.md 2023-10-13 21:01:01 -05:00
jpattWPC
9d7540248b Add ability to disable remote-viewer kiosk mode 2023-10-13 16:04:19 -05:00
jpattWPC
cd83be7680 Add ability to auto-connect VMID 2023-10-13 14:22:08 -05:00
jpattWPC
2bac614194 Add password reset command 2023-10-13 13:42:33 -05:00
4 changed files with 126 additions and 51 deletions

View File

@@ -8,6 +8,28 @@ This project's focus is to create a simple VDI client intended for mass deployme
![VDI View](screenshots/vdiview.png)
## Command Line Usage
No command line options are required for default behavior. The following command line options are available:
usage: vdiclient.py [-h] [--list_themes] [--config_type {file,http}] [--config_location CONFIG_LOCATION]
[--config_username CONFIG_USERNAME] [--config_password CONFIG_PASSWORD] [--ignore_ssl]
Proxmox VDI Client
options:
-h, --help show this help message and exit
--list_themes List all available themes
--config_type {file,http}
Select config type (default: file)
--config_location CONFIG_LOCATION
Specify the config location (default: search for config file)
--config_username CONFIG_USERNAME
HTTP basic authentication username (default: None)
--config_password CONFIG_PASSWORD
HTTP basic authentication password (default: None)
--ignore_ssl HTTPS ignore SSL certificate errors (default: False)
## 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.html) site.
@@ -32,6 +54,20 @@ Run the following commands on a Debian/Ubuntu Linux system to install the approp
cp vdiclient.py /usr/local/bin
chmod +x /usr/local/bin/vdiclient.py
## Build Debian/Ubuntu Linux Binary
Run the following commands if you wish to build a binary on a Debian/Ubuntu Linux system
apt install python3-pip python3-tk virt-viewer git
git clone https://github.com/joshpatten/PVE-VDIClient.git
cd ./PVE-VDIClient/
chmod +x requirements.sh
./requirements.sh
pip3 install pyinstaller
pyinstaller --onefile --noconsole --noconfirm --hidden-import proxmoxer.backends --hidden-import proxmoxer.backends.https --hidden-import proxmoxer.backends.https.AuthenticationError --hidden-import proxmoxer.core --hidden-import proxmoxer.core.ResourceException --hidden-import subprocess.TimeoutExpired --hidden-import subprocess.CalledProcessError --hidden-import requests.exceptions --hidden-import requests.exceptions.ReadTimeout --hidden-import requests.exceptions.ConnectTimeout --hidden-import requests.exceptions.ConnectionError vdiclient.py
Once pyinstaller has finished your binary will be located in dist/vdiclient
## Configuration File
PVE VDI Client **REQUIRES** a configuration file to function. The client searches for this file in the following locations unless **--config** is specified on the commmand line:

2
dist/vdiclient.json vendored
View File

@@ -1,6 +1,6 @@
{
"upgrade_guid" : "46cbad92-353e-4b28-9bee-83950991dad8",
"version" : "1.2.04",
"version" : "1.3.01",
"product_name" : "VDI Client",
"manufacturer" : "Josh Patten",
"name" : "VDI Client",

View File

@@ -11,6 +11,8 @@ logo = vdiclient.png
kiosk = False
# Enable/Disable Fullscreen mode (not applicable in Kiosk mode)
fullscreen = True
# Disable viewer_kiosk mode if kiosk is set to true, this allows overriding remote_viewer kiosk mode
#viewer_kiosk = False
# Enable displaying SPICE ini file before opening virt-viewer
inidebug = False
# Select which guest types to display. Acceptable values: both, lxc, qemu
@@ -34,6 +36,10 @@ tls_verify = false
#token_name = dvi
# API Token Value
#token_value = xxx-x-x-x-xxx
# Password Reset Command Launch. Has to be full executable Command
#pwresetcmd = start chrome --app=http://pwreset.example.com
# Automatically connect to a VMID upon authentication
#auto_vmid = 100
[Hosts]
# Hosts are entered as `IP/FQDN = Port`

View File

@@ -5,9 +5,9 @@ gui = 'TK'
import requests
from datetime import datetime
from configparser import ConfigParser
import argparse
import random
import sys
import copy
import os
import subprocess
from time import sleep
@@ -19,6 +19,7 @@ class G:
hostpool = []
spiceproxy_conv = {}
proxmox = None
icon = None
vvcmd = None
scaling = 1
#########
@@ -30,54 +31,65 @@ class G:
totp = False
imagefile = None
kiosk = False
viewer_kiosk = True
fullscreen = True
verify_ssl = True
icon = None
inidebug = False
show_reset = False
show_hibernate = False
addl_params = None
pwresetcmd = None
auto_vmid = None
theme = 'LightBlue'
guest_type = 'both'
width = None
height = None
def loadconfig(config_location = None):
if config_location:
config = ConfigParser(delimiters='=')
try:
config.read(config_location)
except Exception as e:
win_popup_button(f'Unable to read supplied configuration:\n{e!r}', 'OK')
config_location = None
if not config_location:
if os.name == 'nt': # Windows
config_location = f'{os.getenv("APPDATA")}\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location):
config_location = f'{os.getenv("PROGRAMFILES")}\\VDIClient\\vdiclient.ini'
if not os.path.exists(config_location):
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):
win_popup_button(f'Unable to read supplied configuration from any location!', 'OK')
def loadconfig(config_location = None, config_type='file', config_username = None, config_password = None, ssl_verify = True):
config = ConfigParser(delimiters='=')
if config_type == 'file':
if config_location:
if not os.path.isfile(config_location):
win_popup_button(f'Unable to read supplied configuration:\n{config_location} does not exist!', 'OK')
return False
elif os.name == 'posix': #Linux
config_location = os.path.expanduser('~/.config/VDIClient/vdiclient.ini')
if not os.path.exists(config_location):
config_location = '/etc/vdiclient/vdiclient.ini'
if not os.path.exists(config_location):
config_location = '/usr/local/etc/vdiclient/vdiclient.ini'
if not os.path.exists(config_location):
win_popup_button(f'Unable to read supplied configuration from any location!', 'OK')
return False
config = ConfigParser(delimiters='=')
else:
if os.name == 'nt': # Windows
config_list = [
f'{os.getenv("APPDATA")}\\VDIClient\\vdiclient.ini',
f'{os.getenv("PROGRAMFILES")}\\VDIClient\\vdiclient.ini',
f'{os.getenv("PROGRAMFILES(x86)")}\\VDIClient\\vdiclient.ini',
'C:\\Program Files\\VDIClient\\vdiclient.ini'
]
elif os.name == 'posix': #Linux
config_list = [
os.path.expanduser('~/.config/VDIClient/vdiclient.ini'),
'/etc/vdiclient/vdiclient.ini',
'/usr/local/etc/vdiclient/vdiclient.ini'
]
for location in config_list:
if os.path.exists(location):
config_location = location
break
if not config_location:
win_popup_button(f'Unable to read supplied configuration from any location!', 'OK')
return False
try:
config.read(config_location)
except Exception as e:
win_popup_button(f'Unable to read configuration file:\n{e!r}', 'OK')
config_location = None
return False
elif config_type == 'http':
try:
if config_username and config_password:
r = requests.get(url=config_location, auth=(config_username, config_password), verify = ssl_verify)
else:
r = requests.get(url=config_location, verify = ssl_verify)
config.read_string(r.text)
except Exception as e:
win_popup_button(f"Unable to read configuration from URL!\n{e}", "OK")
return False
if not 'General' in config:
win_popup_button(f'Unable to read supplied configuration:\nNo `General` section defined!', 'OK')
return False
@@ -94,6 +106,8 @@ def loadconfig(config_location = None):
G.imagefile = config['General']['logo']
if 'kiosk' in config['General']:
G.kiosk = config['General'].getboolean('kiosk')
if 'viewer_kiosk' in config['General']:
G.viewer_kiosk = config['General'].getboolean('viewer_kiosk')
if 'fullscreen' in config['General']:
G.fullscreen = config['General'].getboolean('fullscreen')
if 'inidebug' in config['General']:
@@ -117,11 +131,17 @@ def loadconfig(config_location = None):
if 'tls_verify' in config['Authentication']:
G.verify_ssl = config['Authentication'].getboolean('tls_verify')
if 'user' in config['Authentication']:
G.user = config['Authentication']['user']
G.user = config['Authentication']['user']
if 'token_name' in config['Authentication']:
G.token_name = config['Authentication']['token_name']
G.token_name = config['Authentication']['token_name']
if 'token_value' in config['Authentication']:
G.token_value = config['Authentication']['token_value']
G.token_value = config['Authentication']['token_value']
if 'pwresetcmd' in config['Authentication']:
G.pwresetcmd = config['Authentication']['pwresetcmd']
if 'auto_vmid' in config['Authentication']:
G.auto_vmid = config['Authentication'].getint('auto_vmid')
if 'knock_ip' in config['Authentication']:
G.knock
if not 'Hosts' in config:
win_popup_button(f'Unable to read supplied configuration:\nNo `Hosts` section defined!', 'OK')
return False
@@ -180,6 +200,8 @@ def setmainlayout():
layout.append([sg.Button("Log In", font=["Helvetica", 14], bind_return_key=True)])
else:
layout.append([sg.Button("Log In", font=["Helvetica", 14], bind_return_key=True), sg.Button("Cancel", font=["Helvetica", 14])])
if G.pwresetcmd:
layout[-1].append(sg.Button('Password Reset', font=["Helvetica", 14]))
return layout
def getvms(listonly = False):
@@ -388,7 +410,7 @@ def vmaction(vmnode, vmid, vmtype, action='connect'):
closed = iniwin(inistring)
connpop = win_popup(f'Connecting to {vmstatus["name"]}...')
pcmd = [G.vvcmd]
if G.kiosk:
if G.kiosk and G.viewer_kiosk:
pcmd.append('--kiosk')
pcmd.append('--kiosk-quit')
pcmd.append('on-disconnect')
@@ -483,6 +505,11 @@ def loginwindow():
if event == 'Cancel' or event == sg.WIN_CLOSED:
window.close()
return False
elif event == 'Password Reset':
try:
subprocess.check_call(G.pwresetcmd, shell=True)
except Exception as e:
win_popup_button(f'Unable to open password reset command.\n\nError Info:\n{e}', 'OK')
else:
if event in ('Log In', '\r', 'special 16777220', 'special 16777221'):
popwin = win_popup("Please wait, authenticating...")
@@ -513,7 +540,6 @@ def showvms():
win_popup_button('No desktop instances found, please consult with your system administrator', 'OK')
return False
layout = setvmlayout(vms)
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:
@@ -584,19 +610,19 @@ def showvms():
def main():
G.scaling = 1 # TKinter requires integers
config_location = None
if len(sys.argv) > 1:
if sys.argv[1] == '--list_themes':
sg.preview_all_look_and_feel_themes()
return
if sys.argv[1] == '--config':
if len(sys.argv) < 3:
win_popup_button('No config file provided with `--config` parameter.\nPlease provide location of config file!', 'OK')
return
else:
config_location = sys.argv[2]
parser = argparse.ArgumentParser(description='Proxmox VDI Client')
parser.add_argument('--list_themes', help='List all available themes', action='store_true')
parser.add_argument('--config_type', help='Select config type (default: file)', choices=['file', 'http'], default='file')
parser.add_argument('--config_location', help='Specify the config location (default: search for config file)', default=None)
parser.add_argument('--config_username', help="HTTP basic authentication username (default: None)", default=None)
parser.add_argument('--config_password', help="HTTP basic authentication password (default: None)", default=None)
parser.add_argument('--ignore_ssl', help="HTTPS ignore SSL certificate errors (default: False)", action='store_false', default=True)
args = parser.parse_args()
if args.list_themes:
sg.preview_all_look_and_feel_themes()
return
setcmd()
if not loadconfig(config_location):
if not loadconfig(config_location=args.config_location, config_type=args.config_type, config_username=args.config_username, config_password=args.config_password, ssl_verify=args.ignore_ssl):
return False
sg.theme(G.theme)
loggedin = False
@@ -608,6 +634,13 @@ def main():
return 1
break
else:
if G.auto_vmid:
vms = getvms()
for row in vms:
if row['vmid'] == G.auto_vmid:
vmaction(row['node'], row['vmid'], row['type'], action='connect')
return 0
win_popup_button(f'No VDI instance with ID {G.auto_vmid} found!', 'OK')
vmstat = showvms()
if not vmstat:
G.proxmox = None