display disk images
[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 deleteSliversFinished(self):
390         self.disconnect(self.process, SIGNAL('finished()'), self.deleteSliversFinished)
391
392         faultString = self.process.getFaultString()
393         if not faultString:
394             self.setStatus("<font color='green'>Slice data submitted.</font>")
395             QTimer.singleShot(2500, self.refresh)
396         else:
397             self.setStatus("<font color='red'>Slice submit failed: %s</font>" % (faultString))
398
399     def refreshResourcesFinished(self):
400         self.disconnect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
401
402         faultString = self.process.getFaultString()
403         if not faultString:
404             self.setStatus("Refreshing slice RSpec.")
405             self.connect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
406             self.process.retrieveRspec()
407         else:
408             self.setStatus("<font color='red'>Resources refresh failed: %s</font>" % (faultString))
409
410     def refreshRSpecFinished(self):
411         self.disconnect(self.process, SIGNAL('finished()'), self.refreshRSpecFinished)
412
413         faultString = self.process.getFaultString()
414         if not faultString:
415             self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
416         else:
417             self.setStatus("<font color='red'>Slice refresh failed: %s</font>" % (faultString))
418
419         self.updateView()
420         self.parent().signalAll("rspecUpdated")
421
422     def setStatus(self, msg, timeout=None):
423         self.parent().setStatus(msg, timeout)
424
425     def checkRunningProcess(self):
426         if self.process.isRunning():
427             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
428             return True
429         return False
430
431     def search(self, search_string):
432         self.filterModel.setHostNameFilter(str(search_string))
433
434     def filter(self, filter_string):
435         self.filterModel.setNodeStatusFilter(str(filter_string))
436
437     def itemStatus(self, item):
438         statusItem = item.parent().child(item.row(), MEMBERSHIP_STATUS_COLUMN)
439         return str(statusItem.data(Qt.DisplayRole).toString())
440
441     def itemText(self, item):
442         return str(item.data(Qt.DisplayRole).toString())
443
444     # Recursively walk the tree, making changes to the RSpec
445     def process_subtree(self, rspec, rspec_node_names, resource_node_names, item, depth = 0):
446         changeAdd = 0
447         changeRemove = 0
448         changeAddTag = 0
449         changeRemoveTag = 0
450         model = self.nodeModel
451
452         if depth in [0, 1]:
453             pass
454         elif depth == 2: # Hostname
455             hostname = self.itemText(item)
456             testbed = self.itemText(item.parent())
457             status = self.itemStatus(item)
458             if status == node_status['add']:
459                 print "Add hostname: %s" % hostname
460
461                 resource_node = resource_node_names.get(hostname, None)
462
463                 if resource_node==None:
464                     print "Error: Failed to find %s in resources rspec" % hostname
465                 else:
466                     if not (hostname in rspec_node_names):
467                         network_name = Xrn(resource_node['component_manager_id']).get_hrn()
468                         rspec.version.add_network(network_name)
469                         rspec.version.add_nodes([resource_node])
470                     rspec.version.add_slivers([str(hostname)])
471                     changeAdd += 1
472             elif status == node_status['remove']:
473                 print "Remove hostname: %s" % hostname
474                 rspec.version.remove_slivers([str(hostname)])
475                 changeRemove += 1
476         elif depth == 3: # Tag
477             tag, value = self.itemText(item).split(": ")
478             status = self.itemStatus(item)
479             tag = "%s" % tag     # Prevent weird error from lxml
480             value = "%s" % value # Prevent weird error from lxml
481             hostname = self.itemText(item.parent())
482             testbed = self.itemText(item.parent().parent())
483             if status == tag_status['add']:
484                 print "Add tag to (%s, %s): %s/%s " % (testbed, hostname, tag, value)
485                 if hostname.startswith(default_tags):
486                     rspec.version.add_default_sliver_attribute(tag, value, testbed)
487                 else:
488                     node = rspec_node_names.get(hostname, None)
489                     if node:
490                         rspec.version.add_sliver_attribute(node['component_id'], tag, value, testbed)
491                 changeAddTag += 1
492             elif status == tag_status['remove']:
493                 print "Remove tag from (%s, %s): %s/%s " % (testbed, hostname, tag, value)
494                 if hostname.startswith(default_tags):
495                     rspec.version.remove_default_sliver_attribute(tag, value, testbed)
496                 else:
497                     node = rspec_node_names.get(hostname, None)
498                     if node:
499                         rspec.version.remove_sliver_attribute(node['component_id'], tag, value, testbed)
500                 changeRemoveTag += 1
501
502         children = item.rowCount()
503         for row in range(0, children):
504             (c, ca, cr, cat, crt) = self.process_subtree(rspec, rspec_node_names, resource_node_names, item.child(row), depth + 1)
505             changeAdd = changeAdd + ca
506             changeRemove = changeRemove + cr
507             changeAddTag = changeAddTag + cat
508             changeRemoveTag = changeRemoveTag + crt
509
510         change = changeAdd + changeRemove + changeAddTag + changeRemoveTag
511
512         return (change, changeAdd, changeRemove, changeAddTag, changeRemoveTag)
513
514     def submit(self):
515         if self.checkRunningProcess():
516             return
517
518         rspec = SfiData().getSliceRSpec()
519         resources = SfiData().getResourcesRSpec()
520
521         resource_node_names = self.nodesByName(resources.version.get_nodes())
522         rspec_node_names = self.nodesByName(rspec.version.get_nodes_with_slivers())
523
524         (change, changeAdd, changeRemove, changeAddTag, changeRemoveTag) = \
525             self.process_subtree(rspec, rspec_node_names, resource_node_names, self.nodeModel.invisibleRootItem())
526
527         if (change <= 0):
528             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
529             return
530
531         if (changeRemove > 0) and (rspec.version.get_nodes_with_slivers() == []):
532             # removing the last sliver in a slice requires us to call deleteSlivers
533             self.connect(self.process, SIGNAL('finished()'), self.deleteSliversFinished)
534             self.process.deleteSlivers()
535             return
536
537         # Several aggregates have issues with the <statistics> section in the
538         # rspec, so make sure it's not there.
539         stats_elems = rspec.xml.xpath("//statistics")
540         if len(stats_elems)>0:
541             stats_elem = stats_elems[0]
542             parent = stats_elem.xpath("..")[0]
543             parent.remove(stats_elem)
544
545         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
546         self.process.applyRSpec(rspec)
547         self.setStatus("Sending slice data (RSpec). This will take some time...")
548
549     def submit_pg_compat(self):
550         if self.checkRunningProcess():
551             return
552
553         rspec = SfiData().getSliceRSpec()
554         resources = SfiData().getResourcesRSpec()
555
556         (change, changeAdd, changeRemove, changeAddTag, changeRemoveTag) = \
557             self.process_subtree(rspec, rspec, resources, self.nodeModel.invisibleRootItem())
558
559         if (change <= 0):
560             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
561             return
562
563         dlg = ClientSliceManager(self)
564         dlg.submit_pg_compat(rspec)
565         dlg.exec_()
566
567         self.setStatus("<font color='green'>Finished submitting. %d/%d aggs succeeded.</font>" %
568                       (dlg.submit_aggSuccessCount,dlg.submit_aggSuccessCount+dlg.submit_aggFailCount))
569         QTimer.singleShot(2500, self.refresh)
570
571     def renew(self):
572         dlg = RenewWindow(parent=self)
573         dlg.exec_()
574
575     def refresh(self):
576         if not config.getSlice():
577             self.setStatus("<font color='red'>Slice not set yet!</font>")
578             return
579
580         if self.process.isRunning():
581             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
582             return
583
584         self.connect(self.process, SIGNAL('finished()'), self.refreshResourcesFinished)
585
586         self.process.retrieveResources()
587         self.setStatus("Refreshing resources. This will take some time...")
588
589     def nodesByNetwork(self, nodeList):
590         netDict = {}
591         for node in nodeList:
592             network_name = Xrn(node['component_manager_id']).get_hrn()
593             if network_name:
594                 net = netDict.get(network_name, [])
595                 if net == []:
596                     netDict[network_name] = net
597
598                 net.append(node)
599
600         return netDict
601
602     def nodesByName(self, nodeList, nameDict=None):
603         if nameDict==None:
604             nameDict = {}
605         for node in nodeList:
606             hostname = node.get("component_name", None)
607             if hostname and (not hostname in nameDict):
608                 nameDict[hostname] = node
609
610         return nameDict
611
612     def updateView(self):
613         global already_in_nodes
614         already_in_nodes = []
615         self.network_names = []
616         self.nodeModel.clear()
617
618         rspec = SfiData().getSliceRSpec()
619         if not rspec:
620             return None
621
622         resources = SfiData().getResourcesRSpec()
623         if not resources:
624             return None
625
626         rootItem = self.nodeModel.invisibleRootItem()
627
628         networks = []
629         for network in rspec.version.get_networks():
630             network_name = network.get("name", None)
631             if (network_name != None) and (not network_name in networks):
632                 networks.append(network_name)
633         for network in resources.version.get_networks():
634             network_name = network.get("name", None)
635             if (network_name != None) and (not network_name in networks):
636                 networks.append(network_name)
637
638         resources_nodes = self.nodesByNetwork(resources.version.get_nodes())
639         rspec_nodes = self.nodesByNetwork(rspec.version.get_nodes_with_slivers())
640
641         for network in networks:
642             self.network_names.append(network)
643
644             all_nodes = resources_nodes.get(network, [])
645             sliver_nodes = rspec_nodes.get(network, [])
646
647             sliver_node_names = self.nodesByName(sliver_nodes)
648
649             available_nodes = [ node for node in all_nodes if node["component_name"] not in sliver_node_names ]
650
651             msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
652             networkItem = self.nodeView.appendRow(rootItem, network, membership=msg, kind="network")
653
654             already_in_nodes += sliver_node_names.keys()
655
656             # Add default slice tags
657             nodeItem = self.nodeView.appendRow(networkItem, "%s for %s" % (default_tags, network), kind="defaults")
658             attrs = rspec.version.get_default_sliver_attributes(network)
659             for attr in attrs:
660                     name = attr.get("name", None)
661                     value = attr.get("value", None)
662                     tagstring = QString("%s: %s" % (name, value))
663                     self.nodeView.appendRow(nodeItem, tagstring, membership=tag_status['in'], kind = "attribute")
664
665             for node in sliver_nodes:
666                 nodeType = None
667                 if ("hardware_types" in node):
668                     hardware_types = [x["name"] for x in node["hardware_types"]]
669                     nodeType = ",".join(hardware_types)
670                 nodeStatus = node.get("boot_state", "")
671                 if nodeStatus == None:
672                     nodeStatus=""
673                 nodeItem = self.nodeView.appendRow(networkItem,
674                                node["component_name"],
675                                nodeStatus=nodeStatus,
676                                nodeType=nodeType,
677                                membership=node_status['in'],
678                                kind="node")
679
680                 attrs = rspec.version.get_sliver_attributes(node['component_id'], network)
681                 for attr in attrs:
682                     name = attr.get("name", None)
683                     value = attr.get("value", None)
684                     self.nodeView.appendRow(nodeItem,
685                                             "%s: %s" % (name, value),
686                                             membership=tag_status['in'],
687                                             kind="attribute")
688                 disk_images = node.get("disk_image", [])
689                 for disk_image in disk_images:
690                     name = disk_image.get("name", None)
691                     self.noveView.appendRow(nodeItem, name, 
692                                             membership=node_status['in'],
693                                             kind="attribute")                                                                   
694                 
695             for node in available_nodes:
696                 nodeType = None
697                 if ("hardware_types" in node):
698                     hardware_types = [x["name"] for x in node["hardware_types"]]
699                     nodeType = ",".join(hardware_types)
700                 nodeStatus = node.get("boot_state", "")
701                 if nodeStatus == None:
702                     nodeStatus=""
703                 self.nodeView.appendRow(networkItem,
704                                node["component_name"],
705                                nodeStatus=nodeStatus,
706                                nodeType=nodeType,
707                                membership=node_status['out'],
708                                kind="node")
709
710         self.filterModel.setSourceModel(self.nodeModel)
711         self.filterModel.setDynamicSortFilter(True)
712         self.filterModel.sort(NAME_COLUMN)
713
714         headers = QStringList() << "Hostname or Tag" << "Node Type" << "Node Status" << "Membership Status" << "Kind"
715         self.nodeModel.setHorizontalHeaderLabels(headers)
716
717         self.nodeView.setItemDelegateForColumn(NAME_COLUMN, self.nodeNameDelegate)
718         self.nodeView.setItemDelegateForColumn(NODE_STATUS_COLUMN, self.nodeStatusDelegate)
719         self.nodeView.setModel(self.filterModel)
720         self.nodeView.hideColumn(KIND_COLUMN)
721         self.nodeView.expandAll()
722         self.nodeView.resizeColumnToContents(0)
723         self.nodeView.collapseAll()
724
725     def updateSliceName(self):
726         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
727
728     def nodeSelectionChanged(self, hostname):
729         self.parent().nodeSelectionChanged(hostname)
730
731 class MainScreen(SfaScreen):
732     def __init__(self, parent):
733         SfaScreen.__init__(self, parent)
734
735         self.sliceWidget = SliceWidget(self)
736         self.init(self.sliceWidget, "Nodes", "OneLab SFA crawler")
737
738     def rspecUpdated(self):
739         self.mainwin.rspecWindow.updateView()
740
741     def configurationChanged(self):
742         self.widget.updateSliceName()
743         self.widget.updateView()
744         self.mainwin.rspecWindow.updateView()
745
746     def nodeSelectionChanged(self, hostname):
747         self.mainwin.nodeSelectionChanged(hostname)
748
749     def remoteSliceChanged(self):
750         # we're being notified the slice was changed remotely. Download a new
751         # rspec.
752         QTimer.singleShot(2500, self.sliceWidget.refresh)
753