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