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