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