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