Die Bausteine von CFEngine

Die CFEngine-Sprache besteht aus einigen Bausteinen, die hier anhand von Beispielen erklärt werden. Mit den Beispielen werden die Begriffe Promise, Body, Bundle und Methods deutlich. Hier unser erstes Beispiel, von dem wir wissen oder zumindest schnell erahnen können, was es bewirkt:

redhat::
  packages: 
  "http"
    policy => "present",    
    package_module => yum,  
    version => "2.4.18";

Ein Promise wird immer aus Sicht des Agenten gelesen, der auf jedem System läuft, welches von CFEngine verwaltet wird. Der Agent liest das Promise derart:

Bin ich ein Redhat-System? Wenn nein, dann interessiert mich alles weitere nicht, fertig hier. (Es wird zum nächsten Promise-Typen bzw. der nächsten Class-Abfrage weitergegangen.) Wenn ja, soll ich mich um Pakete kümmern. Das Webserver-Paket httpd soll präsent sein. Ich soll yum als Paketmanager verwenden. Ich soll die Version 2.4.18 installieren.

Was ist nun was?

Das gesamte Konstrukt ist ein Promise, ein deklarierter Wunschzustand. redhat:: ist eine Hard-Class. Die harten Klassen werden von CFEngine bei jedem Lauf erkannt. Hard-Classes sind Klassen wie die IP-Adresse, das Betriebssystem samt Patchlevel, die Hardware-Architektur (bspw. x86_64), der Hostname oder auch die Uhrzeit. Hardclasses sind quasi faktische Begebenheiten. Man kann definieren: wenn der Hostname "reporter" und es 6:00 Uhr morgens ist, gebe einen Report von "Guten Morgen" aus. Diese Hard-Classes werden als Entscheider verwendet: „wenn, dann“. Wenn die Klasse redhat zutrifft, wenn es also ein Redhat-System ist, dann geht es weiter, sonst wird alles weitere diese Class betreffende ignoriert. Für diese „wenn, dann“-Entscheidungen wird ein doppel-Doppeltpunkt verwendet:: Neben den Hard-Classes gibt es die Soft-Classes, die man selbst in den Promises definiert; sie lassen sich in Abhängigkeit von dem Resultat eines (anderen) Promises definieren. Angenommen, wir wollen nach der Installation des httpd-Pakets den httpd-Service starten. Dazu fügen wir oben unter "version => „2.4.18“ folgende Zeile ein:

if_repaired(httpd_installed);

Das führt dazu, dass die Soft-Class "httpd_installed" gesetzt wird, sobald das Paket installiert ist. Promises können verschiedene Rückmeldungen geben:

  • repaired: Der Wunschzustand wurde hergestellt, hier wurde also das Paket installiert.
  • kept: Der Wunschzustand bestand bereits, es musste nichts getan werden.
  • failed: Der Wunschzustand konnte nicht hergestellt werden.
  • denied: Die Herstellung des Wunschzustands wurde verweigert.
  • timeout: Der Versuch des Herstellens des Wunschzustands endete in einem Timout.

Es lassen sich über weitere sogenannte body_classes entsprechend der Promise-Rückmeldung Classes definieren. Hier ist ein Link zu den Details, aber wer sich mit dem Grundkonzept von CFEngine vertraut machen möchte, dem sei hier geraten, sich nicht zu früh in allen Details und Möglichkeiten zu ver(w)irren: https://docs.cfengine.com/docs/3.8/reference-standard-library-common.html#results

Wir halten fest: Mittels einer body_class wie hier von if_repaired geliefert, erhalten wir eine Soft-Class, wenn das Promise erfolgreich war. Anhand von unserer erstellten Class "httpd_installed" können wir nun entscheiden, den Webservice zu starten:

    httpd_installed::
      services: 
        "httpd"
          service_policy => "start";

Der Service wird nur gestartet, wenn die Class httpd_installed gesetzt ist. Ist also das Paket (noch) nicht installiert, wird es die Class (noch) nicht geben und die Anweisungen werden ignoriert. Sobald das Paket installiert ist, wird die Class gesetzt und die Anweisungen beim nächsten Lauf des Agenten ausgeführt.

packages:
Das ist der Promise-Typ; der Typ, die Art unseres Versprechens. Unser Wunschzustand auf dem System ist das Vorhandensein eines Pakets, die Installation einer Software, soweit sie noch nicht installiert ist. Wenn es bspw. um eine Datei geht, ob wir sie nun neu anlegen möchten oder ändern oder löschen, dann wäre der Promise-Typ files:.

Der Paketname „httpd“ ist ein Attribut des Promises bzw. des Promise-Typs. Für verschiedene Promise-Typen (z.B. files, services, processes, packages, commands) gibt es verschiedene Attribute: ein Prozess kann gestartet oder gestoppt werden, ein File nicht; ein File kann man anlegen, ändern oder löschen, einen Service nicht, den kann man installieren, starten oder stoppen. So ergeben sich sinngemäß unterschiedliche Attribute zu den verschiedenen Promise-Typen.
Die Attribute werden in der Form "attribute => wert" angegeben.

Promises werden in Bundles verpackt. In einem Bundle kann es viele Promises geben. Bundles können als Container für Promises betrachtet werden, die zusammengehören; bspw. kann es ein Bundle „Webserver“ geben, in dem es nun ein Package-Promise gibt, um das Paket zu installieren, ein File-Promise, um das korrekte Konfigurationsfile sicherzustellen, ggf. auch noch, um Zertifikate zu kopieren, ein Service-Promise, um sicherzustellen, dass der Webserver immer läuft und ein Pager- oder Mail-Promise, dass uns informiert, wenn der Webserver nicht läuft.

methods

Die haben wir in dem einfachen Beispiel nicht, weil sie hier nicht zur Anwendung kommen.
Mit methods: kann man Bundles mit Parametern aufrufen; hier ein Beispiel:

bundle agent example:
{
  vars:
    "userliste" slist => { "mark", "thomas", "stefan", "andrea", "susi" }; 

  methods:
    "any" usebundle => erstelle_user("$(userliste)");
}

In diesem bundle definieren wir eine Variable "userliste" vom Typ String-List (slist), die fünf User enthält. Mit methods: rufen wir nun ein anderes Bundle "erstelle_user" auf und übergeben unsere userliste als Parameter an dieses Bundle.
"erstelle_user" könnte so aussehen:

bundle agent erstelle_user(user)
{
  commands:
    "/usr/sbin/useradd $(user)";
}

Die fünf User in unserer Variablen $(userlist) werden nacheinander als Parameter (user) übergeben; soviele User wir in der Liste haben, so oft wird "erstelle_user" mit dem jeweils nächsten User als Parameter aufgerufen und das Kommando useradd ebenso. "erstelle_user" dient hier als Art Subroutine. Diese Methodik bzw. dieses Feature von CFEngine nennt sich "Implizites Looping". Methods sind sehr mächtig und hilfreich, wenn es darum geht, zahlreiche Aktionen auf einen oder mehrere Parameter durchzuführen.

Man muss nicht alle Promises selbst schreiben, für viele Aufgaben, einfache sowie komplexe (bspw. mit Pager-Alarm, Reporting und Monitoring) gibt es frei verfügbare Promises auf dem CFEngine GitHub: https://github.com/cfengine