1 # Copyright (c) 2010 Citrix Systems, Inc.
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:
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 from OVEStandard import *
16 from OVEConfig import *
17 from OVEFetch import *
18 from OVELogger import *
21 from OVECommonWindow import *
23 from Ui_FlowWindow import *
25 class OVEFlowWindow(QtGui.QMainWindow, OVECommonWindow):
26 LOAD_KEY = 'FlowWindow/window'
27 COMMAND_OVS_DPCTL='/usr/bin/ovs-dpctl'
30 def __init__(self, app, loadIndex = None):
31 QtGui.QMainWindow.__init__(self)
32 self.ui = Ui_FlowWindow()
35 self.currentOpIndex = None
37 self.ssgChecked = False
40 self.lastByteCount = 0
41 OVECommonWindow.__init__(self, app, loadIndex)
44 self.updateDatapaths()
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)
55 def xon_fetchPathsButton_clicked(self):
56 self.updateDatapaths()
58 def xon_hostComboBox_currentIndexChanged(self, index):
59 OVECommonWindow.xon_hostComboBox_currentIndexChanged(self, index)
61 self.updateDatapaths()
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()
69 def updateSsgList(self):
70 currentSsgText = self.ssgText
71 self.ui.ssgComboBox.clear()
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)
81 self.ui.ssgComboBox.setCurrentIndex(-1)
82 self.ui.ssgComboBox.lineEdit().setText(currentSsgText)
84 def xon_ssgDeleteButton_clicked(self):
85 if self.ssgText in OVEConfig.Inst().ssgList:
86 OVEConfig.Inst().ssgList.remove(self.ssgText)
88 OVEConfig.Inst().saveConfig()
91 def xon_ssgComboBox_activated(self, index):
93 itemData = self.ui.ssgComboBox.itemText(index)
94 self.ssgText = str(itemData)
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)
104 def xon_ssgCheckBox_stateChanged(self, state):
105 self.ssgChecked = (state == Qt.Checked)
108 def xon_configUpdated(self):
109 OVECommonWindow.xon_configUpdated(self)
111 self.updateDatapaths()
113 def timerEvent(self, event):
114 OVECommonWindow.timerEvent(self, event)
116 def customEvent(self, event):
117 OVECommonWindow.customEvent(self, event)
119 def updateDatapaths(self):
120 if self.hostUuid == '':
121 self.statusBar().showMessage('No host selected')
124 self.currentOp = 'dump-dps'
125 command = self.COMMAND_OVS_DPCTL+' dump-dps'
126 OVEFetch.Inst(self.hostUuid).execCommandFramed(self, self.currentRef, command)
128 def rebuildTables(self):
129 self.ui.tabWidget.clear() # Let the garbage collector delete the pages
132 self.resizeCount = []
133 headings = OVEUtil.flowDecodeHeadings()
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))
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))
152 table.setSortingEnabled(True)
154 table.sortItems(OVEUtil.getFlowColumn('source mac'))
155 table.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
157 def updateSsgState(self):
158 self.ui.ssgCheckBox.setChecked(self.ssgChecked)
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]
165 if overrideText is not None:
167 elif self.ssgChecked:
172 if exp.startswith('!'):
175 command += " | grep "+opts+"'"+exp+"' ; test ${PIPESTATUS[0]} -eq 0 "
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', ''))
188 self.statusBar().showMessage('Fetching data...')
190 self.currentOp = 'dump-flows'
191 self.currentOpIndex = self.ui.tabWidget.currentIndex()
192 OVEFetch.Inst(self.hostUuid).execCommandFramed(self, self.currentRef, self.updateCommand())
194 message = 'Update failed: '+str(e)
196 self.statusBar().showMessage(message)
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')
212 table = self.dpTables[index]
213 table.setUpdatesEnabled(False)
214 table.setSortingEnabled(False)
216 flows = self.dpFlows[index]
217 table.setRowCount(len(flows))
219 table.setColumnCount(len(flows[0]))
220 for rowNum, flow in enumerate(flows):
222 inport = flow[inportColumn]
223 if flow[actionsColumn] == 'drop':
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)
233 for colNum, data in enumerate(flow):
237 item = table.takeItem(rowNum, colNum)
241 item = QtGui.QTableWidgetItem('')
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))
252 item.setBackground(background)
253 item.setForeground(colour)
255 if colNum == bytesColumn:
256 byteCount += int(data)
258 # PySide 0.2.3 fails to convert long ints to QVariants and logs 'long int too large to convert to int' errors
260 item.setData(Qt.DisplayRole, QVariant(data))
261 item.setToolTip(str(data))
263 item.setText('Error: See tooltip')
264 item.setToolTip(str(e))
265 table.setItem(rowNum, colNum, item)
267 if self.resizeCount[index] < 2:
268 self.resizeCount[index] += 1
269 for i in range(0, table.columnCount()):
270 table.resizeColumnToContents(i)
273 table.setUpdatesEnabled(True)
274 table.setSortingEnabled(True)
276 message = 'Updated at '+str(QtCore.QTime.currentTime().toString())
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)'
287 message += ' ('+str(bitRate/2**30)+' Gbit/s)'
289 self.lastByteCount = byteCount
290 self.lastTime = time.time()
291 if table.rowCount() == 0:
292 message += ' - Table is empty'
293 self.statusBar().showMessage(message)
296 message = 'Table update failed: '+str(e)
298 self.statusBar().showMessage(message)
300 def handleFetchEvent(self, ref, values):
301 if self.currentOp == 'dump-dps':
302 self.dpNames =values.strip().split('\n')
305 elif self.currentOp == 'dump-flows':
306 self.dpFlows[self.currentOpIndex] = OVEUtil.decodeFlows(values)
307 self.writeCurrentTable()
309 def handleFetchFailEvent(self, ref, message):
310 self.statusBar().showMessage(message)
311 OVELog('Fetch ('+self.currentOp+') failed')
313 def customEvent(self, event):
314 OVECommonWindow.customEvent(self, event)
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))
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)