Add ovsdbmonitor GUI tool by Andy Southgate, contributed by Citrix.
[sliver-openvswitch.git] / ovsdb / ovsdbmonitor / OVEFlowWindow.py
1 # Copyright (c) 2010 Citrix Systems, Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at:
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 from OVEStandard import *
16 from OVEConfig import *
17 from OVEFetch import *
18 from OVELogger import *
19 from OVEUtil import *
20
21 from OVECommonWindow import *
22
23 from Ui_FlowWindow import *
24
25 class OVEFlowWindow(QtGui.QMainWindow, OVECommonWindow):
26     LOAD_KEY = 'FlowWindow/window'
27     COMMAND_OVS_DPCTL='/usr/bin/ovs-dpctl'
28     BASE_REF=200000
29     
30     def __init__(self, app, loadIndex = None):
31         QtGui.QMainWindow.__init__(self)
32         self.ui = Ui_FlowWindow()
33         self.dpNames = []
34         self.dpTables = []
35         self.currentOpIndex = None
36         self.resizeCount = []
37         self.ssgChecked = False
38         self.ssgText = ''
39         self.lastTime = None
40         self.lastByteCount = 0
41         OVECommonWindow.__init__(self, app, loadIndex)
42
43         self.updateSsgList()
44         self.updateDatapaths()
45         self.updateSsgState()
46
47         self.connect(self.ui.fetchPathsButton, QtCore.SIGNAL("clicked()"), self.xon_fetchPathsButton_clicked)
48         self.connect(self.ui.ssgSaveButton, QtCore.SIGNAL("clicked()"), self.xon_ssgSaveButton_clicked)
49         self.connect(self.ui.ssgDeleteButton, QtCore.SIGNAL("clicked()"), self.xon_ssgDeleteButton_clicked)
50         self.connect(self.ui.ssgComboBox, QtCore.SIGNAL("activated(int)"), self.xon_ssgComboBox_activated)
51         self.connect(self.ui.ssgComboBox, QtCore.SIGNAL("editTextChanged(QString)"), self.xon_ssgComboBox_editTextChanged)
52         self.connect(self.ui.ssgCheckBox, QtCore.SIGNAL("stateChanged(int)"), self.xon_ssgCheckBox_stateChanged)
53         
54     
55     def xon_fetchPathsButton_clicked(self):
56         self.updateDatapaths()
57     
58     def xon_hostComboBox_currentIndexChanged(self, index):
59         OVECommonWindow.xon_hostComboBox_currentIndexChanged(self, index)
60         if (index >= 0):
61             self.updateDatapaths()
62     
63     def xon_ssgSaveButton_clicked(self):
64         if self.ssgText not in OVEConfig.Inst().ssgList:
65             OVEConfig.Inst().ssgList.append(self.ssgText)
66             OVEConfig.Inst().saveConfig()
67         self.updateSsgList()
68         
69     def updateSsgList(self):
70         currentSsgText = self.ssgText
71         self.ui.ssgComboBox.clear()
72         isFound = False
73         for i, ssgText in enumerate(OVEConfig.Inst().ssgList):
74             self.ui.ssgComboBox.addItem(ssgText)
75             if ssgText == currentSsgText:
76                 # This is the currently selected item
77                 self.ui.ssgComboBox.setCurrentIndex(i)
78                 isFound = True
79         
80         if not isFound:
81             self.ui.ssgComboBox.setCurrentIndex(-1)
82             self.ui.ssgComboBox.lineEdit().setText(currentSsgText)
83
84     def xon_ssgDeleteButton_clicked(self):
85         if self.ssgText in OVEConfig.Inst().ssgList:
86             OVEConfig.Inst().ssgList.remove(self.ssgText)
87             self.ssgText = ''
88             OVEConfig.Inst().saveConfig()
89         self.updateSsgList()
90
91     def xon_ssgComboBox_activated(self, index):
92         if (index >= 0):
93             itemData = self.ui.ssgComboBox.itemText(index)
94             self.ssgText = str(itemData)
95             self.updateTable()
96     
97     def xon_ssgComboBox_editTextChanged(self, text):
98         self.ssgText = str(text)
99         self.statusBar().showMessage('Remote command is: '+self.updateCommand())
100         present = (self.ssgText in OVEConfig.Inst().ssgList)
101         self.ui.ssgDeleteButton.setEnabled(present)
102         self.ui.ssgSaveButton.setEnabled(not present)
103     
104     def xon_ssgCheckBox_stateChanged(self, state):
105         self.ssgChecked = (state == Qt.Checked)
106         self.updateTable()
107     
108     def xon_configUpdated(self):
109         OVECommonWindow.xon_configUpdated(self)
110         self.updateSsgList()
111         self.updateDatapaths()
112     
113     def timerEvent(self, event):
114         OVECommonWindow.timerEvent(self, event)
115
116     def customEvent(self, event):
117         OVECommonWindow.customEvent(self, event)
118     
119     def updateDatapaths(self):
120         if self.hostUuid == '':
121             self.statusBar().showMessage('No host selected')
122         else:
123             self.currentRef += 1
124             self.currentOp = 'dump-dps'
125             command = self.COMMAND_OVS_DPCTL+' dump-dps'
126             OVEFetch.Inst(self.hostUuid).execCommandFramed(self, self.currentRef, command)
127     
128     def rebuildTables(self):
129         self.ui.tabWidget.clear() # Let the garbage collector delete the pages
130         self.dpTables = []
131         self.dpFlows = []
132         self.resizeCount = []
133         headings = OVEUtil.flowDecodeHeadings()
134         
135         for dpName in self.dpNames:
136             pageWidget = QtGui.QWidget()
137             pageWidget.setObjectName(dpName+'_page')
138             gridLayout = QtGui.QGridLayout(pageWidget)
139             gridLayout.setObjectName(dpName+"_gridLayout")
140             table = QtGui.QTableWidget(pageWidget)
141             table.setObjectName(dpName+"_table")
142             table.setColumnCount(len(headings))
143             table.setRowCount(0)
144             gridLayout.addWidget(table, 0, 0, 1, 1)
145             self.dpTables.append(table)
146             self.ui.tabWidget.addTab(pageWidget, dpName)
147             self.dpFlows.append([])
148             self.resizeCount.append(0)
149             for i, heading in enumerate(headings):
150                 table.setHorizontalHeaderItem(i, QtGui.QTableWidgetItem(heading))
151
152             table.setSortingEnabled(True)
153             
154             table.sortItems(OVEUtil.getFlowColumn('source mac'))
155             table.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
156     
157     def updateSsgState(self):
158         self.ui.ssgCheckBox.setChecked(self.ssgChecked)
159     
160     def updateCommand(self, overrideText = None):
161         command = self.COMMAND_OVS_DPCTL+' dump-flows '
162         if self.currentOpIndex is not None:
163             command += self.dpNames[self.currentOpIndex]
164         exp = None
165         if overrideText is not None:
166                 exp = overrideText
167         elif self.ssgChecked:
168                 exp = self.ssgText
169                 
170         if exp is not None:
171             opts='-E '
172             if exp.startswith('!'):
173                 exp =exp[1:]
174                 opts += '-v '
175             command += " | grep "+opts+"'"+exp+"' ; test ${PIPESTATUS[0]} -eq 0 "
176
177         return command
178     
179     def updateTable(self):
180         if self.hostUuid == '':
181             self.statusBar().showMessage('No host selected')
182             self.setWindowTitle('OVS Flows')
183         elif len(self.dpNames) > 0:
184             config = OVEConfig.Inst().hostFromUuid(self.hostUuid)
185             self.setWindowTitle('OVS Flows - '+config.get('address', ''))
186             try:
187                 self.setFetchSkip()
188                 self.statusBar().showMessage('Fetching data...') 
189                 self.currentRef += 1
190                 self.currentOp = 'dump-flows'
191                 self.currentOpIndex = self.ui.tabWidget.currentIndex()
192                 OVEFetch.Inst(self.hostUuid).execCommandFramed(self, self.currentRef, self.updateCommand())
193             except Exception, e:
194                 message = 'Update failed: '+str(e)
195                 OVELog(message)
196                 self.statusBar().showMessage(message)
197     
198     def writeCurrentTable(self):
199         index = self.ui.tabWidget.currentIndex()
200         actionsColumn = OVEUtil.getFlowColumn('actions')
201         usedColumn = OVEUtil.getFlowColumn('used')
202         srcMacColumn = OVEUtil.getFlowColumn('source mac')
203         destMacColumn = OVEUtil.getFlowColumn('destination mac')
204         srcIPColumn = OVEUtil.getFlowColumn('source ip')
205         destIPColumn = OVEUtil.getFlowColumn('destination ip')
206         inportColumn = OVEUtil.getFlowColumn('inport')
207         vlanColumn = OVEUtil.getFlowColumn('vlan')
208         bytesColumn = OVEUtil.getFlowColumn('bytes')
209         
210         byteCount = 0
211         try:
212             table = self.dpTables[index]
213             table.setUpdatesEnabled(False)
214             table.setSortingEnabled(False)
215             try:
216                 flows = self.dpFlows[index]
217                 table.setRowCount(len(flows))
218                 if len(flows) > 0:
219                     table.setColumnCount(len(flows[0]))
220                 for rowNum, flow in enumerate(flows):
221                     
222                     inport = flow[inportColumn]
223                     if flow[actionsColumn] == 'drop':
224                         baseLum=172
225                     else:
226                         baseLum=239
227                     background = QtGui.QColor(baseLum+16*(inport % 2), baseLum+8*(inport % 3), baseLum+4*(inport % 5))
228                     if flow[usedColumn] == 'never':
229                         colour = QtGui.QColor(112,112,112)
230                     else:
231                         colour = Qt.black
232                         
233                     for colNum, data in enumerate(flow):
234                         
235                         item = None
236                         try:
237                             item = table.takeItem(rowNum, colNum)
238                         except:
239                             pass
240                         if item is None:
241                             item = QtGui.QTableWidgetItem('')
242
243                         if colNum == vlanColumn:
244                             item.setBackground(QtGui.QColor(255-(10*data % 192), 255-((17*data) % 192), 255-((37*data) % 192)))
245                         elif colNum == srcMacColumn or colNum == destMacColumn:
246                             cols = [int(x, 16) for x in data.split(':')]
247                             item.setBackground(QtGui.QColor(255-cols[2]*cols[3] % 192, 255-cols[3]*cols[4] % 192, 255-cols[4]*cols[5] % 192))
248                         elif colNum == srcIPColumn or colNum == destIPColumn:
249                             cols = [int(x) for x in data.split('.')]
250                             item.setBackground(QtGui.QColor(255-cols[1]*cols[2] % 192, 255-cols[2]*cols[3] % 192, 255-cols[3]*cols[0] % 192))
251                         else:
252                             item.setBackground(background)
253                             item.setForeground(colour)
254                             
255                         if colNum == bytesColumn:
256                             byteCount += int(data)
257                             
258                         # PySide 0.2.3 fails to convert long ints to QVariants and logs 'long int too large to convert to int' errors
259                         try:
260                             item.setData(Qt.DisplayRole, QVariant(data))
261                             item.setToolTip(str(data))
262                         except Exception, e:
263                             item.setText('Error: See tooltip')
264                             item.setToolTip(str(e))
265                         table.setItem(rowNum, colNum, item)
266                 
267                 if self.resizeCount[index] < 2:
268                     self.resizeCount[index] += 1
269                     for i in range(0, table.columnCount()):
270                         table.resizeColumnToContents(i)
271
272             finally:
273                 table.setUpdatesEnabled(True)
274                 table.setSortingEnabled(True)
275
276             message = 'Updated at '+str(QtCore.QTime.currentTime().toString())
277             
278             if self.lastTime is not None:
279                 timeDiff = time.time() - self.lastTime
280                 byteDiff = byteCount - self.lastByteCount
281                 bitRate = long(8 * byteDiff / timeDiff)
282                 if abs(bitRate) < 10*2**20:
283                     message += ' ('+str(bitRate/2**10)+' kbit/s)'
284                 elif abs(bitRate) < 10*2**30:
285                     message += ' ('+str(bitRate/2**20)+' Mbit/s)'
286                 else:
287                     message += ' ('+str(bitRate/2**30)+' Gbit/s)'
288                 
289             self.lastByteCount = byteCount
290             self.lastTime = time.time()
291             if table.rowCount() == 0:
292                 message += ' - Table is empty'
293             self.statusBar().showMessage(message)
294
295         except Exception, e:
296             message = 'Table update failed: '+str(e)
297             OVELog(message)
298             self.statusBar().showMessage(message)
299
300     def handleFetchEvent(self, ref, values):
301         if self.currentOp == 'dump-dps':
302             self.dpNames =values.strip().split('\n')
303             self.rebuildTables()
304             self.updateTable()
305         elif self.currentOp == 'dump-flows':
306             self.dpFlows[self.currentOpIndex] = OVEUtil.decodeFlows(values)
307             self.writeCurrentTable()
308
309     def handleFetchFailEvent(self, ref, message):
310         self.statusBar().showMessage(message)
311         OVELog('Fetch ('+self.currentOp+') failed')
312
313     def customEvent(self, event):
314         OVECommonWindow.customEvent(self, event)
315         
316     def saveSettings(self, index):
317         settings, key = OVECommonWindow.saveSettings(self, index)
318         settings.setValue(key+"/ssgText", QVariant(self.ssgText))
319         settings.setValue(key+"/ssgChecked", QVariant(self.ssgChecked))
320         
321     def loadSettings(self, index):
322         settings, key = OVECommonWindow.loadSettings(self, index)
323         self.ssgText = str(settings.value(key+"/ssgText", QVariant('10\.80\.226\..*')).toString())
324         self.ssgChecked = settings.value(key+"/ssgChecked", QVariant(False)).toBool()
325         self.ssgRe = re.compile(self.ssgText)