Dies ist der letzte Beitrag in einer dreiteiligen Serie, die die Konzepte und Best Practices für die Erweiterung von Puppet mithilfe von benutzerdefinierten Fakten, benutzerdefinierten Funktionen und benutzerdefinierten Typen sowie Providern behandelt.

Teil 1 untersucht, wie man benutzerdefinierte Fakten erstellt, die es Knoten ermöglichen, Informationen an den Puppet-Server zu senden.

Teil 2 behandelt den Aufbau benutzerdefinierter Funktionen zur Verarbeitung von Daten oder zur Ausführung spezifischer Aufgaben.

Teil 3 (dieser Beitrag) konzentriert sich auf benutzerdefinierte Typen und Provider, mit denen Puppets DSL erweitert und Systemressourcen verwaltet werden können.

Typen stehen im Zentrum der deklarativen DSL von Puppet. Typen beschreiben den gewünschten Zustand des Systems und zielen auf spezifische, konfigurierbare Teile ab (manchmal betriebssystemspezifisch). Puppet bietet eine Reihe von Kern-Typen, die mit jeder Puppet-Agent-Installation verfügbar sind.

Einige der Kern-Typen umfassen:

  • file
  • user
  • group
  • package
  • service

Warum benutzerdefinierte Typen und Provider erstellen:

Es gibt mehrere Gründe, benutzerdefinierte Typen und Provider zu entwickeln:

  • Vermeiden von den oft unzuverlässigen oder schwer zu wartenden exec-Ressourcen.
  • Verwalten einer Anwendung, die CLI-Befehle zur Konfiguration benötigt.
  • Handhaben von Konfigurationen mit hochspezifischer Syntax, die bestehende Puppet-Typen nicht verwalten können (wie file oder concat).

Inhalt:

  1. Allgemeine Konzepte
  2. Typen und Provider in Modulen
  3. Typbeschreibung
  4. Provider-Implementierung
  5. Verwendung benutzerdefinierter Typen in der Puppet DSL
  6. Resourcen-API
  7. Zusammenfassung

Allgemeine Konzepte

Ein Typ besteht aus zwei Hauptteilen:

  • Typdefinition: Diese definiert, wie der Typ in der Puppet DSL verwendet wird.
  • Provider-Implementierung(en): Ein oder mehrere Provider pro Typ definieren, wie mit dem System interagiert wird, um Ressourcen zu verwalten.

Typdefinition

Der Typ beschreibt, wie Puppet-Ressourcen in der DSL deklariert werden. Zum Beispiel, um die Konfiguration einer Anwendung zu verwalten:

app_config { <setting>:
  ensure   => present,
  property => <value>,
  param    => <app_args>,
  provider => <auth_method>,
}

Typdefinition haben einen Namevar (ein Schlüsselbezeichner) und mehrere Parameter und Properties (Eigenschaften).

  • Namevar: Der Schlüssel, der die Ressourceninstanz in der Typdeklaration identifiziert.
  • Properties (Eigenschaften): Diese repräsentieren etwas Messbares im Zielsystem, wie die UID oder GID eines Benutzers oder einen Konfigurationswert einer Anwendung.
  • Parameter: Parameter beeinflussen, wie Puppet eine Ressource verwaltet, spiegeln jedoch nicht direkt etwas Messbares im System wider. Zum Beispiel ist manage_home im user-Typ ein Parameter, der Puppets Verhalten beeinflusst, aber keine Eigenschaft des Benutzerkontos ist.

Der Unterschied zwischen Parametern und Eigenschaften wird auf der Puppet-Typ/Provider-Entwicklungsseite gut beschrieben:

Properties:

"Eigenschaften entsprechen etwas Messbarem im Zielsystem. Zum Beispiel sind die UID und GID eines Benutzerkontos Eigenschaften, da ihr aktueller Zustand abgefragt oder geändert werden kann. Praktisch bedeutet das, dass das Festlegen eines Wertes für eine Eigenschaft eine Methode im Provider aufruft."

Parameter:

"Parameter ändern, wie Puppet eine Ressource verwaltet, entsprechen jedoch nicht unbedingt direkt etwas Messbarem. Zum Beispiel ist das managehome-Attribut des Benutzertyps ein Parameter – sein Wert beeinflusst, was Puppet tut, aber die Frage, ob Puppet ein Home-Verzeichnis verwaltet, ist keine angeborene Eigenschaft des Benutzerkontos."

In unserem Beispiel ist die Eigenschaft der Wert des Konfigurationseinstellungen, während der Parameter die Anwendungsattribute angibt.

Provider-Implementierungen

Provider definieren die Mechanismen zur Verwaltung des Zustands der durch Typen beschriebenen Ressourcen. Sie behandeln:

  • Bestimmung, ob die Ressource bereits existiert
  • Erstellen oder Entfernen von Ressourcen.
  • Ändern von Ressourcen.
  • Optional das Auflisten aller vorhandenen Ressourcen des Typs.

Typen und Provider in Modulen

Benutzerdefinierte Typen und Provider werden im Verzeichnis lib/puppet eines Moduls platziert.

  • Die Typdatei befindet sich in lib/puppet/type und ist nach dem Ressourcentyp benannt (app_config.rb in diesem Fall).
  • Provider-Implementierungen gehen in das Verzeichnis lib/puppet/provider, in ein Unterverzeichnis, das nach dem Ressourcentyp benannt ist. Jeder Provider ist nach seiner Provider-Implementierung benannt (z.B. ruby.rb, cli.rb, cert.rb, token.rb, user.rb)

Beispiel:

  1. Typ: app_config
  2. Provider:
    • cert
    • token
    • user
# <modulpfad>/<modulname>
modules/application/
    \- lib/
        \- puppet/
            |- type/
            |    \- app_config.rb
            \- provider/
                \- app_config/
                    |- cert.rb
                    |- token.rb
                    \- user.rb

Typbeschreibung

Neue benutzerdefinierte Typen können mit zwei verschiedenen API-Versionen erstellt werden. APIv1 ist die alte, klassische Methode, die Getter und Setter innerhalb der Provider verwendet. APIv2 ist eine neue Implementierung, die in PDK integriert ist – diese Implementierung wird auch als Resource-API bezeichnet.

Im nächsten Abschnitt stellen wir die APIv1-Implementierung vor. Die Implementierung der Ressourcen-API wird später in diesem Dokument behandelt.

APIv1

Die traditionelle Methode verwendet Puppet::Type.newtype, die den Typ definiert. Es wird empfohlen, die Typdokumentation in den Code einzufügen. Dies ermöglicht es den Benutzern, puppet describe <type> auf ihrem System auszuführen, um die Dokumentation anzuzeigen.

# lib/puppet/type/app_config.rb
Puppet::Type.newtype(:app_config) do
  @doc = %q{Verwalten der Anwendungsconfig. Es gibt drei Möglichkeiten
    zur Authentifizierung: Zertifikat, Token, Benutzer. Zertifikat ist der Standard
    Provider.

    Beispiel:

      app_config: { 'enable_logging':
        ensure   => present,
        value    => true,
      }
  }
  # ... der Code ...
end

Verwaltung

Die wichtigste Funktion eines Typs besteht darin, etwas zum System hinzuzufügen oder zu entfernen. Dies wird normalerweise mit der ensure-Eigenschaft behandelt. Um ensure zu aktivieren, ist eine einzige Zeile erforderlich: ensurable

# lib/puppet/type/app_config.rb
Puppet::Type.newtype(:app_config) do
  @doc = %q{Verwalten der Anwendungsconfig. Es gibt drei Möglichkeiten
    zur Authentifizierung: Zertifikat, Token, Benutzer. Zertifikat ist der Standard
    Provider.

    Beispiel:

      app_config: { 'enable_logging':
        ensure   => present,
        value    => true,
      }
  }
  ensurable
end

Namevar

Bei der Deklaration des Typs muss ein Titel angegeben werden – dies könnte man als Typinstanz-Identifikator bezeichnen. In der Regel spiegelt dies wider, wofür der Typ verantwortlich ist.

user { 'betadots': # <- Titel
}

Die einfachste Implementierung besteht darin, einen Parameter mit dem Namen :name hinzuzufügen.

# lib/puppet/type/app_config.rb
Puppet::Type.newtype(:app_config) do
  @doc = %q{Verwalten der Anwendungsconfig. Es gibt drei Möglichkeiten,
    sich zu authentifizieren: Zertifikat, Token, Benutzer. Zertifikat ist der Standard
    Provider.

    Beispiel:

      app_config: { 'enable_logging':
        ensure   => present,
        value    => true,
      }
  }
  ensurable
  newparam(:name) do
    desc "Das Anwendungsconfig-Element, das verwaltet werden soll. Siehe app_cli conf --help"
  end
end

Das Übergeben des Schlüssels namevar: true an den Parameter ist eine weitere Möglichkeit, einen Namevar zu identifizieren:

newparam(:key, namevar: true) do
  desc 'Der Schlüssel des Anwendungsconfig-Elements, das verwaltet werden soll. Siehe app_cli conf --help'
end

Eigenschaften

Als Nächstes fügen wir die andere Eigenschaft hinzu. Jede Eigenschaft kann durch ihren Inhalt validiert werden. In unserem Demo-Fall erwarten wir einen Stringwert.

# lib/puppet/type/app_config.rb
Puppet::Type.newtype(:app_config) do
  @doc = %q{Verwalten der Anwendungsconfig. Es gibt drei Möglichkeiten,
    sich zu authentifizieren: Zertifikat, Token, Benutzer. Zertifikat ist der Standard
    Provider.

    Beispiel:

      app_config: { 'enable_logging':
        ensure   => present,
        value    => true,
      }
  }
  ensurable
  newparam(:name) do
    desc "Das Anwendungsconfig-Element, das verwaltet werden soll. Siehe app_cli conf --help"
  end
  newproperty(:value) do
    desc "Der zu setzende Konfigurationswert."
    validate do |value|
      unless value =~ /^\w+/
        raise ArgumentError, "%s ist kein gültiger Wert" % value
      end
    end
  end
end

Außerdem kann man spezifische gültige Werte angeben, die automatisch validiert werden:

newproperty(:enable) do
  newvalue(:true)
  newvalue(:false)
end

Bitte beachten, dass Arrays als Eigenschaftswerte auf andere Weise validiert werden:

Von der Website Puppet benutzerdefinierte Typen:

Standardmäßig wird eine Eigenschaft, die mehrere Werte in einem Array zugewiesen bekommt:Als synchron betrachtet, wenn einer dieser Werte dem aktuellen Wert entspricht.Wenn keiner dieser Werte übereinstimmt, wird der erste Wert beim Synchronisieren der Eigenschaft verwendet.

Wenn alle Array-Werte übereinstimmen sollen, muss die Eigenschaft array_matching auf :all gesetzt werden. Der Standardwert ist :first.

newproperty(:flags, :array_matching => :all) do
  # ...
end

Der Zugriff auf Werte kann auf zwei Arten erfolgen:

  1. should für Eigenschaften, value für Parameter.
  2. value für sowohl Eigenschaften als auch Parameter.

Wir bevorzugen die expliziten Methoden, da dies klarer macht, ob wir es mit einer Eigenschaft oder einem Parameter zu tun haben.

Parameter

Parameter werden auf ähnliche Weise definiert:

# lib/puppet/type/app_config.rb
Puppet::Type.newtype(:app_config) do
  @doc = %q{Verwalten der Anwendungsconfig. Es gibt drei Möglichkeiten,
    sich zu authentifizieren: Zertifikat, Token, Benutzer. Zertifikat ist der Standard
    Provider.

    Beispiel:

      app_config: { 'enable_logging':
        ensure   => present,
        value    => true,
        cli_args => ['-p'], # Persistenz
      }
  }
  ensurable
  newparam(:name) do
    desc "Das Anwendungsconfig-Element, das verwaltet werden soll. Siehe app_cli conf --help"
  end
  newproperty(:value) do
    desc "Der zu setzende Konfigurationswert."
    validate do |value|
      unless value =~ /^\w+/
        raise ArgumentError, "%s ist kein gültiger Wert" % value
      end
    end
  end
  newparam(:cli_args, :array_matching => :all) do
    desc "CLI-Optionen, die während der Befehlsausführung verwendet werden sollen."
    validate do |value|
      unless value.class == Array
        raise ArgumentError, "%s ist kein Array" % value
      end
    end
    defaultto ['-p']
  end
end

Boolean-Parameter

Parameter, die einen booleschen Wert erhalten, sollten auf andere Weise behandelt werden, um Codewiederholungen zu vermeiden.

require 'puppet/parameter/boolean'
# ...
newparam(:force, :boolean => true, :parent => Puppet::Parameter::Boolean)

Automatische Abhängigkeiten

Innerhalb des Typs können wir weiche Abhängigkeiten zwischen verschiedenen Typen angeben.

Beispiel: Das app_cli sollte einen Benutzer verwenden, der im System verfügbar sein muss.

autorequire(:user) do
  ['app']
end

Von nun an kann der neue benutzerdefinierte Typ bereits in Puppet DSL verwendet werden, der Compiler wird ein Katalog erstellen, aber der Agent wird einen Fehler produzieren, da es keine funktionalen Provider gibt.

Vorabprüfung auf der Agentenseite

Es ist möglich, dass der Agent zunächst einige Dinge überprüft, bevor der Katalog angewendet wird. Dafür kann die Methode :pre_run_check verwendet werden.

def pre_run_check
  File.exist?('/opt/app/bin/app.exe') && raise Puppet::Error, "App nicht installiert"
end

Merkmale

Bei der Verwendung mehrerer Provider (ähnlich der Ressourcenpakete) wollen wir sicherstellen, dass der Provider alle erforderlichen Implementierungen (Merkmale - Features) hat. Ein Typ kann ein Merkmal erfordern:

# lib/puppet/type/app_config.rb
Puppet::Type.newtype(:app_config) do
  @doc = %q{Verwalten der Anwendungsconfig. Es gibt drei Möglichkeiten,
    sich zu authentifizieren: Zertifikat, Token, Benutzer. Zertifikat ist der Standard
    Provider.

    Beispiel:

      app_config: { 'enable_logging':
        ensure   => present,
        value    => true,
        file     => '/opt/app/etc/app.cfg',
        cli_args => ['-p'], # Persistenz
      }
  }
  ensurable
  # globales Merkmal
  # feature :cli, "Das CLI-Merkmal erfordert einige Parameter", :methods => [:cli]
  newparam(:name) do
    desc "Das Anwendungsconfig-Element, das verwaltet werden soll. Siehe app_cli conf --help"
  end
  newproperty(:value) do
    desc "Der zu setzende Konfigurationswert."
    validate do |value|
      unless value =~ /^\w+/
        raise ArgumentError, "%s ist kein gültiger Wert" % value
      end
    end
  end
  # Die Eigenschaft config_file muss gesetzt werden, wenn die CLI
  #   Option zur Konfiguration von App verwendet wird.
  # Der CLI-Provider wird nach einem Merkmal suchen.
  newproperty(:file, :required_features => %w{cli}) do
    desc "Die zu verwendende Konfigurationsdatei."
    validate do |value|
      unless (File.expand_path(value) == value)
        raise ArgumentError, "%s ist kein absoluter Pfad" % value
      end
    end
    defaultto '/opt/app/etc/app.cfg'
  end
  newparam(:cli_args, :array_matching => :all) do
    desc "CLI-Optionen, die während der Befehlsausführung verwendet werden sollen."
    validate do |value|
      unless value.class == Array
        raise ArgumentError, "%s ist kein Array" % value
      end
    end
    defaultto ['-p']
  end
end

Provider-Implementierung

Sobald der Typ definiert ist, muss der Anbieter steuern, wie die Ressource verwaltet wird. Anbieter implementieren typischerweise Methoden, um:

  • Überprüfen, ob die Ressource existiert (exists?).
  • Eine neue Ressource erstellen (create).
  • Vorhandene Ressourcenattribute lesen (prefetch oder ein Getter).
  • Eine Ressource ändern (flush oder ein Setter).
  • Eine Ressource löschen (destroy).
# lib/puppet/provider/app_config/cli.rb
Puppet::Type.type(:app_config).provide(:cli) do
  desc "Der CLI-Anbieter des app_config-Typs."
end

Es ist auch möglich, bestehende Anbieterklassen wiederzuverwenden und zu erweitern. Gemeinsamer Code kann in einem generischen Anbieter (lib/puppet/provider/app_config.rb) platziert werden.

# lib/puppet/provider/app_config/cli.rb
Puppet::Type.type(:app_config).provide(:cli, :parent => Puppet::Provider::App_config) do
  desc "Der CLI-Anbieter des app_config-Typs verwendet gemeinsamen Code aus app_config.rb."
end

Wenn eine gemeinsame Funktionalität für mehrere Anbieter hinzugefügt werden soll, kann man den Code im Puppet_X-Modulverzeichnis ablegen: lib/puppet_x/<unternehmens_name>/<eindeutiger Klassenname>.

# lib/puppet/provider/app_config/cli.rb
require_relative '../../puppet_x/betadots/app_api.rb'
Puppet::Type.type(:app_config).provide(:cli) do
  desc "Der CLI-Anbieter des app_config-Typs verwendet gemeinsamen Code aus app_config.rb."
end

Ein neuer Anbieter kann erstellt werden, indem er von einem bestehenden Anbieter erbt und diesen erweitert:

# lib/puppet/provider/app_config/token.rb
Puppet::Type.type(:app_config).provide(:token, :parent => :api, :source => :api) do
  desc "Der Token-Anbieter des app_config-Typs ist eine Erweiterung des API-Anbieters."
end

Author Of article : Martin Alfke Read full article