2023-10-07 15:36:42 -05:00
#!/usr/bin/env python3
2022-03-07 16:56:54 -06:00
import proxmoxer # pip install proxmoxer
2023-02-01 11:06:05 -06:00
import PySimpleGUI as sg # pip install PySimpleGUI
gui = ' TK '
2022-03-07 16:56:54 -06:00
import requests
2023-02-03 15:13:27 -06:00
from datetime import datetime
2022-03-07 16:56:54 -06:00
from configparser import ConfigParser
2023-10-15 11:39:54 -05:00
import argparse
2022-03-07 16:56:54 -06:00
import random
import sys
import os
2023-10-15 19:04:22 -05:00
import json
2022-03-07 16:56:54 -06:00
import subprocess
from time import sleep
from io import StringIO
2022-03-14 21:24:36 -05:00
2022-03-07 16:56:54 -06:00
class G :
spiceproxy_conv = { }
proxmox = None
2023-10-15 11:39:54 -05:00
icon = None
2022-03-07 16:56:54 -06:00
vvcmd = None
2022-03-14 21:24:36 -05:00
scaling = 1
2022-03-07 16:56:54 -06:00
#########
2023-10-15 19:04:22 -05:00
inidebug = False
addl_params = None
2022-03-07 16:56:54 -06:00
imagefile = None
kiosk = False
2023-10-13 16:04:19 -05:00
viewer_kiosk = True
2022-07-11 14:07:32 -05:00
fullscreen = True
2023-09-07 15:52:18 -05:00
show_reset = False
show_hibernate = False
2023-10-15 19:04:22 -05:00
current_hostset = ' DEFAULT '
title = ' VDI Login '
hosts = { }
2022-03-07 16:56:54 -06:00
theme = ' LightBlue '
2022-09-09 17:45:51 -08:00
guest_type = ' both '
2023-09-14 20:35:27 -05:00
width = None
height = None
2022-03-07 16:56:54 -06:00
2023-10-15 11:39:54 -05:00
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 ' )
2022-03-07 16:56:54 -06:00
return False
2023-10-15 11:39:54 -05:00
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
2022-03-07 16:56:54 -06:00
try :
config . read ( config_location )
except Exception as e :
win_popup_button ( f ' Unable to read configuration file: \n { e !r} ' , ' OK ' )
2023-10-15 11:39:54 -05:00
return False
elif config_type == ' http ' :
2023-10-15 12:10:25 -05:00
if not config_location :
win_popup_button ( ' --config_type http defined, yet no URL provided in --config_location parameter! ' , ' OK ' )
return False
2023-10-15 11:39:54 -05:00
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
2022-03-07 16:56:54 -06:00
if not ' General ' in config :
2023-10-15 12:10:25 -05:00
win_popup_button ( ' Unable to read supplied configuration: \n No `General` section defined! ' , ' OK ' )
2022-03-07 16:56:54 -06:00
return False
else :
if ' title ' in config [ ' General ' ] :
G . title = config [ ' General ' ] [ ' title ' ]
if ' theme ' in config [ ' General ' ] :
G . theme = config [ ' General ' ] [ ' theme ' ]
if ' icon ' in config [ ' General ' ] :
if os . path . exists ( config [ ' General ' ] [ ' icon ' ] ) :
G . icon = config [ ' General ' ] [ ' icon ' ]
if ' logo ' in config [ ' General ' ] :
if os . path . exists ( config [ ' General ' ] [ ' logo ' ] ) :
G . imagefile = config [ ' General ' ] [ ' logo ' ]
if ' kiosk ' in config [ ' General ' ] :
G . kiosk = config [ ' General ' ] . getboolean ( ' kiosk ' )
2023-10-13 16:04:19 -05:00
if ' viewer_kiosk ' in config [ ' General ' ] :
G . viewer_kiosk = config [ ' General ' ] . getboolean ( ' viewer_kiosk ' )
2022-07-11 14:07:32 -05:00
if ' fullscreen ' in config [ ' General ' ] :
G . fullscreen = config [ ' General ' ] . getboolean ( ' fullscreen ' )
2022-03-10 16:58:24 -06:00
if ' inidebug ' in config [ ' General ' ] :
G . inidebug = config [ ' General ' ] . getboolean ( ' inidebug ' )
2022-09-09 17:45:51 -08:00
if ' guest_type ' in config [ ' General ' ] :
G . guest_type = config [ ' General ' ] [ ' guest_type ' ]
2023-09-07 15:52:18 -05:00
if ' show_reset ' in config [ ' General ' ] :
G . show_reset = config [ ' General ' ] . getboolean ( ' show_reset ' )
2023-09-14 20:35:27 -05:00
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 ' )
2023-10-15 19:04:22 -05:00
if ' Authentication ' in config : #Legacy configuration
G . hosts [ ' DEFAULT ' ] = {
' hostpool ' : [ ] ,
' backend ' : ' pve ' ,
' user ' : " " ,
' token_name ' : None ,
' token_value ' : None ,
' totp ' : False ,
' verify_ssl ' : True ,
' pwresetcmd ' : None ,
' auto_vmid ' : None ,
' knock_seq ' : [ ]
}
if not ' Hosts ' in config :
win_popup_button ( f ' Unable to read supplied configuration: \n No `Hosts` section defined! ' , ' OK ' )
return False
for key in config [ ' Hosts ' ] :
G . hosts [ ' DEFAULT ' ] [ ' hostpool ' ] . append ( {
' host ' : key ,
' port ' : int ( config [ ' Hosts ' ] [ key ] )
} )
2022-03-07 16:56:54 -06:00
if ' auth_backend ' in config [ ' Authentication ' ] :
2023-10-15 19:04:22 -05:00
G . hosts [ ' DEFAULT ' ] [ ' backend ' ] = config [ ' Authentication ' ] [ ' auth_backend ' ]
2022-03-09 15:14:16 -09:00
if ' user ' in config [ ' Authentication ' ] :
2023-10-15 19:04:22 -05:00
G . hosts [ ' DEFAULT ' ] [ ' user ' ] = config [ ' Authentication ' ] [ ' user ' ]
2022-03-09 15:14:16 -09:00
if ' token_name ' in config [ ' Authentication ' ] :
2023-10-15 19:04:22 -05:00
G . hosts [ ' DEFAULT ' ] [ ' token_name ' ] = config [ ' Authentication ' ] [ ' token_name ' ]
2022-03-09 15:14:16 -09:00
if ' token_value ' in config [ ' Authentication ' ] :
2023-10-15 19:04:22 -05:00
G . hosts [ ' DEFAULT ' ] [ ' token_value ' ] = config [ ' Authentication ' ] [ ' token_value ' ]
if ' auth_totp ' in config [ ' Authentication ' ] :
G . hosts [ ' DEFAULT ' ] [ ' totp ' ] = config [ ' Authentication ' ] . getboolean ( ' auth_totp ' )
if ' tls_verify ' in config [ ' Authentication ' ] :
G . hosts [ ' DEFAULT ' ] [ ' verify_ssl ' ] = config [ ' Authentication ' ] . getboolean ( ' tls_verify ' )
2023-10-13 13:42:33 -05:00
if ' pwresetcmd ' in config [ ' Authentication ' ] :
2023-10-15 19:04:22 -05:00
G . hosts [ ' DEFAULT ' ] [ ' pwresetcmd ' ] = config [ ' Authentication ' ] [ ' pwresetcmd ' ]
2023-10-13 14:22:08 -05:00
if ' auto_vmid ' in config [ ' Authentication ' ] :
2023-10-15 19:04:22 -05:00
G . hosts [ ' DEFAULT ' ] [ ' auto_vmid ' ] = config [ ' Authentication ' ] . getint ( ' auto_vmid ' )
if ' knock_seq ' in config [ ' Authentication ' ] :
try :
G . hosts [ ' DEFAULT ' ] [ ' knock_seq ' ] = json . loads ( config [ ' Authentication ' ] [ ' knock_seq ' ] )
except Exception as e :
win_popup_button ( f ' Knock sequence not valid JSON, skipping! \n { e !r} ' , ' OK ' )
else : # New style config
i = 0
for section in config . sections ( ) :
if section . startswith ( ' Hosts. ' ) :
_ , group = section . split ( ' . ' , 1 )
if i == 0 :
G . current_hostset = group
G . hosts [ group ] = {
' hostpool ' : [ ] ,
' backend ' : ' pve ' ,
' user ' : " " ,
' token_name ' : None ,
' token_value ' : None ,
' totp ' : False ,
' verify_ssl ' : True ,
' pwresetcmd ' : None ,
' auto_vmid ' : None ,
' knock_seq ' : [ ]
}
try :
hostjson = json . loads ( config [ section ] [ ' hostpool ' ] )
except Exception as e :
win_popup_button ( f " Error: could not parse hostpool in section { section } : \n { e !r} " , " OK " )
return False
for key , value in hostjson . items ( ) :
G . hosts [ group ] [ ' hostpool ' ] . append ( {
' host ' : key ,
' port ' : int ( value )
} )
if ' auth_backend ' in config [ section ] :
G . hosts [ group ] [ ' backend ' ] = config [ section ] [ ' auth_backend ' ]
if ' user ' in config [ section ] :
G . hosts [ group ] [ ' user ' ] = config [ section ] [ ' user ' ]
if ' token_name ' in config [ section ] :
G . hosts [ group ] [ ' token_name ' ] = config [ section ] [ ' token_name ' ]
if ' token_value ' in config [ section ] :
G . hosts [ group ] [ ' token_value ' ] = config [ section ] [ ' token_value ' ]
if ' auth_totp ' in config [ section ] :
G . hosts [ group ] [ ' totp ' ] = config [ section ] . getboolean ( ' auth_totp ' )
if ' tls_verify ' in config [ section ] :
G . hosts [ group ] [ ' verify_ssl ' ] = config [ section ] . getboolean ( ' tls_verify ' )
if ' pwresetcmd ' in config [ section ] :
G . hosts [ group ] [ ' pwresetcmd ' ] = config [ section ] [ ' pwresetcmd ' ]
if ' auto_vmid ' in config [ section ] :
G . hosts [ group ] [ ' auto_vmid ' ] = config [ section ] . getint ( ' auto_vmid ' )
if ' knock_seq ' in config [ section ] :
try :
G . hosts [ group ] [ ' knock_seq ' ] = json . loads ( config [ section ] [ ' knock_seq ' ] )
except Exception as e :
win_popup_button ( f ' Knock sequence not valid JSON, skipping! \n { e !r} ' , ' OK ' )
i + = 1
2022-03-07 16:56:54 -06:00
if ' SpiceProxyRedirect ' in config :
for key in config [ ' SpiceProxyRedirect ' ] :
G . spiceproxy_conv [ key ] = config [ ' SpiceProxyRedirect ' ] [ key ]
2022-08-19 21:12:03 -05:00
if ' AdditionalParameters ' in config :
G . addl_params = { }
for key in config [ ' AdditionalParameters ' ] :
G . addl_params [ key ] = config [ ' AdditionalParameters ' ] [ key ]
2022-03-07 16:56:54 -06:00
return True
def win_popup ( message ) :
2023-02-03 15:47:10 -06:00
layout = [
2023-09-07 15:52:18 -05:00
[ sg . Text ( message , key = ' -TXT- ' ) ]
2023-02-03 15:47:10 -06:00
]
2023-09-07 15:52:18 -05:00
window = sg . Window ( ' Message ' , layout , return_keyboard_events = True , no_titlebar = True , keep_on_top = True , finalize = True , )
2022-04-21 11:27:23 -05:00
window . bring_to_front ( )
2023-02-03 15:47:10 -06:00
_ , _ = window . read ( timeout = 10 ) # Fixes a black screen bug
2023-09-07 15:52:18 -05:00
window [ ' -TXT- ' ] . update ( message )
sleep ( .15 )
window [ ' -TXT- ' ] . update ( message )
2022-03-07 16:56:54 -06:00
return window
def win_popup_button ( message , button ) :
layout = [
[ sg . Text ( message ) ] ,
[ sg . Button ( button ) ]
]
window = sg . Window ( ' Message ' , layout , return_keyboard_events = True , no_titlebar = True , keep_on_top = True , finalize = True )
window . Element ( button ) . SetFocus ( )
while True :
event , values = window . read ( )
if event in ( button , sg . WIN_CLOSED , ' Log In ' , ' \r ' , ' special 16777220 ' , ' special 16777221 ' ) :
window . close ( )
return
def setmainlayout ( ) :
2023-10-15 19:04:22 -05:00
readonly = False
if G . hosts [ G . current_hostset ] [ ' user ' ] and G . hosts [ G . current_hostset ] [ ' token_name ' ] and G . hosts [ G . current_hostset ] [ ' token_value ' ] :
readonly = True
2022-03-07 16:56:54 -06:00
layout = [ ]
if G . imagefile :
2023-10-15 19:04:22 -05:00
layout . append (
[
sg . Image ( G . imagefile ) ,
sg . Text (
G . title ,
size = (
18 * G . scaling ,
1 * G . scaling
) ,
justification = ' c ' ,
font = [
" Helvetica " ,
18
]
)
]
)
2022-03-07 16:56:54 -06:00
else :
2023-10-15 19:04:22 -05:00
layout . append (
[
sg . Text (
G . title ,
size = (
30 * G . scaling ,
1 * G . scaling
) ,
justification = ' c ' ,
font = [
" Helvetica " ,
18
]
)
]
)
2022-03-07 16:56:54 -06:00
2023-10-15 19:04:22 -05:00
if len ( G . hosts ) > 1 :
groups = [ ]
for key , _ in G . hosts . items ( ) :
groups . append ( key )
layout . append (
[
sg . Text (
" Server Group: " ,
size = (
12 * G . scaling ,
1 * G . scaling
) ,
font = [
" Helvetica " ,
12
]
) ,
sg . Combo (
groups ,
G . current_hostset ,
key = ' -group- ' ,
font = [
" Helvetica " ,
12
] ,
readonly = True ,
enable_events = True
)
]
)
layout . append (
[
sg . Text (
" Username " ,
size = (
12 * G . scaling ,
1 * G . scaling
) ,
font = [
" Helvetica " ,
12
]
) ,
sg . InputText (
default_text = G . hosts [ G . current_hostset ] [ ' user ' ] ,
key = ' -username- ' ,
font = [
" Helvetica " ,
12
] ,
readonly = readonly
)
]
)
layout . append (
[
sg . Text (
" Password " ,
size = (
12 * G . scaling ,
1 * G . scaling
) ,
font = [
" Helvetica " ,
12
]
) ,
sg . InputText (
key = ' -password- ' ,
password_char = ' * ' ,
font = [
" Helvetica " ,
12
] ,
readonly = readonly
)
]
)
if G . hosts [ G . current_hostset ] [ ' totp ' ] :
layout . append (
[
sg . Text (
" OTP Key " ,
size = (
12 * G . scaling ,
1
) ,
font = [
" Helvetica " ,
12
]
) ,
sg . InputText (
key = ' -totp- ' ,
font = [
" Helvetica " ,
12
]
)
]
)
2022-03-07 16:56:54 -06:00
if G . kiosk :
2023-10-15 19:04:22 -05:00
layout . append (
[
sg . Button (
" Log In " ,
font = [
" Helvetica " ,
14
] ,
bind_return_key = True
)
]
)
2022-03-07 16:56:54 -06:00
else :
2023-10-15 19:04:22 -05:00
layout . append (
[
sg . Button (
" Log In " ,
font = [
" Helvetica " ,
14
] ,
bind_return_key = True
) ,
sg . Button (
" Cancel " ,
font = [
" Helvetica " ,
14
]
)
]
)
if G . hosts [ G . current_hostset ] [ ' pwresetcmd ' ] :
layout [ - 1 ] . append (
sg . Button (
' Password Reset ' ,
font = [
" Helvetica " ,
14
]
)
)
2022-03-07 16:56:54 -06:00
return layout
2023-02-03 15:47:10 -06:00
def getvms ( listonly = False ) :
2022-03-07 16:56:54 -06:00
vms = [ ]
2022-08-19 22:16:08 -05:00
try :
2023-02-15 23:12:51 +11:00
nodes = [ ]
for node in G . proxmox . cluster . resources . get ( type = ' node ' ) :
if node [ ' status ' ] == ' online ' :
nodes . append ( node [ ' node ' ] )
2022-08-19 22:16:08 -05:00
for vm in G . proxmox . cluster . resources . get ( type = ' vm ' ) :
2023-02-15 23:12:51 +11:00
if vm [ ' node ' ] not in nodes :
continue
2022-10-23 22:09:04 -08:00
if ' template ' in vm and vm [ ' template ' ] :
2022-09-09 17:44:28 -08:00
continue
2023-02-03 15:47:10 -06:00
if G . guest_type == ' both ' or G . guest_type == vm [ ' type ' ] :
if listonly :
vms . append (
{
' vmid ' : vm [ ' vmid ' ] ,
' name ' : vm [ ' name ' ] ,
' node ' : vm [ ' node ' ]
}
)
else :
vms . append ( vm )
2022-08-19 22:16:08 -05:00
return vms
except proxmoxer . core . ResourceException as e :
win_popup_button ( f " Unable to display list of VMs: \n { e !r} " , ' OK ' )
return False
2023-09-21 13:21:10 -05:00
except requests . exceptions . ConnectionError as e :
print ( f " Encountered error when querying proxmox: { e !r} " )
return False
2022-03-07 16:56:54 -06:00
def setvmlayout ( vms ) :
layout = [ ]
if G . imagefile :
2022-03-14 21:24:36 -05:00
layout . append ( [ sg . Image ( G . imagefile ) , sg . Text ( G . title , size = ( 18 * G . scaling , 1 * G . scaling ) , justification = ' c ' , font = [ " Helvetica " , 18 ] ) ] )
2022-03-07 16:56:54 -06:00
else :
2022-03-14 21:24:36 -05:00
layout . append ( [ sg . Text ( G . title , size = ( 30 * G . scaling , 1 * G . scaling ) , justification = ' c ' , font = [ " Helvetica " , 18 ] ) ] )
layout . append ( [ sg . Text ( ' Please select a desktop instance to connect to ' , size = ( 40 * G . scaling , 1 * G . scaling ) , justification = ' c ' , font = [ " Helvetica " , 10 ] ) ] )
2022-03-09 21:46:52 -06:00
layoutcolumn = [ ]
2022-03-07 16:56:54 -06:00
for vm in vms :
2022-03-09 15:14:16 -09:00
if not vm [ " status " ] == " unknown " :
2023-09-07 15:52:18 -05:00
vmkeyname = f ' -VM| { vm [ " vmid " ] } - '
2022-03-09 15:14:16 -09:00
connkeyname = f ' -CONN| { vm [ " vmid " ] } - '
2023-09-07 15:52:18 -05:00
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 )
2022-03-09 21:46:52 -06:00
layoutcolumn . append ( [ sg . HorizontalSeparator ( ) ] )
if len ( vms ) > 5 : # We need a scrollbar
2023-09-22 11:31:03 +02:00
layout . append ( [ sg . Column ( layoutcolumn , scrollable = True , size = [ None , None ] ) ] )
2022-03-09 21:46:52 -06:00
else :
for row in layoutcolumn :
layout . append ( row )
2022-03-07 16:56:54 -06:00
layout . append ( [ sg . Button ( ' Logout ' , font = [ " Helvetica " , 14 ] ) ] )
return layout
2022-03-10 16:58:24 -06:00
def iniwin ( inistring ) :
inilayout = [
2022-03-14 21:24:36 -05:00
[ sg . Multiline ( default_text = inistring , size = ( 800 * G . scaling , 600 * G . scaling ) ) ]
2022-03-10 16:58:24 -06:00
]
iniwindow = sg . Window ( ' INI debug ' , inilayout )
while True :
event , values = iniwindow . read ( )
if event == None :
break
iniwindow . close ( )
return True
2023-09-07 15:52:18 -05:00
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
2022-03-07 16:56:54 -06:00
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 ' )
2023-09-07 15:52:18 -05:00
sleep ( .2 )
2022-03-07 16:56:54 -06:00
if vmstatus [ ' status ' ] != ' running ' :
startpop = win_popup ( f ' Starting { vmstatus [ " name " ] } ... ' )
2023-09-07 15:52:18 -05:00
sleep ( .1 )
2022-03-07 16:56:54 -06:00
try :
if vmtype == ' qemu ' :
jobid = G . proxmox . nodes ( vmnode ) . qemu ( str ( vmid ) ) . status . start . post ( timeout = 28 )
else : # Not sure this is even a thing, but here it is...
jobid = G . proxmox . nodes ( vmnode ) . lxc ( str ( vmid ) ) . status . start . post ( timeout = 28 )
except proxmoxer . core . ResourceException as e :
startpop . close ( )
win_popup_button ( f " Unable to start VM, please provide your system administrator with the following error: \n { e !r} " , ' OK ' )
return False
running = False
i = 0
while running == False and i < 30 :
2022-07-19 10:37:26 -05:00
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 = { }
2022-03-07 16:56:54 -06:00
if ' exitstatus ' in jobstatus :
startpop . close ( )
startpop = None
if jobstatus [ ' exitstatus ' ] != ' OK ' :
win_popup_button ( ' Unable to start VM, please contact your system administrator for assistance ' , ' OK ' )
running = True
else :
running = True
status = True
sleep ( 1 )
i + = 1
if not status :
if startpop :
startpop . close ( )
return status
2023-09-07 15:52:18 -05:00
if action == ' reload ' :
return
2023-03-10 09:22:10 -06:00
try :
if vmtype == ' qemu ' :
spiceconfig = G . proxmox . nodes ( vmnode ) . qemu ( str ( vmid ) ) . spiceproxy . post ( )
else : # Not sure this is even a thing, but here it is...
spiceconfig = G . proxmox . nodes ( vmnode ) . lxc ( str ( vmid ) ) . spiceproxy . post ( )
except proxmoxer . core . ResourceException as e :
win_popup_button ( f " Unable to connect to VM { vmid } : \n { e !r} \n Is SPICE display configured for your VM? " , ' OK ' )
return False
2022-03-07 16:56:54 -06:00
confignode = ConfigParser ( )
confignode [ ' virt-viewer ' ] = { }
2022-08-19 21:12:03 -05:00
for key , value in spiceconfig . items ( ) :
2022-03-07 16:56:54 -06:00
if key == ' proxy ' :
2022-05-13 15:50:18 +02:00
val = value [ 7 : ] . lower ( )
2022-03-07 16:56:54 -06:00
if val in G . spiceproxy_conv :
confignode [ ' virt-viewer ' ] [ key ] = f ' http:// { G . spiceproxy_conv [ val ] } '
else :
confignode [ ' virt-viewer ' ] [ key ] = f ' { value } '
else :
confignode [ ' virt-viewer ' ] [ key ] = f ' { value } '
2022-08-19 21:12:03 -05:00
if G . addl_params :
for key , value in G . addl_params . items ( ) :
confignode [ ' virt-viewer ' ] [ key ] = f ' { value } '
2022-03-07 16:56:54 -06:00
inifile = StringIO ( ' ' )
confignode . write ( inifile )
inifile . seek ( 0 )
inistring = inifile . read ( )
2022-03-10 16:58:24 -06:00
if G . inidebug :
closed = iniwin ( inistring )
connpop = win_popup ( f ' Connecting to { vmstatus [ " name " ] } ... ' )
2022-03-09 15:11:26 -09:00
pcmd = [ G . vvcmd ]
2023-10-13 16:04:19 -05:00
if G . kiosk and G . viewer_kiosk :
2022-03-07 16:56:54 -06:00
pcmd . append ( ' --kiosk ' )
pcmd . append ( ' --kiosk-quit ' )
pcmd . append ( ' on-disconnect ' )
2022-07-11 14:07:32 -05:00
elif G . fullscreen :
2022-03-07 16:56:54 -06:00
pcmd . append ( ' --full-screen ' )
pcmd . append ( ' - ' ) #We need it to listen on stdin
process = subprocess . Popen ( pcmd , stdin = subprocess . PIPE , stdout = subprocess . PIPE )
try :
output = process . communicate ( input = inistring . encode ( ' utf-8 ' ) , timeout = 5 ) [ 0 ]
except subprocess . TimeoutExpired :
pass
status = True
connpop . close ( )
return status
def setcmd ( ) :
try :
if os . name == ' nt ' : # Windows
import csv
cmd1 = ' ftype VirtViewer.vvfile '
result = subprocess . check_output ( cmd1 , shell = True )
cmdresult = result . decode ( ' utf-8 ' )
cmdparts = cmdresult . split ( ' = ' )
for row in csv . reader ( [ cmdparts [ 1 ] ] , delimiter = ' ' , quotechar = ' " ' ) :
2022-03-09 22:41:56 -06:00
G . vvcmd = row [ 0 ]
2022-03-07 16:56:54 -06:00
break
elif os . name == ' posix ' :
cmd1 = ' which remote-viewer '
result = subprocess . check_output ( cmd1 , shell = True )
G . vvcmd = ' remote-viewer '
except subprocess . CalledProcessError :
if os . name == ' nt ' :
win_popup_button ( ' Installation of virt-viewer missing, please install from https://virt-manager.org/download/ ' , ' OK ' )
elif os . name == ' posix ' :
win_popup_button ( ' Installation of virt-viewer missing, please install using `apt install virt-viewer` ' , ' OK ' )
sys . exit ( )
2022-08-19 22:16:08 -05:00
def pveauth ( username , passwd = None , totp = None ) :
2023-10-15 19:04:22 -05:00
random . shuffle ( G . hosts [ G . current_hostset ] [ ' hostpool ' ] )
2022-03-07 16:56:54 -06:00
err = None
2023-10-15 19:04:22 -05:00
for hostinfo in G . hosts [ G . current_hostset ] [ ' hostpool ' ] :
2022-03-07 16:56:54 -06:00
host = hostinfo [ ' host ' ]
if ' port ' in hostinfo :
port = hostinfo [ ' port ' ]
else :
port = 8006
connected = False
authenticated = False
if not connected and not authenticated :
try :
2023-10-15 19:04:22 -05:00
if G . hosts [ G . current_hostset ] [ ' token_name ' ] and G . hosts [ G . current_hostset ] [ ' token_value ' ] :
G . proxmox = proxmoxer . ProxmoxAPI (
host ,
user = f " { username } @ { G . hosts [ G . current_hostset ] [ ' backend ' ] } " ,
token_name = G . hosts [ G . current_hostset ] [ ' token_name ' ] ,
token_value = G . hosts [ G . current_hostset ] [ ' token_value ' ] ,
verify_ssl = G . hosts [ G . current_hostset ] [ ' verify_ssl ' ] ,
port = port
)
2022-03-09 15:14:16 -09:00
elif totp :
2023-10-15 19:04:22 -05:00
G . proxmox = proxmoxer . ProxmoxAPI (
host ,
user = f " { username } @ { G . hosts [ G . current_hostset ] [ ' backend ' ] } " ,
otp = totp ,
password = passwd ,
verify_ssl = G . hosts [ G . current_hostset ] [ ' verify_ssl ' ] ,
port = port
)
2022-03-07 16:56:54 -06:00
else :
2023-10-15 19:04:22 -05:00
G . proxmox = proxmoxer . ProxmoxAPI (
host ,
user = f " { username } @ { G . hosts [ G . current_hostset ] [ ' backend ' ] } " ,
password = passwd ,
verify_ssl = G . hosts [ G . current_hostset ] [ ' verify_ssl ' ] ,
port = port
)
2022-03-07 16:56:54 -06:00
connected = True
authenticated = True
return connected , authenticated , err
except proxmoxer . backends . https . AuthenticationError as e :
err = e
connected = True
return connected , authenticated , err
2022-03-14 21:24:36 -05:00
except ( requests . exceptions . ReadTimeout , requests . exceptions . ConnectTimeout , requests . exceptions . ConnectionError ) as e :
2022-03-07 16:56:54 -06:00
err = e
connected = False
return connected , authenticated , err
def loginwindow ( ) :
layout = setmainlayout ( )
2023-10-15 19:04:22 -05:00
if G . hosts [ G . current_hostset ] [ ' user ' ] and G . hosts [ G . current_hostset ] [ ' token_name ' ] and G . hosts [ G . current_hostset ] [ ' token_value ' ] and len ( G . hosts ) == 1 : # We need to skip the login
2022-08-19 22:16:08 -05:00
popwin = win_popup ( " Please wait, authenticating... " )
2023-10-15 19:04:22 -05:00
connected , authenticated , error = pveauth ( G . hosts [ G . current_hostset ] [ ' user ' ] )
2022-08-19 22:16:08 -05:00
popwin . close ( )
if not connected :
win_popup_button ( f ' Unable to connect to any VDI server, are you connected to the Internet? \n Error Info: { error } ' , ' OK ' )
2023-10-15 19:04:22 -05:00
return False , False
2022-08-19 22:16:08 -05:00
elif connected and not authenticated :
win_popup_button ( ' Invalid username and/or password, please try again! ' , ' OK ' )
2023-10-15 19:04:22 -05:00
return False , False
2022-08-19 22:16:08 -05:00
elif connected and authenticated :
2023-10-15 19:04:22 -05:00
return True , False
2022-08-19 22:16:08 -05:00
else :
if G . icon :
window = sg . Window ( G . title , layout , return_keyboard_events = True , resizable = False , no_titlebar = G . kiosk , icon = G . icon )
2022-03-07 16:56:54 -06:00
else :
2022-08-19 22:16:08 -05:00
window = sg . Window ( G . title , layout , return_keyboard_events = True , resizable = False , no_titlebar = G . kiosk )
while True :
event , values = window . read ( )
2023-10-15 19:04:22 -05:00
if event == ' -group- ' and values [ ' -group- ' ] != G . current_hostset :
#Switch cluster
G . current_hostset = values [ ' -group- ' ]
window . close ( )
return False , True
2022-08-19 22:16:08 -05:00
if event == ' Cancel ' or event == sg . WIN_CLOSED :
window . close ( )
2023-10-15 19:04:22 -05:00
return False , False
2023-10-13 13:42:33 -05:00
elif event == ' Password Reset ' :
try :
2023-10-15 19:04:22 -05:00
subprocess . check_call ( G . hosts [ G . current_hostset ] [ ' pwresetcmd ' ] , shell = True )
2023-10-13 13:42:33 -05:00
except Exception as e :
win_popup_button ( f ' Unable to open password reset command. \n \n Error Info: \n { e } ' , ' OK ' )
2022-08-19 22:16:08 -05:00
else :
if event in ( ' Log In ' , ' \r ' , ' special 16777220 ' , ' special 16777221 ' ) :
popwin = win_popup ( " Please wait, authenticating... " )
user = values [ ' -username- ' ]
passwd = values [ ' -password- ' ]
totp = None
if ' -totp- ' in values :
if values [ ' -totp- ' ] not in ( None , ' ' ) :
totp = values [ ' -totp- ' ]
connected , authenticated , error = pveauth ( user , passwd = passwd , totp = totp )
popwin . close ( )
if not connected :
win_popup_button ( f ' Unable to connect to any VDI server, are you connected to the Internet? \n Error Info: { error } ' , ' OK ' )
elif connected and not authenticated :
win_popup_button ( ' Invalid username and/or password, please try again! ' , ' OK ' )
elif connected and authenticated :
window . close ( )
2023-10-15 19:04:22 -05:00
return True , False
2022-08-19 22:16:08 -05:00
#break
2022-03-07 16:56:54 -06:00
def showvms ( ) :
vms = getvms ( )
2023-02-03 15:47:10 -06:00
vmlist = getvms ( listonly = True )
newvmlist = vmlist . copy ( )
if vms == False :
2022-08-19 22:16:08 -05:00
return False
2023-02-03 15:47:10 -06:00
if len ( vms ) < 1 :
2022-03-07 16:56:54 -06:00
win_popup_button ( ' No desktop instances found, please consult with your system administrator ' , ' OK ' )
return False
2023-02-03 15:47:10 -06:00
layout = setvmlayout ( vms )
2022-03-07 16:56:54 -06:00
if G . icon :
2023-09-14 20:35:27 -05:00
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 )
2022-03-07 16:56:54 -06:00
else :
2023-09-14 20:35:27 -05:00
window = sg . Window ( G . title , layout , return_keyboard_events = True , finalize = True , resizable = False , size = ( G . width , G . height ) , no_titlebar = G . kiosk )
2023-02-03 15:13:27 -06:00
timer = datetime . now ( )
2022-03-07 16:56:54 -06:00
while True :
2023-09-07 15:52:18 -05:00
if ( datetime . now ( ) - timer ) . total_seconds ( ) > 5 :
2023-02-03 15:13:27 -06:00
timer = datetime . now ( )
2023-02-03 15:47:10 -06:00
newvmlist = getvms ( listonly = True )
2023-09-21 13:21:10 -05:00
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 )
2023-09-07 15:52:18 -05:00
else :
2023-09-21 13:21:10 -05:00
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 } " )
2023-09-07 15:52:18 -05:00
2023-02-03 15:13:27 -06:00
event , values = window . read ( timeout = 1000 )
2022-03-09 21:48:50 -06:00
if event in ( ' Logout ' , None ) :
2022-03-07 16:56:54 -06:00
window . close ( )
return False
if event . startswith ( ' -CONN ' ) :
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 ' ] )
if not found :
win_popup_button ( f ' VM { vm [ " name " ] } no longer availble, please contact your system administrator ' , ' OK ' )
2023-09-07 15:52:18 -05:00
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 ' )
2022-03-07 16:56:54 -06:00
return True
def main ( ) :
2023-02-03 15:13:27 -06:00
G . scaling = 1 # TKinter requires integers
2023-10-15 11:39:54 -05:00
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
2022-03-07 16:56:54 -06:00
setcmd ( )
2023-10-15 11:39:54 -05:00
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 ) :
2022-03-07 16:56:54 -06:00
return False
2022-03-17 22:37:58 -04:00
sg . theme ( G . theme )
2022-03-07 16:56:54 -06:00
loggedin = False
2023-10-15 19:04:22 -05:00
switching = False
2022-03-07 16:56:54 -06:00
while True :
if not loggedin :
2023-10-15 19:04:22 -05:00
loggedin , switching = loginwindow ( )
if not loggedin and not switching :
if G . hosts [ G . current_hostset ] [ ' user ' ] and G . hosts [ G . current_hostset ] [ ' token_name ' ] and G . hosts [ G . current_hostset ] [ ' token_value ' ] : # This means if we don't exit we'll be in an infinite loop
2022-08-19 22:16:08 -05:00
return 1
2022-03-07 16:56:54 -06:00
break
2023-10-15 19:04:22 -05:00
elif not loggedin and switching :
pass
2022-03-07 16:56:54 -06:00
else :
2023-10-15 19:04:22 -05:00
if G . hosts [ G . current_hostset ] [ ' auto_vmid ' ] :
2023-10-13 14:22:08 -05:00
vms = getvms ( )
for row in vms :
2023-10-15 19:04:22 -05:00
if row [ ' vmid ' ] == G . hosts [ G . current_hostset ] [ ' auto_vmid ' ] :
2023-10-13 14:22:08 -05:00
vmaction ( row [ ' node ' ] , row [ ' vmid ' ] , row [ ' type ' ] , action = ' connect ' )
return 0
2023-10-15 19:04:22 -05:00
win_popup_button ( f " No VDI instance with ID { G . hosts [ G . current_hostset ] [ ' auto_vmid ' ] } found! " , ' OK ' )
2022-03-07 16:56:54 -06:00
vmstat = showvms ( )
if not vmstat :
G . proxmox = None
loggedin = False
2023-10-15 19:04:22 -05:00
if G . hosts [ G . current_hostset ] [ ' user ' ] and G . hosts [ G . current_hostset ] [ ' token_name ' ] and G . hosts [ G . current_hostset ] [ ' token_value ' ] and len ( G . hosts ) == 1 : # This means if we don't exit we'll be in an infinite loop
2022-08-19 22:16:08 -05:00
return 0
2022-03-07 16:56:54 -06:00
else :
return
2023-02-01 11:06:05 -06:00
if __name__ == ' __main__ ' :
sys . exit ( main ( ) )