e6e4fb41d4e597106dff91a574966771085d7a4d
[sface.git] / sface / screens / mainscreen.py
1
2 import datetime
3 import os
4 from PyQt4.QtCore import *
5 from PyQt4.QtGui import *
6
7 #from sfa.util.rspecHelper import RSpec
8 from sfa.rspecs.rspec_parser import parse_rspec
9 from sface.config import config
10 from sface.sfirenew import SfiRenewer
11 from sface.sfiprocess import SfiProcess
12 from sface.screens.sfascreen import SfaScreen
13
14 already_in_nodes = []
15
16 node_status = { "in": "Already Selected",
17                 "out": "Not Selected",
18                 "add": "To be Added",
19                 "remove": "To be Removed"}
20
21 tag_status = { "in": "Already Set",
22                 "out": "Not Set",
23                 "add": "To be Added",
24                 "remove": "To be Removed"}
25
26 default_tags = "Default tags"
27 settable_tags = ['delegations', 'initscript']
28
29 NAME_COLUMN = 0
30 NODE_STATUS_COLUMN = 1
31 MEMBERSHIP_STATUS_COLUMN = 2
32 KIND_COLUMN = 3
33
34 # maximum length of a name to display before clipping
35 NAME_MAX_LEN = 48
36
37 def itemType(index):
38     if index.parent().parent().isValid():
39         return "tag"
40     else:
41         return "node"
42
43
44 class NodeView(QTreeView):
45     def __init__(self, parent):
46         QTreeView.__init__(self, parent)
47
48         self.setAnimated(True)
49         self.setItemsExpandable(True)
50         self.setRootIsDecorated(True)
51         self.setAlternatingRowColors(True)
52 #        self.setSelectionMode(self.MultiSelection)
53         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
54         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
55         self.setToolTip("Double click on a row to change its status.  Right click on a host to add a tag.")
56
57     def mouseDoubleClickEvent(self, event):
58         index = self.currentIndex()
59         model = index.model()
60         status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
61         status_data = status_index.data().toString()
62         node_index = model.index(index.row(), NAME_COLUMN, index.parent())
63         node_data = node_index.data().toString()
64
65         if itemType(node_index) == "tag":
66             data = node_index.data().toString()
67             tagname, value = data.split(": ")
68             if tagname not in settable_tags:
69                 # Pop up error msg
70                 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
71                 return
72             if status_data == tag_status['in']:
73                 model.setData(status_index, QString(tag_status['remove']))
74             elif status_data == tag_status['add']:
75                 model.setData(status_index, QString(tag_status['out']))
76             elif status_data == tag_status['remove']:
77                 model.setData(status_index, QString(tag_status['in']))
78             else: model.setData(status_index, QString(node_status['out']))
79         else:
80             # This is a hostname
81             if status_data == node_status['in']:
82                 model.setData(status_index, QString(node_status['remove']))
83             elif status_data == node_status['out']:
84                 model.setData(status_index, QString(node_status['add']))
85             elif status_data in (node_status['add'], node_status['remove']):
86                 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
87                 else: model.setData(status_index, QString(node_status['out']))
88
89         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
90
91     def mousePressEvent(self, event):
92         QTreeView.mousePressEvent(self, event)
93         if event.button() == Qt.LeftButton:
94             return
95
96         # Right click
97         index = self.currentIndex()
98         model = index.model()
99         status_index = model.index(index.row(), 1, index.parent())
100         status_data = status_index.data().toString()
101         node_index = model.index(index.row(), 0, index.parent())
102         node_data = node_index.data().toString()
103
104         if itemType(node_index) == "node":
105             # This is a hostname
106             if status_data in (node_status['in'], node_status['add'], ""):
107                 # Pop up a dialog box for adding a new attribute
108                 tagname, ok = QInputDialog.getItem(self, "Add tag",
109                                                    "Tag name:", settable_tags)
110                 if ok:
111                     value, ok = QInputDialog.getText(self, "Add tag",
112                                                      "Value for tag '%s'" % tagname)
113                     if ok:
114                         # Add a new row to the model for the tag
115
116                         # For testing with the QStandardItemModel
117                         #nodeItem = model.itemFromIndex(index)
118                         #tagstring = QString("%s: %s" % (tagname, value))
119                         #tagItem = QStandardItem(tagstring)
120                         #status = QStandardItem(QString(tag_status['add']))
121                         #nodeItem.appendRow([tagItem, status])
122
123                         # We're using the QSortFilterProxyModel here
124                         src_index = model.mapToSource(index)
125                         src_model = src_index.model()
126                         nodeItem = src_model.itemFromIndex(src_index)
127                         tagstring = QString("%s: %s" % (tagname, value))
128                         tagItem = QStandardItem(tagstring)
129                         status = QStandardItem(QString(tag_status['add']))
130                         nodeItem.appendRow([tagItem, status])
131
132             elif status_data in (node_status['out'], node_status['remove']):
133                 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
134                 return
135
136         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
137
138     def currentChanged(self, current, previous):
139         model = current.model()
140         node_index = model.index(current.row(), 0, current.parent())
141         node_data = node_index.data().toString()
142         self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
143         
144                 
145
146 class NodeNameDelegate(QStyledItemDelegate):
147     def __init__(self, parent):
148         QStyledItemDelegate.__init__(self, parent)
149
150     def displayText(self, value, locale):
151         data = str(QStyledItemDelegate.displayText(self, value, locale))
152         if (len(data)>NAME_MAX_LEN):
153             data = data[:(NAME_MAX_LEN-3)] + "..."
154         return QString(data)
155
156     def paint(self, painter, option, index):
157         model = index.model()
158         data = str(self.displayText(index.data(), QLocale()))
159         status_index = model.index(index.row(), MEMBERSHIP_STATUS_COLUMN, index.parent())
160         status_data = status_index.data().toString()
161
162         fm = QFontMetrics(option.font)
163         rect = QRect(option.rect)
164
165         rect.setHeight(rect.height() - 2)
166         rect.setWidth(fm.width(QString(data)) + 6)
167         rect.setX(rect.x() + 5)
168         rect.setY(rect.y() - 1)
169
170         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
171
172         path = QPainterPath()
173         path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
174
175         painter.save()
176         painter.setRenderHint(QPainter.Antialiasing)
177
178         if option.state & QStyle.State_Selected:
179             painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
180
181         if itemType(index) == "node":
182             if status_data == node_status['in']: # already in the slice
183                 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
184                 painter.setPen(QColor.fromRgb(0, 0, 0))
185                 painter.drawText(rect, 0, QString(data))
186
187             elif status_data == node_status['add']: # newly added to the slice
188                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
189                 painter.setPen(QColor.fromRgb(0, 0, 0))
190                 painter.drawText(rect, 0, QString(data))
191
192             elif status_data == node_status['remove']: # removed from the slice
193                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
194                 painter.setPen(QColor.fromRgb(0, 0, 0))
195                 painter.drawText(rect, 0, QString(data))
196
197             else:
198                 painter.setPen(QColor.fromRgb(0, 0, 0))
199                 painter.drawText(rect, 0, QString(data))
200
201         else:
202             if status_data == tag_status['in']: # already in the slice
203                 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
204                 painter.setPen(QColor.fromRgb(0, 0, 0))
205                 painter.drawText(rect, 0, QString(data))
206
207             elif status_data == tag_status['add']: # newly added to the slice
208                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
209                 painter.setPen(QColor.fromRgb(0, 0, 0))
210                 painter.drawText(rect, 0, QString(data))
211
212             elif status_data == tag_status['remove']: # removed from the slice
213                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
214                 painter.setPen(QColor.fromRgb(0, 0, 0))
215                 painter.drawText(rect, 0, QString(data))
216
217             else:
218                 painter.setPen(QColor.fromRgb(0, 0, 0))
219                 painter.drawText(rect, 0, QString(data))
220
221         painter.restore()
222
223 class NodeStatusDelegate(QStyledItemDelegate):
224     def __init__(self, parent):
225         QStyledItemDelegate.__init__(self, parent)
226
227     def paint(self, painter, option, index):
228         model = index.model()
229         nodestatus_index = model.index(index.row(), NODE_STATUS_COLUMN, index.parent())
230         nodestatus_data = nodestatus_index.data().toString()
231
232         fm = QFontMetrics(option.font)
233         rect = QRect(option.rect)
234
235         data = index.data().toString()
236         rect.setHeight(rect.height() - 2)
237         rect.setWidth(fm.width(QString(data)) + 6)
238         rect.setX(rect.x() + 5)
239         rect.setY(rect.y() - 1)
240
241         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
242
243         path = QPainterPath()
244         path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
245
246         painter.save()
247         painter.setRenderHint(QPainter.Antialiasing)
248
249         if option.state & QStyle.State_Selected:
250             painter.fillRect(option.rect, option.palette.color(QPalette.Active, QPalette.Highlight))
251
252         if (nodestatus_data == ""):
253                 painter.setPen(QColor.fromRgb(0, 0, 0))
254                 painter.drawText(rect, 0, QString(data))
255         elif (nodestatus_data == "boot"):
256                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
257                 painter.setPen(QColor.fromRgb(0, 0, 0))
258                 painter.drawText(rect, 0, QString(data))
259         else:
260                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
261                 painter.setPen(QColor.fromRgb(0, 0, 0))
262                 painter.drawText(rect, 0, QString(data))
263
264         painter.restore()
265
266 class NodeFilterProxyModel(QSortFilterProxyModel):
267     def __init__(self, parent=None):
268         QSortFilterProxyModel.__init__(self, parent)
269         self.hostname_filter_regex = None
270         self.nodestatus_filter = None
271
272     def setHostNameFilter(self, hostname):
273         self.hostname_filter_regex = QRegExp(hostname)
274         self.invalidateFilter()
275
276     def setNodeStatusFilter(self, status):
277         if (status == "all"):
278             self.nodestatus_filter = None
279         else:
280             self.nodestatus_filter = status
281         self.invalidateFilter()
282
283     def filterAcceptsRow(self, sourceRow, source_parent):
284         kind_data = self.sourceModel().index(sourceRow, KIND_COLUMN, source_parent).data().toString()
285         if (kind_data == "node"):
286             if self.hostname_filter_regex:
287                 name_data = self.sourceModel().index(sourceRow, NAME_COLUMN, source_parent).data().toString()
288                 if (self.hostname_filter_regex.indexIn(name_data) < 0):
289                     return False
290             if self.nodestatus_filter:
291                 nodestatus_data = self.sourceModel().index(sourceRow, NODE_STATUS_COLUMN, source_parent).data().toString()
292                 if (nodestatus_data != self.nodestatus_filter):
293                     return False
294         return True
295
296 class SliceWidget(QWidget):
297     def __init__(self, parent):
298         QWidget.__init__(self, parent)
299
300         self.network_names = []
301         self.process = SfiProcess(self)
302
303         self.slicename = QLabel("", self)
304         self.updateSliceName()
305         self.slicename.setScaledContents(False)
306         filterlabel = QLabel ("Filter: ", self)
307         filterbox = QComboBox(self)
308         filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
309         searchlabel = QLabel ("Search: ", self)
310         searchlabel.setScaledContents(False)
311         searchbox = QLineEdit(self)
312         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
313
314         toplayout = QHBoxLayout()
315         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
316         toplayout.addStretch()
317         toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
318         toplayout.addWidget(filterbox, 0, Qt.AlignRight)
319         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
320         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
321
322         self.nodeView = NodeView(self)
323         self.nodeModel = QStandardItemModel(0, 4, self)
324         self.filterModel = NodeFilterProxyModel(self)
325
326         self.nodeNameDelegate = NodeNameDelegate(self)
327         self.nodeStatusDelegate = NodeStatusDelegate(self)
328
329         refresh = QPushButton("Refresh Slice Data", self)
330         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
331         renew = QPushButton("Renew Slice", self)
332         renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
333         submit = QPushButton("Submit", self)
334         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
335
336         bottomlayout = QHBoxLayout()
337         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
338         bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
339         bottomlayout.addStretch()
340         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
341
342         layout = QVBoxLayout()
343         layout.addLayout(toplayout)
344         layout.addWidget(self.nodeView)
345         layout.addLayout(bottomlayout)
346         self.setLayout(layout)
347         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
348
349         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
350         self.connect(renew, SIGNAL('clicked()'), self.renew)
351         self.connect(submit, SIGNAL('clicked()'), self.submit)
352         self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
353         self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
354         self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
355                      self.nodeSelectionChanged)
356
357         self.updateView()
358
359     def submitFinished(self):
360         self.setStatus("<font color='green'>Slice data submitted.</font>")
361         QTimer.singleShot(1000, self.refresh)
362
363     def refreshFinished(self):
364         self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
365         self.updateView()
366         self.parent().signalAll("rspecUpdated")
367
368     def readSliceRSpec(self):
369         rspec_file = config.getSliceRSpecFile()
370         if os.path.exists(rspec_file):
371             xml = open(rspec_file).read()
372             return parse_rspec(xml)
373         return None
374
375     def setStatus(self, msg, timeout=None):
376         self.parent().setStatus(msg, timeout)
377
378     def checkRunningProcess(self):
379         if self.process.isRunning():
380             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
381             return True
382         return False
383
384     def search(self, search_string):
385         self.filterModel.setHostNameFilter(str(search_string))
386
387     def filter(self, filter_string):
388         self.filterModel.setNodeStatusFilter(str(filter_string))
389
390     def itemStatus(self, item):
391         statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
392         return statusItem.data(Qt.DisplayRole).toString()
393
394     def itemText(self, item):
395         return item.data(Qt.DisplayRole).toString()
396
397     # Recursively walk the tree, making changes to the RSpec
398     def process_subtree(self, rspec, item, depth = 0):
399         change = False
400         model = self.nodeModel
401
402         if depth in [0, 1]:
403             pass
404         elif depth == 2: # Hostname
405             hostname = self.itemText(item)
406             testbed = self.itemText(item.parent())
407             status = self.itemStatus(item)
408             if status == node_status['add']:
409                 print "Add hostname: %s" % hostname
410                 rspec.add_slivers(str(hostname), testbed)
411                 change = True
412             elif status == node_status['remove']:
413                 print "Remove hostname: %s" % hostname
414                 rspec.remove_slivers(str(hostname), testbed)
415                 change = True
416         elif depth == 3: # Tag
417             tag, value = self.itemText(item).split(": ")
418             status = self.itemStatus(item)
419             tag = "%s" % tag     # Prevent weird error from lxml
420             value = "%s" % value # Prevent weird error from lxml
421             node = self.itemText(item.parent())
422             testbed = self.itemText(item.parent().parent())
423             if status == tag_status['add']:
424                 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
425                 if node.startsWith(default_tags):
426                     rspec.add_default_sliver_attribute(tag, value, testbed)
427                 else:
428                     rspec.add_sliver_attribute(node, tag, value, testbed)
429                 change = True
430             elif status == tag_status['remove']:
431                 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
432                 if node.startsWith(default_tags):
433                     rspec.remove_default_sliver_attribute(tag, value, testbed)
434                 else:
435                     rspec.remove_sliver_attribute(node, tag, value, testbed)
436                 change = True
437
438         children = item.rowCount()
439         for row in range(0, children):
440             status = self.process_subtree(rspec, item.child(row), depth + 1)
441             change = change or status
442
443         return change
444
445     def submit(self):
446         if self.checkRunningProcess():
447             return
448
449         rspec = self.readSliceRSpec()
450         change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
451
452         if not change:
453             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
454             return
455
456         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
457         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
458
459         self.process.applyRSpec(rspec)
460         self.setStatus("Sending slice data (RSpec). This will take some time...")
461
462     def renew(self):
463         dlg = RenewWindow(parent=self)
464         if (dlg.exec_() == QDialog.Accepted):
465             self.setStatus("Renewing Slice.")
466
467             self.renewProcess = SfiRenewer(config.getSlice(), dlg.get_new_expiration(), self)
468             self.connect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
469
470     def renewFinished(self):
471         if self.renewProcess.statusMsg:
472             self.setStatus("Renew " + self.renewProcess.status + ": " + self.renewProcess.statusMsg)
473         else:
474             self.setStatus("Renew " + self.renewProcess.status)
475         self.disconnect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
476         self.renewProcess = None
477
478     def refresh(self):
479         if not config.getSlice():
480             self.setStatus("<font color='red'>Slice not set yet!</font>")
481             return
482
483         if self.process.isRunning():
484             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
485             return
486
487         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
488         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
489
490         self.process.getRSpecFromSM()
491         self.setStatus("Refreshing slice data. This will take some time...")
492
493     def updateView(self):
494         global already_in_nodes
495         already_in_nodes = []
496         self.network_names = []
497         self.nodeModel.clear()
498
499         rspec = self.readSliceRSpec()
500         if not rspec:
501             return None
502
503         rootItem = self.nodeModel.invisibleRootItem()
504         #networks = sorted(rspec.get_network_list())
505         networks = rspec.get_networks()
506         for network in networks:
507             self.network_names.append(network)
508
509             #all_nodes = rspec.get_node_list(network)
510             #sliver_nodes = rspec.get_sliver_list(network)
511             all_nodes = rspec.get_nodes(network)
512             sliver_nodes = rspec.get_nodes_with_slivers(network)
513             available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
514
515             networkItem = QStandardItem(QString(network))
516             msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
517             rootItem.appendRow([networkItem, QStandardItem(QString("")), QStandardItem(QString(msg)), QStandardItem(QString("network"))])
518
519             already_in_nodes += sliver_nodes
520
521             # Add default slice tags
522             nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
523             statusItem = QStandardItem(QString(""))
524             nodeStatus = QStandardItem(QString(""))
525             networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("defaults"))])
526             attrs = rspec.get_default_sliver_attributes(network)
527             for (name, value) in attrs:
528                     tagstring = QString("%s: %s" % (name, value))
529                     tagItem = QStandardItem(tagstring)
530                     status = QStandardItem(QString(tag_status['in']))
531                     nodeStatus = QStandardItem(QString(""))
532                     nodeItem.appendRow([tagItem, nodeStatus, status, QStandardItem(QString("attribute"))])
533
534             for node in sliver_nodes:
535                 nodeItem = QStandardItem(QString(node))
536                 statusItem = QStandardItem(QString(node_status['in']))
537                 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
538                 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
539
540                 attrs = rspec.get_sliver_attributes(node, network)
541                 for (name, value) in attrs:
542                     tagstring = QString("%s: %s" % (name, value))
543                     tagItem = QStandardItem(tagstring)
544                     statusItem = QStandardItem(QString(tag_status['in']))
545                     nodeStatus = QStandardItem(QString(""))
546                     nodeItem.appendRow([tagItem, nodeStatus, statusItem, QStandardItem(QString("attribute"))])
547
548             for node in available_nodes:
549                 nodeItem = QStandardItem(QString(node))
550                 statusItem = QStandardItem(QString(node_status['out']))
551                 nodeStatus = QStandardItem(QString(rspec.get_node_element(node, network).attrib.get("boot_state","")))
552                 networkItem.appendRow([nodeItem, nodeStatus, statusItem, QStandardItem(QString("node"))])
553
554         self.filterModel.setSourceModel(self.nodeModel)
555         self.filterModel.setDynamicSortFilter(True)
556
557         headers = QStringList() << "Hostname or Tag" << "Node Status" << "Membership Status" << "Kind"
558         self.nodeModel.setHorizontalHeaderLabels(headers)
559
560         self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
561         self.nodeView.setItemDelegateForColumn(1, self.nodeStatusDelegate)
562         self.nodeView.setModel(self.filterModel)
563         self.nodeView.hideColumn(KIND_COLUMN)
564         self.nodeView.expandAll()
565         self.nodeView.resizeColumnToContents(0)
566         self.nodeView.collapseAll()
567
568     def updateSliceName(self):
569         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
570
571     def nodeSelectionChanged(self, hostname):
572         self.parent().nodeSelectionChanged(hostname)
573
574 class RenewWindow(QDialog):
575     def __init__(self, parent=None):
576         super(RenewWindow, self).__init__(parent)
577         self.setWindowTitle("Renew Slivers")
578
579         self.duration = QComboBox()
580
581         self.expirations = []
582
583         durations = ( (1, "One Week"), (2, "Two Weeks"), (3, "Three Weeks"), (4, "One Month") )
584
585         now = datetime.datetime.utcnow()
586         for (weeks, desc) in durations:
587             exp = now + datetime.timedelta(days = weeks * 7)
588             desc = desc + " " + exp.strftime("%Y-%m-%d %H:%M:%S")
589             self.expirations.append(exp)
590             self.duration.addItem(desc)
591
592         self.duration.setCurrentIndex(0)
593
594         buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
595         buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
596
597         layout = QVBoxLayout()
598         layout.addWidget(self.duration)
599         layout.addWidget(buttonBox)
600         self.setLayout(layout)
601
602         self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
603         self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
604
605     def accept(self):
606         QDialog.accept(self)
607
608     def get_new_expiration(self):
609         index = self.duration.currentIndex()
610         return self.expirations[index]
611
612 class MainScreen(SfaScreen):
613     def __init__(self, parent):
614         SfaScreen.__init__(self, parent)
615
616         slice = SliceWidget(self)
617         self.init(slice, "Nodes", "OneLab SFA crawler")
618
619     def rspecUpdated(self):
620         self.mainwin.rspecWindow.updateView()
621
622     def configurationChanged(self):
623         self.widget.updateSliceName()
624         self.widget.updateView()
625         self.mainwin.rspecWindow.updateView()
626
627     def nodeSelectionChanged(self, hostname):
628         self.mainwin.nodeSelectionChanged(hostname)