attribute stuff working
[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             hostname = 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, hostname, tag, value)
448                 if hostname.startswith(default_tags):
449                     rspec.version.add_default_sliver_attribute(tag, value, testbed)
450                 else:
451                     node = rspec_node_names.get(hostname, None)
452                     if node:
453                         rspec.version.add_sliver_attribute(node['component_id'], tag, value, testbed)
454                 change = True
455             elif status == tag_status['remove']:
456                 print "Remove tag from (%s, %s): %s/%s " % (testbed, hostname, tag, value)
457                 if hostname.startswith(default_tags):
458                     rspec.version.remove_default_sliver_attribute(tag, value, testbed)
459                 else:
460                     node = rspec_node_names.get(hostname, None)
461                     if node:
462                         rspec.version.remove_sliver_attribute(node['component_id'], tag, value, testbed)
463                 change = True
464
465         children = item.rowCount()
466         for row in range(0, children):
467             status = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1)
468             change = change or status
469
470         return change
471
472     def submit(self):
473         if self.checkRunningProcess():
474             return
475
476         rspec = SfiData().getSliceRSpec()
477         resources = SfiData().getResourcesRSpec()
478
479         resource_node_names = self.nodesByName(resources.version.get_nodes())
480         rspec_node_names = self.nodesByName(rspec.version.get_nodes_with_slivers())
481
482         change = self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem())
483
484         if not change:
485             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
486             return
487
488         # Several aggregates have issues with the <statistics> section in the
489         # rspec, so make sure it's not there.
490         stats_elems = rspec.xml.xpath("//statistics")
491         if len(stats_elems)>0:
492             stats_elem = stats_elems[0]
493             parent = stats_elem.xpath("..")[0]
494             parent.remove(stats_elem)
495
496         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
497         self.process.applyRSpec(rspec)
498         self.setStatus("Sending slice data (RSpec). This will take some time...")
499
500     def submit_pg_compat(self):
501         if self.checkRunningProcess():
502             return
503
504         rspec = SfiData().getSliceRSpec()
505         resources = SfiData().getResourcesRSpec()
506         change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
507
508         if not change:
509             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
510             return
511
512         dlg = ClientSliceManager(self)
513         dlg.submit_pg_compat(rspec)
514         dlg.exec_()
515
516         self.setStatus("<font color='green'>Finished submitting. %d/%d aggs succeeded.</font>" %
517                       (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount))
518         QTimer.singleShot(2500, self.refresh)
519
520     def renew(self):
521         dlg = RenewWindow(parent=self)
522         dlg.exec_()
523
524     def refresh(self):
525         if not config.getSlice():
526             self.setStatus("<font color='red'>Slice not set yet!</font>")
527             return
528
529         if self.process.isRunning():
530             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
531             return
532
533         self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
534
535         self.process.retrieveResources()
536         self.setStatus("Refreshing resources. This will take some time...")
537
538     def nodesByNetwork(self, nodeList):
539         netDict = {}
540         for node in nodeList:
541             network_name = Xrn(node['component_manager_id']).get_hrn()
542             if network_name:
543                 net = netDict.get(network_name, [])
544                 if net == []:
545                     netDict[network_name] = net
546
547                 net.append(node)
548
549         return netDict
550
551     def nodesByName(self, nodeList, nameDict=None):
552         if nameDict==None:
553             nameDict = {}
554         for node in nodeList:
555             hostname = node.get("component_name", None)
556             if hostname and (not hostname in nameDict):
557                 nameDict[hostname] = node
558
559         return nameDict
560
561     def updateView(self):
562         global already_in_nodes
563         already_in_nodes = []
564         self.network_names = []
565         self.nodeModel.clear()
566
567         rspec = SfiData().getSliceRSpec()
568         if not rspec:
569             return None
570
571         resources = SfiData().getResourcesRSpec()
572         if not resources:
573             return None
574
575         rootItem = self.nodeModel.invisibleRootItem()
576
577         networks = []
578         for network in rspec.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         for network in resources.get_networks():
583             network_name = network.get("name", None)
584             if (network_name != None) and (not network_name in networks):
585                 networks.append(network_name)
586
587         resources_nodes = self.nodesByNetwork(resources.version.get_nodes())
588         rspec_nodes = self.nodesByNetwork(rspec.version.get_nodes_with_slivers())
589
590         for network in networks:
591             self.network_names.append(network)
592
593             all_nodes = resources_nodes.get(network, [])
594             sliver_nodes = rspec_nodes.get(network, [])
595
596             sliver_node_names = self.nodesByName(sliver_nodes)
597
598             available_nodes = [ node for node in all_nodes if node["component_name"] not in sliver_node_names ]
599
600             msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
601             networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network")
602
603             already_in_nodes += sliver_node_names.keys()
604
605             # Add default slice tags
606             nodeItem = self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults")
607             attrs = rspec.get_default_sliver_attributes(network)
608             for attr in attrs:
609                     name = attr.get("name", None)
610                     value = attr.get("value", None)
611                     tagstring = QString("%s: %s" % (name, value))
612                     self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute")
613
614             for node in sliver_nodes:
615                 nodeItem = self.nodeView.appendRow(networkItem,
616                                node["component_name"],
617                                nodeStatus=node.get("boot_state", ""),
618                                #nodeType=node.get("rspec.get_node_sliver_type(node, network),
619                                membership=node_status['in'],
620                                kind="node")
621
622                 attrs = rspec.get_sliver_attributes(node['component_id'], network)
623                 for attr in attrs:
624                     name = attr.get("name", None)
625                     value = attr.get("value", None)
626                     self.nodeView.appendRow(nodeItem,
627                                             "%s: %s" % (name, value),
628                                             membership=tag_status['in'],
629                                             kind="attribute")
630
631             for node in available_nodes:
632                 self.nodeView.appendRow(networkItem,
633                                node["component_name"],
634                                nodeStatus = node.get("boot_state", ""),
635                                #nodeType= resources.get_node_sliver_type(node, network),
636                                membership=node_status['out'],
637                                kind="node")
638
639         self.filterModel.setSourceModel(self.nodeModel)
640         self.filterModel.setDynamicSortFilter(True)
641         self.filterModel.sort(NAME_COLUMN)
642
643         headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind"
644         self.nodeModel.setHorizontalHeaderLabels(headers)
645
646         self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate)
647         self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate)
648         self.nodeView.setModel(self.filterModel)
649         self.nodeView.hideColumn(KIND_COLUMN)
650         self.nodeView.expandAll()
651         self.nodeView.resizeColumnToContents(0)
652         self.nodeView.collapseAll()
653
654     def updateSliceName(self):
655         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
656
657     def nodeSelectionChanged(self, hostname):
658         self.parent().nodeSelectionChanged(hostname)
659
660 class MainScreen(SfaScreen):
661     def __init__(self, parent):
662         SfaScreen.__init__(self, parent)
663
664         slice = SliceWidget(self)
665         self.init(slice, "Nodes", "OneLab SFA crawler")
666
667     def rspecUpdated(self):
668         self.mainwin.rspecWindow.updateView()
669
670     def configurationChanged(self):
671         self.widget.updateSliceName()
672         self.widget.updateView()
673         self.mainwin.rspecWindow.updateView()
674
675     def nodeSelectionChanged(self, hostname):
676         self.mainwin.nodeSelectionChanged(hostname)