#!/usr/bin/env python3 from birdvisu.annotations import AnnotatedTopology, AnnotatorID from birdvisu.annotations.analysis import TopologyDifference, ShortestPathTree from birdvisu.ospfsock import BirdSocketConnection from birdvisu.providers import BirdSocketTopologyProvider, OspfFileTopologyProvider, OspfFileParseError from birdvisu.topo_v3 import TopologyV3, VertexID from collections import defaultdict from enum import Enum, auto from ipaddress import IPv4Address from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtCore import Slot from random import randint import sys app = QtWidgets.QApplication([]) class MyGraphicsRectItem(QtWidgets.QGraphicsRectItem): 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 self.nei[vtxid]: x1 = self.x() y1 = self.y() other = e.source if e.source != vtxid else e.target x2 = self.shapes[other].x() y2 = self.shapes[other].y() qlinef = QtCore.QLineF(x1, y1, x2, y2) self.shapes[e].setLine(qlinef) return super().mouseMoveEvent(evt) 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(): pen = QtGui.QPen(QtGui.QColor('blue')) pen.setWidth(pen.width() * 3) c = pen.color() c.setAlpha(128) pen.setColor(c) shapes[shk].setPen(pen) # FIXME: also color edges in opposite direction for sh in shapes.values(): self.scene.addItem(sh) main_window = MainWindow() main_window.show() app.exec()