DEV Community

Martin Alfke for betadots

Posted on

Die Ruby-Seite von Puppet - Teil 2 - Benutzerdefinierte Funktionen

Dies ist der zweite Beitrag einer dreiteiligen Serie, in der die Konzepte und Best Practices zum Erweitern von Puppet mithilfe von benutzerdefinierten Facts, benutzerdefinierten Funktionen und benutzerdefinierten Typen und Providern behandelt werden.

Teil 1 behandelt Benutzerdefinierte Facts, die erklären, wie ein Knoten Informationen an den Puppet-Server übermitteln kann.

Teil 2 (dieser Beitrag) konzentriert sich auf Benutzerdefinierte Funktionen und zeigt, wie man Funktionen für Datenverarbeitung und Ausführung entwickelt.

Teil 3 wird darstellen, wie man die DSL-Funktionalität von Puppet mit neuen Typen und Provider erweitert.


Benutzerdefinierte Funktionen in Puppet:

Funktionen in Puppet werden auf dem Puppet-Server, insbesondere im Compiler, ausgeführt. Diese Funktionen haben Zugriff auf alle Facts und Puppet-Variablen, solange sie mit dem Namensraum angegeben werden.

Es gibt zwei Haupttypen von Funktionen in Puppet:

  • Anweisungsfunktionen
  • Rückgabefunktionen

Anweisungsfunktionen können mit den internen Mechanismen von Puppet interagieren. Die gebräuchlichsten Anweisungsfunktionen sind include und contain, die Klassen zum Katalog hinzufügen, oder noop und notice, die alle Ressourcen in den Simulationsmodus versetzen oder Einträge in die Protokolldatei des Puppet-Servers schreiben.

Rückgabefunktionen wie lookup oder read_url geben Werte aus Hiera oder externen Quellen wie Webservern zurück. Einige Rückgabefunktionen können auch Daten manipulieren (each, filter, map, reduce).

Warum benutzerdefinierte Funktionen erstellen?

Hier sind einige Gründe, warum man benutzerdefinierte Funktionen in Puppet entwickeln könnte:

  • Erstellen einer benutzerdefinierten Hiera-Suchfunktion
  • Umwandeln von Datenstrukturen für spezifische Anforderungen

Inhalt:

  1. Funktion-Entwicklung
  2. Funktionen in Modulen
  3. Allgemeine API
  4. Dispatcher und Definition
  5. Verwendung von Funktionen in der Puppet-DSL
  6. Zusammenfassung

Funktion-Entwicklung

Da Funktionen nur zur Kompilierzeit ausgeführt werden, empfiehlt es sich, sich zunächst auf ihre Kernfunktionalität zu konzentrieren, bevor sie in die Funktions-API integriert werden. Dadurch kann die Logik mit lokalen Daten getestet werden, ohne dass Probleme auf dem Puppet-Server auftreten, die die Kompilierung unterbrechen oder den Server sogar zum Absturz bringen könnten.

Als Beispiel wird eine Datenstruktur definiert, die verschiedene Aspekte der Konfiguration eines Systems darstellt. Mit einer Funktion kann man bestimmte Teile dieser Datenstruktur als Variablen verfügbar machen.

Beispiel für eine Datenstruktur:

server_hash:
  'postfix':
    'config':
      'transport': 'mta'
      'ssl': true
  'apache':
    'config':
      'vhost': 'server.domain.tld'
      'document_root': '/srv/www/html/server.domain.tld'
      'ssl': true
      'proxy_pass': 'http://localhost:8080'
  'tomcat':
    'config':
      'application': 'crm'
      'service': 'frontend'
Enter fullscreen mode Exit fullscreen mode

Angenommen, diese Datenstruktur stammt aus Hiera, und man möchte anderen Teams, die ihre eigenen Module schreiben, die Wiederverwendung dieser Daten ohne zusätzliche Abfragen ermöglichen. Dadurch kann man die interne Datenstruktur ändern und die Funktion anpassen, während die Teams weiterhin die benötigten Informationen zur Verfügung gestellt bekommen.

Beispiel für eine Funktion:

# Test Daten
server_hash = {
  'postfix' => {
    'config' => {
      'transport' => 'mta',
      'ssl' => true
    }
  },
  'apache' => {
    'config' => {
      'vhost' => 'server.domain.tld',
      'document_root' => '/srv/www/html/server.domain.tld',
      'ssl' => true,
      'proxy_pass' => 'http://localhost:8080'
    }
  },
  'tomcat' => {
    'config' => {
      'application' => 'crm',
      'service' => 'frontend'
    }
  }
}
# Funktion definition
def read_service_config(data, service)
  data[service]
end
# Testen der Funktion
puts read_service_config(server_hash, 'tomcat')
Enter fullscreen mode Exit fullscreen mode

Funktionen in Modulen

Puppet stellt eine API zum Erstellen benutzerdefinierter Funktionen zur Verfügung. Damit der Puppet Compiler diese Funktionen finden kann, müssen sie in bestimmten Verzeichnissen innerhalb eines Moduls abgelegt werden.

Dateien im lib-Verzeichnis des Moduls werden auf alle Agenten synchronisiert. Es ist nicht möglich, zu steuern, welche Dateien synchronisiert werden.

Benutzerdefinierte Funktionen können zu einer der beiden folgenden Verzeichnisstrukturen hinzugefügt werden:

  • lib/puppet/parser/functions - Funktionen API v1
  • lib/puppet/functions - Funktionen API v2

Die neuere Funktionen-API v2 unterstützt Unterverzeichnisse, sodass man Funktionen in Namespaces organisieren können. Der Dateiname sollte mit dem Funktionsnamen übereinstimmen.

In diesem Beitrag konzentrieren wir uns auf die moderne API v2.

Beispiel für eine Verzeichnisstruktur:

# <modulepath>/stdlib/
  \- lib/
    \- puppet/
      \- functions/
        \- stdlib/
          \- to_yaml.rb
Enter fullscreen mode Exit fullscreen mode
# stdlib/lib/puppet/functions/stdlib/to_yaml.rb
Puppet::Functions.create_function(:'stdlib::to_yaml') do
  # ...
end
Enter fullscreen mode Exit fullscreen mode

Wir empfehlen, benutzerdefinierte Funktionen mit dem Modulnamen zu präfixen, um Namenskonflikte zu vermeiden.

Allgemeine API

Um benutzerdefinierte Funktionen in Puppet zu erstellen, verwendet man die folgende Struktur:

Beispiel:

# <modulepath>/betadots/lib/puppet/functions/betadots/resolve.rb
Puppet::Functions.create_function(:'betadots::resolve') do
  # ...
end
Enter fullscreen mode Exit fullscreen mode

Bitte beachten, dass der Funktions Namespace identisch zum Modulnamen (betadots) sein muss.

Dispatcher und Definition

Innerhalb des do ... end Blocks der Funktion muss ein Dispatcher und eine Definition hinzugefügt werden. Der Dispatcher validiert die Eingabedaten und verknüpft sie mit einer benannten Definition, die den entsprechenden Ruby-Code ausführt.

Der Dispatcher kann Datentypen mithilfe von param prüfen und Werte Ruby-Symbolen zuordnen. Die Definition verwendet den Namen des Dispatchers und die im Dispatcher bereitgestellten Parameter.

Der letzte Befehl in der Definition bestimmt den Rückgabewert.

Beispiel für einen Dispatcher für eine IPv4-Adresse:

# <modulepath>/betadots/lib/puppet/functions/betadots/resolve.rb
Puppet::Functions.create_function(:'betadots::resolve') do
  dispatch :ip do
    param 'Regexp[/^(\d{1,3}).(\d{1,3}).(\d{1,3}).(\d{1,3})$/]', :ipaddr
  end
end
Enter fullscreen mode Exit fullscreen mode

Hier ist eine Tabelle mit gängigen Methoden für Parameter in Dispatchern:

Methode Beschreibung
param oder required_param Ein obligatorisches Argument. Kann mehrfach verwendet werden. Position: Alle Pflichtargumente müssen zuerst erscheinen.
optional_param Ein Argument, das ausgelassen werden kann.
repeated_params oder optional_repeated_params Ein wiederholbares Argument, das null oder mehr Werte annehmen kann.
required_repeated_param Ein wiederholbares Argument, das einen oder mehr Werte annehmen muss.
block_param oder required_block_param Ein obligatorisches Lambda.
optional_block_param Ein optionales Lambda.

Hinzufügen einer Definition:

Beschreibung Dispatcher Definition
Name dispatch :ip def ip
Parameter param ..., :ipaddr def ip(ipaddr)
# <modulepath>/betadots/lib/puppet/functions/betadots/resolve.rb
Puppet::Functions.create_function(:'betadots::resolve') do
  require 'resolv'
  dispatch :ip do
    param 'Regexp[/^(\d{1,3}).(\d{1,3}).(\d{1,3}).(\d{1,3})$/]', :ipaddr
  end

  def ip(ipaddr)
    Resolv.getname ipaddr
  end
end
Enter fullscreen mode Exit fullscreen mode

Nun können zusätzliche Dispatcher und Definitionen hinzugefügt werden:

# <modulepath>/betadots/lib/puppet/functions/betadots/resolve.rb
Puppet::Functions.create_function(:'betadots::resolve') do
  require 'socket'
  dispatch :ip do
    param 'Regexp[/^(\d{1,3}).(\d{1,3}).(\d{1,3}).(\d{1,3})$/]', :ipaddr
  end

  dispatch :dnsname do
    param 'Regexp[/.*/]', :dnsname
  end

  dispatch :local_hostname do
    param 'Regexp[//]', :local_hostname
  end

  def ip(ipaddr)
    Addrinfo.tcp(ipaddr, '80').getnameinfo[0]
  end

  def dnsname(dnsname)
    Addrinfo.ip(dnsname).getnameinfo[0]
  end

  def local_hostname
    Socket.gethostname
  end
end
Enter fullscreen mode Exit fullscreen mode

Verwendung von Funktionen in der Puppet-DSL

Innerhalb von Puppet werden Funktionen in der Regel während der Kompilierzeit ausgeführt.
Eine besondere Ausnahme bildet die deferred function, die auf dem Agenten ausgeführt wird.

Im folgenden betrachten wir die zwei Anwendungsfälle:

  1. Verwendung der resolve-Funktion im Compiler
  2. Verwendung der resolve-Funktion auf dem Agenten

Anwendungsfall 1: Ausführung im Compiler

Die Funktion kann an beliebiger Stelle im Code verwendet werden:

class betadots::resolver (
  Stdlib::Fqdn $local_hostname = betadots::resolve(),
) {
  $ip_from_google = betadots::resolve('www.google.com')
}
Enter fullscreen mode Exit fullscreen mode

Anwendungsfall 2: Ausführung auf dem Agenten

Die Funktion muss in deferred mode deklariert werden, um in Puppet auf dem Agenten ausgeführt zu werden. Die Funktion sollte nicht als Parameter im Header der Klasse verwendet werden, da dies möglicherweise den Funktionsaufruf mit hiera-Daten überschreiben könnte.

class betadots::local_resolve (
) {
  $local_api_ip = Deferred('betadots::resolve', ['api.int.domain.tld'])
}
Enter fullscreen mode Exit fullscreen mode

Zusammenfassung

Puppet bietet eine stabile API zum Erstellen benutzerdefinierter Funktionen.
Bevor eine neue Funktion erstellt wird, bitte prüfen, ob bereits eine Lösung im Stdlib- oder Extlib-Modul vorhanden ist.

Funktionen sollten mit dem Modulnamen zu präfixen, um Konflikte zu vermeiden und deren Herkunft zu kennzeichnen.

Viel Erfolg beim Puppetisieren,
Martin

Top comments (0)