make sure default slice tags are sorted above all other lines
[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     def lessThan(self, left, right):
291         l_str = str(left.data().toString())
292         r_str = str(right.data().toString())
293
294         # make sure default_tags appears before everything else
295         if l_str.startswith(default_tags):
296             return True
297
298         if r_str.startswith(default_tags):
299             return False
300
301         return (l_str < r_str)
302
303
304 class SliceWidget(QWidget):
305     def __init__(self, parent):
306         QWidget.__init__(self, parent)
307
308         self.network_names = []
309         self.process = SfiProcess(self)
310
311         self.slicename = QLabel("", self)
312         self.updateSliceName()
313         self.slicename.setScaledContents(False)
314         filterlabel = QLabel ("Filter: ", self)
315         filterbox = QComboBox(self)
316         filterbox.addItems(["all", "boot", "disabled", "reinstall", "safeboot"])
317         searchlabel = QLabel ("Search: ", self)
318         searchlabel.setScaledContents(False)
319         searchbox = QLineEdit(self)
320         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
321
322         toplayout = QHBoxLayout()
323         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
324         toplayout.addStretch()
325         toplayout.addWidget(filterlabel, 0, Qt.AlignRight)
326         toplayout.addWidget(filterbox, 0, Qt.AlignRight)
327         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
328         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
329
330         self.nodeView = NodeView(self)
331         self.nodeModel = QStandardItemModel(0, 4, self)
332         self.filterModel = NodeFilterProxyModel(self)
333
334         self.nodeNameDelegate = NodeNameDelegate(self)
335         self.nodeStatusDelegate = NodeStatusDelegate(self)
336
337         refresh = QPushButton("Refresh Slice Data", self)
338         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
339         renew = QPushButton("Renew Slice", self)
340         renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
341         submit = QPushButton("Submit", self)
342         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
343
344         bottomlayout = QHBoxLayout()
345         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
346         bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
347         bottomlayout.addStretch()
348         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
349
350         layout = QVBoxLayout()
351         layout.addLayout(toplayout)
352         layout.addWidget(self.nodeView)
353         layout.addLayout(bottomlayout)
354         self.setLayout(layout)
355         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
356
357         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
358         self.connect(renew, SIGNAL('clicked()'), self.renew)
359         self.connect(submit, SIGNAL('clicked()'), self.submit) # _pg_compat)
360         self.connect(searchbox, SIGNAL('textChanged(QString)'), self.search)
361         self.connect(filterbox, SIGNAL('currentIndexChanged(QString)'), self.filter)
362         self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
363                      self.nodeSelectionChanged)
364
365         self.updateView()
366
367     def submitFinished(self):
368         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
369
370         faultString = self.process.getFaultString()
371         if not faultString:
372             self.setStatus("<font color='green'>Slice data submitted.</font>")
373         else:
374             self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
375
376         self.updateView()
377         self.parent().signalAll("rspecUpdated")
378
379     def refreshResourcesFinished(self):
380         self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
381
382         faultString = self.process.getFaultString()
383         if not faultString:
384             self.setStatus("Refreshing slice RSpec.")
385             self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
386             self.process.retrieveRspec()
387         else:
388             self.setStatus("<font color='red'>Resources refresh failed: %s</font>" % (faultString))
389
390     def refreshRSpecFinished(self):
391         self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
392
393         faultString = self.process.getFaultString()
394         if not faultString:
395             self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
396         else:
397             self.setStatus("<font color='red'>Slice refresh failed: %s</font>" % (faultString))
398
399         self.updateView()
400         self.parent().signalAll("rspecUpdated")
401
402     def setStatus(self, msg, timeout=None):
403         self.parent().setStatus(msg, timeout)
404
405     def checkRunningProcess(self):
406         if self.process.isRunning():
407             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
408             return True
409         return False
410
411     def search(self, search_string):
412         self.filterModel.setHostNameFilter(str(search_string))
413
414     def filter(self, filter_string):
415         self.filterModel.setNodeStatusFilter(str(filter_string))
416
417     def itemStatus(self, item):
418         statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
419         return str(statusItem.data(Qt.DisplayRole).toString())
420
421     def itemText(self, item):
422         return str(item.data(Qt.DisplayRole).toString())
423
424     # Recursively walk the tree, making changes to the RSpec
425     def process_subtree(self, rspec, rspec_node_names, resource_node_names, item, depth = 0):
426         change = False
427         model = self.nodeModel
428
429         if depth in [0, 1]:
430             pass
431         elif depth == 2: # Hostname
432             hostname = self.itemText(item)
433             testbed = self.itemText(item.parent())
434             status = self.itemStatus(item)
435             if status == node_status['add']:
436                 print "Add hostname: %s" % hostname
437
438                 resource_node = resource_node_names.get(hostname, None)
439
440                 if resource_node==None:
441                     print "Error: Failed to find %s in resources rspec" % hostname
442                 else:
443                     if not (hostname in rspec_node_names):
444                         network_name = Xrn(resource_node['component_manager_id']).get_hrn()
445                         rspec.version.add_network(network_name)
446                         rspec.version.add_nodes([resource_node])
447                     rspec.version.add_slivers([str(hostname)])
448                     change = True
449             elif status == node_status['remove']:
450                 print "Remove hostname: %s" % hostname
451                 rspec.version.remove_slivers([str(hostname)])
452                 change = True
453         elif depth == 3: # Tag
454             tag, value = self.itemText(item).split(": ")
455             status = self.itemStatus(item)
456             tag = "%s" % tag     # Prevent weird error from lxml
457             value = "%s" % value # Prevent weird error from lxml
458             hostname = self.itemText(item.parent())
459             testbed = self.itemText(item.parent().parent())
460             if status == tag_status['add']:
461                 print "Add tag to (%s, %s): %s/%s " % (testbed, hostname, tag, value)
462                 if hostname.startswith(default_tags):
463                     rspec.version.add_default_sliver_attribute(tag, value, testbed)
464                 else:
465                     node = rspec_node_names.get(hostname, None)
466                     if node:
467                         rspec.version.add_sliver_attribute(node['component_id'], tag, value, testbed)
468                 change = True
469             elif status == tag_status['remove']:
470                 print "Remove tag from (%s, %s): %s/%s " % (testbed, hostname, tag, value)
471                 if hostname.startswith(default_tags):
472                     rspec.version.remove_default_sliver_attribute(tag, value, testbed)
473                 else:
474                     node = rspec_node_names.get(hostname, None)
475                     if node:
476                         rspec.version.remove_sliver_attribute(node['component_id'], tag, value, testbed)
477                 change = True
478
479         children = item.rowCount()
480         for row in range(0, children):
481             status = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1)
482             change = change or status
483
484         return change
485
486     def submit(self):
487         if self.checkRunningProcess():
488             return
489
490         rspec = SfiData().getSliceRSpec()
491         resources = SfiData().getResourcesRSpec()
492
493         resource_node_names = self.nodesByName(resources.version.get_nodes())
494         rspec_node_names = self.nodesByName(rspec.version.get_nodes_with_slivers())
495
496         change = self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem())
497
498         if not change:
499             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
500             return
501
502         # Several aggregates have issues with the <statistics> section in the
503         # rspec, so make sure it's not there.
504         stats_elems = rspec.xml.xpath("//statistics")
505         if len(stats_elems)>0:
506             stats_elem = stats_elems[0]
507             parent = stats_elem.xpath("..")[0]
508             parent.remove(stats_elem)
509
510         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
511         self.process.applyRSpec(rspec)
512         self.setStatus("Sending slice data (RSpec). This will take some time...")
513
514     def submit_pg_compat(self):
515         if self.checkRunningProcess():
516             return
517
518         rspec = SfiData().getSliceRSpec()
519         resources = SfiData().getResourcesRSpec()
520         change = self.process_subtree(rspec, resources, self.nodeModel.invisibleRootItem())
521
522         if not change:
523             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
524             return
525
526         dlg = ClientSliceManager(self)
527         dlg.submit_pg_compat(rspec)
528         dlg.exec_()
529
530         self.setStatus("<font color='green'>Finished submitting. %d/%d aggs succeeded.</font>" %
531                       (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount))
532         QTimer.singleShot(2500, self.refresh)
533
534     def renew(self):
535         dlg = RenewWindow(parent=self)
536         dlg.exec_()
537
538     def refresh(self):
539         if not config.getSlice():
540             self.setStatus("<font color='red'>Slice not set yet!</font>")
541             return
542
543         if self.process.isRunning():
544             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
545             return
546
547         self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
548
549         self.process.retrieveResources()
550         self.setStatus("Refreshing resources. This will take some time...")
551
552     def nodesByNetwork(self, nodeList):
553         netDict = {}
554         for node in nodeList:
555             network_name = Xrn(node['component_manager_id']).get_hrn()
556             if network_name:
557                 net = netDict.get(network_name, [])
558                 if net == []:
559                     netDict[network_name] = net
560
561                 net.append(node)
562
563         return netDict
564
565     def nodesByName(self, nodeList, nameDict=None):
566         if nameDict==None:
567             nameDict = {}
568         for node in nodeList:
569             hostname = node.get("component_name", None)
570             if hostname and (not hostname in nameDict):
571                 nameDict[hostname] = node
572
573         return nameDict
574
575     def updateView(self):
576         global already_in_nodes
577         already_in_nodes = []
578         self.network_names = []
579         self.nodeModel.clear()
580
581         rspec = SfiData().getSliceRSpec()
582         if not rspec:
583             return None
584
585         resources = SfiData().getResourcesRSpec()
586         if not resources:
587             return None
588
589         rootItem = self.nodeModel.invisibleRootItem()
590
591         networks = []
592         for network in rspec.version.get_networks():
593             network_name = network.get("name", None)
594             if (network_name != None) and (not network_name in networks):
595                 networks.append(network_name)
596         for network in resources.version.get_networks():
597             network_name = network.get("name", None)
598             if (network_name != None) and (not network_name in networks):
599                 networks.append(network_name)
600
601         resources_nodes = self.nodesByNetwork(resources.version.get_nodes())
602         rspec_nodes = self.nodesByNetwork(rspec.version.get_nodes_with_slivers())
603
604         for network in networks:
605             self.network_names.append(network)
606
607             all_nodes = resources_nodes.get(network, [])
608             sliver_nodes = rspec_nodes.get(network, [])
609
610             sliver_node_names = self.nodesByName(sliver_nodes)
611
612             available_nodes = [ node for node in all_nodes if node["component_name"] not in sliver_node_names ]
613
614             msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
615             networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network")
616
617             already_in_nodes += sliver_node_names.keys()
618
619             # Add default slice tags
620             nodeItem = self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults")
621             attrs = rspec.version.get_default_sliver_attributes(network)
622             for attr in attrs:
623                     name = attr.get("name", None)
624                     value = attr.get("value", None)
625                     tagstring = QString("%s: %s" % (name, value))
626                     self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute")
627
628             for node in sliver_nodes:
629                 nodeItem = self.nodeView.appendRow(networkItem,
630                                node["component_name"],
631                                nodeStatus=node.get("boot_state", ""),
632                                #nodeType=node.get("rspec.get_node_sliver_type(node, network),
633                                membership=node_status['in'],
634                                kind="node")
635
636                 attrs = rspec.version.get_sliver_attributes(node['component_id'], network)
637                 for attr in attrs:
638                     name = attr.get("name", None)
639                     value = attr.get("value", None)
640                     self.nodeView.appendRow(nodeItem,
641                                             "%s: %s" % (name, value),
642                                             membership=tag_status['in'],
643                                             kind="attribute")
644
645             for node in available_nodes:
646                 self.nodeView.appendRow(networkItem,
647                                node["component_name"],
648                                nodeStatus = node.get("boot_state", ""),
649                                #nodeType= resources.get_node_sliver_type(node, network),
650                                membership=node_status['out'],
651                                kind="node")
652
653         self.filterModel.setSourceModel(self.nodeModel)
654         self.filterModel.setDynamicSortFilter(True)
655         self.filterModel.sort(NAME_COLUMN)
656
657         headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind"
658         self.nodeModel.setHorizontalHeaderLabels(headers)
659
660         self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate)
661         self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate)
662         self.nodeView.setModel(self.filterModel)
663         self.nodeView.hideColumn(KIND_COLUMN)
664         self.nodeView.expandAll()
665         self.nodeView.resizeColumnToContents(0)
666         self.nodeView.collapseAll()
667
668     def updateSliceName(self):
669         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
670
671     def nodeSelectionChanged(self, hostname):
672         self.parent().nodeSelectionChanged(hostname)
673
674 class MainScreen(SfaScreen):
675     def __init__(self, parent):
676         SfaScreen.__init__(self, parent)
677
678         slice = SliceWidget(self)
679         self.init(slice, "Nodes", "OneLab SFA crawler")
680
681     def rspecUpdated(self):
682         self.mainwin.rspecWindow.updateView()
683
684     def configurationChanged(self):
685         self.widget.updateSliceName()
686         self.widget.updateView()
687         self.mainwin.rspecWindow.updateView()
688
689     def nodeSelectionChanged(self, hostname):
690         self.mainwin.nodeSelectionChanged(hostname)