{"id":462,"date":"2022-06-19T19:36:29","date_gmt":"2022-06-19T19:36:29","guid":{"rendered":"https:\/\/www.adriatic-staging.mattbedford.work\/?p=462"},"modified":"2022-07-28T11:31:35","modified_gmt":"2022-07-28T11:31:35","slug":"custom-ansible-modules-with-python-class","status":"publish","type":"post","link":"https:\/\/www.adriaticnetworks.com\/de\/custom-ansible-modules-with-python-class\/","title":{"rendered":"Ma\u00dfgeschneiderte Ansible-Module mit Python Class"},"content":{"rendered":"<p>In jedem ernsten Netzwerkautomatisierungsprojekt kommen wir irgendwann zum Punkt, wo weder eingebaute Ansible-Module noch Ansible Galaxy Module ausreichend sind, um ma\u00dfgeschneiderte Anforderungen implementieren zu k\u00f6nnen. Deswegen finden wir die L\u00f6sung in der Erstellung von unseren eigenen, ma\u00dfgeschneiderten Ansible-Modulen. Dank dem flexiblen Design von Ansible kann man das sehr einfach mithilfe von Python-Programmierung schaffen. Ein funktionaler Python-Code sollte f\u00fcr viele Ansible-Module v\u00f6llig ausreichend sein. In einigen F\u00e4llen, vor allem im Zusammenhang mit der Netzwerkautomatisierung, wird Verst\u00e4ndnis und Aufrechterhaltung von und Bewegung durch den funktionalen Python-Code gelegentlich zu umst\u00e4ndlich. In diesem Tutorium zeigen wir, wie man die gleiche Modulf\u00e4higkeit mit funktionalem Stil und Class-Style (OOP) kodieren schaffen kann.<\/p>\n\n\n\n<p>Angenommen, unsere Aufgabe ist es, ein unkompliziertes Modul zu schaffen, der Junos CLI aus einem vorgegebenen Datenmodel erzeugt. Wir konzentrieren uns ausschlie\u00dflich auf die Adresse und Adressengruppen. F\u00fcr den Adressennamen <strong>ADDRESS_SRC1<\/strong> und Adressensubnetz <strong>10.10.10.0\/24<\/strong> und Adressensubnetz 10.10.10.0\/24 muss man den folgenden Befehlssatz generieren: <strong><code>set security address-book global address ADDRESS_SRC1 10.10.10.0\/24<\/code><\/strong>. Provided that this address object should be associated with address group named <strong>AG_SRC<\/strong> verbunden sein m\u00fcsste, wird das Modul folgende Konfigurationszeile generieren: <strong><code>set security address-book global address-set AG_SRC address ADDRESS_SRC1<\/code><\/strong>.<\/p>\n\n\n\n<p>Unser Datenmodul soll folgenderma\u00dfen aussehen:<\/p>\n\n\n\n<p class=\"gb-headline gb-headline-7776cd12 gb-headline-text filename\"><strong>main.yml<\/strong>   <\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">- hosts: localhost\n  gather_facts: no\n\n  vars:\n    addresses_list_of_dicts:\n      - name: ADDRESS_SRC1\n        subnet: 10.10.10.0\/24\n      - name: ADDRESS_SRC2\n        subnet: 11.11.11.0\/24\n      - name: ADDRESS_DST1\n        subnet: 200.200.200.0\/24\n      - name: ADDRESS_DST2\n        subnet: 222.222.222.0\/24\n    address_groups_list_of_dicts:\n      - name: AG_SRC\n        addresses: \n          - ADDRESS_SRC1\n          - ADDRESS_SRC2\n      - name: AG_DST\n        addresses: \n          - ADDRESS_DST1\n          - ADDRESS_DST2\n\n<\/code><\/pre>\n\n\n\n<p>Jedes Item unter<code> &lt;strong&gt;addresses_list_of_dicts&lt;\/strong&gt;<\/code> m\u00fcsste zu einem neuen Adressenobjekt werden. Ebenso m\u00fcsste f\u00fcr jedes Item unter <code>&lt;strong&gt;address_groups_list_of_dicts&lt;\/strong&gt; <\/code>ein Adressengruppenobjekt erstellt werden.<\/p>\n\n\n\n<p>Wir m\u00f6chten unser ma\u00dfgeschneidertes Ansible-Modul <strong><code>create_junos_config1<\/code><\/strong> nennen, was bedeutet, dass die Datei <strong><code>create_junos_config1.py<\/code><\/strong> im Ordner <code>&lt;strong&gt;library&lt;\/strong&gt; <\/code>sein muss. Wir tragen die Input-Daten folgenderma\u00dfen ein:<\/p>\n\n\n\n<p class=\"gb-headline gb-headline-7645a217 gb-headline-text filename\"><strong>main.yml<\/strong>   <\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">- hosts: localhost\n  gather_facts: no\n\n  vars:\n    addresses_list_of_dicts:\n      - name: ADDRESS_SRC1\n        subnet: 10.10.10.0\/24\n      - name: ADDRESS_SRC2\n        subnet: 11.11.11.0\/24\n      - name: ADDRESS_DST1\n        subnet: 200.200.200.0\/24\n      - name: ADDRESS_DST2\n        subnet: 222.222.222.0\/24\n    address_groups_list_of_dicts:\n      - name: AG_SRC\n        addresses: \n          - ADDRESS_SRC1\n          - ADDRESS_SRC2\n      - name: AG_DST\n        addresses: \n          - ADDRESS_DST1\n          - ADDRESS_DST2\n\n  tasks:\n    - name: Generate Junos CLI commands\n      create_junos_config1:\n        addresses: &quot;{{ addresses_list_of_dicts }}&quot;\n        address_groups: &quot;{{ address_groups_list_of_dicts }}&quot;\n      register: junos_config1\n\n    - name: Print Junos CLI commands\n      debug:\n        msg: &quot;{{ junos_config1 }}&quot;\n<\/code><\/pre>\n\n\n\n<p>Hier ist der Python-Code dieses Moduls:<\/p>\n\n\n\n<p class=\"gb-headline gb-headline-de404031 gb-headline-text filename\"><strong>create_junos_config1.py<\/strong>   <\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-\">#!\/usr\/bin\/python\n\nfrom ansible.module_utils.basic import AnsibleModule\n\ndef main():\n    # Define input params\n    inputparams = {\n        &quot;addresses&quot;: {&quot;required&quot;: True, &quot;type&quot;: &quot;list&quot;},\n        &quot;address_groups&quot;: {&quot;required&quot;: True, &quot;type&quot;: &quot;list&quot;},\n    }\n\n    # Read input params\n    module = AnsibleModule(argument_spec=inputparams)\n\n    addresses = module.params[&quot;addresses&quot;]\n    address_groups = module.params[&quot;address_groups&quot;]\n\n    # Outputs\n    config = []\n\n    # Business logic\n    for addr in addresses:\n        cmd = (\n            f&quot;set security address-book global address {addr[&#039;name&#039;]}&quot;\n            f&quot; {addr[&#039;subnet&#039;]}&quot;\n        )\n        config.append(cmd)\n\n    for ag in address_groups:\n        for addr in ag[&quot;addresses&quot;]:\n            cmd = (\n                f&quot;set security address-book global address-set {ag[&#039;name&#039;]}&quot;\n                f&quot; address {addr}&quot;\n            )\n            config.append(cmd)\n\n    # Exit module, back to playbook\n    module.exit_json(\n        changed=False,\n        config=config,\n    )\n\nif __name__ == &quot;__main__&quot;:\n    main()\n<\/code><\/pre>\n\n\n\n<p>Dazu geh\u00f6ren einige wohlbekannten Abschnitte:<\/p>\n\n\n\n<ul><li>Erforderliches Werkzeug <code>&lt;strong&gt;AnsibleModule&lt;\/strong&gt; <\/code>ist importiert<\/li><li>Der Abschnitt mit Inputs, wie er im Ansible-Playbook aufgebaut werden sollte, ist definiert<\/li><li>Der Abschnitt f\u00fcrs Lesen \u00fcbergebener Parameter in Python aus dem Ansible-Playbook (<code>&lt;strong&gt;addresses&lt;\/strong&gt; <\/code>und <strong><code>address_groups<\/code><\/strong>)<\/li><li>Gesch\u00e4ftslogik, die eigentlich das macht, was wir wollen, dass das Modul macht (Generierung von Befehlen)<\/li><li>Verlassen des (Python) Moduls und R\u00fcckgabe der Ergebnisse an das Ansible-Playbook<\/li><\/ul>\n\n\n\n<p>Ausf\u00fchren von <strong><code>main.yml<\/code><\/strong> Playbook ergibt folgendes:<\/p>\n\n\n\n<p class=\"gb-headline gb-headline-30bc1e78 gb-headline-text filename\"><strong>run<\/strong>   <\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">(ansible) devuser@vmi520322:~\/work\/075_ansible_modules_with_classes$ ansible-playbook main.yml \n[WARNING]: No inventory was parsed, only implicit localhost is available\n[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match &#039;all&#039;\n\nPLAY [localhost] ****************\n\nTASK [Generate Junos CLI commands] ****************\nok: [localhost]\n\nTASK [Print Junos CLI commands] ****************\nok: [localhost] =&gt; {\n    &quot;msg&quot;: {\n        &quot;changed&quot;: false,\n        &quot;config&quot;: [\n            &quot;set security address-book global address ADDRESS_SRC1 10.10.10.0\/24&quot;,\n            &quot;set security address-book global address ADDRESS_SRC2 11.11.11.0\/24&quot;,\n            &quot;set security address-book global address ADDRESS_DST1 200.200.200.0\/24&quot;,\n            &quot;set security address-book global address ADDRESS_DST2 222.222.222.0\/24&quot;,\n            &quot;set security address-book global address-set AG_SRC address ADDRESS_SRC1&quot;,\n            &quot;set security address-book global address-set AG_SRC address ADDRESS_SRC2&quot;,\n            &quot;set security address-book global address-set AG_DST address ADDRESS_DST1&quot;,\n            &quot;set security address-book global address-set AG_DST address ADDRESS_DST2&quot;\n        ],\n        &quot;failed&quot;: false\n    }\n}\n\nPLAY RECAP ****************\nlocalhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  \n<\/code><\/pre>\n\n\n\n<p>Die Ergebnisse sehen gut aus. Die Liste von zur\u00fcckgegebenen Befehlen kann in eine Juniper-Maschine entweder mithilfe von <strong>juniper.device.config<\/strong> oder <strong>junipernetworks.junos.junos_config<\/strong> Ansible-Modulen aufgeladen werden.<\/p>\n\n\n\n<p>Bauen wir nun das ma\u00dfgeschneiderte Ansible-Modul <strong>create_junos_config2<\/strong> auf, das genau die gleiche Funktion beh\u00e4lt, aber der Codestil wird anders. Wir werden Python Class verwenden.<\/p>\n\n\n\n<p class=\"gb-headline gb-headline-1aa7c7c0 gb-headline-text filename\"><strong><strong>create_junos_config2.py<\/strong><\/strong>   <\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-python\">#!\/usr\/bin\/python\n\nfrom ansible.module_utils.basic import AnsibleModule\n\nclass JunosConfig:\n    def __init__(self, module, addresses, address_groups):\n        # inputs\n        self.module = module\n        self.addresses = addresses\n        self.address_groups = address_groups\n\n        # outputs\n        self.config = []\n\n    def _generate_address_config(self):\n        for addr in self.addresses:\n            cmd = (\n                f&quot;set security address-book global address {addr[&#039;name&#039;]}&quot;\n                f&quot; {addr[&#039;subnet&#039;]}&quot;\n            )\n            self.config.append(cmd)\n\n    def _generate_group_config(self):\n        for ag in self.address_groups:\n            for addr in ag[&quot;addresses&quot;]:\n                cmd = (\n                    f&quot;set security address-book global address-set {ag[&#039;name&#039;]}&quot;\n                    f&quot; address {addr}&quot;\n                )\n                self.config.append(cmd)\n\n    def generate_config(self):\n        # Business logic\n        self._generate_address_config()\n        self._generate_group_config()\n\n        # Exit module, back to playbook\n        self.module.exit_json(\n            changed=False,\n            config=self.config,\n        )\n\ndef main():\n    # Define input params\n    inputparams = {\n        &quot;addresses&quot;: {&quot;required&quot;: True, &quot;type&quot;: &quot;list&quot;},\n        &quot;address_groups&quot;: {&quot;required&quot;: True, &quot;type&quot;: &quot;list&quot;},\n    }\n\n    module = AnsibleModule(argument_spec=inputparams)\n\n    juniper_config = JunosConfig(\n        addresses=module.params[&quot;addresses&quot;],\n        address_groups=module.params[&quot;address_groups&quot;],\n        module=module,\n    )\n\n    juniper_config.generate_config()\n\nif __name__ == &quot;__main__&quot;:\n    main()\n<\/code><\/pre>\n\n\n\n<p>Hier muss man einige wichtige Details betonen:<\/p>\n\n\n\n<ul><li>Die Gesch\u00e4ftslogik ist im <strong><code>JunosConfig <\/code><\/strong>Class beinhaltet.<\/li><li>Der einzige Zweck von <strong><code>main()<\/code><\/strong> ist es, die Class zu realisieren und die <strong><code>generate_config<\/code><\/strong> Methode zu aktivieren.<\/li><li>Wir m\u00fcssen auch <code>&lt;strong&gt;module&lt;\/strong&gt; <\/code>an Class instance weitergeben, da der Modulaustritt aus Class gesteuert wird.<\/li><li>Praktischerweise brechen wir die Gesch\u00e4ftslogik der Class in kleinere St\u00fccke durch Implementierung von zwei Hilfsmethoden (<strong><code>_generate_address_config<\/code> <\/strong>und <strong><code>_generate_group_config<\/code><\/strong>).<\/li><\/ul>\n\n\n\n<p>Der Gesamtcode f\u00fcr <strong><code>main.yml<\/code><\/strong> sieht nun so aus:<\/p>\n\n\n\n<p class=\"gb-headline gb-headline-314b35e5 gb-headline-text filename\"><strong><strong><strong>main.yml<\/strong><\/strong><\/strong>   <\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-\">- hosts: localhost\n  gather_facts: no\n\n  vars:\n    addresses_list_of_dicts:\n      - name: ADDRESS_SRC1\n        subnet: 10.10.10.0\/24\n      - name: ADDRESS_SRC2\n        subnet: 11.11.11.0\/24\n      - name: ADDRESS_DST1\n        subnet: 200.200.200.0\/24\n      - name: ADDRESS_DST2\n        subnet: 222.222.222.0\/24\n    address_groups_list_of_dicts:\n      - name: AG_SRC\n        addresses: \n          - ADDRESS_SRC1\n          - ADDRESS_SRC2\n      - name: AG_DST\n        addresses: \n          - ADDRESS_DST1\n          - ADDRESS_DST2\n\n  tasks:\n    - name: Generate Junos CLI commands\n      create_junos_config1:\n        addresses: &quot;{{ addresses_list_of_dicts }}&quot;\n        address_groups: &quot;{{ address_groups_list_of_dicts }}&quot;\n      register: junos_config1\n\n    - name: Print Junos CLI commands\n      debug:\n        msg: &quot;{{ junos_config1 }}&quot;\n\n    - name: Generate Junos CLI commands\n      create_junos_config2:\n        addresses: &quot;{{ addresses_list_of_dicts }}&quot;\n        address_groups: &quot;{{ address_groups_list_of_dicts }}&quot;\n      register: junos_config2\n\n    - name: Print Junos CLI commands\n      debug:\n        msg: &quot;{{ junos_config2 }}&quot;\n<\/code><\/pre>\n\n\n\n<p>Wenn wir ihn jetzt laufen lassen, bekommen wir das identische Ergebnis zweimal:<\/p>\n\n\n\n<p class=\"gb-headline gb-headline-94002535 gb-headline-text filename\"><strong><strong><strong>run<\/strong><\/strong><\/strong>   <\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"836\" height=\"1024\" src=\"https:\/\/adriaticnetworks.com\/wp-content\/uploads\/2022\/06\/image-836x1024.png\" alt=\"\" class=\"wp-image-468\" srcset=\"\" sizes=\"(max-width: 836px) 100vw, 836px\" data-srcset=\"\" \/><\/figure>\n\n\n\n<p><strong>Schlussfolgerung:<\/strong><\/p>\n\n\n\n<p>Die interne Architektur von ma\u00dfgeschneiderten Ansible-Modulen kann lediglich durch eine Funktion oder aber durch Mehrschichtsystem mit einer komplexen Class-Vererbung repr\u00e4sentiert werden. Abh\u00e4ngig von den jeweiligen Bed\u00fcrfnissen kann man sich f\u00fcr ein komplexeres Class-Stil entscheiden, anstatt die Gesch\u00e4ftslogik 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\u00e4tsniveau des Codes erreicht hat.<\/p>","protected":false},"excerpt":{"rendered":"<p>In every serious Network Automation project, we hit the stage where neither built-in Ansible modules nor Ansible Galaxy modules are sufficient to fit our custom requirements. Therefore, we turn to writing our own custom Ansible &#8230; <a title=\"Ma\u00dfgeschneiderte Ansible-Module mit Python Class\" class=\"read-more\" href=\"https:\/\/www.adriaticnetworks.com\/de\/custom-ansible-modules-with-python-class\/\" aria-label=\"Mehr zu Custom Ansible Modules with Python Class\">Weiterlesen &#8230;<\/a><\/p>","protected":false},"author":1,"featured_media":471,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[17],"_links":{"self":[{"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/posts\/462"}],"collection":[{"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/comments?post=462"}],"version-history":[{"count":5,"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/posts\/462\/revisions"}],"predecessor-version":[{"id":469,"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/posts\/462\/revisions\/469"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/media\/471"}],"wp:attachment":[{"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/media?parent=462"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/categories?post=462"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.adriaticnetworks.com\/de\/wp-json\/wp\/v2\/tags?post=462"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}