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