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