Eine Einführung in das Modul ipaddress
**************************************


Überblick
^^^^^^^^^

Dieses Dokument möchte eine sanfte Einführung in das Modul "ipaddress"
geben. Es richtet sich vor allem an Leser, die mit der Begriffswelt
der IP-Vernetzung noch nicht vertraut sind, kann aber auch für
Netzwerktechniker nützlich sein, die einen Überblick darüber suchen,
wie "ipaddress" die Konzepte der IP-Adressierung darstellt.


Adress-, Netzwerk- und Schnittstellenobjekte erzeugen
=====================================================

Da "ipaddress" ein Modul zum Untersuchen und Bearbeiten von IP-
Adressen ist, wirst du als Erstes einige Objekte erzeugen wollen. Mit
"ipaddress" kannst du Objekte aus Zeichenketten und aus Ganzzahlen
erzeugen.


Eine Anmerkung zu den IP-Versionen
----------------------------------

Für Leser, die mit IP-Adressierung nicht besonders vertraut sind, ist
es wichtig zu wissen, dass das Internet Protocol (IP) derzeit von
Version 4 des Protokolls auf Version 6 umgestellt wird. Dieser
Übergang findet im Wesentlichen deshalb statt, weil Version 4 des
Protokolls nicht genügend Adressen bereitstellt, um den Bedarf der
ganzen Welt zu decken – vor allem angesichts der wachsenden Zahl von
Geräten mit direkter Internetverbindung.

Die Unterschiede zwischen den beiden Protokollversionen im Detail zu
erklären, geht über den Rahmen dieser Einführung hinaus. Die Leser
müssen aber zumindest wissen, dass es diese beiden Versionen gibt und
dass es manchmal nötig ist, die Verwendung der einen oder der anderen
Version zu erzwingen.


IP-Host-Adressen
----------------

Adressen, oft als „Host-Adressen“ bezeichnet, sind die grundlegendste
Einheit bei der Arbeit mit IP-Adressierung. Am einfachsten erzeugst du
Adressen mit der Factory-Funktion "ipaddress.ip_address()", die anhand
des übergebenen Werts automatisch bestimmt, ob eine IPv4- oder eine
IPv6-Adresse erzeugt wird:

>>> ipaddress.ip_address('192.0.2.1')
IPv4Address('192.0.2.1')
>>> ipaddress.ip_address('2001:DB8::1')
IPv6Address('2001:db8::1')

Adressen lassen sich auch direkt aus Ganzzahlen erzeugen. Werte, die
in 32 Bit passen, werden als IPv4-Adressen aufgefasst:

   >>> ipaddress.ip_address(3221225985)
   IPv4Address('192.0.2.1')
   >>> ipaddress.ip_address(42540766411282592856903984951653826561)
   IPv6Address('2001:db8::1')

Um die Verwendung von IPv4- oder IPv6-Adressen zu erzwingen, lassen
sich die entsprechenden Klassen direkt aufrufen. Das ist besonders
nützlich, um für kleine Ganzzahlen die Erzeugung von IPv6-Adressen zu
erzwingen:

   >>> ipaddress.ip_address(1)
   IPv4Address('0.0.0.1')
   >>> ipaddress.IPv4Address(1)
   IPv4Address('0.0.0.1')
   >>> ipaddress.IPv6Address(1)
   IPv6Address('::1')


Netzwerke definieren
--------------------

Host-Adressen werden üblicherweise zu IP-Netzwerken zusammengefasst,
daher stellt "ipaddress" einen Weg bereit, Netzwerkdefinitionen zu
erzeugen, zu untersuchen und zu bearbeiten. IP-Netzwerkobjekte werden
aus Zeichenketten gebildet, die den Bereich der Host-Adressen
festlegen, die zu diesem Netzwerk gehören. Die einfachste Form dieser
Angabe ist ein Paar aus „Netzwerkadresse/Netzwerkpräfix“, wobei das
Präfix die Anzahl der führenden Bits festlegt, die verglichen werden,
um zu bestimmen, ob eine Adresse zum Netzwerk gehört, und die
Netzwerkadresse den erwarteten Wert dieser Bits festlegt.

Wie schon für Adressen gibt es auch hier eine Factory-Funktion, die
die richtige IP-Version automatisch bestimmt:

   >>> ipaddress.ip_network('192.0.2.0/24')
   IPv4Network('192.0.2.0/24')
   >>> ipaddress.ip_network('2001:db8::0/96')
   IPv6Network('2001:db8::/96')

Bei Netzwerkobjekten dürfen keine Host-Bits gesetzt sein. Praktisch
bedeutet das, dass "192.0.2.1/24" kein Netzwerk beschreibt. Solche
Definitionen werden als Schnittstellenobjekte bezeichnet, weil die
Schreibweise „IP-in-einem-Netzwerk“ üblicherweise die
Netzwerkschnittstellen eines Rechners in einem bestimmten Netzwerk
beschreibt. Sie werden im nächsten Abschnitt näher beschrieben.

Standardmäßig führt der Versuch, ein Netzwerkobjekt mit gesetzten
Host-Bits zu erzeugen, dazu, dass ein "ValueError" ausgelöst wird.
Damit die zusätzlichen Bits stattdessen auf null gesetzt werden, lässt
sich dem Konstruktor das Flag "strict=False" übergeben:

   >>> ipaddress.ip_network('192.0.2.1/24')
   Traceback (most recent call last):
      ...
   ValueError: 192.0.2.1/24 has host bits set
   >>> ipaddress.ip_network('192.0.2.1/24', strict=False)
   IPv4Network('192.0.2.0/24')

Die Zeichenkettenform bietet deutlich mehr Spielraum, aber Netzwerke
lassen sich genau wie Host-Adressen auch mit Ganzzahlen definieren. In
diesem Fall gilt das Netzwerk als Netzwerk, das nur die eine durch die
Ganzzahl bezeichnete Adresse enthält. Das Netzwerkpräfix umfasst daher
die gesamte Netzwerkadresse:

   >>> ipaddress.ip_network(3221225984)
   IPv4Network('192.0.2.0/32')
   >>> ipaddress.ip_network(42540766411282592856903984951653826560)
   IPv6Network('2001:db8::/128')

Wie bei Adressen lässt sich die Erzeugung einer bestimmten Netzwerkart
erzwingen, indem du den Klassenkonstruktor direkt aufrufst, anstatt
die Factory-Funktion zu verwenden.


Host-Schnittstellen
-------------------

Wie gerade erwähnt, reichen weder die Adress- noch die Netzwerkklassen
aus, wenn du eine Adresse in einem bestimmten Netzwerk beschreiben
musst. Eine Schreibweise wie "192.0.2.1/24" wird von
Netzwerktechnikern und von den Leuten, die Werkzeuge für Firewalls und
Router schreiben, üblicherweise als Kurzform für „der Host "192.0.2.1"
im Netzwerk "192.0.2.0/24"“ verwendet. Entsprechend stellt "ipaddress"
eine Reihe von Mischklassen bereit, die eine Adresse mit einem
bestimmten Netzwerk verknüpfen. Die Schnittstelle zum Erzeugen ist
dieselbe wie beim Definieren von Netzwerkobjekten, nur dass der
Adressteil nicht auf eine Netzwerkadresse beschränkt ist.

>>> ipaddress.ip_interface('192.0.2.1/24')
IPv4Interface('192.0.2.1/24')
>>> ipaddress.ip_interface('2001:db8::1/96')
IPv6Interface('2001:db8::1/96')

Ganzzahlige Eingaben werden (wie bei Netzwerken) akzeptiert, und die
Verwendung einer bestimmten IP-Version lässt sich erzwingen, indem du
den entsprechenden Konstruktor direkt aufrufst.


Adress-, Netzwerk- und Schnittstellenobjekte untersuchen
========================================================

Du hast dir die Mühe gemacht, ein
IPv(4|6)(Address|Network|Interface)-Objekt zu erzeugen, also willst du
vermutlich auch Informationen darüber bekommen. "ipaddress" versucht,
das einfach und naheliegend zu machen.

Die IP-Version auslesen:

   >>> addr4 = ipaddress.ip_address('192.0.2.1')
   >>> addr6 = ipaddress.ip_address('2001:db8::1')
   >>> addr6.version
   6
   >>> addr4.version
   4

Das Netzwerk aus einer Schnittstelle gewinnen:

   >>> host4 = ipaddress.ip_interface('192.0.2.1/24')
   >>> host4.network
   IPv4Network('192.0.2.0/24')
   >>> host6 = ipaddress.ip_interface('2001:db8::1/96')
   >>> host6.network
   IPv6Network('2001:db8::/96')

Herausfinden, wie viele einzelne Adressen ein Netzwerk enthält:

   >>> net4 = ipaddress.ip_network('192.0.2.0/24')
   >>> net4.num_addresses
   256
   >>> net6 = ipaddress.ip_network('2001:db8::0/96')
   >>> net6.num_addresses
   4294967296

Über die „nutzbaren“ Adressen eines Netzwerks iterieren:

   >>> net4 = ipaddress.ip_network('192.0.2.0/24')
   >>> for x in net4.hosts():
   ...     print(x)
   192.0.2.1
   192.0.2.2
   192.0.2.3
   192.0.2.4
   ...
   192.0.2.252
   192.0.2.253
   192.0.2.254

Die Netzmaske (also die Bits, die dem Netzwerkpräfix entsprechen) oder
die Hostmaske (alle Bits, die nicht zur Netzmaske gehören) auslesen:

>>> net4 = ipaddress.ip_network('192.0.2.0/24')
>>> net4.netmask
IPv4Address('255.255.255.0')
>>> net4.hostmask
IPv4Address('0.0.0.255')
>>> net6 = ipaddress.ip_network('2001:db8::0/96')
>>> net6.netmask
IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff::')
>>> net6.hostmask
IPv6Address('::ffff:ffff')

Die Adresse ausschreiben oder verkürzen:

   >>> addr6.exploded
   '2001:0db8:0000:0000:0000:0000:0000:0001'
   >>> addr6.compressed
   '2001:db8::1'
   >>> net6.exploded
   '2001:0db8:0000:0000:0000:0000:0000:0000/96'
   >>> net6.compressed
   '2001:db8::/96'

IPv4 unterstützt zwar weder das Ausschreiben noch das Verkürzen, die
zugehörigen Objekte stellen die entsprechenden Eigenschaften aber
dennoch bereit. So kann versionsneutraler Code mühelos sicherstellen,
dass für IPv6-Adressen die knappste oder die ausführlichste Form
verwendet wird, und gleichzeitig IPv4-Adressen korrekt behandeln.


Netzwerke als Listen von Adressen
=================================

Manchmal ist es nützlich, Netzwerke wie Listen zu behandeln. Das
heißt, dass man sie so indizieren kann:

   >>> net4[1]
   IPv4Address('192.0.2.1')
   >>> net4[-1]
   IPv4Address('192.0.2.255')
   >>> net6[1]
   IPv6Address('2001:db8::1')
   >>> net6[-1]
   IPv6Address('2001:db8::ffff:ffff')

Es bedeutet auch, dass sich Netzwerkobjekte für die Syntax des Listen-
Enthaltenseinstests eignen, etwa so:

   if address in network:
       # do something

Der Enthaltenseinstest wird anhand des Netzwerkpräfixes effizient
durchgeführt:

   >>> addr4 = ipaddress.ip_address('192.0.2.1')
   >>> addr4 in ipaddress.ip_network('192.0.2.0/24')
   True
   >>> addr4 in ipaddress.ip_network('192.0.3.0/24')
   False


Vergleiche
==========

"ipaddress" stellt einige einfache und hoffentlich naheliegende Wege
bereit, Objekte zu vergleichen, wo das sinnvoll ist:

   >>> ipaddress.ip_address('192.0.2.1') < ipaddress.ip_address('192.0.2.2')
   True

Eine "TypeError"-Ausnahme wird ausgelöst, wenn du versuchst, Objekte
unterschiedlicher Versionen oder unterschiedlicher Typen zu
vergleichen.


IP-Adressen mit anderen Modulen verwenden
=========================================

Andere Module, die IP-Adressen verwenden (etwa "socket"), nehmen
Objekte aus diesem Modul üblicherweise nicht direkt an. Sie müssen
stattdessen in eine Ganzzahl oder eine Zeichenkette überführt werden,
die das andere Modul annimmt:

   >>> addr4 = ipaddress.ip_address('192.0.2.1')
   >>> str(addr4)
   '192.0.2.1'
   >>> int(addr4)
   3221225985


Mehr Details bekommen, wenn das Erzeugen einer Instanz scheitert
================================================================

Wenn du Adress-, Netzwerk- oder Schnittstellenobjekte mit den
versionsunabhängigen Factory-Funktionen erzeugst, werden alle Fehler
als "ValueError" mit einer allgemeinen Fehlermeldung gemeldet, die
lediglich sagt, dass der übergebene Wert nicht als Objekt dieses Typs
erkannt wurde. Eine genauere Fehlermeldung fehlt, weil man wissen
müsste, ob der Wert IPv4 oder IPv6 sein *soll*, um näher begründen zu
können, warum er abgelehnt wurde.

Für Anwendungsfälle, in denen der Zugriff auf diese zusätzlichen
Details nützlich ist, lösen die einzelnen Klassenkonstruktoren
tatsächlich die "ValueError"-Unterklassen
"ipaddress.AddressValueError" und "ipaddress.NetmaskValueError" aus,
um genau anzuzeigen, welcher Teil der Definition nicht korrekt geparst
werden konnte.

Bei direkter Verwendung der Klassenkonstruktoren sind die
Fehlermeldungen deutlich ausführlicher. Zum Beispiel:

   >>> ipaddress.ip_address("192.168.0.256")
   Traceback (most recent call last):
     ...
   ValueError: '192.168.0.256' does not appear to be an IPv4 or IPv6 address
   >>> ipaddress.IPv4Address("192.168.0.256")
   Traceback (most recent call last):
     ...
   ipaddress.AddressValueError: Octet 256 (> 255) not permitted in '192.168.0.256'

   >>> ipaddress.ip_network("192.168.0.1/64")
   Traceback (most recent call last):
     ...
   ValueError: '192.168.0.1/64' does not appear to be an IPv4 or IPv6 network
   >>> ipaddress.IPv4Network("192.168.0.1/64")
   Traceback (most recent call last):
     ...
   ipaddress.NetmaskValueError: '64' is not a valid netmask

Beide modulspezifischen Ausnahmen haben allerdings "ValueError" als
Elternklasse. Wenn dich die genaue Fehlerart nicht interessiert,
kannst du also weiterhin Code wie den folgenden schreiben:

   try:
       network = ipaddress.IPv4Network(address)
   except ValueError:
       print('address/netmask is invalid for IPv4:', address)
