change <update> to <refresh>
[sface.git] / sface / screens / mainscreen.py
1
2 import datetime
3 import os
4 from PyQt4.QtCore import *
5 from PyQt4.QtGui import *
6
7 #from sfa.util.rspecHelper import RSpec
8 from sfa.rspecs.rspec_parser import parse_rspec
9 from sface.config import config
10 from sface.sfirenew import SfiRenewer
11 from sface.sfiprocess import SfiProcess
12 from sface.screens.sfascreen import SfaScreen
13
14 already_in_nodes = []
15
16 node_status = { "in": "Already Selected",
17                 "out": "Not Selected",
18                 "add": "To be Added",
19                 "remove": "To be Removed"}
20
21 tag_status = { "in": "Already Set",
22                 "out": "Not Set",
23                 "add": "To be Added",
24                 "remove": "To be Removed"}
25
26 default_tags = "Default tags"
27 settable_tags = ['delegations', 'initscript']
28
29 def itemType(index):
30     if index.parent().parent().isValid():
31         return "tag"
32     else:
33         return "node"
34
35
36 class NodeView(QTreeView):
37     def __init__(self, parent):
38         QTreeView.__init__(self, parent)
39
40         self.setAnimated(True)
41         self.setItemsExpandable(True)
42         self.setRootIsDecorated(True)
43         self.setAlternatingRowColors(True)
44 #        self.setSelectionMode(self.MultiSelection)
45         self.setAttribute(Qt.WA_MacShowFocusRect, 0)
46         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
47         self.setToolTip("Double click on a row to change its status.  Right click on a host to add a tag.")
48
49     def mouseDoubleClickEvent(self, event):
50         index = self.currentIndex()
51         model = index.model()
52         status_index = model.index(index.row(), 1, index.parent())
53         status_data = status_index.data().toString()
54         node_index = model.index(index.row(), 0, index.parent())
55         node_data = node_index.data().toString()
56
57         if itemType(node_index) == "tag":
58             data = node_index.data().toString()
59             tagname, value = data.split(": ")
60             if tagname not in settable_tags:
61                 # Pop up error msg
62                 QMessageBox.warning(self, "Not settable", "Insufficient permission to change '%s' tag" % tagname)
63                 return
64             if status_data == tag_status['in']:
65                 model.setData(status_index, QString(tag_status['remove']))
66             elif status_data == tag_status['add']:
67                 model.setData(status_index, QString(tag_status['out']))
68             elif status_data == tag_status['remove']:
69                 model.setData(status_index, QString(tag_status['in']))
70             else: model.setData(status_index, QString(node_status['out']))
71         else:
72             # This is a hostname
73             if status_data == node_status['in']:
74                 model.setData(status_index, QString(node_status['remove']))
75             elif status_data == node_status['out']:
76                 model.setData(status_index, QString(node_status['add']))
77             elif status_data in (node_status['add'], node_status['remove']):
78                 if node_data in already_in_nodes: model.setData(status_index, QString(node_status['in']))
79                 else: model.setData(status_index, QString(node_status['out']))
80
81         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
82
83     def mousePressEvent(self, event):
84         QTreeView.mousePressEvent(self, event)
85         if event.button() == Qt.LeftButton:
86             return
87
88         # Right click
89         index = self.currentIndex()
90         model = index.model()
91         status_index = model.index(index.row(), 1, index.parent())
92         status_data = status_index.data().toString()
93         node_index = model.index(index.row(), 0, index.parent())
94         node_data = node_index.data().toString()
95
96         if itemType(node_index) == "node":
97             # This is a hostname
98             if status_data in (node_status['in'], node_status['add'], ""):
99                 # Pop up a dialog box for adding a new attribute
100                 tagname, ok = QInputDialog.getItem(self, "Add tag",
101                                                    "Tag name:", settable_tags)
102                 if ok:
103                     value, ok = QInputDialog.getText(self, "Add tag",
104                                                      "Value for tag '%s'" % tagname)
105                     if ok:
106                         # Add a new row to the model for the tag
107
108                         # For testing with the QStandardItemModel
109                         #nodeItem = model.itemFromIndex(index)
110                         #tagstring = QString("%s: %s" % (tagname, value))
111                         #tagItem = QStandardItem(tagstring)
112                         #status = QStandardItem(QString(tag_status['add']))
113                         #nodeItem.appendRow([tagItem, status])
114
115                         # We're using the QSortFilterProxyModel here
116                         src_index = model.mapToSource(index)
117                         src_model = src_index.model()
118                         nodeItem = src_model.itemFromIndex(src_index)
119                         tagstring = QString("%s: %s" % (tagname, value))
120                         tagItem = QStandardItem(tagstring)
121                         status = QStandardItem(QString(tag_status['add']))
122                         nodeItem.appendRow([tagItem, status])
123
124             elif status_data in (node_status['out'], node_status['remove']):
125                 QMessageBox.warning(self, "Not selected", "Can only add tags to selected nodes")
126                 return
127
128         model.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), node_index, node_index)
129
130     def currentChanged(self, current, previous):
131         model = current.model()
132         node_index = model.index(current.row(), 0, current.parent())
133         node_data = node_index.data().toString()
134         self.emit(SIGNAL('hostnameClicked(QString)'), node_data)
135         
136                 
137
138 class NodeNameDelegate(QStyledItemDelegate):
139     def __init__(self, parent):
140         QStyledItemDelegate.__init__(self, parent)
141
142     def paint(self, painter, option, index):
143         model = index.model()
144         status_index = model.index(index.row(), 1, index.parent())
145         status_data = status_index.data().toString()
146
147         fm = QFontMetrics(option.font)
148         rect = option.rect
149
150         data = index.data().toString()
151         rect.setHeight(rect.height() - 2)
152         rect.setWidth(fm.width(QString(data)) + 6)
153         rect.setX(rect.x() + 5)
154         rect.setY(rect.y() - 1)
155
156         x, y, h, w = rect.x(), rect.y(), rect.height(), rect.width()
157
158         path = QPainterPath()
159         path.addRoundedRect(x - 1, y + 1, w, h, 4, 4)
160
161         painter.save()
162         painter.setRenderHint(QPainter.Antialiasing)
163
164         if itemType(index) == "node":
165             if status_data == node_status['in']: # already in the slice
166                 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
167                 painter.setPen(QColor.fromRgb(0, 0, 0))
168                 painter.drawText(option.rect, 0, QString(data))
169
170             elif status_data == node_status['add']: # newly added to the slice
171                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
172                 painter.setPen(QColor.fromRgb(0, 0, 0))
173                 painter.drawText(option.rect, 0, QString(data))
174
175             elif status_data == node_status['remove']: # removed from the slice
176                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
177                 painter.setPen(QColor.fromRgb(0, 0, 0))
178                 painter.drawText(option.rect, 0, QString(data))
179
180             else:
181                 painter.setPen(QColor.fromRgb(0, 0, 0))
182                 painter.drawText(option.rect, 0, QString(data))
183
184         else:
185             if status_data == tag_status['in']: # already in the slice
186                 painter.fillPath(path, QColor.fromRgb(0, 250, 250))
187                 painter.setPen(QColor.fromRgb(0, 0, 0))
188                 painter.drawText(option.rect, 0, QString(data))
189
190             elif status_data == tag_status['add']: # newly added to the slice
191                 painter.fillPath(path, QColor.fromRgb(0, 250, 0))
192                 painter.setPen(QColor.fromRgb(0, 0, 0))
193                 painter.drawText(option.rect, 0, QString(data))
194
195             elif status_data == tag_status['remove']: # removed from the slice
196                 painter.fillPath(path, QColor.fromRgb(250, 0, 0))
197                 painter.setPen(QColor.fromRgb(0, 0, 0))
198                 painter.drawText(option.rect, 0, QString(data))
199
200             else:
201                 painter.setPen(QColor.fromRgb(0, 0, 0))
202                 painter.drawText(option.rect, 0, QString(data))
203
204         painter.restore()
205
206 class SliceWidget(QWidget):
207     def __init__(self, parent):
208         QWidget.__init__(self, parent)
209
210         self.network_names = []
211         self.process = SfiProcess(self)
212
213         self.slicename = QLabel("", self)
214         self.updateSliceName()
215         self.slicename.setScaledContents(False)
216         searchlabel = QLabel ("Search: ", self)
217         searchlabel.setScaledContents(False)
218         searchbox = QLineEdit(self)
219         searchbox.setAttribute(Qt.WA_MacShowFocusRect, 0)
220
221         toplayout = QHBoxLayout()
222         toplayout.addWidget(self.slicename, 0, Qt.AlignLeft)
223         toplayout.addStretch()
224         toplayout.addWidget(searchlabel, 0, Qt.AlignRight)
225         toplayout.addWidget(searchbox, 0, Qt.AlignRight)
226
227         self.nodeView = NodeView(self)
228         self.nodeModel = QStandardItemModel(0, 2, self)
229         self.filterModel = QSortFilterProxyModel(self) # enable filtering
230
231         self.nodeNameDelegate = NodeNameDelegate(self)
232
233         refresh = QPushButton("Refresh Slice Data", self)
234         refresh.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
235         renew = QPushButton("Renew Slice", self)
236         renew.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
237         submit = QPushButton("Submit", self)
238         submit.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
239
240         bottomlayout = QHBoxLayout()
241         bottomlayout.addWidget(refresh, 0, Qt.AlignLeft)
242         bottomlayout.addWidget(renew, 0, Qt.AlignLeft)
243         bottomlayout.addStretch()
244         bottomlayout.addWidget(submit, 0, Qt.AlignRight)
245
246         layout = QVBoxLayout()
247         layout.addLayout(toplayout)
248         layout.addWidget(self.nodeView)
249         layout.addLayout(bottomlayout)
250         self.setLayout(layout)
251         self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
252
253         self.connect(refresh, SIGNAL('clicked()'), self.refresh)
254         self.connect(renew, SIGNAL('clicked()'), self.renew)
255         self.connect(submit, SIGNAL('clicked()'), self.submit)
256         self.connect(searchbox, SIGNAL('textChanged(QString)'), self.filter)
257         self.connect(self.nodeView, SIGNAL('hostnameClicked(QString)'),
258                      self.nodeSelectionChanged)
259
260         self.updateView()
261
262     def submitFinished(self):
263         self.setStatus("<font color='green'>Slice data submitted.</font>")
264         QTimer.singleShot(1000, self.refresh)
265
266     def refreshFinished(self):
267         self.setStatus("<font color='green'>Slice data refreshed.</font>", timeout=5000)
268         self.updateView()
269         self.parent().signalAll("rspecUpdated")
270
271     def readSliceRSpec(self):
272         rspec_file = config.getSliceRSpecFile()
273         if os.path.exists(rspec_file):
274             xml = open(rspec_file).read()
275             return parse_rspec(xml)
276         return None
277
278     def setStatus(self, msg, timeout=None):
279         self.parent().setStatus(msg, timeout)
280
281     def checkRunningProcess(self):
282         if self.process.isRunning():
283             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
284             return True
285         return False
286
287     def filter(self, filter_string):
288         # for hierarchical models QSortFilterProxyModel applies the
289         # sort recursively. if the parent doesn't match the criteria
290         # we won't be able to match the children. so we need to match
291         # parent (by matching the network_names)
292         networks = ["^%s$" % n for n in self.network_names]
293         filters = networks + [str(filter_string)]
294         self.filterModel.setFilterRegExp(QRegExp('|'.join(filters)))
295
296     def itemStatus(self, item):
297         statusItem = item.parent().child(item.row(), 1)
298         return statusItem.data(Qt.DisplayRole).toString()
299
300     def itemText(self, item):
301         return item.data(Qt.DisplayRole).toString()
302
303     # Recursively walk the tree, making changes to the RSpec
304     def process_subtree(self, rspec, item, depth = 0):
305         change = False
306         model = self.nodeModel
307
308         if depth in [0, 1]:
309             pass
310         elif depth == 2: # Hostname
311             hostname = self.itemText(item)
312             testbed = self.itemText(item.parent())
313             status = self.itemStatus(item)
314             if status == node_status['add']:
315                 print "Add hostname: %s" % hostname
316                 rspec.add_slivers(str(hostname), testbed)
317                 change = True
318             elif status == node_status['remove']:
319                 print "Remove hostname: %s" % hostname
320                 rspec.remove_slivers(str(hostname), testbed)
321                 change = True
322         elif depth == 3: # Tag
323             tag, value = self.itemText(item).split(": ")
324             status = self.itemStatus(item)
325             tag = "%s" % tag     # Prevent weird error from lxml
326             value = "%s" % value # Prevent weird error from lxml
327             node = self.itemText(item.parent())
328             testbed = self.itemText(item.parent().parent())
329             if status == tag_status['add']:
330                 print "Add tag to (%s, %s): %s/%s " % (testbed, node, tag, value)
331                 if node.startsWith(default_tags):
332                     rspec.add_default_sliver_attribute(tag, value, testbed)
333                 else:
334                     rspec.add_sliver_attribute(node, tag, value, testbed)
335                 change = True
336             elif status == tag_status['remove']:
337                 print "Remove tag from (%s, %s): %s/%s " % (testbed, node, tag, value)
338                 if node.startsWith(default_tags):
339                     rspec.remove_default_sliver_attribute(tag, value, testbed)
340                 else:
341                     rspec.remove_sliver_attribute(node, tag, value, testbed)
342                 change = True
343
344         children = item.rowCount()
345         for row in range(0, children):
346             status = self.process_subtree(rspec, item.child(row), depth + 1)
347             change = change or status
348
349         return change
350
351     def submit(self):
352         if self.checkRunningProcess():
353             return
354
355         rspec = self.readSliceRSpec()
356         change = self.process_subtree(rspec, self.nodeModel.invisibleRootItem())
357
358         if not change:
359             self.setStatus("<font color=red>No change in slice data. Not submitting!</font>", timeout=3000)
360             return
361
362         self.disconnect(self.process, SIGNAL('finished()'), self.refreshFinished)
363         self.connect(self.process, SIGNAL('finished()'), self.submitFinished)
364
365         self.process.applyRSpec(rspec)
366         self.setStatus("Sending slice data (RSpec). This will take some time...")
367
368     def renew(self):
369         dlg = RenewWindow(parent=self)
370         if (dlg.exec_() == QDialog.Accepted):
371             self.setStatus("Renewing Slice.")
372
373             self.renewProcess = SfiRenewer(config.getSlice(), dlg.get_new_expiration(), self)
374             self.connect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
375
376     def renewFinished(self):
377         if self.renewProcess.statusMsg:
378             self.setStatus("Renew " + self.renewProcess.status + ": " + self.renewProcess.statusMsg)
379         else:
380             self.setStatus("Renew " + self.renewProcess.status)
381         self.disconnect(self.renewProcess, SIGNAL('finished()'), self.renewFinished)
382         self.renewProcess = None
383
384     def refresh(self):
385         if not config.getSlice():
386             self.setStatus("<font color='red'>Slice not set yet!</font>")
387             return
388
389         if self.process.isRunning():
390             self.setStatus("<font color='red'>There is already a process running. Please wait.</font>")
391             return
392
393         self.disconnect(self.process, SIGNAL('finished()'), self.submitFinished)
394         self.connect(self.process, SIGNAL('finished()'), self.refreshFinished)
395
396         self.process.getRSpecFromSM()
397         self.setStatus("Refreshing slice data. This will take some time...")
398
399     def updateView(self):
400         global already_in_nodes
401         already_in_nodes = []
402         self.network_names = []
403         self.nodeModel.clear()
404
405         rspec = self.readSliceRSpec()
406         if not rspec:
407             return None
408
409         rootItem = self.nodeModel.invisibleRootItem()
410         #networks = sorted(rspec.get_network_list())
411         networks = rspec.get_networks()
412         for network in networks:
413             self.network_names.append(network)
414
415             #all_nodes = rspec.get_node_list(network)
416             #sliver_nodes = rspec.get_sliver_list(network)
417             all_nodes = rspec.get_nodes(network)
418             sliver_nodes = rspec.get_nodes_with_slivers(network)
419             available_nodes = [ node for node in all_nodes if node not in sliver_nodes ]
420
421             networkItem = QStandardItem(QString(network))
422             msg = "%s Nodes\t%s Selected" % (len(all_nodes), len(sliver_nodes))
423             rootItem.appendRow([networkItem, QStandardItem(QString(msg))])
424
425             already_in_nodes += sliver_nodes
426
427             # Add default slice tags
428             nodeItem = QStandardItem(QString("%s for %s" % (default_tags, network)))
429             statusItem = QStandardItem(QString(""))
430             networkItem.appendRow([nodeItem, statusItem])
431             attrs = rspec.get_default_sliver_attributes(network)
432             for (name, value) in attrs:
433                     tagstring = QString("%s: %s" % (name, value))
434                     tagItem = QStandardItem(tagstring)
435                     status = QStandardItem(QString(tag_status['in']))
436                     nodeItem.appendRow([tagItem, status])
437
438             for node in sliver_nodes:
439                 nodeItem = QStandardItem(QString(node))
440                 statusItem = QStandardItem(QString(node_status['in']))
441                 networkItem.appendRow([nodeItem, statusItem])
442
443                 attrs = rspec.get_sliver_attributes(node, network)
444                 for (name, value) in attrs:
445                     tagstring = QString("%s: %s" % (name, value))
446                     tagItem = QStandardItem(tagstring)
447                     statusItem = QStandardItem(QString(tag_status['in']))
448                     nodeItem.appendRow([tagItem, statusItem])
449
450             for node in available_nodes:
451                 nodeItem = QStandardItem(QString(node))
452                 statusItem = QStandardItem(QString(node_status['out']))
453                 networkItem.appendRow([nodeItem, statusItem])
454
455         self.filterModel.setSourceModel(self.nodeModel)
456         self.filterModel.setFilterKeyColumn(-1)
457         self.filterModel.setDynamicSortFilter(True)
458
459         headers = QStringList() << "Hostname or Tag" << "Status"
460         self.nodeModel.setHorizontalHeaderLabels(headers)
461
462         self.nodeView.setItemDelegateForColumn(0, self.nodeNameDelegate)
463         self.nodeView.setModel(self.filterModel)
464         self.nodeView.expandAll()
465         self.nodeView.resizeColumnToContents(0)
466         self.nodeView.collapseAll()
467
468     def updateSliceName(self):
469         self.slicename.setText("Slice : %s" % (config.getSlice() or "None"))
470
471     def nodeSelectionChanged(self, hostname):
472         self.parent().nodeSelectionChanged(hostname)
473
474 class RenewWindow(QDialog):
475     def __init__(self, parent=None):
476         super(RenewWindow, self).__init__(parent)
477         self.setWindowTitle("Renew Slivers")
478
479         self.duration = QComboBox()
480
481         self.expirations = []
482
483         durations = ( (1, "One Week"), (2, "Two Weeks"), (3, "Three Weeks"), (4, "One Month") )
484
485         now = datetime.datetime.utcnow()
486         for (weeks, desc) in durations:
487             exp = now + datetime.timedelta(days = weeks * 7)
488             desc = desc + " " + exp.strftime("%Y-%m-%d %H:%M:%S")
489             self.expirations.append(exp)
490             self.duration.addItem(desc)
491
492         self.duration.setCurrentIndex(0)
493
494         buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
495         buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
496
497         layout = QVBoxLayout()
498         layout.addWidget(self.duration)
499         layout.addWidget(buttonBox)
500         self.setLayout(layout)
501
502         self.connect(buttonBox, SIGNAL("accepted()"), self, SLOT("accept()"))
503         self.connect(buttonBox, SIGNAL("rejected()"), self, SLOT("reject()"))
504
505     def accept(self):
506         QDialog.accept(self)
507
508     def get_new_expiration(self):
509         index = self.duration.currentIndex()
510         return self.expirations[index]
511
512 class MainScreen(SfaScreen):
513     def __init__(self, parent):
514         SfaScreen.__init__(self, parent)
515
516         slice = SliceWidget(self)
517         self.init(slice, "Main Window", "OneLab SFA crawler")
518
519     def rspecUpdated(self):
520         self.mainwin.rspecWindow.updateView()
521
522     def configurationChanged(self):
523         self.widget.updateSliceName()
524         self.widget.updateView()
525         self.mainwin.rspecWindow.updateView()
526
527     def nodeSelectionChanged(self, hostname):
528         self.mainwin.nodeSelectionChanged(hostname)