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