RNDMISC-441: Second Milestone

This commit is contained in:
Leon Schoch
2023-09-01 07:13:17 +00:00
parent 911d5a4121
commit 41ece9a82a
6 changed files with 634 additions and 9 deletions

Binary file not shown.

View File

@@ -7,7 +7,7 @@
\chapter*{Abkürzungsverzeichnis} % chapter*{..} --> keine Nummer, kein "Kapitel"
% Nicht ins Inhaltsverzeichnis
% \addcontentsline{toc}{chapter}{Akürzungsverzeichnis} % Damit das doch ins Inhaltsverzeichnis kommt
\addcontentsline{toc}{chapter}{Akürzungsverzeichnis} % Damit das doch ins Inhaltsverzeichnis kommt
% Hier werden die Abkürzungen definiert
\begin{acronym}[DHBW]
@@ -40,6 +40,9 @@
\acro{OSPF}[OSPF]{Open Shortest Path First}
\acro{ASN}[ASN]{Autonome System Nummer}
\acro{JSON}[JSON]{JavaScript Object Notation}
\acro{SSH}[SSH]{Secure Shell}
\acro{TTL}[TTL]{Time to live}
\acro{DRY}[DRY]{Don't repeat yourself}

Binary file not shown.

View File

@@ -179,7 +179,7 @@ Vgl.\cite{\thefield{entrykey}}}
commentstyle=\color{olive},
keywordstyle=\color{magenta},
numberstyle=\tiny\color{lightgray},
stringstyle=\color{violet},
stringstyle=\color{orange},
basicstyle=\ttfamily\footnotesize,
breakatwhitespace=false,
breaklines=true,

View File

@@ -140,7 +140,7 @@ Gutachter der Studienakademie & \BetreuerDHBW \\
\end{titlepage}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\pagenumbering{Roman}
\input{erklaerung.tex}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -163,12 +163,16 @@ Gutachter der Studienakademie & \BetreuerDHBW \\
\newpage
\tableofcontents % Inhaltsverzeichnis hier ausgeben
\listoffigures % Liste der Abbildungen
\addcontentsline{toc}{chapter}{Abbildungsverzeichnis}
\listoftables % Liste der Tabellen
\addcontentsline{toc}{chapter}{Tabellenverzeichnis}
\lstlistoflistings % Liste der Listings
\listofequations % Liste der Formeln
\addcontentsline{toc}{chapter}{Liste der Code Snippets}
% Jetzt kommt der "eigentliche" Text
\include{abk} % Abkürzungsverzeichnis
\pagenumbering{arabic}
\include{kapitel1}
\include{kapitel2}
\include{kapitel3}

View File

@@ -122,20 +122,638 @@ Mit seiner intelligenten Konvertierung und Verwaltung von Routen durch \ac{BGP}
\subsection{Generieren der Config Files für Bird} %TODO RNDMISC-410
Um die Routen an den Bird Routing Daemon übermitteln zu können, müssen diese erst in eine für Bird verständliche Konfigurationsdatei umgewandelt werden.
%Warum verwenden wir Jinja und wie funktionierts?
Für den einfachen Umgang mit Routen wurde für die \verb|routes| Variable eine Python Dataclass angelegt, welche das IP-Präfix, die IP-Version und eine Liste der gesetzen \ac{BGP}-Communities enthält.
\subsubsection{Integrität der Konfigurationsdatei sicherstellen} %TODO RNDMISC-420
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:route-dataclass},
captionpos=b, % Caption unter den Code setzen
caption={Python Dataclass für Routen Objekte}]
@dataclass
class Route:
prefix: str
encode: str
communities: list[str]
def __init__(self, prefix="", encode="", communities=[]):
self.prefix = prefix
self.encode = encode
self.communities = communities
def __str__(self):
return f"{self.encode} {self.prefix} {self.communities}"
def decode_prefix(self):
self.prefix = self.prefix.replace("_", "/")
def encode_prefix(self):
self.prefix = self.prefix.replace("/", "_")
def get_communities(self):
communities = self.communities
return list(map(lambda com: com.replace(":", ","), communities))
\end{lstlisting}
Neben den genannten Feldern, kann die Dataclass auch noch Methoden zur verarbeitung der Felder, ähnlich wie eine normale Klasse beinhalten.
Besonders zu betonen ist hier die \verb|get_communities| Methode, welche die Communities in ein von Bird akzeptiertes Format umwandelt.
\newpage
Die in Consul gespeicherten Routen werden dann periodisch von Consul über dessen eigene \ac{API} abgefragt.
Hierfür ist die \verb|parse_consul_values| Methode zuständig.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:parse-consul-values},
captionpos=b, % Caption unter den Code setzen
caption={parse\_consul\_values Methode}]
def parse_consul_values(values, watched_prefix) -> (list[Route], list[Route]):
if not values:
return [], []
v4_routes = []
v6_routes = []
for entry in values:
route_entry = Route()
route_entry.prefix = entry["Key"].split(watched_prefix)[1]
json_communities = entry["Value"].decode("utf-8")
route_entry.communities = json.loads(json_communities)["communities"]
route_entry.encode, route_entry.prefix = route_entry.prefix.split("/")
route_entry.decode_prefix()
if route_entry.encode == "IPv4":
v4_routes.append(route_entry)
elif route_entry.encode == "IPv6":
v6_routes.append(route_entry)
return v4_routes, v6_routes
\end{lstlisting}
Im Laufe der Methode werden die abgefragten Einträge in Routenobjekte umgewandelt.
Um herauszufinden um welche IP Version es sich bei der Route handelt, wird der entsprechende Key des Pfades ausgelesen, da dieser die IP Version mit im Namen trägt.
Als Resultat gibt die Methode ein Tupel zurück, wobei eines die IPv4 Routen und das andere die IPv6 Routen sind.
Diese Aufteilung ist notwendig, da die in Debian 11 mitgelieferte Version von Bird eine klare Auftrennung dieser fordert.
Neuere Versionen von Bird können auch mit beiden IP Versionen gleichzeitig umgehen.
Durch die Auftrennung der beiden IP-Versionen, muss auch die Konfigurationsdatei für Bird, zweimal generiert werden.
Mit der Methode \verb|generate_bird_files| werden Umgebungsvariablen geladen, welche für die Generierung der Konfiguration benötigt werden.
Neben diesen übergibt die Methode auch die Routen als Parameter weiter.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:generate-bird-files},
captionpos=b, % Caption unter den Code setzen
caption={generate\_bird\_files Methode}]
def generate_bird_files(v4_routes, v6_routes, pybird, pybird6):
gen = BirdConfigGenerator()
click.echo("Generating and committing config files")
gen.bird_config(
v4_routes,
"route_template.j2",
"../config/bird",
"v4.conf",
os.getenv("ROUTER_ID"),
os.getenv("LOCAL_AS"),
os.getenv("REMOTE_AS"),
os.getenv("BGP_NEIGHBOR"),
"ANEXIA Route Injection v4",
)
gen.bird_config(
v6_routes,
"route_template.j2",
"../config/bird",
"v6.conf",
os.getenv("ROUTER_IDv6"),
os.getenv("LOCAL_AS"),
os.getenv("REMOTE_AS"),
os.getenv("BGP_NEIGHBORv6"),
"ANEXIA Route Injection v6",
)
pybird.commit_config()
pybird6.commit_config()
click.echo("Done")
\end{lstlisting}
\newpage
Die Methode \verb|bird_config|, welche um eine gute Struktur zu wahren zu einer gesonderten Datei und Klasse angehört, ruft die \verb|get_communities| Methode der Route Dataclass auf, um die Routen in einer von Bird lesbares Format zu wandeln.
Des Weiteren wird hier die tatsächliche Konfiguration auf das Dateisystem geschrieben.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:bird-config},
captionpos=b, % Caption unter den Code setzen
caption={bird\_config Methode}]
def bird_config(
self,
unconverted_routes,
route_template,
config_path,
config_name,
router_id=None,
local_as=None,
remote_as=None,
bgb_neighbor=None,
description=None,
):
converted_routes = []
for route in unconverted_routes:
route.communities = route.get_communities()
converted_routes.append(route)
target_file = self.__prepare_config_path(config_path, config_name)
self.__write_config_file(
target_file,
converted_routes,
route_template,
router_id,
local_as,
remote_as,
bgb_neighbor,
description,
)
\end{lstlisting}
Um die Routen an den Bird Routing Daemon übermitteln zu können, müssen diese erst in eine für Bird verständliche Konfigurationsdatei umgewandelt werden.
Zur Realisierung wird die Jinja2 Templating Engine verwendet, da diese die Möglichkeit schafft, alle Eigenschaften des Injectors dynamisch zu konfigurieren.
Somit kann der tatsächliche Code aller Injectoren identisch sein, und Variable Eigenschaften wie z.B. die \verb|router_id| oder der \ac{BGP}-Peering Nachbar können beim Ausrollen festgelegt werden.
\begin{lstlisting}[%language=tex,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:route-template},
captionpos=b, % Caption unter den Code setzen
caption={Jinja Template zur Konfigurationsgenerierung}]
protocol static injected_routes {
{% for route in routes %}
route {{route.prefix}} via {{router_id}} {
{% for community in route.communities %}
bgp_community.add(({{community}}));
{% endfor %}
};
{% endfor %}
}
protocol bgp Route_Injection {
description "{{description}}";
local as {{local_as}};
neighbor {{bgp_neighbor}} as {{remote_as}};
next hop self;
export filter {
if proto = "injected_routes" then accept;
reject;
};
}
\end{lstlisting}
In diesem Template finden sich einige Variablen
\begin{itemize}
\item \verb|routes| (Python Liste mit Routen Elementen)
\item \verb|router_id| (IPv4 Addresse des Injectors)
\item \verb|local_as| (Lokales \ac{ASN})
\item \verb|remote_as| (Nachbar \ac{ASN})
\item \verb|bgp_neighbor| (Nachbar \ac{BGP}-Router IPv4 Adresse)
\item \verb|description| (Beschreibung des Protokolls)
\end{itemize}
wieder, welche entweder im Code oder dynamisch beim Ausrollen, also ausrollen des Injectors gesetzt werden müssen.
Jinja kann auch mit Listen und verschachtelten Listen umgehen, was bei der \verb|routes| Variable zum Einsatz kommt.
Jinja kann dann über die Liste der Routenobjekte iterieren und für jede Route einen gesonderten Eintrag mit den jeweiligen \ac{BGP}-Communities erstellen.
Folglich ein Beispiel einer möglichen Konfiguration. \acp{ASN} und \verb|router_id|s können hier entweder als Umgebungsvariable oder von einer .env Datei geladen werden.
Die Routen werden dynamisch während der Programmlaufzeit angegeben, konvertiert und konfiguriert.
\begin{lstlisting}[%language=tex,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:bird-configuration-example},
captionpos=b, % Caption unter den Code setzen
caption={Beispiel einer Konfigurationsdatei}]
protocol static injected_routes {
route 1.1.1.1/32 via 172.20.0.5 {
bgp_community.add((47147,3200));
bgp_community.add((12345,12345));
};
}
protocol bgp Route_Injection {
description "ANEXIA Route Injection v4";
local as 64701;
neighbor 172.20.0.6 as 65001;
next hop self;
export filter {
if proto = "injected_routes" then accept;
reject;
};
\end{lstlisting}
\newpage
Somit können nun Konfigurationsdateien für Bird erstellt werden.
Sollte jedoch während dem Rendern des Templates ein Fehler auftreten, kann es passieren, dass eine inkorrekte oder gar keine Konfigurationsdatei generiert wird.
Dies könnte einen negativen Einfluss auf die Operation des Service haben und muss somit verhindert werden.
Um das Problem zu verhindern, wird die Konfiguration erst in eine temporäre Datei geschrieben.
Wenn dies erfolgreich war, dann wird die temporäre Datei umbenannt und in das echte Konfigurationsverzeichnis geschoben.
Da hier eine Datei überschrieben statt angepasst wird, gehen Dateiberechtigungen verloren und müssen neu gesetzt werden.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:write-config-file},
captionpos=b, % Caption unter den Code setzen
caption={\_\_write\_config\_file Methode}]
def __write_config_file(
self,
target_path,
routes,
template,
router_id=None,
local_as=None,
remote_as=None,
bgp_neighbor=None,
description=None,
):
template = self.env.get_template(template)
with NamedTemporaryFile(delete=False, mode="w") as conf:
conf.write(
template.render(
routes=routes,
router_id=router_id,
local_as=local_as,
remote_as=remote_as,
bgp_neighbor=bgp_neighbor,
description=description,
)
)
try:
os.chmod(conf.name, 0o660)
shutil.move(conf.name, target_path)
except Exception as e:
echo(e)
os.remove(conf.name)
\end{lstlisting}
\newpage
\subsection{Status der Routen von Bird abfragen} %TODO RNDMISC-361
\subsubsection{Evaluation der pybird Bibliothek}
\subsection{Bird und Bird6 aufteilen} %TODO RNDMISC-362
Da die entwickelte \ac{API} über einen Status Endpunkt verfügt, welcher letztendlich von der Anexia Engine abgerufen wird, muss auch der Injektor die benötigten Statusinformationen zur Verfügung stellen.
Hierfür wurde evaluiert, welche Python Bibliothek sich am besten zu diesem Zwecke eignet.
Die Entscheidung für die Verwendung der `pybird` Bibliothek wurde aus folgenden Gründen getroffen:
\begin{enumerate}
\item Funktionalität: Die `pybird` Bibliothek wurde speziell dafür entwickelt mit dem Bird Routing Daemon zu interagieren.
\item Direkte Socket Anbindung: `pybird` unterstützt die direkte Kommunikation mit dem Bird Control Socket, was eine erleichterte Kommunikation ermöglicht.
\item Aktualisierung und Wartung: Da die `pybird` Bibliothek aktiv gepflegt wird, kann sichergestellt werden, dass sie auch mit zukünftigen Versionen des Bird Routing Daemons kompatibel sein wird.
Des Weiteren kann so auch sichergestellt werden, dass das Route Injection Project sich auch in der Zukunft noch auf diese Bibliothek verlassen kann.
\item Open-Source: Durch den offenen Quellcode, kann sichergestellt werden, dass der Code keine Malware/Spyware enthält.
Sollte es nötig sein, kann der Quellcode der Bibliothek geforked, und auf die Bedürfnisse der Anexia angepasst werden.
\end{enumerate}
Über die Methode \verb|get_routes| der \verb|PyBird| Klasse können die von Bird übernommenen Routen abgefragt werden.
Als Parameter kann das Präfix der Route angegeben werden, sodass die Ausgabe auf nur dieses Präfix beschränkt wird.
Pybird gibt die Ausgabe dann in folgendem Format zurück:
\begin{lstlisting}[language=tex,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:pybird-raw-output},
captionpos=b, % Caption unter den Code setzen
caption={Unverarbeitete Ausgabe von Pybird}]
[{'community': '65535:65281',
'prefix': '1.2.3.4/32',
'peer': '172.20.0.3',
'interface': 'eth0',
'source': 'injected_routes',
'time': '13:37:47'}]
\end{lstlisting}
Von dieser Ausgabe wird jedoch nur der Teil, welcher die Communities betrifft benötigt.
Folglich muss die Ausgabe noch im Code angepasst werden.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:respond-state-to-consul},
captionpos=b, % Caption unter den Code setzen
caption={respond\_state\_to\_consul Methode}]
def respond_state_to_consul(
consul: ckv, pybird: PyBird, route: Route, injector_id: str
) -> None:
state = pybird.get_routes(prefix=route.prefix)
route.encode_prefix()
try:
actual_communities = state[0].get("community", "").split(" ")
except IndexError:
actual_communities = []
expected_communities = list(route.communities)
state = get_bird_communities(expected_communities, actual_communities)
state = json.dumps({"communities": state})
try:
consul.kv.put(
f"v1/state/{injector_id}/{route.encode}/{route.prefix}",
state,
)
except requests.exceptions.ConnectionError:
click.echo("Lost consul while reporting route :c")
return
route.decode_prefix()
click.echo(f"Route {route.prefix} with state {actual_communities} pushed to consul")
\end{lstlisting}
Um den Status zu bestimmen, werden die Communities, welche im Routenobjekt abgespeichert sind, mit den Communities welche von Bird zurückgegeben wurde verglichen.
Stimmen diese überein, so kann davon ausgegangen werden, dass Bird alle Communities akzeptiert hat und an den Nachbar Router übermitteln kann.
Sollte es Abweichungen zwischen den Communities geben, bedeutet dies, dass noch nicht alle Communities von Bird akzeptiert wurden.
Als Folge dessen werden auch nur die aktuell in Bird eingetragen Communities zurück an Consul übermittelt.
Die \ac{API}-Komponente des Route Injection Service fragt dann den in Consul eingetragenen Status ab und bestimmt dann selbst, ob der gesamte Prozess erfolgreich, noch im Gange oder fehlerhaft war.
Dies wird dann von der Anexia Engine interpretiert und ist für den Nutzer sichtbar.
\subsection{Realisierung des Heartbeats}
\subsection{Emergency-Mode implementieren} %TODO RNDMISC-363
Um sicherzustellen, dass die API den aktuellen Status der online verfügbaren Injektoren erfassen kann, verwenden die Injektoren ein sogenanntes `Heartbeat`-System, das seine Aktivität in Consul signalisiert.
Dieses Heartbeat wird in Form eines Wertes (Value) in Consul gemeldet.
Dieser Prozess ermöglicht es der API, den Zustand der einzelnen Injektoren zu überwachen und sicherzustellen, dass sie ordnungsgemäß funktionieren.
Jeder Injektor meldet seinen Status durch das Schreiben eines Wertes (Value) in einen spezifischen Schlüssel-Wert-Pfad in Consul.
Dieser Pfad lautet: \verb|v1/state/<injector_id>/heartbeat|.
Hierbei steht \verb|<injector_id>| für die eindeutige Kennung des Injektors.
Der Wert (Value), der in den oben genannten Schlüssel-Wert-Pfad geschrieben wird, hat den Inhalt `\{\}`, was auf ein leeres JSON-Objekt hinweist.
Dieses leere Objekt dient als Platzhalter und signalisiert der API, dass der Injektor aktiv ist und seinen Heartbeat meldet.
Der gemeldete Wert (Value) hat eine \ac{TTL} von 10 Sekunden.
Dies bedeutet, dass nachdem der Injektor seinen Heartbeat gemeldet hat, der Wert für 10 Sekunden in Consul bestehen bleibt.
Wenn innerhalb dieses Zeitraums keine weiteren Heartbeats gemeldet werden, wird der Wert automatisch aus Consul entfernt.
Durch das Heartbeat-System kann die API regelmäßig aktualisierte Informationen erhalten, welche Injektoren online und funktionsfähig sind.
\vspace{1cm}
Um den Heartbeat im Programmcode möglichst modular zu realisieren wurde hierfür eine eigene Methode erstellt.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:create-heartbeat},
captionpos=b, % Caption unter den Code setzen
caption={create\_heartbeat Methode}]
def create_heartbeat(consul, injector_id):
session_id = consul.session.create(behavior="delete", ttl=10)
consul.kv.put(
key=f"v1/state/{injector_id}/heartbeat", value="{}", acquire=session_id
)
return session_id
\end{lstlisting}
Nach dem initialen Anlegen des Heartbeateintrages wird dieser alle fünf Sekunden erneuert und sicherzustellen, dass die \ac{TTL} des Eintrages nicht abläuft.
\subsection{Emergency-Mode} %TODO RNDMISC-363
Um sicherzustellen, dass der Route Injection Service auch in Szenarien von Netzwerkproblemen zwischen der \ac{API} und den Injektoren effizient arbeiten kann, sei es für das Hinzufügen, Ändern oder Löschen von Routen, wurde eine maßgebliche Funktion eingeführt, die als Emergency-Mode, bzw. Notfallmodus bekannt ist.
Diese Funktion wurde entwickelt, um direkten Zugriff auf die Injektoren zu ermöglichen und Routenverwaltungsvorgänge über die Kommandozeile durchzuführen.
Der Emergency-Mode fungiert als eine Art Sicherheitsvorkehrung, die sicherstellt, dass die Verfügbarkeit und Funktionalität des Dienstes aufrechterhalten werden kann, selbst wenn die übliche Kommunikation zwischen der \ac{API} und den Injektoren temporär gestört ist.
Der Namensteil `Mode` lässt vermuten, dass es sich um einen tatsächlichen Operationsmodus handelt.
Dies ist allerdings nicht ganz korrekt.
Der Emergency-Mode ist eher als Funktionalitätserweiterung zu sehen und kann selbst dann aktiviert werden, wenn die Kommunikation zwischen \ac{API} und Injector intakt ist.
Dies ist insbesondere nützlich, wenn dringende Änderungen an den Routingeinstellungen erforderlich sind, die nicht auf die normale Kommunikation warten können.
Um Zugriff auf die Kommandozeile zu erhalten, muss ein Nutzer sich über \ac{SSH} auf den Injector einloggen.
Firmeninterne Automatismen stellen sicher, dass nur befugte Nutzer Zugriff auf das System haben.
Zur Gewährleistung der Integrität des Service müssen die vom Nutzer eingegebene Routen validiert und auf Ihre Korrektheit überprüft werden.
In der Regel wird dies von der \ac{API} übernommen, jedoch werden im Notfallmodus die Routen direkt in den Injector eingespeist, und die Validierung der \ac{API} wird umgangen.
Daher muss diese vom Injector selbst durchgeführt werden.
\newpage
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:validate-route},
captionpos=b, % Caption unter den Code setzen
caption={Methode zur Validierung von Routen}]
def validate_route(prefix: str, communities=None) -> Route:
route = Route()
try:
route.prefix = str(ipaddress.ip_network(prefix))
except ValueError:
raise click.exceptions.BadParameter("Route prefix is invalid")
if ":" in route.prefix:
route.encode = "IPv6"
else:
route.encode = "IPv4"
if communities:
communities = communities.split(",")
for community in communities:
community_parts = community.split(":")
if len(community_parts) != 2:
raise click.exceptions.BadParameter(
f"{community} is not a valid BGP community"
)
try:
if not int(community_parts[0]) in range(1, 65535) or not int(
community_parts[1]
) in range(1, 65535):
raise click.exceptions.BadParameter(
f"{community} is not a valid BGP community"
)
except ValueError:
raise click.exceptions.BadParameter(
f"{community} contains invalid integer value"
)
route.communities = list(communities)
return route
\end{lstlisting}
Der Zweck ist, BGP-Routen, primär in Bezug auf deren Präfixe und Communities zu validieren.
Die Methode akzeptiert ein Präfix als obligatorisches Argument und optional eine Liste von Communities als Zeichenfolge.
Das Hauptziel dieser Funktion ist es, sicherzustellen, dass die angegebenen Informationen den \ac{BGP}-Anforderungen entsprechen und gültig sind.
Zuerst wird ein neues Routenobjekt erstellt, das als Container für die validierten Daten dient.
Die Funktion versucht dann, den übergebenen Präfix als IP-Netzwerk zu interpretieren.
Bei einer ungültigen Eingabe wird eine `BadParameter`-Exception ausgelöst.
Das Präfix wird analysiert, um festzustellen, ob es sich um ein IPv4- oder IPv6-Präfix handelt.
Dies wird im `encode`-Attribut des Routenobjekts vermerkt.
Im Fall von übergebenen Communities werden diese analysiert und validiert.
Jede Community wird auf ihre Struktur überprüft, und die einzelnen Teile werden auf ihre Gültigkeit im Hinblick auf \ac{ASN} und Wertigkeit geprüft.
Fehlerhafte Communities führen zu entsprechenden `BadParameter`-Exceptions.
Abschließend werden die validierten Informationen, einschließlich Präfix und Communities, im Routenobjekt gespeichert.
Die Funktion gibt dieses Objekt zurück, das nun die validierten Daten enthält.
Zur Vereinfachung der Interaktionen mit der Kommandozeile wird die Bibliothek `click` verwendet.
Durch diese können Exceptions leicht an den Benutzer übermittelt werden, und Tests können einfach gestaltet werden.
Der Operator welcher letztendlich den Emergency Mode bedienen wird, hat zwei Eingabemöglichkeiten:
\begin{itemize}
\item add-route <prefix> <communities>
\item delete-route <prefix>
\end{itemize}
Wobei `<>` für Platzhalter des entsprechenden Parameters stehen.
Eine Möglichkeit, schon existierende Routen zu updaten bietet der Emergency Mode nicht.
Routen welche über den Emergency Mode hinzugefügt wurde, haben immer Vorrang gegenüber Routen, welche über Consul geladen wurden.
Eine weitere Anforderung an den Emergency Mode war, dass Routen auch nach Reboot des Injectors erhalten bleiben.
Dies forderte, dass Routen auf einer Weise im Dateisystem erhalten werden.
Um dies zu Realisieren bestünde die Möglichkeit eine Datenbank wie `sqlite` zu nutzen.
Eine einfachere Lösung dieses Problems war es jedoch, die Routen als \ac{JSON} in eine Datei zu schreiben.
Die schon bei den Konfigurationsdateien für Bird, wurden IPv4 und IPv6 aus demselben Grund getrennt.
Um die Konsistenz und Integrität dieser Dateien, auch `Emergency Files` genannt zu gewährleisten, wurde ein Filelock gesetzt.
Zur Vermeidung des Dirty read Problems, welches in der Vorlesung Datenbanken erläutert wurde, wurde das Filelock sowohl für Schreib- als auch für Lesevorgänge gesetzt.
So kann ein zweiter Prozess das Emergency File erst lesen, wenn der erste Prozess den Schreibvorgang abgeschlossen hat.
Dies dient nicht nur zur Mehrbenutzersynchronisation von mehreren Menschen, sondern hauptsächlich, dass der Hauptprozess nicht versucht das Emergency File zu lesen, während ein Operator mittels des Emergency Mode Änderungen vornimmt.
Da Python, beziehungsweise die benutze \ac{JSON} Bibliothek Probleme damit hatte verschachtelte \ac{JSON}s zu de- und enkodieren, wurde eine weitere Dataclass angelegt.
Diese Dataclass dient nur als Container, um eine Liste an Routenobjekten anzulegen.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:route-container},
captionpos=b, % Caption unter den Code setzen
caption={Route Container Dataclass}]
@dataclass_json
@dataclass
class RouteContainer:
routes: list[Route]
def __init__(self, routes):
self.routes = routes
def __str__(self):
return f"{self.routes}"
\end{lstlisting}
Zu Beginn des Programmstarts werden die Pfade der Lockfiles und Emergency Files innerhalb des Docker Containers festgelegt:
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:path-declaration},
captionpos=b, % Caption unter den Code setzen
caption={Deklaration der Dateipfade}]
emergency_file_v4 = "/var/lib/route_injector/emergency_route_v4.json"
emergency_file_v6 = "/var/lib/route_injector/emergency_route_v6.json"
lock_file_v4 = "/var/lib/route_injector/emergency_route_v4.lock"
lock_file_v6 = "/var/lib/route_injector/emergency_route_v6.lock"
\end{lstlisting}
\newpage
Die Methode welche beim Aufruf von \verb|add-route| über die Kommandozeile aufgerufen wird lässt sich wie folgt darstellen:
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:add-route},
captionpos=b, % Caption unter den Code setzen
caption={add\_route Methode}]
@click.argument("communities")
@click.argument("prefix")
@cli.command()
def add_route(prefix, communities):
route = validate_route(prefix, communities)
emergency_file = emergency_file_v4
lockfile = FileLock(lock_file_v4)
if route.encode == "IPv6":
emergency_file = emergency_file_v6
lockfile = FileLock(lock_file_v6)
with lockfile.acquire():
current_routes = read_emergency_file(emergency_file)
new_routes = find_and_remove_in_list(current_routes, route)
new_routes.append(route)
route_container = RouteContainer(new_routes)
write_emergency_file(route_container, emergency_file)
\end{lstlisting}
Zuerst wird über den erwähnten Validierungsprozess sichergestellt, dass die vom Nutzer eingegeben Route eine valide Route ist.
Über eine if Abfrage wird geprüft, ob das Präfix der eingegebenen Route ein IPv6 Präfix ist.
Ist das der Fall, dann wird das entsprechende Emergency File und Lockfile einer Variablen zugewiesen.
Anschließend wird das Filelock auf das entsprechende Emergency File gesetzt, um sicherzustellen, dass keine weiteren Prozesse auf das File zugreifen können.
Im Folgenden werden die schon im Emergency File enthaltenen Routen mit den neu hinzugefügten verglichen.
Sollte eine Route hinzugefügt werden, wessen Präfix schon im aktuellen Emergency File enthalten ist, wird diese über die \verb|find_and_remove_in_list| entfernt.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:find-and-remove-in-list},
captionpos=b, % Caption unter den Code setzen
caption={find\_and\_remove\_in\_list Methode}]
def find_and_remove_in_list(route_list: list, list_element: Route):
for element in route_list:
if element.prefix == list_element.prefix:
route_list.remove(element)
return route_list
\end{lstlisting}
Die neue Route wird danach der Liste von Routen hinzugefügt, und über die RouteContainer Dataclass wieder zu einer verschachtelten \ac{JSON} konvertiert.
Zum Löschen von Routen aus den Emergency Files, gibt es die \verb|delete_route| Methode, welche sich maßgeblich dadurch unterscheidet, dass sie keine \ac{BGP}-Communities als Parameter benötigt, sondern lediglich das Routenpräfix.
Infolgedessen, fehlt in dieser Methode auch der Teil, welcher die neue Route der Routenliste hinzufügt, da hier nur die Route entfernt werden muss.
Da das Lesen und Schreiben der Files mehrmals im Programmcode geschieht, wurde hierfür jeweils eine Methode geschrieben um Codeduplizierung möglichst zu vermeiden und das \ac{DRY} Prinzip einzuhalten.
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:read-emergency-file},
captionpos=b, % Caption unter den Code setzen
caption={read\_emergency\_file Methode}]
def read_emergency_file(emergency_route_file: str) -> list:
if not os.path.exists(emergency_route_file):
return []
with open(emergency_route_file, "r") as emergency_route:
json_routes = emergency_route.read()
routes_from_file = RouteContainer.from_json(json_routes).routes
return routes_from_file
\end{lstlisting}
\begin{lstlisting}[language=python,
frame=single, % Ein Rahmen um den Code
framexleftmargin=15pt, % Rahmen link von den Zahlen
style=algoBericht,
label={lst:write-emergency-file},
captionpos=b, % Caption unter den Code setzen
caption={write\_emergency\_file Methode}]
def write_emergency_file(routes: RouteContainer, emergency_route_file: str):
with NamedTemporaryFile(delete=False, mode="w") as tmp_emergency_route_file:
tmp_emergency_route_file.write(routes.to_json())
try:
shutil.move(tmp_emergency_route_file.name, emergency_route_file)
except Exception as e:
click.echo(e)
os.remove(tmp_emergency_route_file.name)
\end{lstlisting}
\newpage
\section{Testen}