From 3cbb14c1af854ea5bd51af7c8e0b14bc6d1bb965 Mon Sep 17 00:00:00 2001 From: Pavel 'LEdoian' Turinsky Date: Tue, 18 Jul 2023 04:14:14 +0200 Subject: [PATCH] poor: Move everything into the QMainWindow and add basic UI --- birdvisu/providers.py | 4 +- poor_mans_visualisation.py | 371 ++++++++++++++++++++++++------------- 2 files changed, 245 insertions(+), 130 deletions(-) diff --git a/birdvisu/providers.py b/birdvisu/providers.py index 0b786b9..a31a319 100644 --- a/birdvisu/providers.py +++ b/birdvisu/providers.py @@ -496,13 +496,13 @@ class BirdSocketTopologyProvider(TopologyProvider): response = sock.request(request) except BirdError as e: if e.text == 'There are multiple OSPF protocols running\n': - protocols = self.find_runing_ospf(sock) + protocols = self.find_running_ospf(sock) raise BirdMultipleInstancesError(e, protocols) from e raise parser = OspfFileTopologyParser(version=self.version, area=self.area) return parser.parse(response.text, freeze=self.freeze) - def find_runing_ospf(self, bird): + def find_running_ospf(self, bird): resp = bird.request('show protocols') # There are three sections: the header, the table itself and a 0000 at # the end. diff --git a/poor_mans_visualisation.py b/poor_mans_visualisation.py index 00a5229..e2615af 100755 --- a/poor_mans_visualisation.py +++ b/poor_mans_visualisation.py @@ -1,160 +1,275 @@ #!/usr/bin/env python3 -# Get topologies - import sys -from birdvisu.providers import BirdSocketTopologyProvider, OspfFileTopologyProvider - - +from birdvisu.providers import BirdSocketTopologyProvider, OspfFileTopologyProvider, OspfFileParseError ref_topo_file = 'reference.ospf' -if len(sys.argv) > 1: - ref_topo_file = sys.argv[1] - -ref_topo = OspfFileTopologyProvider(ref_topo_file).get_topology() - -def get_empty_topology(): - from birdvisu.topo_v3 import TopologyV3 - topo = TopologyV3() - topo.freeze() - return topo - -if len(sys.argv) > 2 and sys.argv[2] == '--no-bird': - cur_topo = get_empty_topology() -else: - try: - cur_topo = BirdSocketTopologyProvider(instance='ospf1', area=0).get_topology() - except OSError as e: - # I know this is not how you print exceptions. - print('Cannot get topology from BIRD: {e}') - print('Will provide an empty topology.') - cur_topo = get_empty_topology() - -# Create combined topology from birdvisu.topo_v3 import TopologyV3 -combined_topology = TopologyV3.combine_topologies(reference=ref_topo, current=cur_topo) -combined_topology.freeze() - -# Annotate it - from birdvisu.topo_v3 import VertexID from birdvisu.annotations import AnnotatedTopology, AnnotatorID from birdvisu.annotations.analysis import TopologyDifference, ShortestPathTree from ipaddress import IPv4Address -annot_topo = AnnotatedTopology(combined_topology) -annotators = [ - AnnotatorID(TopologyDifference), - AnnotatorID(ShortestPathTree, (VertexID( - family=None, - is_router=True, - address=None, - router_id=int(IPv4Address('172.23.100.10')), - dr_id=None, - discriminator=None - ), 'current')), - ] -for ann_id in annotators: - annot_topo.run_annotator(ann_id) - -# --- - -# Show it - -#from birdvisu.visualisation import annotators -#from birdvisu import maps_new - -# annotators.create_qgritems does not like being run without Qt initialization. from PySide6 import QtCore, QtGui, QtWidgets app = QtWidgets.QApplication([]) -#annotated_topology = maps_new.annotate_topology(combined_topology, -# # A semi-canonical set of annotators: -# [ -# annotators.extract_positions, -# annotators.random_position, -# annotators.assign_brushes, -# annotators.create_qgritems, -# ] -# ) - from random import randint from collections import defaultdict -shapes = dict() -nei = defaultdict(lambda: []) class MyGraphicsRectItem(QtWidgets.QGraphicsRectItem): - #def __init__(self, *a, **kwa): - # return super().__init__(*a, **kwa) + def __init__(self, nei, shapes, *a, **kwa): + self.nei = nei + self.shapes = shapes + return super().__init__(*a, **kwa) #def itemChange(self, change, val): # return super().itemChange(change, val) def mouseMoveEvent(self, evt): vtxid = self.data(0) - for e in nei[vtxid]: + for e in self.nei[vtxid]: x1 = self.x() y1 = self.y() other = e.source if e.source != vtxid else e.target - x2 = shapes[other].x() - y2 = shapes[other].y() + x2 = self.shapes[other].x() + y2 = self.shapes[other].y() qlinef = QtCore.QLineF(x1, y1, x2, y2) - shapes[e].setLine(qlinef) + self.shapes[e].setLine(qlinef) return super().mouseMoveEvent(evt) -for k, v in annot_topo.topology.vertices.items(): - size = 30 if k.is_router else 10 - x, y = randint(0, 1920), randint(0, 1080) - shape = MyGraphicsRectItem(-size/2, -size/2, size, size) - shape.setPos(x,y) - # TODO:brush - label_text = str(IPv4Address(k.router_id)) if k.is_router else str(k.address) # Surprisingly works for all the possible addresses. - label = QtWidgets.QGraphicsSimpleTextItem(label_text, parent=shape) - label.setY(size*0.8) - text_width = label.boundingRect().width() - label.setX(-text_width/2) - shape.setData(0, k) - shape.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable) - shapes[k] = shape - -for e in annot_topo.topology.edges: - start = shapes[e.source].pos() - end = shapes[e.target].pos() - qlinef = QtCore.QLineF(start, end) - line = QtWidgets.QGraphicsLineItem(qlinef) - line.setData(0, e) - nei[e.source].append(e) - nei[e.target].append(e) - shapes[e] = line - -# Render the widget - -scene = QtWidgets.QGraphicsScene() - -#for tagsrc in [ -# annotated_topology.router_annotations.values(), -# annotated_topology.network_annotations.values(), -# annotated_topology.link_annotations.values(), -# ]: -# for taglist in tagsrc: -# assert len(taglist) > 0 -# assert isinstance(taglist[-1], QtWidgets.QGraphicsItem) -# scene.addItem(taglist[-1]) - -for sh in shapes.values(): scene.addItem(sh) - -view = QtWidgets.QGraphicsView(scene) -view.setDragMode(view.DragMode.ScrollHandDrag) -#view.show() - - -main_window = QtWidgets.QMainWindow() -main_window.setCentralWidget(view) - -menu = main_window.menuBar().addMenu('Hello') -act = QtGui.QAction('Hi') -#act.setToolTip('Howdy') -menu.addAction(act) +from enum import Enum, auto +from PySide6.QtCore import Slot +from birdvisu.ospfsock import BirdSocketConnection + +class BirdTopologyLoader(QtWidgets.QDialog): + def __init__(self, *a, **kwa): + super().__init__(*a, **kwa) + self.setModal(True) + self.result_ = (None, None, None) + outer = QtWidgets.QVBoxLayout(self) + # Area + inner = QtWidgets.QHBoxLayout() + inner.addWidget(QtWidgets.QLabel('Area:')) + line = QtWidgets.QLineEdit(self) + line.setText('-1') + self.line = line + inner.addWidget(line) + outer.addLayout(inner) + + inner = QtWidgets.QHBoxLayout() + inner.addWidget(QtWidgets.QLabel('Instance:')) + combo = QtWidgets.QComboBox() + combo.addItems(self.get_instances()) + self.combo = combo + inner.addWidget(combo) + outer.addLayout(inner) + + inner = QtWidgets.QHBoxLayout() + inner.addWidget(QtWidgets.QLabel('Protocol:')) + combo = QtWidgets.QComboBox() + combo.addItems(['Guess!', 'OSPFv2', 'OSPFv3']) + self.combo2 = combo + inner.addWidget(combo) + outer.addLayout(inner) + + buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) + buttons.accepted.connect(self.saveResult) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + outer.addWidget(buttons) + + @Slot() + def saveResult(self): + try: + area = int(self.line.text()) + except ValueError: + area = int(IPv4Address(self.line.text())) + if area == -1: area = None + inst = self.combo.currentText() + proto = { + 'OSPFv2': 2, + 'OSPFv3': 3, + 'Guess!': None, + }[self.combo2.currentText()] + self.result_ = (area, inst, proto) + + def get_instances(self): + # Our code is stupid, so we initialise a BirdSocketTopologyProvider just to get instances and then drop it. + prov = BirdSocketTopologyProvider() + bird = BirdSocketConnection() + return prov.find_running_ospf(bird) + + +class MainWindow(QtWidgets.QMainWindow): + class Tool(Enum): + MoveTopology = auto() + ShortestPath = auto() + + def __init__(self, *a, **kwa): + super().__init__(*a, **kwa) + self.ref_topo_provider = None + self.cur_topo_provider = None + self.annotators = self.get_annotators() + self.annot_topo = None + self.highlighter = None + self.tool = self.Tool.MoveTopology + self.scene = QtWidgets.QGraphicsScene() + self.view = QtWidgets.QGraphicsView(self.scene) + self.view.setDragMode(self.view.DragMode.ScrollHandDrag) + self.setCentralWidget(self.view) + self.statusbar = self.statusBar() + self.statusbar.showMessage('Hello!') + + self.menubar = self.menuBar() + mode_menu = self.menubar.addMenu('&Mode') + short_path_act = QtGui.QAction("Sh. path &DAG", self) + short_path_act.triggered.connect(self.shortestPathMode) + mode_menu.addAction(short_path_act) + + topo_menu = self.menubar.addMenu('&Topology') + open_ref_act = QtGui.QAction("&Load reference", self) + open_ref_act.triggered.connect(self.openRefTopology) + topo_menu.addAction(open_ref_act) + + cur_topo_menu = topo_menu.addMenu("Load ¤t") + running_bird_act = QtGui.QAction('&BIRD', self) + running_bird_act.triggered.connect(self.curTopologyFromBird) + cur_topo_menu.addAction(running_bird_act) + from_file_act = QtGui.QAction('&from file', self) + from_file_act.triggered.connect(self.curTopologyFromFile) + cur_topo_menu.addAction(from_file_act) + + refresh_act = QtGui.QAction("&Refresh", self) + refresh_act.triggered.connect(self.refreshTopologies) + topo_menu.addAction(refresh_act) + + def get_annotators(self): + return [ + AnnotatorID(TopologyDifference), + AnnotatorID(ShortestPathTree, (VertexID( + family=None, + is_router=True, + address=None, + router_id=int(IPv4Address('172.23.100.10')), + dr_id=None, + discriminator=None + ), 'current')), + ] + + @Slot() + def shortestPathMode(self): + self.tool = self.Tool.ShortestPath + + @Slot() + def openRefTopology(self): + filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open reference topology', '.', 'OSPF files(*.ospf);;All files(*)')[0] + if filename == '': return # Do nothing + self.ref_topo_provider = OspfFileTopologyProvider(filename) + try: + ref_topo = self.ref_topo_provider.get_topology() + except OspfFileParseError as e: + warning = QtWidgets.QMessageBox.critical(self, "Bad reference topology", f"The reference topology seems to be malformed: {e}.\nPlease select a valid reference topology.") + self.ref_topo_provider = None + return + self.refreshTopologies() + + @Slot() + def curTopologyFromBird(self): + loader = BirdTopologyLoader(self) + loader.exec() + area, instance, version = loader.result_ + self.cur_topo_provider = BirdSocketTopologyProvider(instance=instance, area=area, version=version) + try: + cur_topo = self.cur_topo_provider.get_topology() + except OspfFileParseError as e: + warning = QtWidgets.QMessageBox.critical(self, "Bad current topology", f"The current topology seems to be malformed: {e}.\nPlease select a valid topology.") + self.cur_topo_provider = None + return + self.refreshTopologies() + + @Slot() + def curTopologyFromFile(self): + filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open current topology', '.', 'OSPF files(*.ospf);;All files(*)')[0] + if filename == '': return # Do nothing + self.cur_topo_provider = OspfFileTopologyProvider(filename) + try: + cur_topo = self.cur_topo_provider.get_topology() + except OspfFileParseError as e: + warning = QtWidgets.QMessageBox.critical(self, "Bad current topology", f"The current topology seems to be malformed: {e}.\nPlease select a valid topology.") + self.cur_topo_provider = None + return + self.refreshTopologies() + + @Slot() + def refreshTopologies(self): + # Pre-checks: + msg = '' + if self.ref_topo_provider is None: msg += 'Please select reference topology. ' + if self.cur_topo_provider is None: msg += 'Please select current topology. ' + if msg: + self.statusbar.showMessage(msg) + # Nothing more to do. + return + # We just drop anything we had before, since we will be re-reading all the files. + try: + ref_topo = self.ref_topo_provider.get_topology() + except OspfFileParseError as e: + warning = QtWidgets.QMessageBox.critical(self, "Bad reference topology", f"The reference topology seems to be malformed: {e}.\nPlease select a valid reference topology.") + self.ref_topo_provider = None + return + try: + cur_topo = self.cur_topo_provider.get_topology() + except OspfFileParseError as e: + warning = QtWidgets.QMessageBox.critical(self, "Bad current topology", f"The current topology seems to be malformed: {e}.\nPlease select a valid topology.") + self.cur_topo_provider = None + return + self.scene.clear() + combined_topology = TopologyV3.combine_topologies(reference=ref_topo, current=cur_topo) + combined_topology.freeze() + self.annot_topo = AnnotatedTopology(combined_topology) + for ann_id in self.annotators: + self.annot_topo.run_annotator(ann_id) + # Draw it + self.set_current_highlighter() + self.ad_hoc_draw_visu() + + def set_current_highlighter(self): + # TODO! + self.highlighter = self.annotators[-1] + + def ad_hoc_draw_visu(self): + shapes = dict() + self.nei = defaultdict(lambda: []) + for k, v in self.annot_topo.topology.vertices.items(): + size = 30 if k.is_router else 10 + x, y = randint(0, 1920), randint(0, 1080) + shape = MyGraphicsRectItem(self.nei, shapes, -size/2, -size/2, size, size) + shape.setPos(x,y) + # TODO:brush + label_text = str(IPv4Address(k.router_id)) if k.is_router else str(k.address) # Surprisingly works for all the possible addresses. + label = QtWidgets.QGraphicsSimpleTextItem(label_text, parent=shape) + label.setY(size*0.8) + text_width = label.boundingRect().width() + label.setX(-text_width/2) + shape.setData(0, k) + shape.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable) + shapes[k] = shape + + for e in self.annot_topo.topology.edges: + start = shapes[e.source].pos() + end = shapes[e.target].pos() + qlinef = QtCore.QLineF(start, end) + line = QtWidgets.QGraphicsLineItem(qlinef) + line.setData(0, e) + self.nei[e.source].append(e) + self.nei[e.target].append(e) + shapes[e] = line + if self.highlighter is not None and self.highlighter in self.annot_topo.annotations: + ann = self.annot_topo.annotations[self.highlighter] + for shk in ann.for_vertex.keys() | ann.for_edge.keys(): + shapes[shk].setPen(QtGui.QPen(QtGui.QColor('blue'))) + for sh in shapes.values(): self.scene.addItem(sh) + +main_window = MainWindow() main_window.show() - app.exec()