-
Notifications
You must be signed in to change notification settings - Fork 12
/
ovirtclient.py
executable file
·1059 lines (874 loc) · 41.7 KB
/
ovirtclient.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file may be used under the terms of the GNU General Public License
# version 3.0 as published by the Free Software Foundation and appearing in
# the file LICENSE included in the packaging of this file. Please review the
# following information to ensure the GNU General Public License version 3.0
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
#
# If you do not wish to use this file under the terms of the GPL version 3.0
# then you may purchase a commercial license. For more information contact
# info@riverbankcomputing.com.
#
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
import sys
import gettext
import configparser
import urllib.request
import threading
from time import sleep, time
from base64 import encodestring
from xml.etree import cElementTree as ET
from random import randint
from subprocess import Popen
from os import remove, access, X_OK
from os.path import isfile
from ssl import SSLContext, PROTOCOL_TLSv1
from globalconf import *
from credentials import Credentials
from about import About
from version import VERSION
from ovirtsdk4 import Error
from PyQt5.QtWidgets import QApplication, QDesktopWidget, QMessageBox, QGridLayout, QLabel, QWidget, QProgressBar, QScrollArea, QVBoxLayout, QAction, QToolBar
from PyQt5.QtGui import QImage, QPixmap, QIcon
from PyQt5.QtCore import Qt, QObjectCleanupHandler, pyqtSignal
class VmData:
"""
A simple class whose objects will store (VMid, VMname, VMstatus, VMtype) tuples
"""
vmid = None
vmname = None
vmstatus = None
vmtype = None
class OvirtClient(QWidget):
"""
This class will handle the main window where all user's VMs will be listed.
This will be rendered only if authentication was successfull since it doesn't
make sense otherwise. Additionally, a Thread will be started checking VM status
changes and update the board accordingly.
"""
stopThread = False # Sentinel for stopping the Thread execution
autologoutWarn = False # Has the user been warned about autologout yet?
openviewer_vms = [] # Initiated VMs in terms of the viewer
updatesignal = pyqtSignal(int, str) # Signal to update the status icons on status changes
reloadsignal = pyqtSignal() # Signal to reload the main widget
warnlogoutsignal = pyqtSignal() # Signal to warn the user about an imminent autologout
logoutsignal = pyqtSignal(bool) # Signal to logout the current user and require credentials again
lastclick = int(time()) # Timestamp of the last click. If a timeout policy is defined and
# the time exceeds this value, an autologout will be performed.
def __init__(self):
QWidget.__init__(self)
self.initUI()
def vm_based_resize(self, vmnum):
"""
Description: Depending on the number of VMs which the user has permissions on,
we need to resize the main window accordingly in height.The fewer
they are, the smaller the window will be. If MAXHEIGHT is reached,
a scrollbar will also be shown (that's processed in a different
place, though)
Parameters: The number of VMs.
Returns: The actual window height.
"""
global MAXHEIGHT
if not vmnum:
# If user has no machines, resize window to the minimum
winheight = 150
no_machines = QLabel(_('no_vms'))
no_machines.setWordWrap(True)
no_machines.setAlignment(Qt.AlignCenter)
self.grid.addWidget(no_machines, 0, 0)
self.setMinimumHeight(winheight)
else:
# User has at least one VM
if vmnum > 5:
# More than 5 means resizing the window to the maximum
winheight = MAXHEIGHT
self.setFixedHeight(winheight)
else:
# Otherwise, resize depending on the number of VMs
winheight = vmnum * 100 + 50
self.setFixedHeight(winheight)
return winheight
def get_os_icon(self, os):
"""
Description: Depending on the VM's OS, this method returns
which icon should be used to illustrate it.
Arguments: oVirt-style string representing the VM's OS
Returns: The file name under IMGDIR that should be shown.
"""
if 'ubuntu' in os:
return 'ubuntu'
elif 'rhel' in os:
return 'redhat'
elif 'centos' in os:
return 'centos'
elif 'debian' in os:
return 'debian'
elif 'linux' in os:
return 'linux'
elif 'win' in os:
return 'windows'
return 'unknown'
def generate_toolbar(self):
"""
Description: There will be a toolbar in the main widget with some buttons,
this method will render them.
Arguments: None
Returns: Nothing
"""
global IMGDIR, conf
self.toolBar = QToolBar(self)
refreshAction = QAction(QIcon(IMGDIR + 'refresh.png'), _('refresh'), self)
refreshAction.setShortcut('Ctrl+R')
refreshAction.triggered.connect(self.refresh_grid)
self.toolBar.addAction(refreshAction)
self.forgetCredsAction = QAction(QIcon(IMGDIR + 'forget.png'), _('forget_credentials'), self)
self.forgetCredsAction.setShortcut('Ctrl+F')
self.forgetCredsAction.triggered.connect(self.forget_creds)
if not isfile(conf.USERCREDSFILE):
self.forgetCredsAction.setDisabled(True)
self.toolBar.addAction(self.forgetCredsAction)
aboutAction = QAction(QIcon(IMGDIR + 'about.png'), _('about'), self)
aboutAction.setShortcut('Ctrl+I')
aboutAction.triggered.connect(self.about)
self.toolBar.addAction(aboutAction)
exitAction = QAction(QIcon(IMGDIR + 'exit.png'), _('exit'), self)
exitAction.setShortcut('Ctrl+Q')
exitAction.triggered.connect(self.quit_button)
self.toolBar.addAction(exitAction)
self.grid.addWidget(self.toolBar, 0, 3, Qt.AlignRight)
def make_button(self, filename, tooltip, alignment=Qt.AlignCenter):
"""
Description: Creates a QLabel which will contain an image and will
simulate a button. No associated text will be shown.
Arguments: 1. filename: The filename of the icon/image to show
2. tooltip: Some text to show as a tooltip to the image
3. alignment: Alignment of the button (center by default)
Returns: The created QLabel button
"""
global STANDARDCELLCSS, IMGDIR
filepath = '%s%s%s' % (IMGDIR, filename, '.png')
icon = QImage(filepath)
image = QLabel()
image.setToolTip('<span style="color:#B9B900">%s</span>' % (tooltip))
image.setStyleSheet(STANDARDCELLCSS)
image.setPixmap(QPixmap.fromImage(icon))
image.setAlignment(alignment)
return image
def compare_vms(self, vm1, vm2):
"""
Description: VM list will be sorted by names. This method is the comparison
method for two VMs. We just sort them alphabetically.
Arguments: Two VMs (oVirt VM objects)
Returns: -1, 0 or 1 depending on the name-based sorting
"""
if vm1.name.lower() < vm2.name.lower():
return -1
if vm1.name.lower() == vm2.name.lower():
return 0
return 1
def p22p3_compare_vms(self, cmpfunct):
""" Wrapper for Python 2 -> 3 conversion for the old cmp= method of sorted()"""
class K:
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return cmpfunct(self.obj, other.obj) < 0
def __gt__(self, other):
return cmpfunct(self.obj, other.obj) > 0
def __eq__(self, other):
return cmpfunct(self.obj, other.obj) == 0
def __le__(self, other):
return cmpfunct(self.obj, other.obj) <= 0
def __ge__(self, other):
return cmpfunct(self.obj, other.obj) >= 0
def __ne__(self, other):
return cmpfunct(self.obj, other.obj) != 0
return K
def current_vm_status(self, vmstatus):
"""
Description: Single translation between oVirt-like status to human-readable status
Arguments: oVirt-like status
Returns: Human-readable status
"""
if vmstatus == 'up':
hrstatus = _('up')
elif vmstatus == 'down':
hrstatus = _('down')
elif vmstatus == 'powering_down':
hrstatus = _('powering_down')
elif vmstatus == 'wait_for_launch':
hrstatus = _('wait_for_launch')
elif vmstatus == 'powering_up':
hrstatus = _('powering_up')
elif vmstatus == 'reboot_in_progress':
hrstatus = _('rebooting')
else:
hrstatus = 'unknown'
return hrstatus
def toggle_vm_action(self, vmstatus):
"""
Description: Returns the available action for the current VM's status. If machine is up,
available action is turn it off and viceversa.
Arguments: Current vm status
Returns: Toggle action for the current status.
"""
if vmstatus == 'up':
vmaction = _('shut_down')
if vmstatus == 'down':
vmaction = _('power_on')
return vmaction
def toggle_action_text(self, vmstatus):
"""
Description: One of the columns shows the current VM's status. This method returns
the toggle tooltip text so the user know what will happen if they click
on the status icon.
Arguments: Current vm status
Returns: The tooltip's informative text.
"""
rettxt = '%s <b>%s</b>.' % (_('current_vm_status'), self.current_vm_status(vmstatus))
if vmstatus == 'up':
rettxt += ' %s %s' % (_('click_to_action'), _('shut_down'))
if vmstatus == 'down':
rettxt += ' %s %s' % (_('click_to_action'), _('power_on'))
return rettxt
def change_status(self, rowid):
"""
Description: If the user clicks on the column which determines VM's status, we'll allow them
to change VM's status. This method shows a confirmation dialog and if accepted,
it will be notified to oVirt.
Arguments: The row id that has been clicked. This relationship is stored using the VmData class.
Returns: Nothing
"""
global conf
self.lastclick = int(time()) # Last click timestamp update
curvmstatus = self.vmdata[rowid].vmstatus
if curvmstatus != 'up' and curvmstatus != 'down':
QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('vm_in_unchangeable_status'))
return
reply = QMessageBox.question(None, _('apptitle') + ': ' + _('confirm'), '%s <b>%s</b>. %s: <b>%s</b>.' % (_('current_vm_status'), self.current_vm_status(curvmstatus), _('confirm_vm_status_change'), self.toggle_vm_action(curvmstatus)), QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
try:
vms_service = conf.OVIRTCONN.vms_service()
vm = vms_service.list(search='id=%s' % (self.vmdata[rowid].vmid))[0]
except Error:
QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('unexpected_connection_drop'))
quit()
if curvmstatus == 'up':
try:
vm_service = vms_service.vm_service(id=self.vmdata[rowid].vmid)
vm_service.shutdown()
QMessageBox.information(None, _('apptitle') + ': ' + _('success'), _('shutting_down_vm'))
except Error:
QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('vm_in_unchangeable_status'))
if curvmstatus == 'down':
try:
vm_service = vms_service.vm_service(id=self.vmdata[rowid].vmid)
vm_service.start()
QMessageBox.information(None, _('apptitle') + ': ' + _('success'), _('powering_up_vm'))
except Error:
QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('vm_in_unchangeable_status'))
def get_viewer_ticket(self, vmid):
"""
Description: Connecting to the machine involves two steps, the first one is obtaining a 'ticket' string
for the connection request. This is done making a request to the oVirt API and then parsing
the resulting XML document to get the ticket hash. Also, the request may return more than
one ticket: One for SPICE and another for VNC. In this case, we'll return the one that
the user defined in the settings file (SPICE as default).
Arguments: The VM UUID in oVirt-format
Returns: The ticket hash string
"""
global conf
req = urllib.request.Request('%s/%s/%s/%s' % (conf.CONFIG['ovirturl'], 'vms', vmid, 'graphicsconsoles'))
# Python 2 -> 3 conversion: encodestring expects a byte-like string, not str
base64str = encodestring(('%s:%s' % (conf.USERNAME + '@' + conf.CONFIG['ovirtdomain'], conf.PASSWORD)).encode()).decode().replace('\n', '')
req.add_header('Authorization', 'Basic ' + base64str)
req.add_header('filter', 'true')
unverified_ctxt = SSLContext(PROTOCOL_TLSv1)
tickethash = urllib.request.urlopen(req, context=unverified_ctxt).read()
xmlcontent = ET.fromstring(tickethash)
ticket = None
for data in xmlcontent.findall('graphics_console'):
proto = data.findall('protocol')[0]
if proto.text.lower() == conf.CONFIG['prefproto'].lower():
return data.get('id')
else:
ticket = data.get('id')
return ticket
def store_vv_file(self, vmid, ticket):
"""
Description: Connecting to the machine involves two steps, the second one is obtaining a 'vv' file with the
connection parameters, which we can later pipe to virt-viewer and the connection will be opened.
Arguments: 1. vmid: The VM UUID in oVirt-format.
2. ticket: The ticket obtained in the first step (method get_viewer_ticket)
Returns: The temporary filename with all the parameters to connect to the machine (piped to virt-viewer)
"""
global conf
if not ticket:
return False
req = urllib.request.Request('%s/%s/%s/%s/%s' % (conf.CONFIG['ovirturl'], 'vms', vmid, 'graphicsconsoles', ticket))
# Python 2 -> 3 conversion: encodestring expects a byte-like string, not str
base64str = encodestring(('%s:%s' % (conf.USERNAME + '@' + conf.CONFIG['ovirtdomain'], conf.PASSWORD)).encode()).decode().replace('\n', '')
req.add_header('Authorization', 'Basic ' + base64str)
req.add_header('Content-Type', 'application/xml')
req.add_header('Accept', 'application/x-virt-viewer')
req.add_header('filter', 'true')
unverified_ctxt = SSLContext(PROTOCOL_TLSv1)
try:
contents = urllib.request.urlopen(req, context=unverified_ctxt).read()
if conf.CONFIG['fullscreen'] == '1':
contents = contents.replace('fullscreen=0', 'fullscreen=1')
filename = '/tmp/viewer-' + str(randint(10000, 99999))
f = open(filename, 'wb')
f.write(contents)
f.close()
return filename
except urllib.request.HTTPError as em:
QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('unexpected_request_error') + '(' + str(em.code) + '): ' + em.reason + '. ' + _('check_vm_config_updated'))
return None
def viewer_exit(self, vmname):
self.openviewer_vms.remove(vmname) # Remove the VM from the list of opened viewers
self.reloadsignal.emit() # Enforce a reload signal to update the status icon ASAP
def create_viewer_thread(self, vmname, filename):
global conf
def runInThread(vmname, onExit, popenArgs):
viewer = Popen(popenArgs)
viewer.wait()
onExit(vmname)
return
thread = threading.Thread(target=runInThread, args=(vmname, self.viewer_exit, [conf.CONFIG['remote_viewer_path'], '-t', vmname, '-f', '--', 'file://%s' % (filename)]))
thread.start()
# Returns immediately after the thread starts
return thread
def connect2machine(self, vmid, vmname):
"""
Description: Connecting to the machine involves two steps, this method does both and
makes sure everything is ok to call virt-viewer afterwards.
Arguments: 1. vmid: The VM UUID in oVirt-format.
2. vmname: Just for displaying purposes, the VM name
Returns: Nothing. Opens the view-viewer display.
"""
viewer_ticket = self.get_viewer_ticket(vmid)
filename = self.store_vv_file(vmid, viewer_ticket)
if filename:
self.create_viewer_thread(vmname, filename)
else:
if vmname in self.openviewer_vms:
self.openviewer_vms.remove(vmname) # Remove the VM from the list of opened viewers
QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('no_viewer_file'))
def connect(self, rowid):
"""
Description: Whenever the user clicks on the 'connect' row, this method will make
sure the VM status is up and only then will call the connect2machine method.
Arguments: The row id that has been clicked. This relationship is stored using the VmData class.
Returns: Nothing
"""
self.lastclick = int(time()) # Last click timestamp update
vmid = self.vmdata[rowid].vmid
vmname = self.vmdata[rowid].vmname
vmstatus = self.vmdata[rowid].vmstatus
if vmstatus != 'up':
QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('cannot_connect_if_vm_not_up'))
return
if vmname in self.openviewer_vms:
QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('cannot_open_more_viewer_sessions'))
return
self.openviewer_vms.append(vmname)
self.refresh_grid() # Enforce a dashboard reload to make the icon refresh
self.connect2machine(vmid, vmname)
def acquire_vm_from_vmpool(self, rowid):
"""
Description: A machine will be acquired by a user if they click on the icon of a VmPool
Arguments: The row id that has been clicked. This relationship is stored using the VmData class.
Returns: Nothing
"""
self.lastclick = int(time()) # Last click timestamp update
vmtype = self.vmdata[rowid].vmtype
if vmtype == 'vmpool':
try:
QMessageBox.information(None, _('apptitle') + ': ' + _('info'), _('acquiring_vm_from_pool'))
vmpool_service = conf.OVIRTCONN.vm_pools_service()
vmp = vmpool_service.pool_service(id=self.vmdata[rowid].vmid)
vmp.allocate_vm()
self.refresh_grid()
except Error as e:
QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), str(e))
else:
QMessageBox.warning(None, _('apptitle') + ': ' + _('warning'), _('object_is_not_a_vmpool'))
def refresh_grid(self):
"""
Description: Invoked when the user clicks on the 'Refresh' button in the toolbar. Reloads the board.
Arguments: None
Returns: Nothing
"""
self.lastclick = int(time()) # Last click timestamp update
self.load_vms()
def about(self):
"""
Description: Invoked when the user clicks on the 'About' button in the toolbar.
Arguments: None
Returns: Nothing
"""
self.lastclick = int(time()) # Last click timestamp update
About()
def forget_creds(self):
"""
Description: Invoked when the user clicks on the 'Forget credentials' button in the toolbar.
Arguments: None
Returns: Nothing
"""
global conf
self.lastclick = int(time()) # Last click timestamp update
reply = QMessageBox.question(None, _('apptitle') + ': ' + _('confirm'), _('confirm_forget_creds'), QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
remove(conf.USERCREDSFILE)
self.forgetCredsAction.setDisabled(True)
QMessageBox.information(None, _('apptitle') + ': ' + _('success'), _('creds_forgotten'))
def list_vmpools(self, row, delta, step, vmpools):
"""
Description: Creates one row per VmPool that the user has access to.
Arguments: 1. The index of the first row to loop over.
2. Delta step to sum to the progress bar.
3. The current step of the progress bar
4. The oVirt list of VmPools.
Returns: The final step of the progress bar
"""
# For cleanness reasons, we'll firstly show available VmPools
for vm in vmpools:
vmname = vm.name
# OS icon
ostype = 'vmpool'
imageOsicon = self.make_button(ostype, '<b>' + _('vmpool') + '</b>')
# Machine name
gridvmname = QLabel(vmname)
gridvmname.setStyleSheet(STANDARDCELLCSS)
gridvmname.setAlignment(Qt.AlignCenter)
# Acquire VM button
connect = self.make_button('grab', _('grab_vm_vmpool'));
connect.mousePressEvent = lambda x, r=row: self.acquire_vm_from_vmpool(r)
# Fill row with known info
self.grid.addWidget(imageOsicon, row, 0)
self.grid.addWidget(gridvmname, row, 1)
self.grid.addWidget(connect, row, 2)
# Store the correspondence between row number <-> VMPool data
vmd = VmData()
vmd.vmid = vm.id
vmd.vmname = vm.name
vmd.vmstatus = None
vmd.vmtype = 'vmpool'
self.vmdata[row] = vmd
row += 1
step += delta
self.pbar.setValue(step)
return step
def list_vms(self, row, delta, step, vms):
"""
Description: Creates one row per VM that the user has access to.
Arguments: 1. The index of the first row to loop over.
2. Delta step to sum to the progress bar.
3. The current step of the progress bar
4. The oVirt list of VMs.
Returns: Nothing
"""
# Now we'll show up the VMs
for vm in vms:
vmname = vm.name
vmstatus = vm.status.value
# OS icon
ostype = self.get_os_icon(vm.os.type.lower())
imageOsicon = self.make_button(ostype, '<b>%s</b> OS' % (ostype.capitalize()))
# Machine name
gridvmname = QLabel(vmname)
gridvmname.setStyleSheet(STANDARDCELLCSS)
gridvmname.setAlignment(Qt.AlignCenter)
# Connect button. Depending on whether it has already been hit, a different icon
# will be shown and the behavior will also be different.
if vmname not in self.openviewer_vms:
connect = self.make_button('connect', _('connect'));
connect.mousePressEvent = lambda x, r=row: self.connect(r)
else:
connect = self.make_button('viewer', _('viewer_already_opened'));
# Status icon
curaction = self.current_vm_status(vmstatus)
imageSticon = self.make_button(vmstatus, self.toggle_action_text(vmstatus))
imageSticon.mousePressEvent = lambda x, r=row: self.change_status(r)
# Fill row with known info
self.grid.addWidget(imageOsicon, row, 0)
self.grid.addWidget(gridvmname, row, 1)
self.grid.addWidget(imageSticon, row, 2)
self.grid.addWidget(connect, row, 3)
# Store the correspondence between row number <-> VM data
vmd = VmData()
vmd.vmid = vm.id
vmd.vmname = vm.name
vmd.vmstatus = vmstatus
vmd.vmtype = 'vm'
self.vmdata[row] = vmd
row += 1
step += delta
self.pbar.setValue(step)
def load_vms(self):
"""
Description: Main core VM loader method. Will connect to oVirt, get the VM list and render them.
Arguments: None
Returns: Nothing
"""
global conf, MAXHEIGHT, BACKGROUNDCSS, STANDARDCELLCSS
QObjectCleanupHandler().add(self.layout())
if not conf.USERNAME:
quit()
# Used to store row <-> VM correspondence
self.vmdata = {}
step = 0
self.pbarlayout = QGridLayout(self)
self.pbar = QProgressBar(self)
self.grid = QGridLayout()
self.grid.setHorizontalSpacing(0)
# Initially, set the layout to the progress bar
self.pbar.setGeometry(250, 250, 200, 100)
self.pbar.setValue(0)
self.pbarlayout.addWidget(self.pbar, 0, 0)
self.setLayout(self.pbarlayout)
self.setStyleSheet(BACKGROUNDCSS)
try:
# Try getting the VM list from oVirt
vms_serv = conf.OVIRTCONN.vms_service()
vmpools_serv = conf.OVIRTCONN.vm_pools_service()
vms = sorted(vms_serv.list(), key=self.p22p3_compare_vms(self.compare_vms))
vmpools = sorted(vmpools_serv.list(), key=self.p22p3_compare_vms(self.compare_vms))
except Error:
QMessageBox.critical(None, _('apptitle') + ': ' + _('error'), _('unexpected_connection_drop'))
quit()
# Set the main widget height based on the number of VMs
winheight = self.vm_based_resize(len(vms) + len(vmpools))
if len(vms) + len(vmpools) > 0:
delta = int(100 / (len(vms) + len(vmpools)))
else:
delta = int(100)
if vmpools:
step = self.list_vmpools(1, delta, step, vmpools)
if vms:
self.list_vms(len(vmpools) + 1, delta, step, vms)
# Once loading has concluded, progress bar is dismissed and the layout set to the QGridLayout
self.pbar.hide()
QObjectCleanupHandler().add(self.layout())
# First row is special: Number of VMs + Toolbar
total_machines = QLabel(_('total_machines') + ': <font color="#AA8738">' + str(len(vms)) + '</font>, ' + _('total_vmpools') + ': <font color="#AA8738">' + str(len(vmpools)) + '</font>', self)
self.grid.addWidget(total_machines, 0, 0, 1, 3, Qt.AlignCenter)
self.generate_toolbar()
# We wrap the main widget inside another widget with a vertical scrollbar
wrapper = QWidget()
wrapper.setLayout(self.grid)
scroll = QScrollArea()
scroll.setWidget(wrapper)
scroll.setWidgetResizable(True)
scroll.setFixedHeight(winheight)
layout = QVBoxLayout()
layout.addWidget(scroll)
layout.setContentsMargins(0, 0, 0, 20)
self.setLayout(layout)
def update_status_icon(self, i, newstatus):
"""
Description: Invoked when the background thread emits the signal announcing a status
change, so the corresponding VM status icon should be updated.
Arguments: i: Row that has changed their status. The VM can be matched with VmData().
newstatus: The new status for the VM.
Returns: Nothing
"""
imageSticon = self.make_button(newstatus, self.toggle_action_text(newstatus))
imageSticon.mousePressEvent = lambda x, r=i: self.change_status(r)
self.grid.addWidget(imageSticon, i, 2)
def logout_warn(self):
"""
Description: Called if the warn_autologout setting has been set in the config. It
will warn user to reset their idle, otherwise an enforced logout will
be performed by calling the logout() method.
Arguments: None
Returns: Nothing
"""
self.autologoutwarnwin = QMessageBox(None)
self.autologoutwarnwin.setIcon(QMessageBox.Warning)
self.autologoutwarnwin.setText(_('auto_logout_warn'))
self.autologoutwarnwin.setWindowTitle(_('auto_logout_warn_title'))
self.autologoutwarnwin.setStandardButtons(QMessageBox.Ok)
self.autologoutwarnwin.buttonClicked.connect(self.autologoutwarn_accepted)
self.autologoutwarnwin.exec_()
def logout(self, reconnect=False):
"""
Description: Invoked when the autologout parameter is set in the config and the
idle time is overreached. This should require authentication again.
Arguments: None
Returns: Nothing
"""
if conf.OVIRTCONN:
try:
conf.SOCKOBJ.close()
except Error:
pass
self.stopThread = True
conf.USERNAME = None
self.autologoutWarn = False
# Hide the layout so next user doesn't see the previous content
self.hide()
try:
self.autologoutwarnwin.accept()
except AttributeError:
# Usually when the notify_autologout setting has not been set
pass
# This will be called upon autologout events
if reconnect:
creds = Credentials(None)
creds.finished.connect(self.start_vmpane)
creds.exec_()
def autologoutwarn_accepted(self):
"""
Description: Callback issued when the user accepts the message box warning
about an imminent autologout event.
Arguments: None
Returns: Nothing
"""
self.lastclick = int(time())
self.autologoutWarn = False # This will make the warning be shown next times as well
def refresh_statuses(self):
"""
Description: Background thread that will look for VM status changes and
send a signal to the main thread so the corresponding icons
are updated. Also, if there's a change in the number of VMs
that the user controls, the main Widgets will be reloaded.
It'll also check the autologout setting & act accordingly.
Arguments: None
Returns: Nothing ("infinite" loop)
"""
global UPDATESLEEPINTERVAL
autologout = False
while 1 and not self.stopThread:
if conf.OVIRTCONN:
try:
vms_service = conf.OVIRTCONN.vms_service()
ovirt_num_machines = len(vms_service.list())
except Error:
sys.exit('[ERROR] ' + _('unexpected_connection_drop'))
if ovirt_num_machines != len(self.vmdata):
# If the number of VMs has changed, we should reload the main widget
self.reloadsignal.emit()
else:
for i in self.vmdata:
vmid = self.vmdata[i].vmid
vmstatus = self.vmdata[i].vmstatus
try:
ovirtvm = vms_service.list(search='id=%s' % (vmid))[0]
except Error:
sys.exit('[ERROR] ' + _('unexpected_connection_drop'))
if ovirtvm:
curstatus = ovirtvm.status.value
if vmstatus != curstatus:
# If there has been a status change, emit the signal to update icons
self.vmdata[i].vmstatus = curstatus
self.updatesignal.emit(i, curstatus)
# If there is any currently open viewer, we'll reset the idle time so we don't close the session
# while there still is any open session.
if self.openviewer_vms:
self.lastclick = int(time()) # Last click timestamp update
# If the autologout warning has not been shown yet and it's configured, we do so
if conf.CONFIG['autologout'] != 0 and conf.CONFIG['notify_autologout'] != 0 and not self.autologoutWarn and \
(int(time() - self.lastclick) >= (conf.CONFIG['autologout'] - conf.CONFIG['notify_autologout']) * 60):
self.autologoutWarn = True
self.warnlogoutsignal.emit()
# If there's no credentials file and autologout is set, we check for the last
# click and if surpassed, a logout will be performed.
if conf.CONFIG['autologout'] != 0 and not isfile(conf.USERCREDSFILE):
if (int(time()) - self.lastclick) >= (conf.CONFIG['autologout'] * 60):
self.stopThread = True
autologout = True
sleep(UPDATESLEEPINTERVAL)
else:
return
if autologout:
self.logoutsignal.emit(True)
def restart_thread(self):
"""
Description: Simply starts or restarts the background thread
Arguments: None
Returns: Nothing
"""
self.stopThread = False
self.lastclick = int(time())
self.thread = threading.Thread(target=self.refresh_statuses, args=())
self.thread.daemon = True # Daemonize thread
self.thread.start()
def start_vmpane(self):
"""
Description: This method will be called when the Credentials dialog is closed.
This should happen on successful authentication. We should then
load the main widget and fill it with the VMs that the user has
permissions on. Additionally, a background thread will be started
to check VM status changed so the main widget is updated should
this happen.
Arguments: None
Returns: Nothing
"""
self.show()
self.load_vms()
self.center()
self.restart_thread()
def confirm_quit(self):
"""
Description: Asks for confirmation from the user's side to close the app.
Arguments: None
Returns: True if the user wants to close the app, False otherwise.
"""
global conf
reply = QMessageBox.question(None, _('apptitle') + ': ' + _('confirm'), _('confirm_quit'), QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
self.logout(reconnect=False)
return True
else:
return False
def quit_button(self):
"""
Description: Triggered when the Exit button in the Toolbar is hit. Confirmation will
be required.
Arguments: None
Returns: Nothing, exits if user confirms.
"""
self.lastclick = int(time()) # Last click timestamp update
if self.confirm_quit():
quit()
def closeEvent(self, event):
"""
Description: The red 'x'. If the user hits it, we'll ask for confirmation.
Arguments: None
Returns: Nothing, exits if users confirms.
"""
if self.confirm_quit():
event.accept()
else:
event.ignore()
def initUI(self):
"""
Description: Sets the size of the widget, the window title, centers
the window, connects signals to methods and opens the
Credentials dialog if needed.
Arguments: None
Returns: Nothing.
"""
global conf, MAXWIDTH, MAXHEIGHT, IMGDIR, VERSION
self.setFixedSize(MAXWIDTH, MAXHEIGHT)
self.center()
self.setWindowTitle(_('apptitle') + ' ' + VERSION)
self.setWindowIcon(QIcon(IMGDIR + 'appicon.png'))
self.show()
self.updatesignal.connect(self.update_status_icon)
self.logoutsignal.connect(self.logout)
self.warnlogoutsignal.connect(self.logout_warn)
self.reloadsignal.connect(self.load_vms)
if not conf.USERNAME:
creds = Credentials(self)
creds.finished.connect(self.start_vmpane)
creds.exec_()
def center(self):
"""
Description: Just centers the window
Arguments: None
Returns: Nothing
"""
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def checkConfig():
"""
Description: Loads configuration from config file. Also checks for syntax.
Arguments: None
Returns: The gettext object (lang)
"""
global conf
if not isfile(conf.CONFIGFILE):
sys.exit("[ERROR] Configuration file (%s) does not exist" % (conf.CONFIGFILE))
config = configparser.ConfigParser()
config.read(conf.CONFIGFILE)
try:
ovirturl = config.get('ovirt', 'url')
except configparser.NoOptionError:
sys.exit("[ERROR] Configuration file (%s) is missing a mandatory parameter: Section: ovirt, parameter: url. Check config." % (conf.CONFIGFILE))
try:
cafile = config.get('ovirt', 'cafile')
except configparser.NoOptionError:
sys.exit("[ERROR] Configuration file (%s) is missing a mandatory parameter: Section: ovirt, parameter: cafile. Check config." % (conf.CONFIGFILE))
if not isfile(cafile):
sys.exit("[ERROR] Cannot find the CA file (%s). Check if file exists and if so, check if you have reading permissions in your config file." % (cafile))
try:
ovirtdomain = config.get('ovirt', 'domain')
except configparser.NoOptionError:
sys.exit("[ERROR] Configuration file (%s) is missing a mandatory parameter: Section: ovirt, parameter: domain. Check config." % (conf.CONFIGFILE))
try:
applang = config.get('app', 'lang')
except configparser.NoOptionError:
applang = 'en'
try:
conntimeout = config.get('app', 'connection_timeout')
except configparser.NoOptionError:
conntimeout = 15
try:
prefproto = config.get('app', 'preferred_protocol')
if not prefproto:
prefproto = 'spice'
elif prefproto.lower() != 'vnc' and prefproto.lower() != 'spice':
prefproto = 'spice'
except configparser.NoOptionError:
prefproto = 'spice'
try:
fullscreen = config.get('app', 'fullscreen')
if fullscreen != '0' and fullscreen != '1':
fullscreen = '0'