WSUS Offline und Ansible

Ansible ist schon sehr nice. Benutzt man es mit Windows, ist es auch noch nice aber manchmal fühlt man sich dabei wie ein Ritt auf einem Vulkan, durch die Hölle... oder sowas. Meist funktioniert es aber was im Hintergrund passiert gleicht Feen-Magie.

Da wir es wagen einen Proxy zu benutzen scheint es völlig unmöglich zu sein an Windows Updates zu kommen. Und in letzter Zeit scheinen auch meine Workarounds nicht zu fruchten. Immer wieder hängen die Updates tagelang in der Pipeline und können nicht runtergeladen oder installiert werden. Also schaute ich mir mal wieder einen alten Bekannten an: WSUS Offline Update. Ein Paket aus Scripten das Updates zentral runterlädt und dann offline installiert werden kann. Das schöne daran: Das Download-Script liegt auch in Shell vor und kann so direkt auf dem Linux Samba Server ausgeführt werden. Nur wie kommen die Updates auf die Clients? Wie immer lautet die Antwort: Per Ansible Playbook:

---
- name: mount updates share and run update
  raw: "net use U: \\\\meinsambaserver\\updates /persistent:no; cmd /c U:\\wsusoffline\\client\\cmd\\DoUpdate.cmd"
  register: command_result
  changed_when: "'Installation successful' in command_result.stdout"
  failed_when:
    - "'Nothing to do!' not in command_result.stdout"
    - "'Installation successful.' not in command_result.stdout"

WSUS Offline liegt dabei auf einem Updates-Share. Die zwei schwierigsten Sachen war das Escaping der Orte für den Kommandoaufruf und den richtigen Status erkennen. Aus irgendeinem Grund hat WSUS Offline immer den Return Code 1 raus wenn er was installiert hat. Mit der Hilfe von changed_when und failed_when suche ich in dem STDOUT-Output nach bestimmten Stichworten um den richtigen Status zu bekommen. Bestimmt decke ich nicht alles ab, aber in meinen ersten Tests funktioniert es.

Ansible, win_package und Upgrades

source: http://childofmoonlight.tumblr.com/post/44755113585/im-a-grumpy-guss-enjoy-this-gif-set-of-grumpy

Das Ansible Modul win_package beruht auf die Annahme das die product_id in der Registry vorhanden ist oder eben nicht. Davon macht es abhängig ob ein Paket installiert werden soll oder ob es schon vorhanden ist. Nun kann es ja auch vorkommen das man ein Paket, obwohl es laut Registry schon installiert ist, es noch einmal installieren möchte. Quasi ein Upgrade machen. Schön wäre es wenn er nicht nur schaut ob das Paket installiert ist, sondern auch die installierte Version. Daran könnte man Task Entscheidungen treffen. Dies mache ich nun manuell. Ein Beispiel für VLC:

---

# Der ganz normale Install-Task. Es wird nach der product_id gesucht gegebenenfalls installiert
- name: install
  win_package:
    product_id="VLC media player"
    path="//myserver/updates/software/vlc/vlc-2.2.2-win32.exe"
    arguments="/L=1031 /S"

# Ein Powershell Snippet das die Version des installierten Pakets in der Variabel "version" speichert
- name: check version
  raw: (Get-ItemProperty "HKLM:\SOFTWARE\wow6432node\Microsoft\Windows\CurrentVersion\Uninstall\VLC media player").DisplayVersion
  register: version

- name: upgrade
  win_package:
    product_id="VLC media player upgrade" # Muss anders sein damit das Paket nochmal installiert wird
    path="//myserver/updates/software/vlc/vlc-2.2.2-win32.exe"
    arguments="/L=1031 /S"
  register: upgrade_results # Speichert das Ergebnis des Tasks in die Variabel "upgrade_results"
  changed_when: '"was installed" in upgrade_results.msg' # Ändert den Status auf "changed" wenn der String "was installed" im Ergebnis ist
  failed_when: '"was installed" not in upgrade_results.msg' # Status "failed" wenn "was installed" nicht im Ergebnis ist
  ignore_errors: True # Sonst bricht er ab bevor der Task überhaupt die Variabel "upgrade_results" befüllt
  when: '"2.2.2" not in version.stdout_lines' # Den Task nur ausführen wenn die Version eine andere ist

Ansible, win_package und die product_id

Gerade läuft das letzte Windows 7 auf Windows 10 Update. Diese Möglichkeit habe ich genutzt um für die Administration der Clients von SaltStack zu Ansible zu wechseln. Windows Administration ist für mich eher so ein leidiges Thema, was mich ziemlich oft auf die Palme bringt. Ansible benutze ich auf den Linux Servern für fast alles. Mal schnell ein paar Update einspielen oder auch für das ausrollen meines batcaves.

Master vorbereiten

Neben Ansible sollte man noch das Modul pywinrm installieren:

pip install "pywinrm>=0.1.1"

Clients vorbereiten

Um Ansible unter Windows zu benutzen muss ein PowerShell Script ausgeführt werden. Damit dies funktioniert muss erst eine PowerShell Policy angepasst werden. Dies tut man mit set-executionpolicy remotesigned. Danach Script ausführen und alles sollte eingerichtet sein.

Inventory

Auch die hosts-Datei muss ein wenig angepasst werden. Es werden ein paar zusätzliche Daten benötigt. Wenn das hosts-File so aussieht:

[windows]
192.168.1.5
192.168.1.6
192.168.1.7

habe ich eine group_vars/windows.yml angelegt die so aussieht:

ansible_user: winadminuser
ansible_password: winadminpassword
ansible_port: 5986
ansible_connection: winrm
ansible_winrm_server_cert_validation: ignore

Dieses File kann man mit ansible-vault encrypt group_vars/windows.yml verschlüßeln.

Der erste Test

ansible windows -m win_ping --ask-vault-pass sollte für einen ersten Versuch reichen.

Pakete installieren

Hier ein einfaches Beispiel um ein ein Paket zu installieren:

---
- name: Install the vc thingy
  win_package:
    name="Microsoft Visual C thingy"
    path="http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe"
    Product_Id="{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}"
    Arguments="/install /passive /norestart"

Ich hatte mit fast allen Paketen Probleme die product_id herauszufinden. Erst dachte ich es handelt sich hierbei um den Titel der Software in den Systemeinstellungen unter Windows. Pustekuchen. Man muss sich diese ID aus den Registry popeln. Diese befindet sich unter HKLM:\Software\microsoft\windows\currentversion\uninstall. Aber die sah bei mir ziemlich dünn aus und war bei weiten nicht mit allen Sachen gefüllt die ich installiert hatte. Stellt sich herraus das ein Teil der Registry sich unter HKLM:\Software\wow6432node\microsoft\windows\currentversion\uninstall versteckt. Und zwar wenn es sich um 32bit Pakete handelt.

Ansible Playbooks und variable Hosts

Ich benutze Ansible für viele Sachen. Unter anderem habe ich ein Set an Playbooks und Roles um meine Server so anzupassen das ich mich auf ihnen wohl fühle. Bis jetzt habe ich immer Ansible auch auf den Servern installiert und es dann lokal ausgeführt. Aber dabei nutze ich nicht das eigentlich Ansible Feature: das Ganze Remote über SSH auszurollen. Problem: In den Playbooks muss es die Zeile - hosts: geben. Aber eigentlich will ich das alles Variabel ausführen können. Zum Beispiel nutze ich die gleichen Files auch um meine Vagrant Container einzurichten. Wieso also beim Aufruf die Hosts dem Playbook nicht einfach übergeben? Die Lösung ist dann doch wieder einmal einfacher als man denkt. Man benutzt die Möglichkeit in Playbooks und Roles Variabeln einzusetzen.

---
- hosts: "{{ hosts }}"
  roles:
    - git
    - tmux
    - vim
    - zsh

Diese Variabel übergeben wir beim Aufruf von Ansible.

ansible-playbook base.yml --extra-vars="hosts=myservers"

Ansible dazu benutzen um einen Fileserver zu migrieren

Ab und zu kommt es dann doch einmal vor das man seinen Fileserver umziehen muss. Diese Migration kann man gut benutzen um zum Beispiel mal ein paar Permissions gerade zu ziehen oder andere Kleinigkeiten anzupassen. Nun hatte ich mich dran gemacht ein Script zu schreiben mit allen Befehlen die ich dazu brauche. Wir kennen alle die Nachteile von schnell gehackten Bash-Scripten. Irgendwo ein Dreher drin und die Daten müssen aus dem Backup geholt werden oder man vergisst User oder Gruppen.

Ich werde immer mehr zum Ansible Fan. Vor allem das strukturiere definieren von Tasks hilft mir sehr meine Aufgabe darzustellen. Ein weiterer Vorteil ist das Ansible sich um so Sachen wie "Errorhandling" kümmert und es auch möglich ist Results aus dem einem Task im anderen wieder aufzugreifen.

Hier passiert nichts wildes. Wir bereiten den neuen Fileserver vor mit allen Benutzern, Gruppen und den Files die wir benötigen. Dazu gibt es ein paar Tasks die die Permissions gerade ziehen während wir migrieren. Hier das Playbook was ich dafür geschrieben habe. Ich habe es versucht zu kommentieren.

Update: Ich habe einen kapitalen Fehler begangen. Ich habe zweimal eine Variabel mit dem Namen task registriert. Da die notifizierten Tasks erst am Ende ausgeführt werden, wird task einfach überschrieben. Jetzt haben beide Tasks zwei verschiedene Variabeln.

---
# Ich führe das ganze nicht remote aus sondern lokal.
#
- hosts: localhost
  sudo: yes

  tasks:

    - name: install rsync
      apt: name=rsync
           state=present

    # Hier legen wir zwei Gruppen an.
    #
    - name: add groups
      group: name={{ item }}
             state=present
      with_items:
        - intern
        - extern

    # Wir legen zwei Benutzer an. Die im Dict angegebenen Gruppen werden der
    # Gruppe "sambashare" hinzugefügt. Nichts besonderes. Die Benutzer
    # haben "/bin/false" als Shell und ein Homeverzeichnis in "/home"
    #
    - name: add users
      user: name={{ item.user }}
            state=present
            shell=/bin/false
            home=/home/{{ item.user }}
            groups=sambashare,{{ item.groups }}
            append=yes
      with_items:
        - { user: 'user1', groups: 'intern' }
        - { user: 'user2', groups: 'intern,extern' }

    # Ansible hat ein "synchronize"-Modul was ein Wrapper für Rsync ist.
    # Wirklich schön. So muss man nicht ein Script zusammen hacken sondern
    # kann die kompletten Möglichkeiten von Ansible zu nutzen.
    #
    # Wir speichern die Task-Results in der Variabel "task" damit wir sie in
    # den Handlern benutzen können. Ein Möglichkeit um zum Beispiel nur
    # die Permissions anzuwenden an das Item welches sich verändert hat.
    # Im Normalfall würde er bei nur einem geänderten Item die Handler laufen
    # lassen. Diese würden dann über alle Verzeichnisse rüber laufen und
    # die Permissions ändern. Da es sich um große Verzeichnisse handelt,
    # musste es eine bessere Lösung geben. Die Logik der Handler werden
    # später erklärt.
    #
    - name: sync freigaben
      synchronize: src=root@192.168.1.1:/srv/samba/{{ item.src }}/
                   dest=/srv/samba/{{ item.dest }}
                   perms=no
                   group=no
                   delete=yes
      with_items:
        - { src: 'Erstefreigabe', dest: 'erstefreigabe' }
        - { src: 'Zweitefreigabe', dest: 'zweitefreigabe' }
      register: freigaben
      notify:
        - set freigaben group
        - set freigaben file permissions
        - set freigaben group permissions
        - set freigaben permissions

    - name: sync homes
      synchronize: src=root@192.168.1.1:/home/{{ item }}/
                   dest=/home/{{ item }}
                   group=no
                   delete=yes
      with_items:
        - user1
        - user2
      register: homes
      notify:
        - set home group

  handlers:

    # Hier haben wir so einen Handler. Erstmal iteriert er über alle
    # Task-Results. Und nun kommt "when" ins Spiel: Er führt das Kommando
    # nur aus wenn ein Item als geändert gilt. So gibt es wenig Overhead
    # beim Ausführen der Handler. Sie werden nur ausgeführt wenn es auch
    # wirklich nötig ist.
    #
    - name: set freigaben group
      command: chgrp -R {{ item.item.dest }} /srv/samba/{{ item.item.dest }}
      with_items: freigaben.results
      when: item.changed == True

    - name : set freigaben file permissions
      command: find /srv/samba/{{ item.item.dest }} -type f -exec chmod 0664 {} \;
      with_items: freigaben.results
      when: item.changed == True

    - name : set freigaben group permissions
      command: find /srv/samba/{{ item.item.dest }} -type d -exec chmod 2775 {} \;
      with_items: freigaben.results
      when: item.changed == True

    - name: set freigaben permissions
      file: path=/srv/samba/{{ item.item.dest }}
            state=directory
            mode=2775
            owner=root
            group={{ item.item.dest }}
      with_items: freigaben.results
      when: item.changed == True

    - name: set home group
      command: chgrp -R {{ item.item }} /home/{{ item.item }}
      with_items: homes.results
      when: item.changed == True