nornir - Berechnung von IP Adressen

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
unique_79
User
Beiträge: 5
Registriert: Samstag 26. Dezember 2020, 12:58

Hallo zusammen,

ich benutze python zusammen mit nornir um Konfigurationen (jinja template) für Router zu erstellen und anschließend auf die Geräte zu pushen.
Für die Berechnung der IP-Adressen habe ich die Funktion "def calc_ip" erstellt. Der Input (subnet)kommt aus einem yaml Dictianory. s. unten
Die Berechnung der IP Adressen klappt zwar ( wenn auch sehr umständlich..) aber ich bekomme es nur für ein von zwei Routerpärchen hin.
Ich würde die Funktion gern auch für das andere Routerpärchen verwenden.
Könnt ihr mich dazu mal in die richtige Richtung schubsen ;-), habe schon einiges probiert, aber es klappt nicht so recht.

Für die Eingabe der Inputs möchte ich nicht die host oder group Datei verwenden (nornir inventory)

Hier mal die wesentlichen Infos:

Code: Alles auswählen

data_input.yaml
---
global_var_pe_rtr:
  rt_id: 66666
global_var_pe_rtr_ab:
  vrf_id: test1
  vlan_id: 3281
  rd_id: 14225
  subnet: 172.23.130.0      -> wird für die Berechnung verwendet
  static_routes: enable
  prefix:
   - prfx: 10.0.0.0/8
   - prfx: 172.16.0.0/12
  pe_rtr_ab:   
    rtr_1_a:
      group:
        - ios-xr_rtr
      data:
        loopback: 1.1.1.1
    rtr_2_b:
      group:
          - ios-xr_rtr
      data:
        loopback: 2.2.2.2
 
global_var_pe_rtr_cd:
  vrf_id: test2
  rd_id: 14225
  vlan_id: 204
  subnet: 172.23.61.0		 -> soll auch für die Berechnung verwendet werden
  static_routes: enable
  prefix:
   - prfx: 10.0.0.0/8
  pe_rtr_cd:   
    rtr_1_c:
      group:
        - ios_rtr
      data:
        loopback: 3.3.3.3
    rtr_2_d:
      group:
          - ios_rtr
      data:
        loopback: 4.4.4.4
Python Script

Code: Alles auswählen

***********************
python script:
***********************


def read_file(input_file):
  with open(input_file) as f:
    input_data = yaml.load(f)
  return input_data

def calc_ip (sn):

  subnet1 = sn['global_var_pe_rtr_ab']['subnet']
  
  parts = subnet1.split('.')
  
  oktet1 = int(parts[0])
  oktet2 = int(parts[1])
  oktet3 = int(parts[2])
  oktet4 = int(parts[3])
  
  hsrp_lst = [oktet1, oktet2, oktet3, oktet4+1]
  rtr1_lst = [oktet1, oktet2, oktet3, oktet4+2]
  rtr2_lst = [oktet1, oktet2, oktet3, oktet4+3]
  fw_lst = [oktet1, oktet2, oktet3, oktet4+4]
  
  ip_hsrp = '.'.join(str(x) for x in hsrp_lst)
  ip_rtr1 = '.'.join(str(x) for x in rtr1_lst)
  ip_rtr2 = '.'.join(str(x) for x in rtr2_lst)
  ip_fw = '.'.join(str(x) for x in fw_lst)
  
  return ip_hsrp,ip_rtr1,ip_rtr2,ip_fw


def render_configs(task, id, ipadd):
    filename = task.host["j2_template_file"]

    r = task.run(
        task= template_file,
        name="Base Template Configuration",
        template=filename,
        path="./templates",
        **id,
        ip=ipadd
    )
    task.host["config"] = r.result


if __name__ == "__main__":
  input_data=read_file(input_file = "data_input.yaml")
  pprint(input_data)
  calc_task = calc_ip(sn = input_data)
  #print(calc_task)
  render_task = nr.run(task=render_configs, id=input_data, ipadd = calc_task )
  print_result(render_task)
  
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@unique_79: Das sieht grundsätzlich einfach so aus als wolltest Du die den Namen des Routers oder vielleicht besser nur die IP als Argument übergeben wollen statt in der Funktion fest auf die erste IP zuzugreifen.

Eingerückt wird in Python mit vier Leerzeichen pro Ebene.

Namen sollten keine kryptischen Abkürzungen enthalten oder gar nur daraus bestehen. Also nicht `f` wenn man eigentlich `file` meint. Echt schlimm liest sich ``return ip_hsrp,ip_rtr1,ip_rtr2,ip_fw``. Das ist doch nur noch kryptische Buchstabensalat. Grunddatentypen haben nichts in Namen verloren.

Extrem verwirrend ist es auch bekannte Abkürzungen für etwas völlig anderes zu verwenden. Ich habe einen kleinen Augenblick gebraucht um dahinter zu kommen warum ein komplettes Wörterbuch als ID verwendet werden kann. Weil das `id` hier offenbar gar nicht für `id` steht sondern für `input_data`.

Aber auch ausgeschriebene Namen können extrem verwirren wenn sie falsch sind. Es gibt in dem Rahmenwerk ja Tasks. Bei `calc_task` denkt also der Leser erst einmal, dass es sich wohl um so ein `Task`-Objekt oder zumindest eine Funktion oder Methode handelt die von so einem `Task` ausgeführt wird. Es ist aber ein Tupel mit IP-Adressen in Zeichenkettenform. Das kommt doch kein Mensch drauf bei dem Namen. Gleiches gilt für `render_task`.

Bei `calc_ip()` ist falsch, da gar nicht *eine* IP berechnet wird, sondern mehrere. Das sollte also `calc_ips()` oder besser `calculate_ips()` heissen.

Was soll `sn` bedeuten?

Die IPs lassen sich mit dem `ipaddress`-Modul aus der Standardbibliothek leichter und etwas sicherer erstellen.

`render_configs()` hält sich nicht an die API. Das sollte ein `Result`-Objekt zurückgeben. Und warum mehrzahl? Rendert die Funktion tatsächlich mehr als eine Konfiguration (pro Aufruf)? Ich vermute mal eher nicht.

Da fehlen offensichtlich Importe und `nr` und `template_file` sind undefiniert.

`template_file()` ist ein schlechter Name für eine Funktion. Das wäre ein passender Name für eine Vorlagendatei, also ein eher passives ”Ding”. Funktionen und Methoden werden üblicherweise nach der Tätigkeit benannt, die sie ausführen. `fill_template()` vielleicht?

Der Code soweit überarbeitet (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
from ipaddress import IPv4Network
from itertools import islice
from pprint import pprint

import yaml
from nornir import InitNornir
from nornir.core.task import Result
from nornir_utils.plugins.functions import print_result


def read_file(filename):
    with open(filename, "rb") as file:
        return yaml.load(file)


def calculate_ips(data):
    host_count = 4
    prefix_length = IPv4Network(0).max_prefixlen - host_count.bit_length()
    network = IPv4Network(
        (data["global_var_pe_rtr_ab"]["subnet"], prefix_length)
    )
    return tuple(map(str, islice(network.hosts(), host_count)))


def fill_template(task, name, template_filename, path, ip_addresses, data):
    ...
    return Result(...)


def render_configs(task, data, ip_addresses):
    result = task.run(
        fill_template,
        name="Base Template Configuration",
        template_filename=task.host["j2_template_file"],
        path="./templates",
        ip_addresses=ip_addresses,
        data=data,
    )
    task.host["config"] = result.result
    #
    # FIXME Not returning a `Result` isn't compliant with the API!
    #


def main():
    nr = InitNornir("config.yaml")
    data = read_file("data_input.yaml")
    pprint(data)
    ip_addresses = calculate_ips(data)
    result = nr.run(render_configs, id=data, ip_addresses=ip_addresses)
    print_result(result)


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
unique_79
User
Beiträge: 5
Registriert: Samstag 26. Dezember 2020, 12:58

danke für die ehrlichen Worte, blackjack (ich bin Netzwerker und kein Programmierer..)
Deinen Vorschlag schaue ich mir morgen in Ruhe an.
unique_79
User
Beiträge: 5
Registriert: Samstag 26. Dezember 2020, 12:58

die Info mit dem ipaddrees modul war gut. Sie macht eigentlich das was ich brauche.

Ich beschreibe nochmal Stichpunktartig was ich benötige, dass kam im ersten Post natürlich nicht gut rüber.
Also:
- bei ip_net handelt es sich um ein Subnetz, in meinem Fall um /29 Netz
- ip_net gibt es mind. 2 mal in der data_input.yaml (s. Beispiel oben)
- aus jedem Subnetz, soll die HSRP IP , die Router IPs und die Firewall IP zurückgegeben werden
- die zurückgegeben IPs werden in der Funktion render_configs benötigt um die Konfiguration von mind.4 Routern zu erstellen

Ich habe die Funktion calculate_ip mal so angepasst, dass sie für ein Subnetz die benötigten IPs ausgibt. Das klappt ganz gut.
Nur wie bekomme ich es hin, dass auch für das zweite und ggf. dritte Subnetz die IPs berechnet werden und an render_config übergeben werden ?

Code: Alles auswählen


*** input aus yaml***
"global_var_pe_rtr_ab:
     subnet: 172.16.1.0/29
"global_var_pe_rtr_cd:
     subnet: 172.16.2.0/29    
*********************

def calculate_ips(ip_net):
    try:
        net = ipaddress.ip_network(ip_net, strict=False)
        ip_hsrp = str([x for x in net.hosts()][0])
        ip_rtr1 = str([x for x in net.hosts()][1])
        ip_rtr2 = str([x for x in net.hosts()][2])
        ip_fw = str([x for x in net.hosts()][3])
        print(ip_hsrp)
        print(ip_rtr1)
        print(ip_rtr2) 
        print(ip_fw)        
    except ValueError:
        print('Your input format is incorrect, please check it!')
        
if __name__ == '__main__':
   ip_net = data["global_var_pe_rtr_ab"]["subnet"]
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@unique_79: Der ``try``-Block ist viel zu lang und der `ValueError` kann auch gar nicht auftreten weil Du `strict` als `False` angegeben hast. Warum? Wenn die Maske falsch angegeben ist, möchte man das doch eigentlich nicht ignorieren.

Die Funktion gibt nichts zurück, soll sie am Ende ja aber wohl, wenn mit den IPs an anderer Stelle weitergearbeitet werden soll. In dem Fall ist aber die Ausnahmebehandlung kaputt, denn einfach was ausgeben und dann versuchen so weiter zu machen als wäre nichts passiert wird entweder zu falschen Ergebnissen oder Folgefehlern führen.

Sich viermal alle Hostadressen zu genieren, nur um dann davon jedes mal nur *eine* aus der Liste zu picken ist ineffizient.

Wenn das für alle Schlüssel in den Daten gemacht werden soll die "subnet" in der Unterstruktur haben, dann musst Du halt genau das programmieren. Und Dir überlegen in welcher Form das dann weitergegeben werden soll.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
unique_79
User
Beiträge: 5
Registriert: Samstag 26. Dezember 2020, 12:58

@blackjack, lass uns beim wesentlichen bleiben. Das die Funktion kein Ouput liefert ist mir klar und das eine Liste mit 8 IPs erzeugt wird, bei der ich mir 4 herauspicke finde ich jetzt nicht wirklich schlimm. Try excetpt ist copy paste aus einem Beispiel.
Klar kann man ggf effizienter machen.

Nochmal zurück zum eigentlichen Problem.
Inzwischen habe ich es hinbekommen, dass die Funktion "calculate_ips" mit zwei Subnetzen durchgeführt wird.
Dazu habe ich zunächst beide Subnetze in eine Liste gepackt und sie als Argument an die Funktion übergeben.
Aktuell versuche ich die beiden Subnetze aus dem dictionary (yaml input) auszulesen und sie dann in eine Liste zu schreiben. Das haut aber bei verschachtelten Dictionarys nicht so einfach hin.
Ich habe es mit der Methode get auf Dictionarys versucht und es mal hiermit versucht:

ip_net = list( map(data.get, keys) )

Ich habe Probleme die keys so in eine Liste zu packen, dass sie vernünftig angewendet werden können.
so funktioniert es jedenfalls nicht:

keys = ['global_var_pe_rtr_ab','subnet']


An der Stelle bräuchte ich Deine Unterstützung.
Benutzeravatar
sparrow
User
Beiträge: 4164
Registriert: Freitag 17. April 2009, 10:28

@unique_79: Was __blackjack__ schreibt _ist_ wesentlich. Denn es offenbart zusammen mit deiner Antwort ein großes Problem: Du verstehst nicht, was dein Code tut. Räum deinen Code auf. Es geht nicht darum, ob es "effizienter" wird - es wird vor allem weniger fehleranfällig. Und dann geht es zum nächsten Schritt.
Und was zu tun ist, hat __blackjack__ dir hervorragend erklärt.
unique_79
User
Beiträge: 5
Registriert: Samstag 26. Dezember 2020, 12:58

@blackjack
ich möchte mich bedanken. Die Funktion "def calculate_ips" ist perfekt. Sie hat bei mir nicht auf anhieb funktioniert, daher meine eigenen Versuche.
Den Rest probiere ich in den nächsten Tagen selber
Antworten