In jedem ernsten Netzwerkautomatisierungsprojekt kommen wir irgendwann zum Punkt, wo weder eingebaute Ansible-Module noch Ansible Galaxy Module ausreichend sind, um maßgeschneiderte Anforderungen implementieren zu können. Deswegen finden wir die Lösung in der Erstellung von unseren eigenen, maßgeschneiderten Ansible-Modulen. Dank dem flexiblen Design von Ansible kann man das sehr einfach mithilfe von Python-Programmierung schaffen. Ein funktionaler Python-Code sollte für viele Ansible-Module völlig ausreichend sein. In einigen Fällen, vor allem im Zusammenhang mit der Netzwerkautomatisierung, wird Verständnis und Aufrechterhaltung von und Bewegung durch den funktionalen Python-Code gelegentlich zu umständlich. In diesem Tutorium zeigen wir, wie man die gleiche Modulfähigkeit mit funktionalem Stil und Class-Style (OOP) kodieren schaffen kann.
Angenommen, unsere Aufgabe ist es, ein unkompliziertes Modul zu schaffen, der Junos CLI aus einem vorgegebenen Datenmodel erzeugt. Wir konzentrieren uns ausschließlich auf die Adresse und Adressengruppen. Für den Adressennamen ADDRESS_SRC1 und Adressensubnetz 10.10.10.0/24 und Adressensubnetz 10.10.10.0/24 muss man den folgenden Befehlssatz generieren: set security address-book global address ADDRESS_SRC1 10.10.10.0/24
. Provided that this address object should be associated with address group named AG_SRC verbunden sein müsste, wird das Modul folgende Konfigurationszeile generieren: set security address-book global address-set AG_SRC address ADDRESS_SRC1
.
Unser Datenmodul soll folgendermaßen aussehen:
main.yml
- hosts: localhost
gather_facts: no
vars:
addresses_list_of_dicts:
- name: ADDRESS_SRC1
subnet: 10.10.10.0/24
- name: ADDRESS_SRC2
subnet: 11.11.11.0/24
- name: ADDRESS_DST1
subnet: 200.200.200.0/24
- name: ADDRESS_DST2
subnet: 222.222.222.0/24
address_groups_list_of_dicts:
- name: AG_SRC
addresses:
- ADDRESS_SRC1
- ADDRESS_SRC2
- name: AG_DST
addresses:
- ADDRESS_DST1
- ADDRESS_DST2
Jedes Item unter <strong>addresses_list_of_dicts</strong>
müsste zu einem neuen Adressenobjekt werden. Ebenso müsste für jedes Item unter <strong>address_groups_list_of_dicts</strong>
ein Adressengruppenobjekt erstellt werden.
Wir möchten unser maßgeschneidertes Ansible-Modul create_junos_config1
nennen, was bedeutet, dass die Datei create_junos_config1.py
im Ordner <strong>library</strong>
sein muss. Wir tragen die Input-Daten folgendermaßen ein:
main.yml
- hosts: localhost
gather_facts: no
vars:
addresses_list_of_dicts:
- name: ADDRESS_SRC1
subnet: 10.10.10.0/24
- name: ADDRESS_SRC2
subnet: 11.11.11.0/24
- name: ADDRESS_DST1
subnet: 200.200.200.0/24
- name: ADDRESS_DST2
subnet: 222.222.222.0/24
address_groups_list_of_dicts:
- name: AG_SRC
addresses:
- ADDRESS_SRC1
- ADDRESS_SRC2
- name: AG_DST
addresses:
- ADDRESS_DST1
- ADDRESS_DST2
tasks:
- name: Generate Junos CLI commands
create_junos_config1:
addresses: "{{ addresses_list_of_dicts }}"
address_groups: "{{ address_groups_list_of_dicts }}"
register: junos_config1
- name: Print Junos CLI commands
debug:
msg: "{{ junos_config1 }}"
Hier ist der Python-Code dieses Moduls:
create_junos_config1.py
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
def main():
# Define input params
inputparams = {
"addresses": {"required": True, "type": "list"},
"address_groups": {"required": True, "type": "list"},
}
# Read input params
module = AnsibleModule(argument_spec=inputparams)
addresses = module.params["addresses"]
address_groups = module.params["address_groups"]
# Outputs
config = []
# Business logic
for addr in addresses:
cmd = (
f"set security address-book global address {addr['name']}"
f" {addr['subnet']}"
)
config.append(cmd)
for ag in address_groups:
for addr in ag["addresses"]:
cmd = (
f"set security address-book global address-set {ag['name']}"
f" address {addr}"
)
config.append(cmd)
# Exit module, back to playbook
module.exit_json(
changed=False,
config=config,
)
if __name__ == "__main__":
main()
Dazu gehören einige wohlbekannten Abschnitte:
- Erforderliches Werkzeug
<strong>AnsibleModule</strong>
ist importiert - Der Abschnitt mit Inputs, wie er im Ansible-Playbook aufgebaut werden sollte, ist definiert
- Der Abschnitt fürs Lesen übergebener Parameter in Python aus dem Ansible-Playbook (
<strong>addresses</strong>
undaddress_groups
) - Geschäftslogik, die eigentlich das macht, was wir wollen, dass das Modul macht (Generierung von Befehlen)
- Verlassen des (Python) Moduls und Rückgabe der Ergebnisse an das Ansible-Playbook
Ausführen von main.yml
Playbook ergibt folgendes:
run
(ansible) devuser@vmi520322:~/work/075_ansible_modules_with_classes$ ansible-playbook main.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [localhost] ****************
TASK [Generate Junos CLI commands] ****************
ok: [localhost]
TASK [Print Junos CLI commands] ****************
ok: [localhost] => {
"msg": {
"changed": false,
"config": [
"set security address-book global address ADDRESS_SRC1 10.10.10.0/24",
"set security address-book global address ADDRESS_SRC2 11.11.11.0/24",
"set security address-book global address ADDRESS_DST1 200.200.200.0/24",
"set security address-book global address ADDRESS_DST2 222.222.222.0/24",
"set security address-book global address-set AG_SRC address ADDRESS_SRC1",
"set security address-book global address-set AG_SRC address ADDRESS_SRC2",
"set security address-book global address-set AG_DST address ADDRESS_DST1",
"set security address-book global address-set AG_DST address ADDRESS_DST2"
],
"failed": false
}
}
PLAY RECAP ****************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Die Ergebnisse sehen gut aus. Die Liste von zurückgegebenen Befehlen kann in eine Juniper-Maschine entweder mithilfe von juniper.device.config oder junipernetworks.junos.junos_config Ansible-Modulen aufgeladen werden.
Bauen wir nun das maßgeschneiderte Ansible-Modul create_junos_config2 auf, das genau die gleiche Funktion behält, aber der Codestil wird anders. Wir werden Python Class verwenden.
create_junos_config2.py
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
class JunosConfig:
def __init__(self, module, addresses, address_groups):
# inputs
self.module = module
self.addresses = addresses
self.address_groups = address_groups
# outputs
self.config = []
def _generate_address_config(self):
for addr in self.addresses:
cmd = (
f"set security address-book global address {addr['name']}"
f" {addr['subnet']}"
)
self.config.append(cmd)
def _generate_group_config(self):
for ag in self.address_groups:
for addr in ag["addresses"]:
cmd = (
f"set security address-book global address-set {ag['name']}"
f" address {addr}"
)
self.config.append(cmd)
def generate_config(self):
# Business logic
self._generate_address_config()
self._generate_group_config()
# Exit module, back to playbook
self.module.exit_json(
changed=False,
config=self.config,
)
def main():
# Define input params
inputparams = {
"addresses": {"required": True, "type": "list"},
"address_groups": {"required": True, "type": "list"},
}
module = AnsibleModule(argument_spec=inputparams)
juniper_config = JunosConfig(
addresses=module.params["addresses"],
address_groups=module.params["address_groups"],
module=module,
)
juniper_config.generate_config()
if __name__ == "__main__":
main()
Hier muss man einige wichtige Details betonen:
- Die Geschäftslogik ist im
JunosConfig
Class beinhaltet. - Der einzige Zweck von
main()
ist es, die Class zu realisieren und diegenerate_config
Methode zu aktivieren. - Wir müssen auch
<strong>module</strong>
an Class instance weitergeben, da der Modulaustritt aus Class gesteuert wird. - Praktischerweise brechen wir die Geschäftslogik der Class in kleinere Stücke durch Implementierung von zwei Hilfsmethoden (
_generate_address_config
und_generate_group_config
).
Der Gesamtcode für main.yml
sieht nun so aus:
main.yml
- hosts: localhost
gather_facts: no
vars:
addresses_list_of_dicts:
- name: ADDRESS_SRC1
subnet: 10.10.10.0/24
- name: ADDRESS_SRC2
subnet: 11.11.11.0/24
- name: ADDRESS_DST1
subnet: 200.200.200.0/24
- name: ADDRESS_DST2
subnet: 222.222.222.0/24
address_groups_list_of_dicts:
- name: AG_SRC
addresses:
- ADDRESS_SRC1
- ADDRESS_SRC2
- name: AG_DST
addresses:
- ADDRESS_DST1
- ADDRESS_DST2
tasks:
- name: Generate Junos CLI commands
create_junos_config1:
addresses: "{{ addresses_list_of_dicts }}"
address_groups: "{{ address_groups_list_of_dicts }}"
register: junos_config1
- name: Print Junos CLI commands
debug:
msg: "{{ junos_config1 }}"
- name: Generate Junos CLI commands
create_junos_config2:
addresses: "{{ addresses_list_of_dicts }}"
address_groups: "{{ address_groups_list_of_dicts }}"
register: junos_config2
- name: Print Junos CLI commands
debug:
msg: "{{ junos_config2 }}"
Wenn wir ihn jetzt laufen lassen, bekommen wir das identische Ergebnis zweimal:
run

Schlussfolgerung:
Die interne Architektur von maßgeschneiderten Ansible-Modulen kann lediglich durch eine Funktion oder aber durch Mehrschichtsystem mit einer komplexen Class-Vererbung repräsentiert werden. Abhängig von den jeweiligen Bedürfnissen kann man sich für ein komplexeres Class-Stil entscheiden, anstatt die Geschäftslogik in kleinere, schwer steuerbare Teile der funktionalen Programmierung zu brechen. In diesem Artikel haben wir gezeigt, wie man das gleiche Ergebnis mit beiden Arbeitsweisen erreichen kann. Als Faustregel gilt folgendes: wenn man sich nicht sicher ist, welche Arbeitsweise man benutzen soll, soll man mit dem funktionalen Stil anfangen und zum Class-Stil wechseln, wenn man ein bestimmtes Komplexitätsniveau des Codes erreicht hat.