table_widgets.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. #!/usr/bin/python2
  2. # -*- coding: utf8 -*-
  3. #
  4. # The Qubes OS Project, http://www.qubes-os.org
  5. #
  6. # Copyright (C) 2014 Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU General Public License
  10. # as published by the Free Software Foundation; either version 2
  11. # of the License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program; if not, write to the Free Software
  20. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21. import os
  22. from PyQt4 import QtGui
  23. from PyQt4.QtCore import QSize, Qt
  24. from PyQt4.QtGui import QTableWidgetItem, QHBoxLayout, QIcon, QLabel, QWidget, \
  25. QSizePolicy, QSpacerItem, QFont, QColor, QProgressBar, QPainter, QPen
  26. import time
  27. from qubes.qubes import vm_files
  28. import main
  29. qubes_dom0_updates_stat_file = '/var/lib/qubes/updates/dom0-updates-available'
  30. power_order = Qt.DescendingOrder
  31. update_order = Qt.AscendingOrder
  32. row_height = 30
  33. class VmIconWidget (QWidget):
  34. def __init__(self, icon_path, enabled=True, size_multiplier=0.7,
  35. tooltip = None, parent=None, icon_sz = (32, 32)):
  36. super(VmIconWidget, self).__init__(parent)
  37. self.label_icon = QLabel()
  38. if icon_path[0] in ':/':
  39. icon = QIcon (icon_path)
  40. else:
  41. icon = QIcon.fromTheme(icon_path)
  42. icon_sz = QSize (row_height * size_multiplier, row_height * size_multiplier)
  43. icon_pixmap = icon.pixmap(icon_sz, QIcon.Disabled if not enabled else QIcon.Normal)
  44. self.label_icon.setPixmap (icon_pixmap)
  45. self.label_icon.setFixedSize (icon_sz)
  46. if tooltip != None:
  47. self.label_icon.setToolTip(tooltip)
  48. layout = QHBoxLayout()
  49. layout.addWidget(self.label_icon)
  50. layout.setContentsMargins(0,0,0,0)
  51. self.setLayout(layout)
  52. def setToolTip(self, tooltip):
  53. if tooltip is not None:
  54. self.label_icon.setToolTip(tooltip)
  55. else:
  56. self.label_icon.setToolTip('')
  57. class VmTypeWidget(VmIconWidget):
  58. class VmTypeItem(QTableWidgetItem):
  59. def __init__(self, value, vm):
  60. super(VmTypeWidget.VmTypeItem, self).__init__()
  61. self.value = value
  62. self.vm = vm
  63. def set_value(self, value):
  64. self.value = value
  65. def __lt__(self, other):
  66. if self.value == other.value:
  67. return self.vm.qid < other.vm.qid
  68. else:
  69. return self.value < other.value
  70. def __init__(self, vm, parent=None):
  71. (icon_path, tooltip) = self.get_vm_icon(vm)
  72. super (VmTypeWidget, self).__init__(icon_path, True, 0.8, tooltip, parent)
  73. self.vm = vm
  74. self.tableItem = self.VmTypeItem(self.value, vm)
  75. def get_vm_icon(self, vm):
  76. if vm.qid == 0:
  77. self.value = 0
  78. return (":/dom0.png", "Dom0")
  79. elif vm.is_netvm() and not vm.is_proxyvm():
  80. self.value = 1
  81. return (":/netvm.png", "NetVM")
  82. elif vm.is_proxyvm():
  83. self.value = 2
  84. return (":/proxyvm.png", "ProxyVM")
  85. elif vm.is_appvm() and vm.template is None:
  86. self.value = 4
  87. return (":/standalonevm.png", "StandaloneVM")
  88. elif vm.is_template():
  89. self.value = 3
  90. return (":/templatevm.png", "TemplateVM")
  91. elif vm.is_appvm() or vm.is_disposablevm():
  92. self.value = 5 + vm.label.index
  93. return (":/appvm.png", "AppVM")
  94. class VmLabelWidget(VmIconWidget):
  95. class VmLabelItem(QTableWidgetItem):
  96. def __init__(self, value, vm):
  97. super(VmLabelWidget.VmLabelItem, self).__init__()
  98. self.value = value
  99. self.vm = vm
  100. def set_value(self, value):
  101. self.value = value
  102. def __lt__(self, other):
  103. if self.value == other.value:
  104. return self.vm.qid < other.vm.qid
  105. else:
  106. return self.value < other.value
  107. def __init__(self, vm, parent=None):
  108. icon_path = self.get_vm_icon_path(vm)
  109. super (VmLabelWidget, self).__init__(icon_path, True, 0.8, None, parent)
  110. self.vm = vm
  111. self.tableItem = self.VmLabelItem(self.value, vm)
  112. def get_vm_icon_path(self, vm):
  113. if vm.qid == 0:
  114. self.value = 100
  115. return ":/off.png"
  116. else:
  117. self.value = vm.label.index
  118. return vm.label.icon
  119. class VmNameItem (QTableWidgetItem):
  120. def __init__(self, vm):
  121. super(VmNameItem, self).__init__()
  122. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  123. self.setText(vm.name)
  124. self.setTextAlignment(Qt.AlignVCenter)
  125. self.qid = vm.qid
  126. class VmStatusIcon(QLabel):
  127. def __init__(self, vm, parent=None):
  128. super (VmStatusIcon, self).__init__(parent)
  129. self.vm = vm
  130. self.set_on_icon()
  131. self.previous_power_state = vm.last_power_state
  132. def update(self):
  133. if self.previous_power_state != self.vm.last_power_state:
  134. self.set_on_icon()
  135. self.previous_power_state = self.vm.last_power_state
  136. def set_on_icon(self):
  137. if self.vm.last_power_state == "Running":
  138. icon = QIcon (":/on.png")
  139. elif self.vm.last_power_state in ["Paused"]:
  140. icon = QIcon (":/paused.png")
  141. elif self.vm.last_power_state in ["Transient", "Halting", "Dying"]:
  142. icon = QIcon (":/transient.png")
  143. else:
  144. icon = QIcon (":/off.png")
  145. icon_sz = QSize (row_height * 0.5, row_height *0.5)
  146. icon_pixmap = icon.pixmap(icon_sz)
  147. self.setPixmap (icon_pixmap)
  148. self.setFixedSize (icon_sz)
  149. class VmInfoWidget (QWidget):
  150. class VmInfoItem (QTableWidgetItem):
  151. def __init__(self, upd_info_item, vm):
  152. super(VmInfoWidget.VmInfoItem, self).__init__()
  153. self.upd_info_item = upd_info_item
  154. self.vm = vm
  155. def __lt__(self, other):
  156. self_val = self.upd_info_item.value
  157. other_val = other.upd_info_item.value
  158. if self.tableWidget().horizontalHeader().sortIndicatorOrder() == update_order:
  159. # the result will be sorted by upd, sorting order: Ascending
  160. self_val += 1 if self.vm.is_running() else 0
  161. other_val += 1 if other.vm.is_running() else 0
  162. if self_val == other_val:
  163. return self.vm.qid < other.vm.qid
  164. else:
  165. return self_val > other_val
  166. elif self.tableWidget().horizontalHeader().sortIndicatorOrder() == power_order:
  167. #the result will be sorted by power state, sorting order: Descending
  168. self_val = -(self_val/10 + 10*(1 if self.vm.is_running() else 0))
  169. other_val = -(other_val/10 + 10*(1 if other.vm.is_running() else 0))
  170. if self_val == other_val:
  171. return self.vm.qid < other.vm.qid
  172. else:
  173. return self_val > other_val
  174. else:
  175. #it would be strange if this happened
  176. return
  177. def __init__(self, vm, parent = None):
  178. super (VmInfoWidget, self).__init__(parent)
  179. self.vm = vm
  180. layout = QHBoxLayout ()
  181. self.on_icon = VmStatusIcon(vm)
  182. self.upd_info = VmUpdateInfoWidget(vm, show_text=False)
  183. self.error_icon = VmIconWidget(":/warning.png")
  184. self.blk_icon = VmIconWidget(":/mount.png")
  185. self.rec_icon = VmIconWidget(":/mic.png")
  186. layout.addWidget(self.on_icon)
  187. layout.addWidget(self.upd_info)
  188. layout.addWidget(self.error_icon)
  189. layout.addItem(QSpacerItem(0, 10, QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
  190. layout.addWidget(self.blk_icon)
  191. layout.addWidget(self.rec_icon)
  192. layout.setContentsMargins(5,0,5,0)
  193. self.setLayout(layout)
  194. self.rec_icon.setVisible(False)
  195. self.blk_icon.setVisible(False)
  196. self.error_icon.setVisible(False)
  197. self.tableItem = self.VmInfoItem(self.upd_info.tableItem, vm)
  198. def update_vm_state(self, vm, blk_visible, rec_visible=None):
  199. self.on_icon.update()
  200. self.upd_info.update_outdated(vm)
  201. if blk_visible != None:
  202. self.blk_icon.setVisible(blk_visible)
  203. if rec_visible != None:
  204. self.rec_icon.setVisible(rec_visible)
  205. self.error_icon.setToolTip(vm.qubes_manager_state[main.QMVmState
  206. .ErrorMsg])
  207. self.error_icon.setVisible(vm.qubes_manager_state[main.QMVmState
  208. .ErrorMsg] is not None)
  209. class VmTemplateItem (QTableWidgetItem):
  210. def __init__(self, vm):
  211. super(VmTemplateItem, self).__init__()
  212. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  213. self.vm = vm
  214. if vm.template is not None:
  215. self.setText(vm.template.name)
  216. else:
  217. font = QFont()
  218. font.setStyle(QFont.StyleItalic)
  219. self.setFont(font)
  220. self.setTextColor(QColor("gray"))
  221. if vm.is_appvm(): # and vm.template is None
  222. self.setText("StandaloneVM")
  223. elif vm.is_template():
  224. self.setText("TemplateVM")
  225. elif vm.qid == 0:
  226. self.setText("AdminVM")
  227. elif vm.is_netvm():
  228. self.setText("NetVM")
  229. else:
  230. self.setText("---")
  231. self.setTextAlignment(Qt.AlignVCenter)
  232. def __lt__(self, other):
  233. if self.text() == other.text():
  234. return self.vm.qid < other.vm.qid
  235. else:
  236. return super(VmTemplateItem, self).__lt__(other)
  237. class VmNetvmItem (QTableWidgetItem):
  238. def __init__(self, vm):
  239. super(VmNetvmItem, self).__init__()
  240. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  241. self.vm = vm
  242. if vm.is_netvm() and not vm.is_proxyvm():
  243. self.setText("n/a")
  244. elif vm.netvm is not None:
  245. self.setText(vm.netvm.name)
  246. else:
  247. self.setText("---")
  248. self.setTextAlignment(Qt.AlignVCenter)
  249. def __lt__(self, other):
  250. if self.text() == other.text():
  251. return self.vm.qid < other.vm.qid
  252. else:
  253. return super(VmNetvmItem, self).__lt__(other)
  254. class VmInternalItem(QTableWidgetItem):
  255. def __init__(self, vm):
  256. super(VmInternalItem, self).__init__()
  257. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  258. self.internal = vm.internal
  259. if self.internal:
  260. self.setText("Yes")
  261. else:
  262. self.setText("")
  263. class VmUsageBarWidget (QWidget):
  264. class VmUsageBarItem (QTableWidgetItem):
  265. def __init__(self, value, vm):
  266. super(VmUsageBarWidget.VmUsageBarItem, self).__init__()
  267. self.value = value
  268. self.vm = vm
  269. def set_value(self, value):
  270. self.value = value
  271. def __lt__(self, other):
  272. if self.value == other.value:
  273. return self.vm.qid < other.vm.qid
  274. else:
  275. return int(self.value) < int(other.value)
  276. def __init__(self, min, max, format, update_func, vm, load, hue=210, parent = None):
  277. super (VmUsageBarWidget, self).__init__(parent)
  278. self.min = min
  279. self.max = max
  280. self.update_func = update_func
  281. self.value = min
  282. self.widget = QProgressBar()
  283. self.widget.setMinimum(min)
  284. self.widget.setMaximum(max)
  285. self.widget.setFormat(format)
  286. self.widget.setStyleSheet(
  287. "QProgressBar:horizontal{" +\
  288. "border: 1px solid hsv({0}, 100, 250);".format(hue) +\
  289. "border-radius: 4px;\
  290. background: white;\
  291. text-align: center;\
  292. }\
  293. QProgressBar::chunk:horizontal {\
  294. background: qlineargradient(x1: 0, y1: 0.5, x2: 1, y2: 0.5, " +\
  295. "stop: 0 hsv({0}, 170, 207),".format(hue) +
  296. " stop: 1 white); \
  297. }"
  298. )
  299. layout = QHBoxLayout()
  300. layout.addWidget(self.widget)
  301. self.setLayout(layout)
  302. self.tableItem = self.VmUsageBarItem(min, vm)
  303. self.update_load(vm, load)
  304. def update_load(self, vm, load):
  305. self.value = self.update_func(vm, load)
  306. self.widget.setValue(self.value)
  307. self.tableItem.set_value(self.value)
  308. class ChartWidget (QWidget):
  309. class ChartItem (QTableWidgetItem):
  310. def __init__(self, value, vm):
  311. super(ChartWidget.ChartItem, self).__init__()
  312. self.value = value
  313. self.vm = vm
  314. def set_value(self, value):
  315. self.value = value
  316. def __lt__(self, other):
  317. if self.value == other.value:
  318. return self.vm.qid < other.vm.qid
  319. else:
  320. return self.value < other.value
  321. def __init__(self, vm, update_func, hue, load = 0, parent = None):
  322. super (ChartWidget, self).__init__(parent)
  323. self.update_func = update_func
  324. self.hue = hue
  325. if hue < 0 or hue > 255:
  326. self.hue = 255
  327. self.load = load
  328. assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
  329. self.load_history = [self.load]
  330. self.tableItem = ChartWidget.ChartItem(self.load, vm)
  331. def update_load (self, vm, load):
  332. self.load = self.update_func(vm, load)
  333. assert self.load >= 0, "load = {0}".format(self.load)
  334. # assert self.load >= 0 and self.load <= 100, "load = {0}".format(self.load)
  335. if self.load > 100:
  336. # FIXME: This is an ugly workaround for cpu_load:/
  337. self.load = 100
  338. self.load_history.append (self.load)
  339. self.tableItem.set_value(self.load)
  340. self.repaint()
  341. def paintEvent (self, Event = None):
  342. p = QPainter (self)
  343. dx = 4
  344. W = self.width()
  345. H = self.height() - 5
  346. N = len(self.load_history)
  347. if N > W/dx:
  348. tail = N - W/dx
  349. N = W/dx
  350. self.load_history = self.load_history[tail:]
  351. assert len(self.load_history) == N
  352. for i in range (0, N-1):
  353. val = self.load_history[N- i - 1]
  354. sat = 70 + val*(255-70)/100
  355. color = QColor.fromHsv (self.hue, sat, 255)
  356. pen = QPen (color)
  357. pen.setWidth(dx-1)
  358. p.setPen(pen)
  359. if val > 0:
  360. p.drawLine (W - i*dx - dx, H , W - i*dx - dx, H - (H - 5) * val/100)
  361. class VmUpdateInfoWidget(QWidget):
  362. class VmUpdateInfoItem (QTableWidgetItem):
  363. def __init__(self, value, vm):
  364. super(VmUpdateInfoWidget.VmUpdateInfoItem, self).__init__()
  365. self.value = 0
  366. self.vm = vm
  367. self.set_value(value)
  368. def set_value(self, value):
  369. if value in ("outdated", "to-be-outdated"):
  370. self.value = 30
  371. elif value == "update":
  372. self.value = 20
  373. else:
  374. self.value = 0
  375. def __lt__(self, other):
  376. if self.value == other.value:
  377. return self.vm.qid < other.vm.qid
  378. else:
  379. return self.value < other.value
  380. def __init__(self, vm, show_text=True, parent = None):
  381. super (VmUpdateInfoWidget, self).__init__(parent)
  382. layout = QHBoxLayout ()
  383. self.show_text = show_text
  384. if self.show_text:
  385. self.label=QLabel("")
  386. layout.addWidget(self.label, alignment=Qt.AlignCenter)
  387. else:
  388. self.icon = QLabel("")
  389. layout.addWidget(self.icon, alignment=Qt.AlignCenter)
  390. self.setLayout(layout)
  391. self.previous_outdated_state = None
  392. self.previous_update_recommended = None
  393. self.value = None
  394. self.tableItem = VmUpdateInfoWidget.VmUpdateInfoItem(self.value, vm)
  395. def update_outdated(self, vm):
  396. if vm.type == "HVM":
  397. return
  398. if vm.is_outdated():
  399. outdated_state = "outdated"
  400. # During TemplateVM shutdown, there's an interval of a few seconds
  401. # during which vm.template.is_running() returns false but
  402. # vm.is_outdated() does not yet return true, so the icon disappears.
  403. # This looks goofy, but we've decided not to fix it at this time
  404. # (2015-02-09).
  405. elif vm.template and vm.template.is_running():
  406. outdated_state = "to-be-outdated"
  407. else:
  408. outdated_state = None
  409. if outdated_state != self.previous_outdated_state:
  410. self.update_status_widget(outdated_state)
  411. self.previous_outdated_state = outdated_state
  412. if not vm.is_updateable():
  413. return
  414. if vm.qid == 0:
  415. update_recommended = self.previous_update_recommended
  416. if os.path.exists(qubes_dom0_updates_stat_file):
  417. update_recommended = True
  418. else:
  419. update_recommended = False
  420. else:
  421. update_recommended = self.previous_update_recommended
  422. stat_file_path = vm.dir_path + '/' + vm_files["updates_stat_file"]
  423. if not os.path.exists(stat_file_path):
  424. update_recommended = False
  425. else:
  426. if (not hasattr(vm, "updates_stat_file_read_time")) or vm.updates_stat_file_read_time <= os.path.getmtime(stat_file_path):
  427. stat_file = open(stat_file_path, "r")
  428. updates = stat_file.read().strip()
  429. stat_file.close()
  430. if updates.isdigit():
  431. updates = int(updates)
  432. else:
  433. updates = 0
  434. if updates == 0:
  435. update_recommended = False
  436. else:
  437. update_recommended = True
  438. vm.updates_stat_file_read_time = time.time()
  439. if update_recommended and not self.previous_update_recommended:
  440. self.update_status_widget("update")
  441. elif self.previous_update_recommended and not update_recommended:
  442. self.update_status_widget(None)
  443. self.previous_update_recommended = update_recommended
  444. def update_status_widget(self, state):
  445. self.value = state
  446. self.tableItem.set_value(state)
  447. if state == "update":
  448. label_text = "<font color=\"#CCCC00\">Check updates</font>"
  449. icon_path = ":/update-recommended.png"
  450. tooltip_text = "Updates pending!"
  451. elif state == "outdated":
  452. label_text = "<font color=\"red\">VM outdated</font>"
  453. icon_path = ":/outdated.png"
  454. tooltip_text = "The VM must be restarted for its filesystem to reflect the template's recent committed changes."
  455. elif state == "to-be-outdated":
  456. label_text = "<font color=\"#800000\">TemplateVM running</font>"
  457. icon_path = ":/to-be-outdated.png"
  458. tooltip_text = "The TemplateVM must be stopped before changes from its current session can be picked up by this VM."
  459. elif state == None:
  460. label_text = ""
  461. icon_path = None
  462. tooltip_text = None
  463. if self.show_text:
  464. self.label.setText(label_text)
  465. else:
  466. self.layout().removeWidget(self.icon)
  467. self.icon.deleteLater()
  468. if icon_path != None:
  469. self.icon = VmIconWidget(icon_path, True, 0.7)
  470. self.icon.setToolTip(tooltip_text)
  471. else:
  472. self.icon = QLabel(label_text)
  473. self.layout().addWidget(self.icon, alignment=Qt.AlignCenter)
  474. class VmSizeOnDiskItem (QTableWidgetItem):
  475. def __init__(self, vm):
  476. super(VmSizeOnDiskItem, self).__init__()
  477. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  478. self.vm = vm
  479. self.value = 0
  480. self.update()
  481. self.setTextAlignment(Qt.AlignVCenter)
  482. def update(self):
  483. if self.vm.qid == 0:
  484. self.setText("n/a")
  485. else:
  486. self.value = self.vm.get_disk_utilization()/(1024*1024)
  487. self.setText( str(self.value) + " MiB")
  488. def __lt__(self, other):
  489. if self.value == other.value:
  490. return self.vm.qid < other.vm.qid
  491. else:
  492. return self.value < other.value
  493. class VmIPItem(QTableWidgetItem):
  494. def __init__(self, vm):
  495. super(VmIPItem, self).__init__()
  496. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  497. self.ip = vm.ip
  498. if self.ip:
  499. self.setText(self.ip)
  500. else:
  501. self.setText("n/a")
  502. class VmIncludeInBackupsItem(QTableWidgetItem):
  503. def __init__(self, vm):
  504. super(VmIncludeInBackupsItem, self).__init__()
  505. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  506. self.vm = vm
  507. if self.vm.include_in_backups:
  508. self.setText("Yes")
  509. else:
  510. self.setText("")
  511. def __lt__(self, other):
  512. if self.vm.include_in_backups == other.vm.include_in_backups:
  513. return self.vm.qid < other.vm.qid
  514. else:
  515. return self.vm.include_in_backups < other.vm.include_in_backups
  516. class VmLastBackupItem(QTableWidgetItem):
  517. def __init__(self, vm):
  518. super(VmLastBackupItem, self).__init__()
  519. self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
  520. self.vm = vm
  521. if self.vm.backup_timestamp:
  522. self.setText(str(self.vm.backup_timestamp.date()))
  523. else:
  524. self.setText("")
  525. def __lt__(self, other):
  526. if self.vm.backup_timestamp == other.vm.backup_timestamp:
  527. return self.vm.qid < other.vm.qid
  528. elif not self.vm.backup_timestamp:
  529. return False
  530. elif not other.vm.backup_timestamp:
  531. return True
  532. else:
  533. return self.vm.backup_timestamp < other.vm.backup_timestamp