DEV Community

Martin Alfke for betadots

Posted on

Die Ruby-Seite von Puppet - Teil 3 - Benutzerdefinierte Typen und Provider

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>,
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

newproperty(:enable) do
  newvalue(:true)
  newvalue(:false)
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Zusätzlich kann man jeden Anbieter von jedem Typ wiederverwenden:

# lib/puppet/provider/app_config/file.rb
Puppet::Type.type(:app_config).provide(:file, :parent => Puppet::Type.type(:ini_setting).provider(:ruby)) do
  desc "Der Datei-Anbieter des app_config-Typs ist eine Erweiterung des API-Anbieters."
end
Enter fullscreen mode Exit fullscreen mode

Auswahl des Anbieters

Der Puppet-Agent muss wissen, welcher Anbieter zu verwenden ist, wenn mehrere Anbieter vorhanden sind. Man kann den provider-Meta-Parameter verwenden oder die Anbieter Überprüfungen durchführen lassen, um festzustellen, ob sie für das System gültig sind. Die Optionen umfassen:

Überprüfung Beispiel Beschreibung
Befehle commands :app => "/opt/app/bin/app.exe" Gültig, wenn der Befehl /opt/app/bin/app.exe existiert. Der Befehl kann später mit :app verwendet werden.
Einschränkung - existiert confine :exists => "/opt/app/etc/app.conf" Gültig, wenn die Datei existiert.
Einschränkung - bool confine :true => /^10\./.match(%x{/opt/app/bin/app.exec version}) Gültig, wenn die Version 10.x ist.
Einschränkung - Fakt confine 'os.family' => :debian Gültig, wenn der Faktwert übereinstimmt.
Einschränkung - Funktion confine :feature => :cli Gültig, wenn der Anbieter die Funktion hat.
defaultfor defaultfor 'os.family' => :debian Anbieter als Standard für Debian-basierte Systeme.

Lesen und Anwenden von Konfigurationen

Der Anbieter benötigt die Fähigkeit, einzelne Konfigurationszustände zu erstellen, zu lesen, zu aktualisieren und zu löschen (CRUD-API).

Jede Konfigurationseigenschaft benötigt eine getter- (lesen) und eine setter- (ändern) Methode.

# lib/puppet/provider/app_config/cli.rb
#   app_config: { 'enable_logging':
#     ensure   => present,
#     value    => true,
#   }
#
Puppet::Type.type(:app_config).provide(:cli) do
  desc "Der CLI-Anbieter des app_config-Typs."

  # Einschränkung auf bestehenden Befehl
  commands :app => "/opt/app/bin/app.exe"

  def exists?
    @result = app_cli('list').split(%r{\n}).grep(%r{^#{resource[:key]}})
    if @result.length > 1
      raise ParserError, 'Mehrere Konfigurationselemente gefunden, bitte beheben Sie dies'
    end
    return false if @result.empty?
    return true unless @result.empty?
  end

  def create
    app(['set', resource[:name], resource[:value]])
  end

  def destroy
    app(['rm', resource[:name]])
  end

  # getter
  def value()
    @result[0].split[1]
  end

  # setter
  def value=(value)
    app(['set', resource[:name], resource[:value]])
  end
end
Enter fullscreen mode Exit fullscreen mode

Es wird empfohlen, auch die instances-Klassenmethode zu erstellen, die alle Instanzen eines Ressourcentyps in einem Hash sammeln kann. Der Namevar ist der Hash-Schlüssel, der einen Hash von Parametern und Eigenschaften als Wert hat.

require 'yaml'
def self.instances
  instances = []
  YAML.load(app_cli('list')).each do |key, value|
    attributes_hash = { key: key, ensure: :present, value: value, name: key}
    instances << new(attributes_hash)
  end
  instances
end
Enter fullscreen mode Exit fullscreen mode

Manchmal ist es nicht möglich, alle Instanzen zu sammeln, z. B. beim file-Ressourcentyp. In solchen Fällen wird keine instance-Methode definiert.

Die instance-Methode wird indirekt von jedem Typ verwendet, wenn prefetch aufgerufen wird, um die aktuelle Konfiguration zu erhalten und die @property_hash-Instanzvariable zurückzugeben.

def self.prefetch(resources)
  resources.each_key do |name|
    provider = instances.find { |instance| instance.name == name }
    resources[name].provider = provider if provider
  end
end
Enter fullscreen mode Exit fullscreen mode

Dies ermöglicht es dem Typ, Getter und Setter zu verwenden, um die Instanzvariable zu lesen und zu manipulieren, anstatt für jeden Typ getter- und setter-Methoden zu schreiben. Dieses Verhalten wird durch die Deklaration der mk_resource_methods-Klassenmethode hinzugefügt.

Sobald dies implementiert ist, kann man den Befehl puppet resource app_config ausführen, um alle vorhandenen Konfigurationen abzurufen.

Refresh-Ereignisse

In einigen Fällen ist es erforderlich, eine Ressource zu aktualisieren (refresh), beispielsweise um einen Dienst neu zu starten, ein Laufwerk erneut zu mounten oder eine Exec-Ressource erneut auszuführen. Damit ein Typ/Anbieter auf ein Aktualisierungsereignis reagieren kann, ist eine spezielle Behandlung erforderlich.

Innerhalb des Typs muss die refreshable-Funktion aktiviert werden, und eine refresh-Definition wird hinzugefügt.

Die folgenden Beispiele stammen vom Puppet service Typ und Anbieter. Die Funktion beschreibt, welche Anbieterdefinition bei einem Aktualisierungsereignis ausgeführt werden soll.

# lib/puppet/type/services.rb
# ...
  feature :refreshable, "Der Anbieter kann den Dienst neu starten.",
    :methods => [:restart]
# ...
  def refresh
    # Nur neu starten, wenn wir tatsächlich laufen
    if (@parameters[:ensure] || newattr(:ensure)).retrieve == :running
      provider.restart
    else
      debug "Neustart überspringen; Dienst läuft nicht"
    end
  end
# ...
Enter fullscreen mode Exit fullscreen mode

Verwendung benutzerdefinierter Typen in Puppet DSL

Sobald der benutzerdefinierte Typ und Anbieter implementiert sind, kann man diesen in Puppet Manifesten wie folgt verwenden:

# Typdeklaration
app_config { 'enable_logging':
  ensure    => present,
  value     => true,
  require   => File['/opt/app/etc/app.cfg'],
  subscribe => Service['app'],
}
# Typreferenz
service { 'app':
  ensure    => running,
  subscribe => App_config['enable_logging'],
}
# Typdeklaration mit Lambda und Splat-Operator
$config_hash.each |String $key, Hash $hash| {
  app_config { $key:
    * => $hash,
  }
}
# Virtuelle oder exportierte Ressource
@@app_config { 'app_mount':
  ensure => present,
  value  => "${facts['networking']['fqdn']}/srv/app_mount",
  tag    => 'blog',
}
# Ressourcen-Sammler
App_config <<| tag == 'blog' |>>
Enter fullscreen mode Exit fullscreen mode

Resourcen-API

Die moderne Ressourcen-API hat eine Einschränkung: Man kann Typen nicht aktualisieren! Außerdem besteht sie aus Typen und Anbietern, die am selben Ort wie die bestehende Implementierung platziert werden müssen.

Ressourcen-API-Typ

Der Ressourcen-API-Typ verwendet einen Attributs-Hash, um alle Parameter und Eigenschaften aufzulisten. Innerhalb des type-Schlüssels kann man jeden der integrierten Puppet-Datentypen verwenden.

# lib/puppet/type/app_config.rb
require 'puppet/resource_api'

Puppet::ResourceApi.register_type(
  name: 'app_config',
  docs: <<-EOS,
      @summary Der Typ zur Verwaltung von App-Konfigurationseinstellungen
      @example
      app_config { 'key':
        ensure => present,
        value  => 'value',
      }

      Dieser Typ bietet Puppet die Möglichkeit, unsere Anwendungsconfig zu verwalten.
    EOS
  features: []
  attributes: {
    ensure: {
      type:    'Enum[present, absent]',
      desc:    'Ob die Einstellung hinzugefügt oder entfernt werden soll',
      default: 'present',
    },
    name: {
      type:     'String',
      desc:     'Die Einstellung, die verwaltet werden soll',
      behavior: :namevar,
    },
    value: {
      type: 'Variant[String, Integer, Array]',
      desc: 'Der Wert, der gesetzt werden soll'
    }
  }
)
Enter fullscreen mode Exit fullscreen mode

Ressourcen-API-Anbieter

Die einfachste Lösung ist die Verwendung der Klasse SimpleProvider. Dieser Anbieter benötigt bis zu 4 Definitionen:

  • get
  • create
  • update
  • delete
# lib/puppet/provider/app_config/cli.rb
require 'puppet/resource_api/simple_provider'
require 'open3'

class Puppet::Provider::AppConfig::Cli < Puppet::ResourceApi::SimpleProvider
  $app_command = '/opt/app/bin/app.exe'

  # Get: Alle lesbaren Konfigurationseinstellungen in einen Hash abrufen
  def get(context)
    context.debug('Gibt Daten vom Befehl zurück')

    @result = []
    stdout, stderr, status = Open3.capture3('/opt/app/bin/app.exe list')
    raise ParseError "Fehler beim Ausführen des Befehls: \n#{stderr}" unless status.success?
    # Fehlende Fehlerbehandlung. Dies dient nur zur Demo
    @command_result = stdout.split(%r{\n})
    @command_result.each do |line|
      next if line == '---'
      key = line.split(':')[0]
      value = line.split(':')[1].strip
      hash = {
        'ensure': 'present',
        'name': key,
        'value': value,
      }
      @result += [ hash ]
    end
    @result
  end

  # Create: Eine neue Konfigurationseinstellung mit Name und Sollwert hinzufügen
  def create(context, name, should)
    context.notice("Erstelle '#{name}' mit #{should[:value]}")
    _stdout, _stderr, status = Open3.capture3("/opt/app/bin/app.exe set #{name} #{should[:value]}")
    # Fehlende Fehlerbehandlung. Dies dient nur zur Demo
    return true unless status.success?
  end

  # Update: Eine vorhandene Einstellung mit Namen korrigieren und durch den Sollwert ersetzen
  def update(context, name, should)
    context.notice("Aktualisiere '#{name}' mit #{should[:value]}")
    _stdout, _stderr, status = Open3.capture3("/opt/app/bin/app.exe set #{name} #{should[:value]}")
    # Fehlende Fehlerbehandlung. Dies dient nur zur Demo
    return true unless status.success?
  end

  # Delete: Eine Konfigurationseinstellung entfernen
  def delete(context, name)
    context.notice("Lösche '#{name}'")
    _stdout, _stderr, status = Open3.capture3("/opt/app/bin/app.exe rm #{name}")
    # Fehlende Fehlerbehandlung. Dies dient nur zur Demo
    return true unless status.success?
  end
end
Enter fullscreen mode Exit fullscreen mode

Zusammenfassung

Typen und Anbieter sind nicht komplex. Sie beschreiben im Grunde das CRUD-Verhalten von Puppet. Der Typ definiert, wie man die Puppet DSL verwendet, während der Anbieter steuert, wie spezifische Einstellungen überprüft und behandelt werden.

Eine exzessive Nutzung der exec Resource ist zu vermweiden. In den meisten Fällen ermöglicht ein einfacher Typ und Anbieter eine bessere Analyse, Fehlerbehandlung und Kontrolle über das, was passiert. Darüber hinaus: benutzerdefinierte Typen und Anbieter führen schneller aus im Vergleich zur Verwendung des exec-Typs - viel schneller!

Die Beispielanwendung und die funktionierenden Typen und Anbieter aus diesem Posting sind auf GitHub verfügbar:

App-Nutzung:

# APP installieren:
git clone https://github.com/betadots/workshop-demo-app /opt/app
/opt/app/bin/app.exe

# Puppet-Modul installieren:
git clone https://github.com/betadots/workshop-demo-module -b ruby_workshop modules/app
# Type API V1
puppet resource app_config --modulepath modules
# Type Resource API
puppet resource app_config2 --modulepath modules
Enter fullscreen mode Exit fullscreen mode

Viel Spaß beim Puppetisieren,

Martin

Top comments (0)