30 Mai 2025

Puppet: Benutzerdefinierte Typen und Provider

Einführung

Puppet ist eine Software-Konfigurationsmanagement-Lösung zur Verwaltung von IT-Infrastrukturen. Eines der ersten Dinge, die man über Puppet lernen muss, ist seine domänenspezifische Sprache – die Puppet-DSL – und die damit verbundenen Konzepte.

Benutzer können ihren Code in Klassen und Modulen organisieren und vordefinierte Ressourcentypen verwenden, um Ressourcen wie Dateien, Pakete, Dienste und andere zu verwalten.

Die am häufigsten verwendeten Typen sind Teil des Puppet-Kerns und in Ruby implementiert. Zusammengesetzte Ressourcentypen können über die Puppet-DSL von bereits bekannten Typen durch den Puppet-Benutzer selbst definiert oder als Teil eines externen Puppet-Moduls importiert werden, das von externen Modulentwicklern gepflegt wird.

Es kommt vor, dass Puppet-Benutzer lange Zeit innerhalb der Puppet-DSL bleiben können, selbst wenn sie regelmäßig mit Puppet arbeiten.

Das erste Mal, dass ich einen Einblick in dieses Thema bekam, war, als Debian Stable Puppet 5.5 auslieferte, was noch nicht allzu lange her ist. Die Puppet 5.5-Dokumentation enthält ein Kapitel über benutzerdefinierte Typen bzw. Provider-Entwicklung, aber für mich wirkten sie unvollständig und es fehlten eigenständige Beispiele. Anscheinend war ich nicht der Einzige, der so empfand, obwohl Puppets Gem-Dokumentation einen guten Überblick darüber gibt, was prinzipiell möglich ist.

Gary Larizzas Blogbeitrag ist mehr als zehn Jahre alt. Ich habe mir kürzlich die Dokumentation für Puppet 7 zu diesem Thema noch einmal angesehen, da dies die aktuelle Puppet-Version in Debian Stable ist.

Der Puppet 5.5-Ansatz zur Typ- & Provider-Entwicklung wird heute als Low-Level-Methode bezeichnet, und seine Dokumentation hat sich nicht wesentlich geändert. Puppet 6 und höhere Versionen empfehlen jedoch eine neue Methode zur Erstellung benutzerdefinierter Typen & Provider über die sogenannte Resource-API, deren Dokumentation eine wesentliche Verbesserung gegenüber der Low-Level-Methode darstellt. Die Resource-API ist jedoch kein Ersatz und weist mehrere dokumentierte Einschränkungen auf.

Nichtsdestotrotz werden wir für den restlichen Blogbeitrag einen kleinen Teil der Funktionalität von file sowohl mit der Low-Level-Methode als auch mit der Resource-API neu prototypisieren, nämlich die Eigenschaften ensure und content.

Vorbereitungen

Die folgenden Vorbereitungen sind in einem Agent-Server-Setup nicht notwendig. Wir verwenden bundle, um eine puppet ausführbare Datei für diese Demo zu erhalten.

demo@85c63b50bfa3:~$ cat  >  Gemfile  <<EOF
source 'https://rubygems.org'

gem 'puppet', '>= 6'
EOF
demo@85c63b50bfa3:~$ bundle install
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies...
...
Installing puppet 8.10.0
Bundle complete! 1 Gemfile dependency, 17 gems now installed. 
Bundled gems are installed into `./.vendor`
demo@85c63b50bfa3:~$ cat  >  file_builtin.pp  <<EOF
$file = '/home/demo/puppet-file-builtin'
file {$file: content =>  'This is madness'}
EOF
demo@85c63b50bfa3:~$ bin/puppet apply file_builtin.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.01 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-builtin]/ensure: defined content as '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e'
Notice: Applied catalog in 0.04 seconds

demo@85c63b50bfa3:~$ sha256sum /home/demo/puppet-file-builtin
0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e /home/demo/puppet-file-builtin

Beachten Sie die von Puppet ausgegebenen Informationen zur Statusänderung.

Low-Level-Prototyp

Benutzerdefinierte Typen und Provider, die nicht über ein Gem installiert werden, müssen Teil eines Puppet-Moduls sein, damit sie über den Pluginsync-Mechanismus auf Puppet-Agenten kopiert werden können.

Ein üblicher Speicherort für Puppet-Module ist das Verzeichnis modules innerhalb einer Puppet-Umgebung. Für diese Demo deklarieren wir ein demo Modul.

Grundlegende Funktionalität

Unser erster Versuch ist die folgende Typdefinition für einen neuen Typ, den wir nennen werden: file_llmethod. Es verfügt über keine Dokumentation oder Validierung von Eingabewerten.

# modules/demo/lib/puppet/type/file_llmethod.rb

Puppet::Type.newtype(:file_llmethod) do
  newparam(:path, namevar: true) {}
  newproperty(:content) {}
  newproperty(:ensure) do
  newvalues(:present, :absent)
  defaultto(:present)
  end
end

Wir haben einen path Parameter deklariert, der als namevar für diesen Typ dient – es kann keine anderen file_llmethod Instanzen geben, die dieselbe path verwalten. Die Eigenschaft ensure ist auf zwei Werte beschränkt und standardmäßig auf present gesetzt.

Die folgende Provider-Implementierung besteht aus einem Getter und einem Setter für jede der beiden Eigenschaften content und ensure.

# modules/demo/lib/puppet/provider/file_llmethod/ruby.rb

Puppet::Type.type(:file_llmethod).provide(:ruby) do
  def ensure
  File.exist?(@resource[:path]) ? :present : :absent
  end

  def ensure=(value)
  if value == :present
  # reuse setter
  self.content=(@resource[:content])
  else
  File.unlink(@resource[:path])
  end
  end

  def content
  File.read(@resource[:path])
  end

  def content=(value)
  File.write(@resource[:path], value)
  end
end

Dies ergibt Folgendes:

demo@85c63b50bfa3:~$ cat  >  file_llmethod_create.pp  <<EOF
$file = '/home/demo/puppet-file-lowlevel-method-create'
file {$file: ensure =>  absent} ->
file_llmethod {$file: content =>  'This is Sparta!'}
EOF

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_create.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.01 seconds
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-create]/ensure: defined 'ensure' as 'present'

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_create.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.01 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-lowlevel-method-create]/ensure: removed
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-create]/ensure: defined 'ensure' as 'present'

demo@85c63b50bfa3:~$ cat  >  file_llmethod_change.pp  <<EOF
$file = '/home/demo/puppet-file-lowlevel-method-change'
file {$file: content =>  'This is madness'} ->
file_llmethod {$file: content =>  'This is Sparta!'}
EOF

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_change.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-lowlevel-method-change]/ensure: defined content as '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e'
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-change]/content: content changed 'This is madness' to 'This is Sparta!'

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_change.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-lowlevel-method-change]/content: content changed '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2' to '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e'
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-change]/content: content changed 'This is madness' to 'This is Sparta!'

Unser benutzerdefinierter Typ funktioniert bereits einigermaßen, obwohl wir keinen expliziten Vergleich von Ist- und Soll-Zustand implementiert haben. Puppet erledigt dies für uns basierend auf dem Puppet-Katalog und den Rückgabewerten des Property-Getters. Unsere definierten Setter werden von Puppet auch nur bei Bedarf aufgerufen.

Wir können auch sehen, dass die Statusänderungsbenachrichtigung ensure defined 'ensure' as 'present' ist und das gewünschte content in keiner Weise berücksichtigt, während die Statusänderungsbenachrichtigung content Klartext anzeigt. Beides sagt uns, dass die SHA256-Prüfsumme aus dem file_builtin.pp Beispiel bereits etwas nicht Triviales ist.

Eingabe validieren

Als nächsten Schritt fügen wir eine Validierung für path und content.

# modules/demo/lib/puppet/type/file_llmethod.rb

Puppet::Type.newtype(:file_llmethod) do
  newparam(:path, namevar: true) do
  validate do |value|
  fail "# {value}  is not a String" unless value.is_a?(String)
  fail "# {value}  is not an absolute path" unless File.absolute_path?(value)
  end
  end

  newproperty(:content) do
  validate do |value|
  fail "# {value}  is not a String" unless value.is_a?(String)
  end
  end

  newproperty(:ensure) do
  newvalues(:present, :absent)
  defaultto(:present)
  end
end

Fehlgeschlagene Validierungen sehen wie folgt aus:

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules --exec 'file_llmethod {"./relative/path": }'
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Error: Parameter path failed on File_llmethod[./relative/path]: ./relative/path is not an absolute path (line: 1)

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules --exec 'file_llmethod {"/absolute/path": content =>  42}'
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Error: Parameter content failed on File_llmethod[/absolute/path]: 42 is not a String (line: 1)

Inhalts-Prüfsummen

Wir überschreiben change_to_s, damit Statusänderungen Inhalts-Prüfsummen enthalten:

# modules/demo/lib/puppet/type/file_llmethod.rb

require 'digest'

Puppet::Type.newtype(:file_llmethod) do
  newparam(:path, namevar: true) do
  validate do |value|
  fail "# {value}  is not a String" unless value.is_a?(String)
  fail "# {value}  is not an absolute path" unless File.absolute_path?(value)
  end
  end

  newproperty(:content) do
  validate do |value|
  fail "# {value}  is not a String" unless value.is_a?(String)
  end
  define_method(:change_to_s) do |currentvalue, newvalue|
  old = "{sha256}#{Digest::SHA256.hexdigest(currentvalue)}"
  new = "{sha256}#{Digest::SHA256.hexdigest(newvalue)}"
  "content changed '#{old}' to '#{new}'"
  end
  end

  newproperty(:ensure) do
  define_method(:change_to_s) do |currentvalue, newvalue| if currentvalue == :absent should = @resource.property(:content).should digest = "{sha256}#{Digest::SHA256.hexdigest(should)}" "defined content as '#{digest}'" else super(currentvalue, newvalue) end end newvalues(:present, :absent) defaultto(:present) end end

Die obige Typdefinition ergibt:

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_create.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-lowlevel-method-create]/ensure: removed
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-create]/ensure: defined content as '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2'

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_change.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-lowlevel-method-change]/content: content changed '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2' to '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e'
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-change]/content: content changed '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e' to '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2'

Verbesserung des Speicherbedarfs

Soweit so gut. Obwohl unsere aktuelle Implementierung anscheinend funktioniert, weist sie mindestens einen großen Fehler auf. Wenn die verwaltete Datei bereits existiert, speichert der Provider den gesamten Inhalt der Datei im Speicher.

demo@85c63b50bfa3:~$ cat  >  file_llmethod_change_big.pp  <<EOF
$file = '/home/demo/puppet-file-lowlevel-method-change_big'
file_llmethod {$file: content =>  'This is Sparta!'}
EOF

demo@85c63b50bfa3:~$ rm -f /home/demo/puppet-file-lowlevel-method-change_big
demo@85c63b50bfa3:~$ ulimit -Sv 200000

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_change_big.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-change_big]/ensure: defined content as '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2'
Notice: Applied catalog in 0.02 seconds

demo@85c63b50bfa3:~$ dd if=/dev/zero of=/home/demo/puppet-file-lowlevel-method-change_big seek=8G bs=1 count=1
1+0 records in
1+0 records out
1 byte copied, 8.3047e-05 s, 12.0 kB/s

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_change_big.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Error: Could not run: failed to allocate memory

Stattdessen sollte die Implementierung nur die Prüfsumme speichern, damit Puppet anhand der Prüfsummen entscheiden kann, ob unser content= Setter aufgerufen werden muss.

Dies bedeutet auch, dass die content des Puppet-Katalogs von munge mit einer Prüfsumme versehen werden muss, bevor sie von Puppets interner Vergleichsroutine verarbeitet wird. Glücklicherweise haben wir auch über shouldorig Zugriff auf den ursprünglichen Wert.

# modules/demo/lib/puppet/type/file_llmethod.rb

require 'digest'

Puppet::Type.newtype(:file_llmethod) do
  newparam(:path, namevar: true) do
  validate do |value|
  fail "# {value}  is not a String" unless value.is_a?(String)
  fail "# {value}  is not an absolute path" unless File.absolute_path?(value)
  end
  end

  newproperty(:content) do
  validate do |value|
  fail "# {value}  is not a String" unless value.is_a?(String)
  end
  munge do |value|
  "{sha256}#{Digest::SHA256.hexdigest(value)}"
  end
  # No need to override change_to_s with munging
  end

  newproperty(:ensure) do
  define_method(:change_to_s) do |currentvalue, newvalue|
  if currentvalue == :absent
  should = @resource.property(:content).should
  "defined content as '#{should}'"
  else
  super(currentvalue, newvalue)
  end
  end
  newvalues(:present, :absent)
  defaultto(:present)
  end
end
# modules/demo/lib/puppet/provider/file_llmethod/ruby.rb

Puppet::Type.type(:file_llmethod).provide(:ruby) do
  ...

  def content
  File.open(@resource[:path], 'r') do |file|
  sha = Digest::SHA256.new
  while chunk = file.read(2**16)
  sha  <<  chunk
  end
  "{sha256}#{sha.hexdigest}"
  end
  end

  def content=(value)
  # value is munged, but we need to write the original
  File.write(@resource[:path], @resource.parameter(:content).shouldorig[0])
  end
end

Jetzt können wir große Dateien verwalten:

demo@85c63b50bfa3:~$ ulimit -Sv 200000
demo@85c63b50bfa3:~$ dd if=/dev/zero of=/home/demo/puppet-file-lowlevel-method-change_big seek=8G bs=1 count=1
1+0 records in
1+0 records out
1 byte copied, 9.596e-05 s, 10.4 kB/s
demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_change_big.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-change_big]/content: Inhalt geändert von '{sha256}ef17a425c57a0e21d14bec2001d8fa762767b97145b9fe47c5d4f2fda323697b' zu '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df'

Stellen Sie es auf die Puppet-Art sicher

Da stimmt immer noch etwas nicht. Vielleicht haben Sie bemerkt, dass der content-Getter unseres Providers bedingungslos versucht, eine Datei zu öffnen, und dennoch hat der file_llmethod_create.pp-Lauf keinen Fehler erzeugt. Es scheint, dass ein ensure-Übergang von absent zu present den content-Getter kurzschließt, obwohl wir keinen Wunsch geäußert haben, dies zu tun.

Es stellt sich heraus, dass eine ensure-Eigenschaft von Puppet besonders behandelt wird. Wenn wir versucht hätten, stattdessen eine makeitso-Eigenschaft anstelle von ensure zu verwenden, gäbe es keinen Kurzschluss und der content-Getter würde eine Ausnahme auslösen.

Wir werden den content-Getter jedoch nicht reparieren. Wenn Puppet eine spezielle Behandlung für ensure hat, sollten wir Puppets beabsichtigten Mechanismus dafür verwenden und den Typ ensurable deklarieren:

# modules/demo/lib/puppet/type/file_llmethod.rb

require 'digest'

Puppet::Type.newtype(:file_llmethod) do
  ensurable

  newparam(:path, namevar: true) do
  validate do |value|
  fail "#{value}  ist keine Zeichenkette", es sei denn, value.is_a?(String)
  fail "#{value}  ist kein absoluter Pfad", es sei denn, File.absolute_path?(value)
  end
  end

  newproperty(:content) do
  validate do |value|
  fail "#{value}  ist keine Zeichenkette", es sei denn, value.is_a?(String)
  end
  munge do |value|
  "{sha256}#{Digest::SHA256.hexdigest(value)}"
  end
  end
end

Mit ensurable muss der Provider drei neue Methoden implementieren, aber wir können die ensure-Accessoren weglassen:

# modules/demo/lib/puppet/provider/file_llmethod/ruby.rb

Puppet::Type.type(:file_llmethod).provide(:ruby) do
  def exists?
  File.exist?(@resource[:name])
  end

  def create
  self.content=(:dummy)
  end

  def destroy
  File.unlink(@resource[:name])
  end

  def content
  File.open(@resource[:path], 'r') do |file|
  sha = Digest::SHA256.new
  while chunk = file.read(2**16)
  sha  <<  chunk
  end
  "{sha256}#{sha.hexdigest}"
  end
  end

  def content=(value)
  # value is munged, but we need to write the original
  File.write(@resource[:path], @resource.parameter(:content).shouldorig[0])
  end
end

Jetzt haben wir jedoch den SHA256-Prüfsumme bei der Dateierstellung verloren:

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_create.pp Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds Notice: /Stage[main]/Main/File[/home/demo/puppet-file-lowlevel-method-create]/ensure: removed Notice: /Stage[main]/Main/File_llmethod[/home/demo/puppet-file-lowlevel-method-create]/ensure: created

Um es zurückzubekommen, ersetzen wir ensurable durch eine angepasste Implementierung davon, die unsere vorherige change_to_s-Überschreibung enthält:

newproperty(:ensure, :parent =>  Puppet::Property::Ensure) do
  defaultvalues
  define_method(:change_to_s) do |currentvalue, newvalue|
  if currentvalue == :absent
  should = @resource.property(:content).should
  "defined content as '#{should}'"
  else
  super(currentvalue, newvalue)
  end
  end
end
demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_llmethod_create.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.02 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-lowlevel-method-create]/ensure: removed
Notice: /stage[main]/main/File_llmethod[/home/demo/puppet-file-lowlevel-method-create]/ensure: defined content as '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2'

Unser endgültiger Low-Level-Prototyp sieht also wie folgt aus.

Finaler Low-Level-Prototyp

# modules/demo/lib/puppet/type/file_llmethod.rb

# frozen_string_literal: true

require 'digest'

Puppet::Type.newtype(:file_llmethod) do
  newparam(:path, namevar: true) do
  validate do |value|
  fail "#{value}  ist keine Zeichenkette", es sei denn, value.is_a?(String)
  fail "#{value}  ist kein absoluter Pfad", es sei denn, File.absolute_path?(value)
  end
  end

  newproperty(:content) do
  validate do |value|
  fail "#{value}  ist keine Zeichenkette", es sei denn, value.is_a?(String)
  end
  munge do |value|
  "{sha256}#{Digest::SHA256.hexdigest(value)}"
  end
  end

  newproperty(:ensure, :parent =>  Puppet::Property::Ensure) do
  defaultvalues
  define_method(:change_to_s) do |currentvalue, newvalue|
  if currentvalue == :absent
  should = @resource.property(:content).should
  "defined content as '#{should}'"
  else
  super(currentvalue, newvalue)
  end
  end
  end
end
# modules/demo/lib/puppet/provider/file_llmethod/ruby.rb

# frozen_string_literal: true

Puppet::Type.type(:file_llmethod).provide(:ruby) do
  def exists?
  File.exist?(@resource[:name])
  end

  def create
  self.content=(:dummy)
  end

  def destroy
  File.unlink(@resource[:name])
  end

  def content
  File.open(@resource[:path], 'r') do |file|
  sha = Digest::SHA256.new
  while (chunk = file.read(2**16))
  sha  <<  Chunk
  Ende
  "{sha256}#{sha.hexdigest}"
  Ende
  Ende

  def content=(_value)
  # Der Wert wird verändert, aber wir müssen das Original schreiben
  File.write(@resource[:path], @resource.parameter(:content).shouldorig[0])
  Ende
Ende

Resource-API Prototyp

Gemäß der Resource-API Dokumentation müssen wir unseren neuen file_rsapi Typ definieren, indem wir Puppet::ResourceApi.register_type mit mehreren Parametern aufrufen, darunter die gewünschten Attribute, sogar ensure.

# modules/demo/lib/puppet/type/file_rsapi.rb

require 'puppet/resource_api'

Puppet::ResourceApi.register_type(
  name: 'file_rsapi',  
  attributes: {
  content: {
  desc: 'Beschreibung des Inhaltsparameters',
  type: 'String'  
  },
  ensure: {
  default: 'present',
  desc: 'Beschreibung des Sicherstellungsparameters',
  type: 'Enum[present, absent]'
  },
  path: {
  behaviour: :namevar,
  desc: 'Beschreibung des Pfadparameters',
  type: 'Pattern[/\A\/([^\n\/\0]+\/*)*\z/]'
  },
  },
  desc: 'Beschreibung von file_rsapi'
)

Das path type verwendet einen eingebauten Puppet-Datentyp. Stdlib::Absolutepath wäre bequemer gewesen, aber externe Datentypen sind mit der Resource-API noch nicht möglich.

Im Vergleich zu unserem Low-Level-Prototyp hat die obige Typdefinition keine SHA256-Munging- und SHA256-Ausgabe-Gegenstücke. Das canonicalize Provider-Feature sieht ähnlich aus wie Munging, aber wir überspringen es vorerst.

Die Resource-API Dokumentation sagt uns, dass wir eine get und eine set Methode in unserem Provider implementieren sollen, mit der Aussage

Die Get-Methode meldet den aktuellen Zustand der verwalteten Ressourcen. Sie gibt eine Aufzählung aller vorhandenen Ressourcen zurück. Jede Ressource ist ein Hash mit Attributnamen als Schlüssel und ihren jeweiligen Werten als Werte.

Diese Forderung ist der erste Wermutstropfen, da wir definitiv nicht alle Dateien mit ihrem Inhalt lesen und im Speicher speichern wollen. Wir können diese Forderung ignorieren – woher sollte die Resource-API das auch wissen.

Die dokumentierte Signatur ist jedoch def get(context) {...}, wobei context keine Informationen über die Ressource hat, die wir verwalten wollen.

Dies wäre ein Show-Stopper gewesen, wenn das simple_get_filter-Provider-Feature nicht existieren würde, das die Signatur in def get(context, names = nil) {...} ändert.

Unsere erste Version von file_rsapi ist also die folgende.

Grundlegende Funktionalität

# modules/demo/lib/puppet/type/file_rsapi.rb

require 'puppet/resource_api'

Puppet::ResourceApi.register_type(
  name: 'file_rsapi',
  features: %w[simple_get_filter],
  attributes: {
  content: {
  desc: 'Beschreibung des Inhaltsparameters',
  type: 'String'  
  },
  ensure: {
  default: 'present',
  desc: 'Beschreibung des Sicherstellungsparameters',
  type: 'Enum[present, absent]'
  },
  path: {
  behaviour: :namevar,
  desc: 'Beschreibung des Pfadparameters',
  type: 'Pattern[/\A\/([^\n\/\0]+\/*)*\z/]'
  },
  },
  desc: 'Beschreibung von file_rsapi'
)
# modules/demo/lib/puppet/provider/file_rsapi/file_rsapi.rb

require 'digest'

class Puppet::Provider::FileRsapi::FileRsapi
  def get(context, names)
  (names or []).map do |name|
  File.exist?(name) ? {
  path: name,
  ensure: 'present',
  content: filedigest(name),
  } : nil
  end.compact # remove non-existing resources
  end

  def set(context, changes)
  changes.each do |path, change|
  if change[:should][:ensure] == 'present'
  File.write(path, change[:should][:content])
  elsif File.exist?(path)
  File.delete(path)
  end
  end
  end

  def filedigest(path)
  File.open(path, 'r') do |file|
  sha = Digest::SHA256.new
  while chunk = file.read(2**16)
  sha  <<  chunk
  end
  "{sha256}#{sha.hexdigest}"
  end
  end
end

Das gewünschte content wird korrekt in die Datei geschrieben, aber wir haben wieder keine SHA256-Prüfsumme bei der Erstellung sowie unnötige Schreibvorgänge, da die Prüfsumme von get nicht mit dem Klartext aus dem Katalog übereinstimmt:

demo@85c63b50bfa3:~$ cat  >  file_rsapi_create.pp  <<EOF
$file = '/home/demo/puppet-file-rsapi-create'
file {$file: ensure =>  absent} ->
file_rsapi {$file: content =>  'This is Sparta!'}
EOF

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_rsapi_create.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.03 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-rsapi-create]/ensure: removed
Notice: /stage[main]/main/File_rsapi[/home/demo/puppet-file-rsapi-create]/ensure: defined 'ensure' as 'present'
Notice: Applied catalog in 0.02 seconds

demo@85c63b50bfa3:~$ cat  >  file_rsapi_change.pp  <<EOF
$file = '/home/demo/puppet-file-rsapi-change'
file {$file: content =>  'This is madness'} ->
file_rsapi {$file: content =>  'This is Sparta!'}
EOF

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_rsapi_change.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.03 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-rsapi-change]/content: content changed '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2' to '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e'
Notice: /stage[main]/main/File_rsapi[/home/demo/puppet-file-rsapi-change]/content: content changed '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e' to 'This is Sparta!'
Notice: Applied catalog in 0.03 seconds

Daher aktivieren und implementieren wir das canonicalize-Provider-Feature.

Kanonisieren

Gemäß der Dokumentation wird canonicalize auf die Ergebnisse von get sowie auf die Katalogeigenschaften angewendet. Einerseits wollen wir den Inhalt der Datei nicht in den Speicher lesen, andererseits können wir den Inhalt der Datei nicht zweimal prüfen.

Ein einfacher Weg wäre zu prüfen, ob der canonicalized-Aufruf nach dem get-Aufruf erfolgt:

def canonicalize(context, resources)
  if @stage == :get
  # do nothing, is-state canonicalization already performed by get()
  else
  # catalog canonicalization
  ...
  end
end

def get(context, paths)
  @stage = :get

  ...
end

Obwohl dies für die aktuelle Implementierung der Resource-API funktioniert, gibt es keine Garantie für die Reihenfolge der canonicalize-Aufrufe. Stattdessen erben wir von String und behandeln die Prüfsummenbildung intern. Wir fügen auch einige Zustandsänderungsaufrufe zu den entsprechenden context-Methoden hinzu.

Unsere endgültige Resource-API-basierte Prototypimplementierung ist:

# modules/demo/lib/puppet/type/file_rsapi.rb

# frozen_string_literal: true

require 'puppet/resource_api'

Puppet::ResourceApi.register_type(
  name: 'file_rsapi',
  features: %w[simple_get_filter canonicalize],
  attributes: {
  content: {
  desc: 'Beschreibung des Inhaltsparameters',
  type: 'String'
  },
  ensure: {
  default: 'present',
  desc: 'Beschreibung des Sicherstellungsparameters',
  type: 'Enum[present, absent]'
  },
  path: {
  behaviour: :namevar,
  desc: 'Beschreibung des Pfadparameters',
  type: 'Pattern[/\A\/([^\n\/\0]+\/*)*\z/]'
  }
  },
  desc: 'Beschreibung von file_rsapi'
)
# modules/demo/lib/puppet/provider/file_rsapi/file_rsapi.rb

# frozen_string_literal: true

require 'digest'
require 'pathname'

class Puppet::Provider::FileRsapi::FileRsapi
  class CanonicalString  <  String
  attr_reader :original

  def class
  # Mask as String for YAML.dump to mitigate
  # Error: Transaction store file /var/cache/puppet/state/transactionstore.yaml
  # is corrupt ((/var/cache/puppet/state/transactionstore.yaml): Tried to
  # load unspecified class: Puppet::Provider::FileRsapi::FileRsapi::CanonicalString)
  String
  end

  def self.from(obj)
  return obj if obj.is_a?(self)
  return new(filedigest(obj)) if obj.is_a?(Pathname)

  new("{sha256}#{Digest::SHA256.hexdigest(obj)}", obj)
  end

  def self.filedigest(path)
  File.open(path, 'r') do |file|
  sha = Digest::SHA256.new
  while (chunk = file.read(2**16))
  sha  <<  chunk
  end
  "{sha256}#{sha.hexdigest}"
  end
  end

  def initialize(canonical, original = nil)
  @original = original
  super(canonical)
  end
  end

  def canonicalize(_context, resources)
  resources.each do |resource|
  next if resource[:ensure] == 'absent'

  resource[:content] = CanonicalString.from(resource[:content])
  end
  resources
  end

  def get(_context, names)
  (names or []).map do |name|
  next unless File.exist?(name)

  {
  content: CanonicalString.from(Pathname.new(name)),
  ensure: 'present',
  path: name
  }
  end.compact # remove non-existing resources
  end

  def set(context, changes)
  changes.each do |path, change|
  if change[:should][:ensure] == 'present'
  File.write(path, change[:should][:content].original)
  if change[:is][:ensure] == 'present'
  # The only other possible change is due to content,
  # but content change transition info is covered implicitly
  else
  context.created("#{path}  with content '#{change[:should][:content]}'")
  end
  elsif File.exist?(path)
  File.delete(path)
  context.deleted(path)
  end
  end
  end
end

Es gibt uns:

demo@85c63b50bfa3:~$ cat  >  file_rsapi_create.pp  <<EOF
$file = '/home/demo/puppet-file-rsapi-create'
file {$file: ensure =>  absent} ->
file_rsapi {$file: content =>  'This is Sparta!'}
EOF

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_rsapi_create.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.03 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-rsapi-create]/ensure: removed
Notice: /stage[main]/main/File_rsapi[/home/demo/puppet-file-rsapi-create]/ensure: defined 'ensure' as 'present'
Notice: file_rsapi: Created: /home/demo/puppet-file-rsapi-create with content '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2'

demo@85c63b50bfa3:~$ cat  >  file_rsapi_change.pp  <<EOF
$file = '/home/demo/puppet-file-rsapi-change'
file {$file: content =>  'This is madness'} ->
file_rsapi {$file: content =>  'This is Sparta!'}
EOF

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_rsapi_change.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.03 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-rsapi-change]/content: content changed '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2' to '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e'
Notice: /stage[main]/main/File_rsapi[/home/demo/puppet-file-rsapi-change]/content: content changed '{sha256}0549defd0a7d6d840e3a69b82566505924cacbe2a79392970ec28cddc763949e' to '{sha256}823cbb079548be98b892725b133df610d0bff46b33e38b72d269306d32b73df2'

demo@85c63b50bfa3:~$ cat  >  file_rsapi_remove.pp  <<EOF
$file = '/home/demo/puppet-file-rsapi-remove'
file {$file: ensure =>  present} ->
file_rsapi {$file: ensure =>  absent}
EOF

demo@85c63b50bfa3:~$ bin/puppet apply --modulepath modules file_rsapi_remove.pp
Notice: Compiled catalog for 85c63b50bfa3 in environment production in 0.03 seconds
Notice: /stage[main]/main/file[/home/demo/puppet-file-rsapi-remove]/ensure: created
Notice: /stage[main]/main/File_rsapi[/home/demo/puppet-file-rsapi-remove]/ensure: undefined 'ensure' from 'present'
Notice: file_rsapi: Deleted: /home/demo/puppet-file-rsapi-remove

Gedanken

Die scheinbare Notwendigkeit einer CanonicalString Maskierung als String lässt es so aussehen, als ob uns etwas fehlt. Wenn die Resource-API nur Datentypen nach der Kanonisierung überprüft, könnten wir get etwas einfacheres als CanonicalString zurückgeben, um einen bereits kanonisierten Wert zu signalisieren.

Die Standardanforderung der Resource-API, alle vorhandenen Ressourcen zurückzugeben, vereinfacht die Entwicklung, wenn man dies ohnehin vorhatte. Die Low-Level-Version hierfür ist oft eine Kombination aus prefetch und instances.

Kategorien: Automatisierung HowTos
Tags: Puppet

Array

über den Autor

dlu


Beitrag teilen: