centos:ansible:playbooks1

Ansible - Playbookbeispiele

Bild: Ansible Logo

Nachdem wir uns bereits eingehend mit den Grundlagen und auch schon das benötigte Programmpaket auf unserer Admin-Workstation zur Orchestrierung installiert haben, werden wir uns nun ein paar Beispile ansehen, wie man sich das Leben mit Ansible-Playbooks leichter gestalten kann.

In den beiden Kapiteln Playbooks und YAML - was ist das? hatten wir uns schon eingehend mit den Hintergrundinformationen zu diesen beiden Themenblöcken beschäftigt, so dass wir uns nun direkt mit unserem ersten Playbook beschäftigen können.

Bei unserem ersten Playbook-Beispiel wollen wir mit Hilfe von Ansible einen Benutzer auf unserem Zielsystem automatisiert anlegen. Bevor wir unser erstes Script schreiben, wechseln wir in unser zuvor angelegtes Zielverzeichnis:

 $ cd ~/ansible

Script anlegen

Hier legen wir nun unser erstes Script ab.

 $ vim 01_create-user.yml
01_create-user.yml
---
- hosts: centos8
  become: true
  vars:
    sudoers: ansible
    createguid: '1010'
    createuser: 'ruben'
    createusername: 'Ruben Nausch'
    createpassword: 'M31nP4p4157d3r4113r83573!'
 
  tasks:
    - name: Make sure we have a group '{{ createuser }}' for our new admin-user '{{ createuser }}'
      group:
        name: '{{ createuser }}'
        gid: '{{ createguid }}'
        state: present
 
    - name: Add the user '{{ createuser }}' with a specific uid and a primary group of '{{ createuser }}'
      user:
        name: '{{ createuser }}'
        comment: '{{ createusername }}'
        uid: '{{ createguid }}'
        group: '{{ createuser }}'
        state: present
 
    - name: Initial password generation for user '{{ createuser }}'
      shell: usermod -p $(echo '{{ createpassword }}' | openssl passwd -1 -stdin) {{ createuser }}
...

Script Beschreibung

Die einzelnen Zeilen haben dabei folgende Funktionen und Aufgaben. Zeile:

  • Zeile 1: --- : Start-Ziele einer jeden YAML-Datei
  • Zeile 2: - hosts: centos8 : Das Script soll auf allen Systemen, die in unserer Inventory-Datei die Eigenschaft CentOS 8 zugewiesen bekommen haben.
  • Zeile 3: become: true : Wir benötigen eine Rechteerweiterung zu root-Rechten, damit wir einen neuen Nutzer auf dem Zielsystem anlegen können.
  • Zeile 4: vars: Wir möchten unser Script so gestalten, dass dies später für ggf. weiter Nutzer möglichst einfach zu verwenden ist. Daher verwenden wir hier Variablen.
  • Zeile 5: sudoers: ansible Nutzer auf dem Zielsystem, der für die Administrationsaufgaben benutzt werden soll.
  • Zeile 6: createguid: '1010' Variable die der UID bzw. der GID unseres Nutzers 'ruben' zugewiesen werden soll.
  • Zeile 7: createuser: 'ruben' Variable mit dem Nutzernamen 'ruben'
  • Zeile 8: createusername: 'Ruben Nausch' Variable mit dem vollen Namen des Benutzers 'ruben'
  • Zeile 9: createpassword: 'M31nP4p4157d3r4113r8357' Variable mit dem initialen Klartextpasswortes der Benutzers 'ruben'
  • Zeile 10:
  • Zeile 11: tasks Schlüsselwort mit den nachfolgenden Aufgaben, die mit dem Playbook dann abgearbeitet werden sollen.
  • Zeile 12: name: Beschreibender Text(Make sure we have a group 'ruben' for our new admin-user 'ruben'), der später beim Aufruf von ansible-playbooks ausgegeben werden soll.
  • Zeile 13 - 16: group: Ansible Module group welches zum Anlegen, verändern und auch Löschen von Gruppen herangezogen werden kann.
  • Zeile 17:
  • Zeile 18: name: Beschreibender Text(Add the user 'ruben' with a specific uid and a primary group of 'ruben'), der später beim Aufruf von ansible-playbooks ausgegeben werden soll.
  • Zeile 19 - 24: user: Ansible Module user welches zum Anlegen, verändern und auch Löschen von Benutzern herangezogen werden kann.
  • Zeile 25:
  • Zeile 26: name: Beschreibender Text(Initial password generation for user 'ruben'), der später beim Aufruf von ansible-playbooks ausgegeben werden soll.
  • Zeile 27: shell: Ansible Module shell welches zum Ausführen von Shell-Befehlen auf dem Zielsystem verwendet werden kann. Hiermit setzen wir das Passwort unseres gerade angelegten Users 'ruben'.
  • Zeile 28: ... Endekennzeichen der YML-Datei

Script ausführen

Nun wollen wir unser ersten Playbook ausführen, um auf dem Zielhost den gewünschten Benutzer anzulegen; hierzu rufen wir unser Script wie folgt auf:

 $ ansible-playbook -v 01_create-user.yml --limit=demo

Da wir den User erst einmal nur auf dem Host demo anlegen wollen,
schränken wir beim Aufruf des Playbooks die Ausführung mit dem Parameter limit= entsprechend ein.

Using /etc/ansible/ansible.cfg as config file
BECOME password: 

PLAY [centos8] ****************************************************************************************************************************************

TASK [Gathering Facts] ****************************************************************************************************************************************
ok: [www8.dmz.nausch.org]
TASK [Make sure we have a group 'ruben' for our new admin-user 'ruben'] **************************************************************** changed: [www8.dmz.nausch.org] => {"changed": true, "gid": 1010, "name": "ruben", "state": "present", "system": false}
TASK [Add the user 'ruben' with a specific uid and a primary group of 'ruben'] ********************************************************* changed: [www8.dmz.nausch.org] => {"changed": true, "comment": "Ruben Nausch", "create_home": true, "group": 1010, "home": "/home/ruben", "name": "ruben", "shell": "/bin/bash", "state": "present", "stderr": "Creating mailbox file: File exists\n", "stderr_lines": ["Creating mailbox file: File exists"], "system": false, "uid": 1010}
TASK [Initial password generation for user 'ruben'] ************************************************************************************ changed: [www8.dmz.nausch.org] => {"changed": true, "cmd": "usermod -p $(echo 'M31nP4p4157d3r4113r83573!' | openssl passwd -1 -stdin) ruben", "delta": "0:00:00.375778", "end": "2020-01-04 20:00:16.435753", "rc": 0, "start": "2020-01-04 20:00:16.059975", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
PLAY RECAP *************************************************************************************************************************************** www8.dmz.nausch.org : ok=4 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Script Ablaufbeschreibung

Bei jedem Ansible-Playbook werden beim Ausführen zu aller erst die sog. Facts durch Ansible gesammelt.

Anschließend werden die einzelnen Tasks (Aufgaben) der Reihe nach abgearbeitet. In der Zeile nach TASK wird dann die Beschreibung ausgegeben, die wir im YAML-Script nach dem Schlüsselwort name: angegeben hatten.

Bei der Ausgabe werden Stati mit einer entsprechneden Farbe hinterlegt:

  • grün : Werden lediglich von Ansible Daten erhoben, oder wenn Aufgaben keine Änderungen nach sich ziehen, werden die entsprechenden Zeilen mit der Farbe grün dargestellt.
  • ockergelb : Werden Änderungen durch das Script verursacht erfolgt die Anzeige in der Farbe ockergelb angezeigt.
  • rot : Fehler werden in der Farbe rot dargestellt.

Eine Zusammenfasssung für jeden Host wir am ende unter dem Kennzeichen PLAY RECAP nochmals zusammengefasst.

Protokollierung

Auf dem Zielhost werden die Aktivitäten des Ansible-Playbooks entsprechend protokolliert.

 # less /var/log/secure
Jan  4 19:59:51 vml000090 sshd[11916]: Postponed publickey for ansible from 10.0.0.27 port 50940 ssh2 [preauth]
Jan  4 19:59:51 vml000090 sshd[11916]: Accepted publickey for ansible from 10.0.0.27 port 50940 ssh2: ED25519 SHA256:jTZQUDbCqZaV648fKVBfx3L4+tBMWL+z+iUCBY3kKMQ
Jan  4 19:59:51 vml000090 systemd[11920]: pam_unix(systemd-user:session): session opened for user ansible by (uid=0)
Jan  4 19:59:51 vml000090 sshd[11916]: pam_unix(sshd:session): session opened for user ansible by (uid=0)
Jan  4 19:59:58 vml000090 sudo[12053]: ansible : TTY=pts/1 ; PWD=/home/ansible ; USER=root ; COMMAND=/bin/sh -c echo BECOME-SUCCESS-lxiprplnioypvfmjjylwxvdjapibaahs ; /usr/libexec/platform-python /home/ansible/.ansible/tmp/ansible-tmp-1578164696.339052-252470145168513/AnsiballZ_setup.py
Jan  4 19:59:58 vml000090 sudo[12053]: pam_systemd(sudo:session): Cannot create session: Already running in a session or user slice
Jan  4 19:59:58 vml000090 sudo[12053]: pam_unix(sudo:session): session opened for user root by ansible(uid=0)
Jan  4 20:00:06 vml000090 sudo[12053]: pam_unix(sudo:session): session closed for user root
Jan  4 20:00:12 vml000090 sudo[12213]: ansible : TTY=pts/1 ; PWD=/home/ansible ; USER=root ; COMMAND=/bin/sh -c echo BECOME-SUCCESS-dkoativjxmvilrtomekhdjnqzyyyuqqr ; /usr/libexec/platform-python /home/ansible/.ansible/tmp/ansible-tmp-1578164713.323935-211226042197181/AnsiballZ_group.py
Jan  4 20:00:12 vml000090 sudo[12213]: pam_systemd(sudo:session): Cannot create session: Already running in a session or user slice
Jan  4 20:00:12 vml000090 sudo[12213]: pam_unix(sudo:session): session opened for user root by ansible(uid=0)
Jan  4 20:00:12 vml000090 groupadd[12222]: group added to /etc/group: name=ruben, GID=1010
Jan  4 20:00:12 vml000090 groupadd[12222]: group added to /etc/gshadow: name=ruben
Jan  4 20:00:12 vml000090 groupadd[12222]: new group: name=ruben, GID=1010
Jan  4 20:00:12 vml000090 sudo[12213]: pam_unix(sudo:session): session closed for user root
Jan  4 20:00:14 vml000090 sudo[12330]: ansible : TTY=pts/1 ; PWD=/home/ansible ; USER=root ; COMMAND=/bin/sh -c echo BECOME-SUCCESS-qrempbbpxxpgsigefdvnuctemlnerdpu ; /usr/libexec/platform-python /home/ansible/.ansible/tmp/ansible-tmp-1578164719.7395723-99484507541654/AnsiballZ_user.py
Jan  4 20:00:14 vml000090 sudo[12330]: pam_systemd(sudo:session): Cannot create session: Already running in a session or user slice
Jan  4 20:00:14 vml000090 sudo[12330]: pam_unix(sudo:session): session opened for user root by ansible(uid=0)
Jan  4 20:00:14 vml000090 useradd[12339]: new user: name=ruben, UID=1010, GID=1010, home=/home/ruben, shell=/bin/bash
Jan  4 20:00:14 vml000090 sudo[12330]: pam_unix(sudo:session): session closed for user root
Jan  4 20:00:15 vml000090 sudo[12448]: ansible : TTY=pts/1 ; PWD=/home/ansible ; USER=root ; COMMAND=/bin/sh -c echo BECOME-SUCCESS-guzlsexqnsjzxvmqkxlzyfmeywwkmtxf ; /usr/libexec/platform-python /home/ansible/.ansible/tmp/ansible-tmp-1578164721.6724513-118758141371611/AnsiballZ_command.py
Jan  4 20:00:15 vml000090 sudo[12448]: pam_systemd(sudo:session): Cannot create session: Already running in a session or user slice
Jan  4 20:00:15 vml000090 sudo[12448]: pam_unix(sudo:session): session opened for user root by ansible(uid=0)
Jan  4 20:00:16 vml000090 usermod[12455]: change user 'ruben' password
Jan  4 20:00:16 vml000090 sudo[12448]: pam_unix(sudo:session): session closed for user root

Bei unserem zweiten Beispiel wollen wir lediglich dafür sorgen, dass die Nutzer der Gruppe wheel beim Ausführen von Befehlen, die root-Berechtigungen erfordern, ihr Passwort eingeben müssen. Hierzu werden wir im Verzeichnis /etc/sudoers.d/ eine Datei mit dem Namen 10_passwd_sudo_wheel ablegen, die nachfolgenden Inhalt aufweist.

# Allows people in group wheel to run all command
%wheel	ALL=(ALL)	ALL

Script anlegen

Hier legen wir nun unser erstes Script ab.

 $ vim 02_passwd_sudo_wheel.yml
02_passwd_sudo_wheel.yml
---
- hosts: centos8
  become: true
  vars:
    sudoers: ansible
 
  tasks:
    - name: All users from groub 'wheel' are allowed sudo users
      copy:
        content: "# Allows people in group wheel to run all command\n%wheel    ALL=(ALL)       ALL\n"
        dest: /etc/sudoers.d/10_passwd_sudo_wheel
        owner: root
        group: root
        mode: "0440"
        validate: visudo -cf %s
...

Script Beschreibung

Die einzelnen Zeilen/Blöcke haben dabei folgende Funktionen und Aufgaben. Zeile:

  • Zeile 1 - 5: Der bereits bekannte Block aus dem ersten Beispiel, der Aussagen trifft, wo das Script laufen soll, unter welchen User-Rechten das passiern soll etc. pp..
  • Zeile 6:
  • Zeile 7: tasks Schlüsselwort mit den nachfolgenden Aufgaben, die mit dem Playbook dann abgearbeitet werden sollen.
  • Zeile 8: name: Beschreibender Text (All users from groub 'wheel' are allowed sudo users), der später beim Aufruf von ansible-playbooks ausgegeben werden soll.
  • Zeile 9 - 15: copy: Ansible Module copy welches zum Kopieren, verändern und auch Löschen von Dateien herangezogen werden kann.
  • Zeile 16: ... Endekennzeichen der YML-Datei

Script ausführen

Nun wollen wir unser ersten Playbook ausführen, um auf dem Zielhost eine Datei mit dem gewünschten Inhalt ablegen; hierzu rufen wir unser Script wie folgt auf:

 $ ansible-playbook -v 02_passwd_sudo_wheel.yml

Using /etc/ansible/ansible.cfg as config file
BECOME password: 

PLAY [centos8] ****************************************************************************************************************************************

TASK [Gathering Facts] ****************************************************************************************************************************************
ok: [www8.dmz.nausch.org]
TASK [All users from groub 'wheel' are allowed sudo users] ***************************************************************************** changed: [www8.dmz.nausch.org] => {"changed": true, "checksum": "b51f017f799aca0d0aef9fa29b7da87006ea5c29", "dest": "/etc/sudoers.d/10_passwd_sudo_wheel", "gid": 0, "group": "root", "md5sum": "a7c4cc84eb0dbbf844d2a8d4fbe64164", "mode": "0440", "owner": "root", "secontext": "system_u:object_r:etc_t:s0", "size": 80, "src": "/home/ansible/.ansible/tmp/ansible-tmp-1578174240.9800038-82649856412743/source", "state": "file", "uid": 0}
PLAY RECAP *************************************************************************************************************************************** www8.dmz.nausch.org : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ergebnis

Auf dem Zielhost findet sich nun unsere gewünschte Datei mit dem zugehörigen Inhalt.

 # cat /etc/sudoers.d/10_passwd_sudo_wheel
/etc/sudoers.d/10_passwd_sudo_wheel
# Allows people in group wheel to run all command
%wheel    ALL=(ALL)       ALL

Beim drittem Playbook-Beispiel wollen wir mit Hilfe von Ansible nicht nur ein Admin-Konto sondern gleich mehrere anlegen. Dabei kopieren wir dann auch noch gleich die zugehörigen öffentlichen SSH-Schlüssel an Ort und Stelle. Zu guter Letzt stellen wir noch sicher dass der Eintrag %wheel ALL=(ALL) ALL gesetzt ist, hierzu binden wird das YAML-Playbook von Beispiel 02 ein.

Bevor wir unser erstes Script schreiben, wechseln wir in unser zuvor angelegtes Zielverzeichnis:

 $ cd ~/ansible

Script anlegen

Nun legen wir nun unser erstes Script ab.

 $ vim 03_create-admins.yml
03_create-admins.yml
---
- hosts: centos8
  become: true
  vars:
    sudoers: ansible
 
  tasks:
    - name: add several users to the system
      user:
        name: "{{ item.name }}"
        comment: "{{ item.fullname }}"
        uid: "{{ item.uid }}"
        groups: "{{ item.groups }}"
        state: present
      with_items:
         - { name: bofh, fullname: "Bastard Operator from Hell", uid: 1020, groups: "wheel, users" }
         - { name: ruben, fullname: "Ruben Nausch", uid: 1010, groups: wheel }
         - { name: ansible, fullname: "Ansible Systemuser", uid: 2003, groups: wheel }
 
    - name: Initial password generation for each user
      shell: usermod -p $(echo '{{ item.secret }}' | openssl passwd -1 -stdin) {{ item.name }}
      with_items:
         - { name: bofh, secret: "/ImTAxBwi++W2Y26195+Q72GbH73i/zQyaq12wsx" }
         - { name: ruben, secret: "lop5YtypT+E6qhOjpZEoAlnyiLH7HlIF1k212qyo" }
         - { name: ansible, secret: "X4z3AEx6WZ2+DDzvuzjx0mBERQ-o03f12qwPOSyx" }
 
    - name: Set authorized keys for each user
      authorized_key:
        user: "{{ item.name }}"
        state: present
        key: "{{ lookup('file', '/home/django/ansible/authkeys/{{ item.name }}.pub') }}"
      with_items:
         - {name: bofh }
         - {name: ruben }
         - {name: ansible }
 
    - include_tasks: 02_passwd_sudo_wheel.yml
...

Script Beschreibung

Das Script ist soweit selbsterklärend, werden doch der Reihe nach vier Tasks abgearbeitet:

  • Task 1 : Drei Userkonten anlegen - die betreffenden Daten für unsere User holen wir uns dabei aus einer Liste (array)
  • Task 2 : Setzen der initialen Passwörter je Userkonto
  • Task 3 : Kopieren der jeweiligen öffentlichen SSH-Schlüssel der User. In diesem Konfigurationsbeispiel liegen diese im Verzeichnis /home/django/ansible/authkeys/.
     $ ~/ansible/authkeys/ 
    insgesamt 12
    -rw-r--r--. 1 django django 100  4. Jan 22:12 ansible.pub
    -rw-r--r--. 1 django django 108  4. Jan 22:13 bofh.pub
    -rw-r--r--. 1 django django  98  4. Jan 22:16 ruben.pub
  • Task 4 : Das Anpassen der sudoers-Eigenschaften haben wir in diesem Konfigurationsbeispiel in eine separate YAML-Datei ausgelagert. Hierzu nutzen wir das YAML-Modul include. Das entsprechende Include-Verzeichnis legen wir gleich mal an.
     $ mkdir ~/ansible/includes

    Hier legen wir uns eine YAML-Datei an, die nur den entsprechenden task beinhaltet.

     $ vim  includes/sudoers.yml
    includes/sudoers.yml
    ---
        - name: All users from groub 'wheel' are allowed sudo users
          copy:
            content: "# Allows people in group wheel to run all command\n%wheel    ALL=(ALL)       ALL\n"
            dest: /etc/sudoers.d/10_passwd_sudo_wheel
            owner: root
            group: root
            mode: "0440"
            validate: visudo -cf %s

Script ausführen

Nun wollen wir unser Playbook ausführen, um auf dem Zielhost den gewünschten Benutzer anzulegen; hierzu rufen wir unser Script wie folgt auf:

 $ ansible-playbook -v 03_create-admins.yml03_create-admins.yml

Using /etc/ansible/ansible.cfg as config file
BECOME password: 

PLAY [centos8] ****************************************************************************************************************************************

TASK [Gathering Facts] ****************************************************************************************************************************************
ok: [www8.dmz.nausch.org]
TASK [add several users to the system] ************************************************************************************************* changed: [www8.dmz.nausch.org] => (item={'name': 'bofh', 'fullname': 'Bastard Operator from Hell', 'uid': 1020, 'groups': 'wheel, users'}) => {"ansible_loop_var": "item", "changed": true, "comment": "Bastard Operator from Hell", "create_home": true, "group": 1020, "groups": "wheel, users", "home": "/home/bofh", "item": {"fullname": "Bastard Operator from Hell", "groups": "wheel, users", "name": "bofh", "uid": 1020}, "name": "bofh", "shell": "/bin/bash", "state": "present", "system": false, "uid": 1020} changed: [www8.dmz.nausch.org] => (item={'name': 'ruben', 'fullname': 'Ruben Nausch', 'uid': 1010, 'groups': 'wheel'}) => {"ansible_loop_var": "item", "changed": true, "comment": "Ruben Nausch", "create_home": true, "group": 1010, "groups": "wheel", "home": "/home/ruben", "item": {"fullname": "Ruben Nausch", "groups": "wheel", "name": "ruben", "uid": 1010}, "name": "ruben", "shell": "/bin/bash", "state": "present", "system": false, "uid": 1010} ok: [www8.dmz.nausch.org] => (item={'name': 'ansible', 'fullname': 'Ansible Systemuser', 'uid': 500, 'groups': 'wheel'}) => {"ansible_loop_var": "item", "append": false, "changed": false, "comment": "Ansible Systemuser", "group": 500, "groups": "wheel", "home": "/home/ansible", "item": {"fullname": "Ansible Systemuser", "groups": "wheel", "name": "ansible", "uid": 500}, "move_home": false, "name": "ansible", "shell": "/bin/bash", "state": "present", "uid": 500}
TASK [Initial password generation for each user] *************************************************************************************** changed: [www8.dmz.nausch.org] => (item={'name': 'bofh', 'secret': '/ImTAxBwi++W2Y26195+Q72GbH73i/zQyaq12wsx'}) => {"ansible_loop_var": "item", "changed": true, "cmd": "usermod -p $(echo '/ImTAxBwi++W2Y26195+Q72GbH73i/zQyaq12wsx' | openssl passwd -1 -stdin) bofh", "delta": "0:00:00.389066", "end": "2020-01-05 17:04:51.991293", "item": {"name": "bofh", "secret": "/ImTAxBwi++W2Y26195+Q72GbH73i/zQyaq12wsx"}, "rc": 0, "start": "2020-01-05 17:04:51.602227", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []} changed: [www8.dmz.nausch.org] => (item={'name': 'ruben', 'secret': 'lop5YtypT+E6qhOjpZEoAlnyiLH7HlIF1k212qyo'}) => {"ansible_loop_var": "item", "changed": true, "cmd": "usermod -p $(echo 'lop5YtypT+E6qhOjpZEoAlnyiLH7HlIF1k212qyo' | openssl passwd -1 -stdin) ruben", "delta": "0:00:00.382204", "end": "2020-01-05 17:04:53.167841", "item": {"name": "ruben", "secret": "lop5YtypT+E6qhOjpZEoAlnyiLH7HlIF1k212qyo"}, "rc": 0, "start": "2020-01-05 17:04:52.785637", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []} changed: [www8.dmz.nausch.org] => (item={'name': 'ansible', 'secret': 'X4z3AEx6WZ2+DDzvuzjx0mBERQ-o03f12qwPOSyx'}) => {"ansible_loop_var": "item", "changed": true, "cmd": "usermod -p $(echo 'P1r473np4r731' | openssl passwd -1 -stdin) ansible", "delta": "0:00:00.386751", "end": "2020-01-05 17:04:54.313829", "item": {"name": "ansible", "secret": "X4z3AEx6WZ2+DDzvuzjx0mBERQ-o03f12qwPOSyx"}, "rc": 0, "start": "2020-01-05 17:04:53.927078", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
TASK [Set authorized keys for each user] *********************************************************************************************** hanged: [www8.dmz.nausch.org] => (item={'name': 'bofh'}) => {"ansible_loop_var": "item", "changed": true, "comment": null, "exclusive": false, "follow": false, "item": {"name": "bofh"}, "key": "ssh-ed25519 AAAAC3NzaC2lZDI1NTE5AAAAILxi47aZOS3tfvNFxVVqkJAfSKXjpvemB3kRZEQ5q/kf Bastard Operator from Hell", "key_options": null, "keyfile": "/home/bofh/.ssh/authorized_keys", "manage_dir": true, "path": null, "state": "present", "user": "bofh", "validate_certs": true} changed: [www8.dmz.nausch.org] => (item={'name': 'ruben'}) => {"ansible_loop_var": "item", "changed": true, "comment": null, "exclusive": false, "follow": false, "item": {"name": "ruben"}, "key": "ssh-ed25519 AAAAC3TzaC2lZ60DI1NTE5AAILxi47aZOS3tfvNFxq16293SKXjp4tsB3kRZffQ5q/kf ruben@nausch.org", "key_options": null, "keyfile": "/home/ruben/.ssh/authorized_keys", "manage_dir": true, "path": null, "state": "present", "user": "ruben", "validate_certs": true} ok: [www8.dmz.nausch.org] => (item={'name': 'ansible'}) => {"ansible_loop_var": "item", "changed": false, "comment": null, "exclusive": false, "follow": false, "item": {"name": "ansible"}, "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILti47aZOSetfvNFxVVqkJAfSKXjyvemB3kRZEQ5q/kf Ansible Systemuser", "key_options": null, "keyfile": "/home/ansible/.ssh/authorized_keys", "manage_dir": true, "path": null, "state": "present", "user": "ansible", "validate_certs": true}
TASK [All users from groub 'wheel' are allowed sudo users] ***************************************************************************** included: /home/django/ansible/includes/sudoers.yml for www8.dmz.nausch.org
TASK [All users from groub 'wheel' are allowed sudo users] ***************************************************************************** ok: [www8.dmz.nausch.org] => {"changed": false, "checksum": "b51f017f799aca0d0aef9fa29b7da87006ea5c29", "dest": "/etc/sudoers.d/10_passwd_sudo_wheel", "gid": 0, "group": "root", "mode": "0440", "owner": "root", "path": "/etc/sudoers.d/10_passwd_sudo_wheel", "secontext": "system_u:object_r:etc_t:s0", "size": 80, "state": "file", "uid": 0}
PLAY RECAP *************************************************************************************************************************************** www8.dmz.nausch.org : ok=6 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ergebnis

Auf dem Zielhost findet sich nun unsere gewünschte Datei mit dem zugehörigen Inhalt.

 # cat /etc/sudoers.d/10_passwd_sudo_wheel
/etc/sudoers.d/10_passwd_sudo_wheel
# Allows people in group wheel to run all command
%wheel    ALL=(ALL)       ALL

Auch unsere drei Nutzer-/Admin-Konten sind angelet und deren öffentlicher SSH-Schlüssel in diue Datei .ssh/authorized_keys im jeweiligen Homeverzeichnis der User kopiert.

/home/
├── ansible
│   ├── .bash_logout
│   ├── .bash_profile
│   ├── .bashrc
│   ├── .ssh
│   │   └── authorized_keys
├── bofh
│   ├── .bash_logout
│   ├── .bash_profile
│   ├── .bashrc
│   └── .ssh
│       └── authorized_keys
└── ruben
    ├── .bash_logout
    ├── .bash_profile
    ├── .bashrc
    └── .ssh
        └── authorized_keys

Im vierten Beispiel wollen wir die Konfigurationsdatei mailserver.guru.repo für das Repository mailserver.guru auf alle unsere definierten CentOS-Hosts kopieren. Dabei müssen wir natürlich beachten, dass sich die Datei zwischen den Versionen CentOS 7 und CentOS 8 unterscheiden, wenn auch nur geringfügig!

Script anlegen

Das Script legen wir wie auch schon bei den anderen Beispielen zuvor im Verzeichnis ~/ansible an

 $ vim 04_repro.yml
04_repro.yml
---
- hosts: all
  become: true
  vars:
    sudoers: ansible
 
  tasks:
    - name: Place repo-file mailserver.guru for CentOS 8 right in place
      copy:
        src: /home/django/ansible/files/CentOS8/mailserver.guru.repo 
        dest: /etc/yum.repos.d/
      when:
        - ansible_facts['distribution'] == "CentOS"
        - ansible_facts['distribution_major_version'] == "8"
 
    - name: Place repo-file mailserver.guru for CentOS 7 right in place 
      copy:
        src: files/CentOS7/mailserver.guru.repo 
        dest: /etc/yum.repos.d/
      when:
        - ansible_facts['distribution'] == "CentOS"
        - ansible_facts['distribution_major_version'] == "7"
...

Im Arbeitsverzeichnis für unsere Ansible hatten wir zu Beginn bereits ein Verzeichnis für lokale (Konfigurationsdateien) angelegt. Zur besseren Unterscheidung legen wir uns dort noch zwei Verzeichnisse an, in denen wir die Releaseversionsabhängigen Dateien vorhalten werden.

 $ mkdir ~/ansible/files/CentOS7
 $ mkdir ~/ansible/files/CentOS8


Die CentOS 7 spezifische Repo-Datei mailserver.guru.repo speichern wir nun im Verzeichnis ~/ansible/files/CentOS7.

 $ vim ~/ansible/files/CentOS7/mailserver.guru.repo
~/ansible/files/CentOS7/mailserver.guru.repo
# Repository mailserver.guru
[mailserver.guru-os]
name=Extra (Mailserver-)Packages for Enterprise Linux 7 - $basearch
baseurl=http://repo.mailserver.guru/7/os/$basearch
priority=5
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/MAILSERVER.GURU-RPM-GPG-KEY-CentOS-7
 
 
[mailserver.guru-testing]
name=Testing (Mailserver-)Packages for Enterprise Linux 7 - $basearch
baseurl=http://repo.mailserver.guru/7/testing/$basearch/
#priority=5
enabled=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/MAILSERVER.GURU-RPM-GPG-KEY-CentOS-7

Die CentOS 8 spezifische Repo-Datei mailserver.guru.repo speichern wir entsprechend im Verzeichnis ~/ansible/files/CentOS8 ab.

 $ vim ~/ansible/files/CentOS8/mailserver.guru.repo
~/ansible/files/CentOS8/mailserver.guru.repo
# Repository mailserver.guru
[mailserver.guru-os]
name=Extra (Mailserver-)Packages for Enterprise Linux 8 - $basearch
baseurl=http://repo.mailserver.guru/8/os/$basearch
priority=5
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/MAILSERVER.GURU-RPM-GPG-KEY-CentOS-8
 
 
[mailserver.guru-testing]
name=Testing (Mailserver-)Packages for Enterprise Linux 8 - $basearch
baseurl=http://repo.mailserver.guru/8/testing/$basearch/
#priority=5
enabled=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/MAILSERVER.GURU-RPM-GPG-KEY-CentOS-8

Script Beschreibung

Unser Playbook, welches alle Hosts anspricht besteht im Wesentlichen aus zwei Tasks/Aufgaben. Abhängig von den beiden Ansible Facts distribution und distribution_major_version wird die Repository-Konfigurations-Datei mailserver.guru.repo auf den entsprechenden Zielhost kopiert.

Zum Kopieren wird dann das Ansible Modul copy verwendet. Da wir das RPM-Paket ansible-doc installiert hatten, könne wir auch auf der Konsole direkt Informationen zu dem Modul beziehen. Allgemeine Infos zu dem Modul erhalten wir mit folgendem Befehl:

 $ ansible-doc copy
> COPY    (/usr/lib/python2.7/site-packages/ansible/modules/files/copy.py)

        The `copy' module copies a file from the local or remote machine to a location on the remote
        machine. Use the [fetch] module to copy files from remote locations to the local box. If you need
        variable interpolation in copied files, use the [template] module. For Windows targets, use the
        [win_copy] module instead.

  * note: This module has a corresponding action plugin.

OPTIONS (= is mandatory):

- attributes
        Attributes the file or directory should have. To get supported flags look at the man page for
        `chattr' on the target system. This string should contain the attributes in the same order as the
        one displayed by `lsattr'.
        (Aliases: attr)[Default: None]
        version_added: 2.3

- backup
        Create a backup file including the timestamp information so you can get the original file back if
        you somehow clobbered it incorrectly.
        [Default: no]
        type: bool
        version_added: 0.7

- content
        When used instead of `src', sets the contents of a file directly to the specified value. For
        anything advanced or with formatting also look at the template module.
        [Default: (null)]
        version_added: 1.1

- decrypt
        This option controls the autodecryption of source files using vault.
        [Default: Yes]
        type: bool
        version_added: 2.4

= dest
        Remote absolute path where the file should be copied to. If `src' is a directory, this must be a
        directory too. If `dest' is a nonexistent path and if either `dest' ends with "/" or `src' is a
        directory, `dest' is created. If `src' and `dest' are files, the parent directory of `dest' isn't
        created: the task fails if it doesn't already exist.


- directory_mode
        When doing a recursive copy set the mode for the directories. If this is not set we will use the
        system defaults. The mode is only set on directories which are newly created, and will not affect
        those that already existed.
        [Default: (null)]
        version_added: 1.5

- follow
        This flag indicates that filesystem links in the destination, if they exist, should be followed.
        [Default: no]
        type: bool
        version_added: 1.8

- force
        the default is `yes', which will replace the remote file when contents are different than the
        source. If `no', the file will only be transferred if the destination does not exist.
        (Aliases: thirsty)[Default: yes]
        type: bool
        version_added: 1.1

- group
        Name of the group that should own the file/directory, as would be fed to `chown'.
        [Default: None]

- local_follow
        This flag indicates that filesystem links in the source tree, if they exist, should be followed.
        [Default: yes]
        type: bool
        version_added: 2.4

- mode
        Mode the file or directory should be. For those used to `/usr/bin/chmod' remember that modes are
        actually octal numbers (like 0644). Leaving off the leading zero will likely have unexpected
        results. As of version 1.8, the mode may be specified as a symbolic mode (for example, `u+rwx' or
        `u=rw,g=r,o=r').
        [Default: None]

- owner
        Name of the user that should own the file/directory, as would be fed to `chown'.
        [Default: None]

- remote_src
        If `no', it will search for `src' at originating/master machine.
        If `yes' it will go to the remote/target machine for the `src'. Default is `no'.
        Currently `remote_src' does not support recursive copying.
        [Default: no]
        type: bool
        version_added: 2.0

- selevel
        Level part of the SELinux file context. This is the MLS/MCS attribute, sometimes known as the
        `range'. `_default' feature works as for `seuser'.
        [Default: s0]

- serole
        Role part of SELinux file context, `_default' feature works as for `seuser'.
        [Default: None]

- setype
        Type part of SELinux file context, `_default' feature works as for `seuser'.
        [Default: None]

- seuser
        User part of SELinux file context. Will default to system policy, if applicable. If set to
        `_default', it will use the `user' portion of the policy if available.
        [Default: None]

- src
        Local path to a file to copy to the remote server; can be absolute or relative. If path is a
        directory, it is copied recursively. In this case, if path ends with "/", only inside contents of
        that directory are copied to destination. Otherwise, if it does not end with "/", the directory
        itself with all contents is copied. This behavior is similar to Rsync.
        [Default: (null)]

- unsafe_writes
        Normally this module uses atomic operations to prevent data corruption or inconsistent reads from
        the target files, sometimes systems are configured or just broken in ways that prevent this. One
        example are docker mounted files, they cannot be updated atomically and can only be done in an
        unsafe manner.
        This boolean option allows ansible to fall back to unsafe methods of updating files for those
        cases in which you do not have any other choice. Be aware that this is subject to race conditions
        and can lead to data corruption.
        [Default: False]
        type: bool
        version_added: 2.2

- validate
        The validation command to run before copying into place. The path to the file to validate is
        passed in via '%s' which must be present as in the example below. The command is passed securely
        so shell features like expansion and pipes won't work.
        [Default: None]


NOTES:
      * The [copy] module recursively copy facility does not scale to lots (>hundreds) of files.
        For alternative, see [synchronize] module, which is a wrapper around `rsync'.
      * For Windows targets, use the [win_copy] module instead.

AUTHOR: Ansible Core Team, Michael DeHaan
EXTENDS_DOCUMENTATION_FRAGMENT: files, validate, decrypt
        METADATA:
          status:
          - stableinterface
          supported_by: core
        

EXAMPLES:
# Example from Ansible Playbooks
- copy:
    src: /srv/myfiles/foo.conf
    dest: /etc/foo.conf
    owner: foo
    group: foo
    mode: 0644

# The same example as above, but using a symbolic mode equivalent to 0644
- copy:
    src: /srv/myfiles/foo.conf
    dest: /etc/foo.conf
    owner: foo
    group: foo
    mode: u=rw,g=r,o=r

# Another symbolic mode example, adding some permissions and removing others
- copy:
    src: /srv/myfiles/foo.conf
    dest: /etc/foo.conf
    owner: foo
    group: foo
    mode: u+rw,g-wx,o-rwx

# Copy a new "ntp.conf file into place, backing up the original if it differs from the copied version
- copy:
    src: /mine/ntp.conf
    dest: /etc/ntp.conf
    owner: root
    group: root
    mode: 0644
    backup: yes

# Copy a new "sudoers" file into place, after passing validation with visudo
- copy:
    src: /mine/sudoers
    dest: /etc/sudoers
    validate: /usr/sbin/visudo -cf %s

# Copy a "sudoers" file on the remote machine for editing
- copy:
    src: /etc/sudoers
    dest: /etc/sudoers.edit
    remote_src: yes
    validate: /usr/sbin/visudo -cf %s

# Create a CSV file from your complete inventory using an inline template
- hosts: all
  tasks:
  - copy:
      content: |
        HOSTNAME;IPADDRESS;FQDN;OSNAME;OSVERSION;PROCESSOR;ARCHITECTURE;MEMORY;
        {% for host in hostvars %}
        {%   set vars = hostvars[host|string] %}
        {{ vars.ansible_hostname }};{{ vars.remote_host }};{{ vars.ansible_fqdn }};{{ vars.ansible_distribution }};{{ vars.ansible_d
        {% endfor %}
      dest: /some/path/systems.csv
      backup: yes
    run_once: yes
    delegate_to: localhost

RETURN VALUES:


dest:
    description: destination file/path
    returned: success
    type: string
    sample: /path/to/file.txt
src:
    description: source file used for the copy on the target machine
    returned: changed
    type: string
    sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
md5sum:
    description: md5 checksum of the file after running copy
    returned: when supported
    type: string
    sample: 2a5aeecc61dc98c4d780b14b330e3282
checksum:
    description: sha1 checksum of the file after running copy
    returned: success
    type: string
    sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
backup_file:
    description: name of backup file created
    returned: changed and if backup=yes
    type: string
    sample: /path/to/file.txt.2015-02-12@22:09~
gid:
    description: group id of the file, after execution
    returned: success
    type: int
    sample: 100
group:
    description: group of the file, after execution
    returned: success
    type: string
    sample: httpd
owner:
    description: owner of the file, after execution
    returned: success
    type: string
    sample: httpd
uid:
    description: owner id of the file, after execution
    returned: success
    type: int
    sample: 100
mode:
    description: permissions of the target, after execution
    returned: success
    type: string
    sample: 0644
size:
    description: size of the target, after execution
    returned: success
    type: int
    sample: 1220
state:
    description: state of the target, after execution
    returned: success
    type: string
    sample: file

Wollen wir direkt ein snippet für unser Playbook zum Kopieren haben, geben wir bei dem Befehl ansible-doc copy noch die Option -s an.

 $ ansible-doc copy -s
- name: Copies files to remote locations
  copy:
      attributes:            # Attributes the file or directory should have. To get supported flags look at the man page for
                               `chattr' on the target system. This string should contain the
                               attributes in the same order as the one displayed by `lsattr'.
      backup:                # Create a backup file including the timestamp information so you can get the original file back if
                               you somehow clobbered it incorrectly.
      content:               # When used instead of `src', sets the contents of a file directly to the specified value. For
                               anything advanced or with formatting also look at the template
                               module.
      decrypt:               # This option controls the autodecryption of source files using vault.
      dest:                  # (required) Remote absolute path where the file should be copied to. If `src' is a directory, this
                               must be a directory too. If `dest' is a nonexistent path and if
                               either `dest' ends with "/" or `src' is a directory, `dest' is
                               created. If `src' and `dest' are files, the parent directory of
                               `dest' isn't created: the task fails if it doesn't already exist.
      directory_mode:        # When doing a recursive copy set the mode for the directories. If this is not set we will use the
                               system defaults. The mode is only set on directories which are newly
                               created, and will not affect those that already existed.
      follow:                # This flag indicates that filesystem links in the destination, if they exist, should be followed.
      force:                 # the default is `yes', which will replace the remote file when contents are different than the
                               source. If `no', the file will only be transferred if the destination
                               does not exist.
      group:                 # Name of the group that should own the file/directory, as would be fed to `chown'.
      local_follow:          # This flag indicates that filesystem links in the source tree, if they exist, should be followed.
      mode:                  # Mode the file or directory should be. For those used to `/usr/bin/chmod' remember that modes are
                               actually octal numbers (like 0644). Leaving off the leading zero will
                               likely have unexpected results. As of version 1.8, the mode may be
                               specified as a symbolic mode (for example, `u+rwx' or
                               `u=rw,g=r,o=r').
      owner:                 # Name of the user that should own the file/directory, as would be fed to `chown'.
      remote_src:            # If `no', it will search for `src' at originating/master machine. If `yes' it will go to the
                               remote/target machine for the `src'. Default is `no'. Currently
                               `remote_src' does not support recursive copying.
      selevel:               # Level part of the SELinux file context. This is the MLS/MCS attribute, sometimes known as the
                               `range'. `_default' feature works as for `seuser'.
      serole:                # Role part of SELinux file context, `_default' feature works as for `seuser'.
      setype:                # Type part of SELinux file context, `_default' feature works as for `seuser'.
      seuser:                # User part of SELinux file context. Will default to system policy, if applicable. If set to
                               `_default', it will use the `user' portion of the policy if
                               available.
      src:                   # Local path to a file to copy to the remote server; can be absolute or relative. If path is a
                               directory, it is copied recursively. In this case, if path ends with
                               "/", only inside contents of that directory are copied to
                               destination. Otherwise, if it does not end with "/", the directory
                               itself with all contents is copied. This behavior is similar to
                               Rsync.
      unsafe_writes:         # Normally this module uses atomic operations to prevent data corruption or inconsistent reads from
                               the target files, sometimes systems are configured or just broken in
                               ways that prevent this. One example are docker mounted files, they
                               cannot be updated atomically and can only be done in an unsafe
                               manner. This boolean option allows ansible to fall back to unsafe
                               methods of updating files for those cases in which you do not have
                               any other choice. Be aware that this is subject to race conditions
                               and can lead to data corruption.
      validate:              # The validation command to run before copying into place. The path to the file to validate is passed
                               in via '%s' which must be present as in the example below. The
                               command is passed securely so shell features like expansion and pipes
                               won't work.

Script ausführen

Zum Kopieren der unterschiedlichen Dateien rufen wir nun unser Playbook wie folgt auf:

 $ ansible-playbook -v 04_repro.yml 

Using /etc/ansible/ansible.cfg as config file
BECOME password: 

PLAY [all] *****************************************************************************************************************************

TASK [Gathering Facts] *****************************************************************************************************************
ok: [www7.dmz.nausch.org]
ok: [www8.dmz.nausch.org]
TASK [Place repo-file mailserver.guru for CentOS 8 right in place] ********************************************************************* skipping: [www7.dmz.nausch.org] => {"changed": false, "skip_reason": "Conditional result was False"} changed: [www8.dmz.nausch.org] => {"changed": true, "checksum": "e9ab494c29df71d4e869c5b0bf68caf865ef74c9", "dest": "/etc/yum.repos.d/mailserver.guru.repo", "gid": 0, "group": "root", "md5sum": "f21be9d7340e512c004747204b54a2b5", "mode": "0644", "owner": "root", "secontext": "system_u:object_r:system_conf_t:s0", "size": 614, "src": "/home/ansible/.ansible/tmp/ansible-tmp-1578253275.4362092-25566445227282/source", "state": "file", "uid": 0}
TASK [Place repo-file mailserver.guru for CentOS 7 right in place] ********************************************************************* skipping: [www8.dmz.nausch.org] => {"changed": false, "skip_reason": "Conditional result was False"} changed: [www7.dmz.nausch.org] => {"changed": true, "checksum": "0fac1360785f0e02e074b4cc4f785e181f6620b9", "dest": "/etc/yum.repos.d/mailserver.guru.repo", "gid": 0, "group": "root", "md5sum": "90d8ca6369ff514b3c3dc3ddfac4ebdb", "mode": "0644", "owner": "root", "secontext": "system_u:object_r:system_conf_t:s0", "size": 614, "src": "/home/ansible/.ansible/tmp/ansible-tmp-1578253277.3541374-16074731401428/source", "state": "file", "uid": 0}
PLAY RECAP *************************************************************************************************************************************** www7.dmz.nausch.org : ok=2 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 www8.dmz.nausch.org : ok=2 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0


Die blau markierten Zeilen bzw. in der Zusammenfassung genannten skipped=1 brauchen uns nicht beunruhigen.
Die Ursache hierzu ist einfach erklärt: Da wir die Prüfung auf die CentOS-Version 7 und 8 auf beiden Hosts ausführen, führt natürlich immer nur zu einem Treffer, der jeweils „falsche“ wird einfach übersprungen.

Ergebnis

Auf dem Zielhost findet sich nun unsere gewünschte Datei mit dem zugehörigen Inhalt an Ort und Stelle.

 # ll /etc/yum.repos.d/mailserver.guru.repo
-rw-r--r--. 1 root root 614 Jan  5 20:41 /etc/yum.repos.d/mailserver.guru.repo

In folgendem Beispiel Nummer fünf wollen wir auf unseren CentOS 8-Hosts den NTP-Deamon chrony installieren und auch entsprechend als Client konfigurieren.

Script anlegen

Das Script legen wir wie auch schon bei den anderen Beispielen zuvor im Verzeichnis ~/ansible an

 $ vim 05_chrony.yml
05_chrony.yml
---
- hosts: centos8
  become: true
  vars:
    sudoers: ansible
    config_file: /etc/chrony.conf
    # chronyd client config-options
    chrony_pool: "server time.dmz.nausch.org iburst"
    chrony_stratumweight: "stratumweight 0"
    chrony_makestep: "makestep 10 3"
 
  tasks:
    - name: Install chrony ntp Deamon
      dnf: 
      #https://docs.ansible.com/ansible/latest/modules/dnf_module.html
        name: chrony
        state: latest
 
    - name: Check if /etc/chrony.orig does exists
      stat:
      #https://docs.ansible.com/ansible/latest/modules/stat_module.html
        path: /etc/chrony.conf.orig
      register: stat_result
 
    - name: Make a copy of /etc/chrony.conf as /etc/chrony.conf.orig  
      copy:
      #https://docs.ansible.com/ansible/latest/modules/copy_module.html
        remote_src: yes
        src: /etc/chrony.conf
        dest: /etc/chrony.conf.orig
      when: stat_result.stat.exists == False
 
    - name: Copy template config-file in place
      template:
      #https://docs.ansible.com/ansible/latest/modules/template_module.html        
        src: templates/CentOS8/chrony-client.conf.j2 
        dest: "{{ config_file }}"
 
    - name: Make sure Chrony is started up
      service: 
      #https://docs.ansible.com/ansible/latest/modules/service_module.html
        name: chronyd 
        state: started 
        enabled: yes
...

Die Konfigurationsdatei unseres chrony-Daemon werden wir im Arbeitsbereich unserer ansible-Umgebung auf dem Admin-Rechner/-Server in einem eigenen Verzeichnis vorhalten. Diese Verzeichnis erstellen wir uns nun noch.

 $ mkdir -p ~/ansible/templates/CentOS8/

Ansible nutzt die Jinja2 Template Engine zum abgleich der verwendeten Variablen in einem Playbook. Wir werden also unsere Konfigurationsdatei entsprechend präparieren und dort ablegen. Als Datei-Extension verwenden wir hier .j2, um dies optisch abzutrennen. Wir könnten auch andere Datei-Extension verwenden, da Ansible selbst nur den Inhalt bzw. die Formatierung der Variablen interprätiert.

 $ vim ~/ansible/templates/CentOS8/chrony-client.conf.j2
~/ansible/templates/CentOS8/chrony-client.conf.j2
# Use public servers from the pool.ntp.org project.
# Please consider joining the pool (http://www.pool.ntp.org/join.html).
{{ chrony_pool }}
 
# Ignore stratum in source selection
{{ chrony_stratumweight }}
 
# Record the rate at which the system clock gains/losses time.
driftfile /var/lib/chrony/drift
 
# Allow the system clock to be stepped in the first three updates
# if its offset is larger than 1 second.
makestep 1.0 3
 
# Enable kernel synchronization of the real-time clock (RTC).
rtcsync
 
# In first three updates step the system clock instead of slew
# if the adjustment is larger than 10 seconds.
{{ chrony_makestep }}
 
# Enable hardware timestamping on all interfaces that support it.
#hwtimestamp *
 
# Increase the minimum number of selectable sources required to adjust
# the system clock.
#minsources 2
 
# Allow NTP client access from local network.
#allow 192.168.0.0/16
 
# Serve time even if not synchronized to a time source.
#local stratum 10
 
# Specify file containing keys for NTP authentication.
keyfile /etc/chrony.keys
 
# Get TAI-UTC offset and leap seconds from the system tz database.
leapsectz right/UTC
 
# Specify directory for log files.
logdir /var/log/chrony
 
# Select which information is logged.
#log measurements statistics tracking

Die drei Konfigurationsoptionen, die wir für unsere chrony-client Konfiguration später setzen und ggf. verändern wollen haben wir hier mit einer Variable belegt:

  • {{ chrony_pool }} : Server von dem bzw. denen wir später die Zeit beziehen wollen.
  • {{ chrony_stratumweight }} : Ignorieren der stratum Bewertung, da wir hier nur einen Zielhost zur Zeit befragen
  • {{ chrony_makestep }} : Definition wie bei den ersten Aktualisierungsschritten zu verfahren ist.

Script Beschreibung

Im Playbook greifen wir auf folgende Ansible-Module zurück:

  • dnf zum Installieren des Paketes
  • stat und copy zum Sichern der originalen Konfigurationsdatei.
  • template zur Konfiguration unseres Daemon
  • service zum (automatischen) Starten (beim Systemstart des Hosts)).

In unserem Playbook werden am Anfang den entsprechenden Variablen ihre werte zugewiesen. Im Anschluss daran werden fünft tasks definiert:

  1. Aufgabe: Installation des chrony NTP-Daemon
  2. Aufgabe: Überprüfen ob von der Konfigurationsdatei, die das RPM-Paket mitbrachte schon eine Sicherungskopie erstellt wurde.
  3. Aufgabe: Sofern bei der Prüfung in Aufgabe 2 noch keine Sicherungskopie erstellt wurde, wird eine Sicherungsopie erstellt.
  4. Aufgabe: Konfigurieren unseres chrony-Daemon
  5. Aufgabe: Starten des chrony-Daemon und aktivieren des automatischen Starts beim Starten des Hosts

Script ausführen

Zum Kopieren der unterschiedlichen Dateien rufen wir nun unser Playbook wie folgt auf:

 $ ansible-playbook -v 05_chrony.yml 

Using /etc/ansible/ansible.cfg as config file
BECOME password: 

PLAY [centos8] *************************************************************************************************************************

TASK [Gathering Facts] *****************************************************************************************************************
ok: [www7.dmz.nausch.org]
ok: [www8.dmz.nausch.org]
TASK [Install chrony ntp Deamon] ******************************************************************************************************* changed: [www8.dmz.nausch.org] => {"changed": true, "msg": "", "rc": 0, "results": ["Installed: chrony", "Installed: chrony-3.3-3.el8.x86_64"]}
TASK [Check if /etc/chrony.orig does exists] ******************************************************************************************* ok: [www8.dmz.nausch.org] => {"changed": false, "stat": {"exists": false}}
TASK [Make a copy of /etc/chrony.conf as /etc/chrony.conf.orig] ************************************************************************ changed: [www8.dmz.nausch.org] => {"changed": true, "checksum": "89175e7c294dedf12bd473a952014e2cefd5766d", "dest": "/etc/chrony.conf.orig", "gid": 0, "group": "root", "md5sum": "97078948a9e2c1b99ab3e38d26a3311d", "mode": "0644", "owner": "root", "secontext": "system_u:object_r:etc_t:s0", "size": 1085, "src": "/etc/chrony.conf", "state": "file", "uid": 0}
TASK [Copy template config-file in place] ********************************************************************************************** changed: [www8.dmz.nausch.org] => {"changed": true, "checksum": "37539ecdd11393937e5596894db41a02c6121c5f", "dest": "/etc/chrony.conf", "gid": 0, "group": "root", "md5sum": "adde7eeb1766f7f83bd3fba6cc30ec23", "mode": "0644", "owner": "root", "secontext": "system_u:object_r:etc_t:s0", "size": 1265, "src": "/home/ansible/.ansible/tmp/ansible-tmp-1578323551.4891849-132640554634531/source", "state": "file", "uid": 0}
TASK [Make sure Chrony is started up] ************************************************************************************************** changed: [www8.dmz.nausch.org] => {"changed": true, "enabled": true, "name": "chronyd", "state": "started", "status": {"ActiveEnterTimestampMonotonic": "0", "ActiveExitTimestampMonotonic": "0", "ActiveState": "inactive", "After": "sysinit.target system.slice -.mount systemd-tmpfiles-setup.service sntp.service ntpdate.service tmp.mount systemd-journald.socket ntpd.service basic.target", "AllowIsolate": "no", "AmbientCapabilities": "", "AssertResult": "no", "AssertTimestampMonotonic": "0", "Before": "multi-user.target shutdown.target", "BlockIOAccounting": "no", "BlockIOWeight": "[not set]", "CPUAccounting": "no", "CPUQuotaPerSecUSec": "infinity", "CPUSchedulingPolicy": "0", "CPUSchedulingPriority": "0", "CPUSchedulingResetOnFork": "no", "CPUShares": "[not set]", "CPUUsageNSec": "[not set]", "CPUWeight": "[not set]", "CacheDirectoryMode": "0755", "CanIsolate": "no", "CanReload": "no", "CanStart": "yes", "CanStop": "yes", "CapabilityBoundingSet": "cap_chown cap_dac_override cap_dac_read_search cap_fowner cap_fsetid cap_kill cap_setgid cap_setuid cap_setpcap cap_linux_immutable cap_net_bind_service cap_net_broadcast cap_net_admin cap_net_raw cap_ipc_lock cap_ipc_owner cap_sys_module cap_sys_rawio cap_sys_chroot cap_sys_ptrace cap_sys_pacct cap_sys_admin cap_sys_boot cap_sys_nice cap_sys_resource cap_sys_time cap_sys_tty_config cap_mknod cap_lease cap_audit_write cap_audit_control cap_setfcap cap_mac_override cap_mac_admin cap_syslog cap_wake_alarm cap_block_suspend", "CollectMode": "inactive", "ConditionResult": "no", "ConditionTimestampMonotonic": "0", "ConfigurationDirectoryMode": "0755", "Conflicts": "systemd-timesyncd.service shutdown.target ntpd.service", "ControlPID": "0", "DefaultDependencies": "yes", "Delegate": "no", "Description": "NTP client/server", "DevicePolicy": "auto", "Documentation": "man:chronyd(8) man:chrony.conf(5)", "DynamicUser": "no", "EnvironmentFiles": "/etc/sysconfig/chronyd (ignore_errors=yes)", "ExecMainCode": "0", "ExecMainExitTimestampMonotonic": "0", "ExecMainPID": "0", "ExecMainStartTimestampMonotonic": "0", "ExecMainStatus": "0", "ExecStart": "{ path=/usr/sbin/chronyd ; argv[]=/usr/sbin/chronyd $OPTIONS ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }", "ExecStartPost": "{ path=/usr/libexec/chrony-helper ; argv[]=/usr/libexec/chrony-helper update-daemon ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }", "FailureAction": "none", "FileDescriptorStoreMax": "0", "FragmentPath": "/usr/lib/systemd/system/chronyd.service", "GID": "[not set]", "GuessMainPID": "yes", "IOAccounting": "no", "IOSchedulingClass": "0", "IOSchedulingPriority": "0", "IOWeight": "[not set]", "IPAccounting": "no", "IPEgressBytes": "18446744073709551615", "IPEgressPackets": "18446744073709551615", "IPIngressBytes": "18446744073709551615", "IPIngressPackets": "18446744073709551615", "Id": "chronyd.service", "IgnoreOnIsolate": "no", "IgnoreSIGPIPE": "yes", "InactiveEnterTimestampMonotonic": "0", "InactiveExitTimestampMonotonic": "0", "JobRunningTimeoutUSec": "infinity", "JobTimeoutAction": "none", "JobTimeoutUSec": "infinity", "KeyringMode": "private", "KillMode": "control-group", "KillSignal": "15", "LimitAS": "infinity", "LimitASSoft": "infinity", "LimitCORE": "infinity", "LimitCORESoft": "infinity", "LimitCPU": "infinity", "LimitCPUSoft": "infinity", "LimitDATA": "infinity", "LimitDATASoft": "infinity", "LimitFSIZE": "infinity", "LimitFSIZESoft": "infinity", "LimitLOCKS": "infinity", "LimitLOCKSSoft": "infinity", "LimitMEMLOCK": "16777216", "LimitMEMLOCKSoft": "16777216", "LimitMSGQUEUE": "819200", "LimitMSGQUEUESoft": "819200", "LimitNICE": "0", "LimitNICESoft": "0", "LimitNOFILE": "4096", "LimitNOFILESoft": "1024", "LimitNPROC": "31132", "LimitNPROCSoft": "31132", "LimitRSS": "infinity", "LimitRSSSoft": "infinity", "LimitRTPRIO": "0", "LimitRTPRIOSoft": "0", "LimitRTTIME": "infinity", "LimitRTTIMESoft": "infinity", "LimitSIGPENDING": "31132", "LimitSIGPENDINGSoft": "31132", "LimitSTACK": "infinity", "LimitSTACKSoft": "8388608", "LoadState": "loaded", "LockPersonality": "no", "LogLevelMax": "-1", "LogsDirectoryMode": "0755", "MainPID": "0", "MemoryAccounting": "yes", "MemoryCurrent": "[not set]", "MemoryDenyWriteExecute": "no", "MemoryHigh": "infinity", "MemoryLimit": "infinity", "MemoryLow": "0", "MemoryMax": "infinity", "MemorySwapMax": "infinity", "MountAPIVFS": "no", "MountFlags": "", "NFileDescriptorStore": "0", "NRestarts": "0", "Names": "chronyd.service", "NeedDaemonReload": "no", "Nice": "0", "NoNewPrivileges": "no", "NonBlocking": "no", "NotifyAccess": "none", "OOMScoreAdjust": "0", "OnFailureJobMode": "replace", "PIDFile": "/var/run/chrony/chronyd.pid", "PermissionsStartOnly": "no", "Perpetual": "no", "PrivateDevices": "no", "PrivateMounts": "no", "PrivateNetwork": "no", "PrivateTmp": "yes", "PrivateUsers": "no", "ProtectControlGroups": "no", "ProtectHome": "yes", "ProtectKernelModules": "no", "ProtectKernelTunables": "no", "ProtectSystem": "full", "RefuseManualStart": "no", "RefuseManualStop": "no", "RemainAfterExit": "no", "RemoveIPC": "no", "Requires": "sysinit.target -.mount system.slice", "RequiresMountsFor": "/var/tmp", "Restart": "no", "RestartUSec": "100ms", "RestrictNamespaces": "no", "RestrictRealtime": "no", "Result": "success", "RootDirectoryStartOnly": "no", "RuntimeDirectoryMode": "0755", "RuntimeDirectoryPreserve": "no", "RuntimeMaxUSec": "infinity", "SameProcessGroup": "no", "SecureBits": "0", "SendSIGHUP": "no", "SendSIGKILL": "yes", "Slice": "system.slice", "StandardError": "inherit", "StandardInput": "null", "StandardInputData": "", "StandardOutput": "journal", "StartLimitAction": "none", "StartLimitBurst": "5", "StartLimitIntervalUSec": "10s", "StartupBlockIOWeight": "[not set]", "StartupCPUShares": "[not set]", "StartupCPUWeight": "[not set]", "StartupIOWeight": "[not set]", "StateChangeTimestampMonotonic": "0", "StateDirectoryMode": "0755", "StatusErrno": "0", "StopWhenUnneeded": "no", "SubState": "dead", "SuccessAction": "none", "SyslogFacility": "3", "SyslogLevel": "6", "SyslogLevelPrefix": "yes", "SyslogPriority": "30", "SystemCallErrorNumber": "0", "TTYReset": "no", "TTYVHangup": "no", "TTYVTDisallocate": "no", "TasksAccounting": "yes", "TasksCurrent": "[not set]", "TasksMax": "26213", "TimeoutStartUSec": "1min 30s", "TimeoutStopUSec": "1min 30s", "TimerSlackNSec": "50000", "Transient": "no", "Type": "forking", "UID": "[not set]", "UMask": "0022", "UnitFilePreset": "enabled", "UnitFileState": "enabled", "UtmpMode": "init", "WantedBy": "multi-user.target", "WatchdogTimestampMonotonic": "0", "WatchdogUSec": "0"}}
PLAY RECAP *************************************************************************************************************************************** www8.dmz.nausch.org : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ergebnis

Auf dem Zielhost finden wir nun die Sicherungskopie der originalen chrony.conf aus dem RPM-Paket.

 # ll /etc/chrony.conf*
-rw-r--r--. 1 root root 1265 Jan  6 16:12 /etc/chrony.conf
-rw-r--r--. 1 root root 1085 Apr  4  2018 /etc/chrony.conf.orig

Der Dienst chrony.conf wurde entsprechend gestartet und läuft:

 # systemctl status chronyd.service

chronyd.service - NTP client/server
   Loaded: loaded (/usr/lib/systemd/system/chronyd.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2020-01-06 16:12:34 CET; 47s ago
     Docs: man:chronyd(8)
           man:chrony.conf(5)
  Process: 10697 ExecStartPost=/usr/libexec/chrony-helper update-daemon (code=exited, status=0/SUCCESS)
  Process: 10693 ExecStart=/usr/sbin/chronyd $OPTIONS (code=exited, status=0/SUCCESS)
 Main PID: 10695 (chronyd)
    Tasks: 1 (limit: 26213)
   Memory: 1.1M
   CGroup: /system.slice/chronyd.service
           └─10695 /usr/sbin/chronyd

Jan 06 16:12:34 vml000090.dmz.nausch.org systemd[1]: Starting NTP client/server...
Jan 06 16:12:34 vml000090.dmz.nausch.org chronyd[10695]: chronyd version 3.3 starting (+CMDMON +NTP +REFCLOCK +RTC +PRIVDROP +SCFILTER +SIGND +ASYNCDNS +SECHASH +IPV6 +DEBUG)
Jan 06 16:12:34 vml000090.dmz.nausch.org chronyd[10695]: Initial frequency 10.454 ppm
Jan 06 16:12:34 vml000090.dmz.nausch.org chronyd[10695]: Using right/UTC timezone to obtain leap second data

Beim Starten des Servers/Hosts wird der chronyd-Daemon auch automatisch gestartet. Dies können wir wie folgt überprüfen:

 # systemctl is-enabled chronyd.service
enabled

In diesem Konfigurationsbeispiel befassen wir uns nun mit einer Besonderheit in Sachen Zielhosts. Nicht immer hat man auf einem Zielsystem Python zur Verfügung, wie z.B. bei Freifunk-Hardware, die auf gluon und OpenWrt basieren. Aber auch hier gibt es eine Lösungsmöglichkeit, in dem man für diesen speziellen Fall auf das Modul raw zurückgreift.

Script anlegen

Auch dieses Beispiel-Script speichern wir im Verzeichnis ~/ansible ab.

 $ vim 06_change_contact.yml
06_change_contact.yml
---
- hosts: ffmuc_gluon
  gather_facts: False
 
  tasks:
    - name: "Update new contact-address on own ffmuc-nodes"
      #https://docs.ansible.com/ansible/latest/modules/raw_module.html
      raw: uci set gluon-node-info.@owner[0].contact=' Django [BOfH] django@nausch.org | chat -> @django' ; uci commit
...

Script Beschreibung

Dieses Playbook besteht nur aus einer Aufgabe, nämlich dem Ändern der Kontakt-Adresse unserer Freifunk-WLAN-Knoten, die allesamt auf Gluon/OpenWRT basieren. Daher macht es auch keinen Sinn, dass Ansible versuchen wird, die Facts der Zielhosts zu ermitteln, da dort Python nicht zur Verfügung steht, welches für die Ermittlung der systemdaten benötigt wird.

Wir deaktivieren also mit gather_facts: False diese Funktion zu Beginn.

Zum Ausführen der Kommandos auf den Freifunk-Knoten verwenden wird das Ansible Modul raw. Zum Ändern der Kontaktdaten benutzen wir die beiden Befehle:

uci set gluon-node-info.@owner[0].contact='Django [BOfH] django@nausch.org | chat -> @django'
uci commit

Script ausführen

Nun wollen wir unser ersten Playbook ausführen, um auf den Freifunk-Knoten die Kontaktdaten zu aktualisieren; hierzu rufen wir unser Script wie folgt auf:

 $ ansible-playbook -v 06_change_contact.yml

Using /etc/ansible/ansible.cfg as config file
BECOME password: 

PLAY [ffmuc_gluon] *********************************************************************************************************************

TASK [Update new contact-address on own ffmuc-nodes] ***********************************************************************************
changed: [ff_pliening_gbw_ug] => {"changed": true, "rc": 0, "stderr": "Shared connection to 2001:608:a01:102:32b5:c2ff:fe56:62b1 closed.\r\n", "stderr_lines": ["Shared connection to 2001:608:a01:102:32b5:c2ff:fe56:62b1 closed."], "stdout": "", "stdout_lines": []}
changed: [ff_pliening_gbw_egod] => {"changed": true, "rc": 0, "stderr": "Shared connection to 2001:608:a01:102:1ae8:29ff:fea9:22ed closed.\r\n", "stderr_lines": ["Shared connection to 2001:608:a01:102:1ae8:29ff:fea9:22ed closed."], "stdout": "", "stdout_lines": []}
changed: [ff_pliening_gbw_ogod] => {"changed": true, "rc": 0, "stderr": "Shared connection to 2001:608:a01:102:1ae8:29ff:fec0:aaae closed.\r\n", "stderr_lines": ["Shared connection to 2001:608:a01:102:1ae8:29ff:fec0:aaae closed."], "stdout": "", "stdout_lines": []}
changed: [ff_pliening_gbw_dgod] => {"changed": true, "rc": 0, "stderr": "Shared connection to 2001:608:a01:102:1ae8:29ff:fec6:c8eb closed.\r\n", "stderr_lines": ["Shared connection to 2001:608:a01:102:1ae8:29ff:fec6:c8eb closed."], "stdout": "", "stdout_lines": []}
changed: [ff_pliening_gbw_cpod] => {"changed": true, "rc": 0, "stderr": "Shared connection to 2001:608:a01:102:1ae8:29ff:fec6:c8dd closed.\r\n", "stderr_lines": ["Shared connection to 2001:608:a01:102:1ae8:29ff:fec6:c8dd closed."], "stdout": "", "stdout_lines": []}
changed: [ff_pliening_gbw_kvm_ol] => {"changed": true, "rc": 0, "stderr": "Shared connection to 2001:608:a01:102:5054:ff:fe9e:b358 closed.\r\n", "stderr_lines": ["Shared connection to 2001:608:a01:102:5054:ff:fe9e:b358 closed."], "stdout": "", "stdout_lines": []}
PLAY RECAP ***************************************************************************************************************************** ff_pliening_gbw_cpod : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ff_pliening_gbw_dgod : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ff_pliening_gbw_egod : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ff_pliening_gbw_kvm_ol : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ff_pliening_gbw_ogod : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 ff_pliening_gbw_ug : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ergebnis

Alle unsere eigenen definierten Freifunk Knoten haben nun auf der Freifunk München Karte unseren aktualisierte Kontaktdaten.

In diesem Konfigurationsbeispiel wollen wir möglichst einfach und schnell einen Offloader für Freifunk München auf Basis eines Raspberry 4B befassen. Dabei gehen wir auf unterschiedliche Konfigurations-Optionen ein und wollen dennoch die Einstiegshürden für den ungeübteren Ansible und Linux-User möglichst tief ansetzen.

Die detaillierte Beschreibung hierzu ist im Kapitel Bau eines Freifunk-Offloaders auf Basis eines Raspberry 4B zu finden.

Links

Cookies helfen bei der Bereitstellung von Inhalten. Durch die Nutzung dieser Seiten erklären Sie sich damit einverstanden, dass Cookies auf Ihrem Rechner gespeichert werden. Weitere Information
  • centos/ansible/playbooks1.txt
  • Zuletzt geändert: 28.06.2020 14:10.
  • von django