Puppet Archiv - credativ®

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.

Wenn es darum geht, mit Puppet eine große Anzahl Systeme in komplexen Umgebungen zu managen, führt früher oder später kein Weg daran vorbei, sich Gedanken über die Organisation der eigenen, oft gewachsenen Puppet-Codebase zu machen. Ein Entwurfsmuster hierfür ist das „Role-/Profile-Pattern“: ein Ansatz der darauf abzielt eine schnelle Orientierung zu ermöglichen und Konfigurationen leichter nachzuvollziehen. Dieses soll im Folgenden näher erläutert werden.

Rollen

In der Regel lassen sich Systeme mit einem Begriff beschreiben, der oft auch für Nicht-Techniker Rückschlüsse auf den Zweck eines Systems zulässt.

So handelt es sich bei einem Mailserver, Webserver oder Applikationsserver um eine Rolle die ein System erfüllt – all diesen Begriffen gemeinsam ist, dass kein direkter Bezug auf die verwendeten Komponenten oder gar detaillierte Konfiguration genommen werden muss, um die Funktion zu verdeutlichen.

Einen guten Hinweis auf Role-Bezeichnungen können in vielen Fällen Logische Netzwerkdiagramme liefern, und für das Verständnis ist es sicherlich hilfreich, bereits im Team bekannte Begriffe wiederzuverwenden.

Profile

Bei einem Profil geht es um die konkreten Technologie, die für eine Rolle benötigt wird, und dessen Organisationsspezifische Konfiguration. An dem Wort „Organisationsspezifisch“ erkennt man bereits, das hier kein Fokus auf der organisationsübergreifenden Wiederverwendbarkeit wie bei Komponentenmodulen gelegt werden soll.

Wie könnte beispielsweise eine Rolle Webserver für eine konkrete PHP-Applikation aussehen? Wenn man das für einen anderen Techniker beschreiben müsste, liefert das gute Hinweise auf Profile. So könnte eine Aufzählung möglicherweise lauten:

  • apache2-Webserver
  • Verschiedene PHP-Komponenten
  • Die Applikation selbst (sofern diese nicht mit einem spezialisierten Tool deployed wird)

Zudem ergeben sich bei einer solchen Aufzählung auch „Konfigurationen“, die für alle oder zumindest mehrere Systeme gelten:

  • Software-Quellen
  • Systemübergreifende Netzwerkkonfiguration (DNS- und NTP-Server)
  • SSH-Server und Einstellungen für diesen
  • Benutzer und SSH-Keys der Administratoren

Oft können solche Komponenten nicht direkt einer spezifischen Komponente zugeordnet werden oder benötigen zusätzliche individuelle Konfiguration: etwa könnten für eine unternehmenseigene Webapplikation zusätzliche Verzeichnisse benötigt werden oder eigene Health-Checker für einen Loadbalancer.

Im „Role-/Profile-Pattern“ stellen Profile die Brücke zwischen den funktionalen Rollen und einzelnen Komponentenmodulen dar. Was das genau bedeutet, wird im Nachfolgenden noch erläutert.

Komponentenmodule

Auf der nächst tieferen Ebene finden sich Puppet-Module, um einzelne Komponenten (apache, postgresql, dovecot, tomcat, etc.) zu konfigurieren oder Unterstützung für zusätzliche Ressourcen bereitzustellen, die Puppet nicht von Haus aus unterstützt.

Da diese für viele unterschiedliche Puppet-Nutzer nützlich sein können, sind diese Module im Idealfall so generisch, dass sie auch von anderen Organisationen mit möglichst wenig oder keinen Anpassungen verwendet werden können. Das schließt beispielsweise in den Klassen definierte Abhängigkeiten auf andere Komponentenmodule (beispielsweise Konfiguration der Datenbank für eine Webapplikation) weitestgehend aus, weil ein anderer Nutzer vielleicht ein anderes DBMS verwenden will.

Vor allem lohnt sich der Blick auf PuppetForge, wo eine Vielzahl bereits fertiger Module angeboten werden.

Wie wird das Pattern technisch umgesetzt?

Auf Ebene des tatsächlichen Puppetcode werden sowohl Rollen und Profile als Klassen umgesetzt, die wiederum in Modulen gebündelt werden.

Dabei gelten grob die folgenden Regeln:

  • Je System gibt es eine einzige Role
  • Eine Role beinhaltet Includes auf mehrere Profiles, die den eigentlichen Servertyp definieren
  • Ein Profile greift auf verschiedene (Komponenten-)module und nach Bedarf ergänzenden Ressourcen zurück, um einen logischen Technologiestack zu definieren

Das Pattern sieht außerdem vor, die Namespace-Features der Puppet-DSL zu nutzen, um Rollen und Profile einerseits voneinander und andererseits von Komponentenmodulen abzugrenzen.

Ob dabei tatsächlich jeweils ein eigenes Module für die Rollen und eines für die Profile verwendet wird, ist vor allem eine Frage der eigenen Anforderungen:

  • Sollen Rollen und Profile unabhängig voneinander wiederverwendet werden?
  • Ist ein Prefix gewünscht, um Rollen und Profile des einen Setups von einem anderen unterscheiden zu können?

Wie Komponentenmodule am besten geschrieben werden bzw. wie man Komponentenmodule von Dritten auswählt, damit sie sich gut in das Muster einfügen, ist ein eigenes Thema, das sich für einen weiteren Artikel eignet.

Die Faustregel lautet: Komponentenmodule verwenden Ressourcen und kümmern sich nur um Aspekte, die die Komponente betreffen, für die sie geschrieben wurden.

Praxisbeispiel

Für ein Setup, bestehend aus Loadbalancern und Webservern, könnte die (stark vereinfachte) Puppet-Konfiguration wie folgt aussehen (natürlich aufgeteilt auf verschiedene Dateien gemäß Puppet Autoload Layout).

Rollen

Die Rollen erlauben einen schnellen Überblick, was auf einem Loadbalancer oder einem Webserver läuft, ohne all zu sehr in die Details zu gehen.

# role/manifests/loadbalancer.pp
class role::loadbalancer {
  include profile::base
  include profile::loadbalancer
}
 
# role/manifests/webserver.pp
class role::webserver {
  include profile::base
  include profile::webserver
  include profile::webapplication
}

Profile

In den Profilen dagegen lässt sich die tatsächliche Konfiguration ablesen, und wie man sieht, ist ein Profil nicht darauf beschränkt, eine einzelne Komponente zu konfigurieren. Man kann, muss aber nicht zwingend für Basiskomponenten wie NTP, SSH und dergleichen eigene Profile definieren. Hier empfiehlt sich einfach eine Abwägung zwischen dem Umfang, was konfiguriert werden muss und wie wahrscheinlich es für diese Konfiguration ist, dass man sie in manchen Rollen benötigt, in manchen aber nicht. Auch Abhängigkeiten zwischen den Profilen können in die Überlegungen mit einfließen.

# profile/manifests/base.pp
class profile::base {
   # Paketquellen, User etc.  
  class { '::openssh':
    permit_root_login => 'no'
  }
  class { '::ntp':
    servers => ['ntp1.credativ.lan', 'ntp2.credativ.lan']
  }
}

Im Gegenteil: Hier wird getan, was nötig ist, damit eine Funktionalität lauffähig ist – und wenn es das Setzen von sysctl-Parametern einschließt.

# profile/manifests/loadbalancer.pp
class profile::loadbalancer {
  class { '::keepalived':
    virtual_ip_address => '1.2.3.4',
    ...
  }
  sysctl::value { 'ip_forward': value => 1 }
}
 
# profile/manifests/webserver.pp
class profile::webserver {
  file { '/var/www':
    ensure => directory
  }
  class { '::apache':
    ...
  }
}
 
# profile/manifests/webapplication.pp
class profile::webapplication {
  require profile::webserver
  # Install a custom webapplication
  #  ...
}

Daten für Profile

Zuletzt bleibt noch das Problem von veränderlichen Daten, beispielsweise um andere Paketquellen in Test und Produktion oder andere NTP-Server für unterschiedliche Standorte zu verwenden.

Für diesen Zweck hat sich im Puppet-Ökosystem das Tool Hiera etabliert, das hier jedoch nicht näher erläutert werden soll. Im Bezug auf das Role-/Profile-Pattern bleibt nur die Frage, wo die Lookups idealerweise erfolgen sollen.

Wie eingangs bereits erwähnt, stellen Profile die Brücke zwischen Rollen und Komponenten dar. Daher erfolgen Hiera-Lookups idealerweise in Profilen. Was man an Daten aus Hiera bezieht und wie man die Daten idealerweise speichert, ist eine Philosophie für sich. Erfahrungsgemäß ist es aber sinnvoll möglichst nur Daten aus Hiera zu beziehen, die in mehreren Profilen benutzt werden oder die sich zwischen unterschiedlichen Hierarchieebenen unterscheiden. Ansonsten wird es schnell unübersichtlich. Weiterhin sollte vermieden werden, Werte im Hiera doppelt zu hinterlegen. So macht es beispielsweise wenig Sinn, einen Datenbankbenutzer der sowohl im Datenbank-Profil als auch im Anwendungs-Profil benötigt wird in Hiera unter zwei verschiedenen Schlüsselnamen abzulegen.

Weiterführende Informationen

Weiterführende Informationen finden sich auf den folgenden Seiten (die teilweise auch als Quelle für diesen Artikel gedient haben):

 

Dieser Artikel wurde ursprünglich geschrieben von Patrick Schönfeld.

Owncloud erfreut sich zunehmender Beliebtheit. Neben Universtitäten und Privatpersonen finden auch immer mehr Unternehmen Gefallen an der offenen und einfach zu benutzenden Filesharing-Lösung, bei der die Daten auf eigenen Servern verbleiben können. Um die Installation einer Owncloud-Umgebung zu vereinfachen beschreiben wir im Folgenden, wie diese inklusive der Anbindung an ein LDAP automatisiert möglich ist.

Datei-Austausch-Dienste wie Dropbox und Google Drive erfreuen sich reger Beliebtheit. Mit ihrer Hilfe können Dateien wie Urlaubsfotos, aber auch Protokolle und Geschäftsberichte einfach zwischen Nutzern und verschiedenen Geräten ausgetauscht werden. Doch gerade geschäftskritische oder persönliche Daten möchten viele Nutzer nur ungern auf die Server fremder Firmen hochladen. Gerade in Hinblick auf mögliche Datenschutz-Bedenken, aber auch unter Berücksichtigung der Veröffentlichungen von Edward Snowden, haben daher so genannte private Cloud-Lösungen mehr und mehr an Bedeutung gewonnen.

Das vor 5 Jahren ins Leben gerufene ownCloud Projekt ist hier ein Vorreiter: es bietet Nutzern die Möglichkeit, in eigener, kontrollierter Umgebung eine Filehosting-Umgebung zur Verfügung zu stellen, die sehr benutzerfreundlich zu bedienen ist und auf einer großen Auswahl von Geräten und Betriebssystemen funktioniert. So haben sich z.B. mehrere Universitäten in Deutschland zusammengeschlossen und bieten auf Basis von ownCloud unter dem Projekt sciebo einen Datei-Austausch-Dienst für mehr als 300.000 Studenten an, der insgesamt 5 Petabyte Speicherplatz bereitstellt.

Auch Unternehmen greifen vermehrt auf ownCloud als eigene, interne Filehosting-Lösung zurück. Dabei greift ownCloud für die Authentifizierung meist auf eine LDAP-Schnittstelle zurück, in der die relevanten Unternehmens-Mitarbeiter eingetragen sind. Die Herausforderung im Enterprise-Umfeld ist aber das automatische Deployment: umfangreiche IT-Setups werden heutzutage üblicherweise mit Puppet, Ansible oder ähnlichen Werkzeugen verwaltet und in Docker-Containern oder VMs bereit gestellt. Diese erfordern, dass die Software vollständig ohne manuelle Interaktion installiert und konfiguriert wird. Der bei ownCloud übliche Ansatz ist jedoch, nach der Installation die Webseite der ownCloud-Instanz aufzurufen und diverse Schritte von Hand durchzuführen.

Die Installation von ownCloud kann über Pakete erfolgen. Dies erfordert üblicherweise keine Benutzerinteraktion. Die Erstkonfiguration, bei der im Webinterface der Admin-Nutzer und die Datenbank angegeben werden müssen, kann automatisiert erfolgen, in dem entsprechende Angaben in der Datei $owncloud/config/autoconfig.php hinterlegt werden: bei jedem ownCloud-start wird geprüft, ob die Datei vorliegt. In dem Fall werden alle notwendigen Einstellungen übernommen und damit die maßgebliche config.php befüllt. Die autoconfig.php ist ähnlich aufgebaut wie die config.php:

<?php
$AUTOCONFIG = array (
  'directory' => '/var/www/html/owncloud/data',
  'adminlogin'    => 'mmu',
  'adminpass'     => '123456',
  'dbtype'        => 'pgsql',
  'dbname'        => 'owncloud',
  'dbuser'        => 'postgres',
  'dbpass'        => '123456',
  'dbhost'        => '192.168.123.45',
  'dbtableprefix' => 'oc_',
);

Die config.php kann in dem Fall trotz allem schon existieren und andere statische Konfigurations-Optionen wie zum Beispiel für Proxies enthalten. Weitere Informationen zur Auto-Konfiguration finden sich in entsprechenden Abschnitt des ownCloud-Admin-Handbuchs.

Damit die automatische Konfiguration alle notwendigen Einträge erstellt, muss aber ownCloud einmalig aufgerufen werden. Dies geht am einfachsten mit einem curl-Aufruf: curl -s -k 127.0.0.1/owncloud/ > /dev/null. Dieser veranlasst ownCloud dazu, einmalig alle notwendigen Schritte durchzuführen, die DB-Einträge zu erstellen, etc. Mit ownCloud 8.1 wird es übrigens einen ownCloud-eigenen Befehl dafür geben, siehe auch pull request #14416 bei Github.

Im Anschluss daran muss die LDAP-App aktiviert und konfiguriert werden. Seit ownCloud 8.0 und dank dieses Patches von mark0n ist es möglich, dies vollständig über die Kommandozeile mit Hilfe des ownCloud-eigenen PHP-Scripts occ umzusetzen. Die wesentlichen Schritte dazu sind:

php -f $ocpath/occ app:enable user_ldap
php -f $ocpath/occ ldap:create-empty-config
php -f $ocpath/occ ldap:set-config "" ldapHost 192.168.123.11
php -f $ocpath/occ ldap:set-config "" ldapPort 389
php -f $ocpath/occ ldap:set-config "" ldapBase \"dc=example,dc=net\"
php -f $ocpath/occ ldap:set-config "" ldapConfigurationActive 1

Dabei ist zu beachten, dass alle Befehle als der Nutzer des Webservers ausgeführt werden. Dass die Konfiguration korrekt übernommen wurde kann ebenfalls mit Hilfe von occ geprüft werden: php -f $ocpath/occ ldap:show-config. Die Ausgabe zeigt die aktuell bestehende LDAP-Konfiguration von ownCloud, und damit auch alle möglichen weiteren Konfigurationsoptionen.

Abschließend lässt sich sagen, dass ownCloud auch mit LDAP-Anbindung gut automatisiert aufgesetzt werden kann, sofern in Kauf genommen wird, dass einige direkte Befehle abgesetzt werden. Wünschenswert wäre hier, wenn das dahinterliegende Konfigurationsformat besser dokumentiert wäre, um dies direkt zu verteilen statt php-Befehle auszuführen.

Alle bisher erschienen Einträge zum Thema Automatisierung finden sich in der Kategorie Konfigurationsmanagement. Bei weiteren Fragen zu Services und Support rund um ownCloud, Puppet oder Ansible, insbesondere bei komplexeren Setups, stehen wir natürlich auch gerne zur Verfügung.

 

Dieser Artikel wurde ursprünglich geschrieben von Roland Wolters.

Dieser Blog Eintrag ist ein Tutorial, um die Benutzung von PuppetDB aufzuzeigen und ein paar Beispiele für die Benutzung zu geben.

Was ist PuppetDB

PuppetDB ist ein Backend für Puppet, um verschiedene Informationen aus dem Puppetmaster heraus zu speichern und wieder verfügbar zu machen. Sie ist der Nachfolger der ActiveRecord basierten Storeconfigs.

PuppetDB kann Fakten, Reports, Events und andere Informationen sammeln. Auf diese Daten kann dann über eine REST ähnliche Schnittstelle zugegriffen werden. Zusätzlich gibt es ein Puppetmodul mit dem andere Module auf diese Daten zugreifen zu können. Da PuppetDB weitestgehend mit dem alten „storeconfig“ Mechanismus kompatibel ist, kann Puppet genutzt werden um Ressourcen zu exportieren und anderen Knoten verfügbar zu machen. So können beispielsweise alle bekannten Hostschlüssel von den Knoten gesammelt und in /etc/ssh/ssh_known_hosts gespeichert werden.

Die Installation

Installation der Pakete

Bei der Installation gehen wir davon aus das der Puppetmaster und die PuppetDB auf dem selben Host installiert werden sollen. Zur PuppetDB Installation wird das Puppetlabs APT Repository benötigt. Dieses lässt sich ganz einfach in das System einbinden:

# wget https://apt.puppetlabs.com/puppetlabs-release-<RELEASE>.deb
# sudo dpkg -i puppetlabs-release-<RELEASE>.deb
# sudo apt-get update

<RELEASE> ist hierbei durch den Releasenamen der verwendeten Distribution zu ersetzen. Im Falle von Debian 7 wäre dies puppetlabs-release-wheezy.deb. Eine Liste der verfügbaren Releases kann man unter https://apt.puppetlabs.com einsehen.

Anschliessend kann das puppetdb Paket installiert werden:

# sudo puppet resource package puppetdb ensure=latest
# sudo puppet resource service puppetdb ensure=running enable=true

Anschliessend ist das PuppetDB Paket in der letzten Version installiert und aktiviert. In der Standardkonfiguration ist PuppetDB so konfiguriert das sie auf localhost:8080 unverschlüsselt und auf *:8081 mit SSL lauscht.

Im nächsten Schritt installieren wir den Puppetsupport für PuppetDB:

# sudo puppet resource package puppetdb-terminus ensure=latest

Danach sind alle Komponenten installiert, die Puppet benötigt um mit der PuppetDB zu kommunizieren.

Einbindung in Puppet

Damit Puppet weiss wo er die PuppetDB findet müssen wir die Datei /etc/puppet/puppetdb.conf anlegen:

[main]
server = <fqdn des puppetdb servers>
port = 8081

Es ist wichtig in der Konfigurationsdatei den Hostnamen des PuppetDB Servers zu verwenden. Das Puppetmodul prüft hier den Namen des Zertifikats und wenn dies nicht mit dem Servernamen in der puppetdb.conf zusammenpasst wird ein Fehler generiert.

Damit der Puppetmaster die Daten auch Richtung PuppetDB schickt müssen wir in der Datei /etc/puppet/puppet.conf folgende Zeilen in der [master] Sektion ergänzen:

[master]
storeconfigs = true
storeconfigs_backend = puppetdb

Wenn man die Reports auch in der PuppetDB speichern möchte, muss die puppet.conf zusätzlich um folgendes ergänzt werden:

[master]
reports = store,puppetdb

Damit die Informationen in der PuppetDB auch innerhalb von Puppet verwendet werden können, muss noch eine letzte Datei angelegt werden. /etc/puppet/routes.yaml:

---
master:
  facts:
    terminus: puppetdb
    cache: yaml

Damit zieht Puppet für Fakten die PuppetDB als Quelle heran. Die Fakten werden in yaml Dateien zwischengespeichert um die Datenbank zu entlasten.

Wenn alles funktioniert hat sollte ein puppet agent -t ohne Fehlermeldungen durchlaufen:

# puppet agent -t
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Loading facts
Info: Caching catalog for foreman.novalocal
Info: Applying configuration version '1427207789'
Notice: Finished catalog run in 0.23 seconds

In der Logdatei der PuppetDB sollte daraufhin Meldungen erscheinen das die Informationen eingeliefert und gespeichert wurden:

tail -n 3 /var/log/puppetdb/puppetdb.log
2015-03-24 16:45:48,145 INFO  [c.p.p.command] [2af3b94d-7ec6-49e0-802d-3dec939dd5ab] [replace facts] foreman.novalocal
2015-03-24 16:45:48,958 INFO  [c.p.p.command] [25546f85-25b4-43b8-91f0-98d386a7d36f] [replace catalog] foreman.novalocal
2015-03-24 16:45:50,079 INFO  [c.p.p.command] [b223b4db-a3d9-4ab5-b50e-0f31f92ac4b8] [store report] puppet v3.7.4 - foreman.novalocal

Die Benutzung

Zugriff via curl

Nachfolgend wird anhand von ein paar Beispielen der Zugriff auf die HTTP API mittels curl gezeigt. Die Antworten werden von PuppetDB im JSON Format zurückgeliefert. Die vollständige Dokumentation der API findet man unter https://docs.puppetlabs.com/puppetdb/2.3/api/index.html.

Die API unterstüzt verschiedene Endpunkte um auf Informationen in der PuppetDB zuzugreifen.

Wenn man sich alle aktiven Knoten anzeigen lassen möchte kann man folgendes Query verwenden:

curl -X GET http://localhost:8080/v3/nodes  --data-urlencode 'query=["=", ["node", "active"], true]'

PuppetDB liefert daraufhin JSON formatiert eine Liste aller Knoten zurück (gekürzt):

[ {
  "name" : "foreman.novalocal",
  "deactivated" : null,
  "catalog_timestamp" : "2015-03-24T16:45:48.918Z",
  "facts_timestamp" : "2015-03-24T16:45:48.022Z",
  "report_timestamp" : "2015-03-24T16:45:48.462Z"
}...]

Der Parameter query weist PuppetDB an unsere Daten anhand des querys zu filtern. Die Dokumentation zu den Filtern findet man in in der PuppetDB Dokumentation.

Eine Liste aller verwendeten Betriebssysteme kann man mit dem Facts Endpunkt abfragen:

# curl -X GET http://localhost:8080/v3/facts --data-urlencode 'query=["=", "name", "operatingsystem"]'

Auch hier kommt wieder ein JSON formatiertes Objekt zurück:

[ {
  "value" : "Debian",
  "name" : "operatingsystem",
  "certname" : "foreman.novalocal"
} ]

Das Ganze lässt sich auch kürzer schreiben:

curl -X GET http://localhost:8080/v3/facts/operatingsystem

Oder wenn ich das ganze auf Debian Systeme einschränken möchte:

curl -X GET http://localhost:8080/v3/facts/operatingsystem/Debian

Der Reports Endpunkt liefert die letzten Reports. Dieser beinhaltet allerdings nicht die eigentlichen Informationen sondern nur die Metadaten zum Report:

curl -X GET http://localhost:8080/v3/reports
 
[ {
  "hash" : "050d4697b9aff4ae48e1b0c1e5102487a7f8db8d",
  "puppet-version" : "3.7.4",
  "receive-time" : "2015-03-24T13:15:38.866Z",
  "report-format" : 4,
  "start-time" : "2015-03-24T13:15:33.083Z",
  "end-time" : "2015-03-24T13:15:34.705Z",
  "transaction-uuid" : "ee73330d-e031-450d-8f4e-cde317ca22c7",
  "configuration-version" : "1427199788",
  "certname" : "foreman.novalocal"
} ... ]

Wenn man herausfinden möchte was sich während eines Puppetdurchlaufes geändert hat, dann können wir dazu den Hash des Reports nehmen. Der Events Endpunkt kann nun alle Events liefern die zu einem bestimmten Report gehören:

curl -X GET http://localhost:8080/v3/events --data-urlencode 'query=["=", "report", "050d4697b9aff4ae48e1b0c1e5102487a7f8db8d"]'
[ {
  "containment-path" : [ "Stage[main]", "Ntp::Config", "File[/etc/ntp.conf]" ],
  "new-value" : "{md5}96de9ac4d747c521ce12a111ff24fb66",
  "containing-class" : "Ntp::Config",
  "report-receive-time" : "2015-03-24T13:15:38.866Z",
  "report" : "050d4697b9aff4ae48e1b0c1e5102487a7f8db8d",
  "resource-title" : "/etc/ntp.conf",
  "property" : "content",
  "file" : "/etc/puppet/modules/ntp/manifests/config.pp",
  "old-value" : "{md5}27060798ca930cc9fa4c6a7a9d798f75",
  "run-start-time" : "2015-03-24T13:15:33.083Z",
  "line" : 20,
  "status" : "success",
  "run-end-time" : "2015-03-24T13:15:34.705Z",
  "resource-type" : "File",
  "timestamp" : "2015-03-24T13:15:36.045Z",
  "configuration-version" : "1427199788",
  "certname" : "foreman.novalocal",
  "message" : "content changed '{md5}27060798ca930cc9fa4c6a7a9d798f75' to '{md5}96de9ac4d747c521ce12a111ff24fb66'"
} ]

In diesem Beispiel wurde z.B. die ntp.conf vom NTP Modul editiert.

Die Benutzung in Puppet

Kommen wir auf das Beispiel mit den SSH-Hostkeys zurück: Hierzu kann ein Mechanismus namens „Exported Resources verwendet werden. Dabei wird jeder Knoten angewiesen bestimmte Informationen als Objekt in der PuppetDB zu speichern. Diese Objekte können dann benutzt werden um z.B. eine Konfigurationsdatei zu realisieren. Genaue Informationen dazu finden sich unter https://docs.puppetlabs.com/guides/exported_resources.html

class ssh {
  # Deklaration:
  @@sshkey { $hostname:
    type => dsa,
    key => $sshdsakey,
  }
  # Collect:
  Sshkey <<| |>>
}
 
include 'ssh'

Hier verwenden wir die sshkey Ressource um die SSH Hostkeys aller Clients in der PuppetDB zu speichern. Der Aufruf von Sshkey <<| |>> „realisiert“ diese SSH Hostkeys dann auf dem Clientsystem.

cat /etc/ssh/ssh_known_hosts 
# HEADER: This file was autogenerated at 2015-03-24 14:41:24 +0000
# HEADER: by puppet.  While it can still be managed manually, it
# HEADER: is definitely not recommended.
foreman ssh-dss AAAAB3NzaC1kc3MAAACBAPU8MHiLvP44gGW4KnUHZ0ikEV/OteJVPCmpi6+odRiWXmzFEcWZrIDPAeusb+9JnPsYUgrDKe5ZxIRWVkZPCFV+A5DwJbOzPLd2s3sngtUvPD8yDjx1fiqsBWaWV3TtRBdGOsgPnEHI8kTEF2rk3tzSQOAmfRdTEIRbSjyJIx93AAAAFQDW1m3d6uedEpk7jqfl/Q8VKaL5wQAAAIB9z4Kg7SgJGEfoMz42rCT6nsfz3CIk5MPlkNQaGKz2nxI6BvxhrqOHpYNuIrZqy75Z/PaStRfztr+Ks7VAD7yUxB0++Rm1iXfh/laapk/sa9L5V53g6notB5ux3aBzUYzfIckiJlifGUkxodpTlOdHKFfvRdStvGNzUfaoV1tJYQAAAIEAoBsiK8y65Mp7XJRria6lXDGhB326r+DVZsohBYI4O72Gfx71loBwCye/6pvezy+96B838qpJaYvHUk+x3/8NH8pvaRnb/qrWlg1WEbQQ9LBD03ea/hSEIpGySnOZs8zbgV/gUWFy939p4G6SxUiUSKQHcqm/POMTyneQvXX0+pE=
puppetclient ssh-dss AAAAB3NzaC1kc3MAAACBAN14x/OOcU5uLng13pszxQLwyuEcN9rsAaBKEh33t4/BrrBWIiM7XUQ86tToH4LeJwmQMXtZHEojXcsi1XD3N9qnluQ4u6K3zIdqrqQzJoFHnsLYDU4zF9FSeBw47m/0/4DL/BohczhJJ88W00Kfj6ItFP4mNCfEw7iMOLKY+gP5AAAAFQC7Wivw9jUjJ/9nMXCUVbh/ZeIIZwAAAIEAye4plFIRpcVnflmZQmDwCg6BNd5m5g1NkqI9Qz7HTbYzyJars9qzB3BZiVBpz0waX+KjUlcWfGDsDnNCl0RMOuhfTnzfW2JeBILoArEjaY0thkkeDUtgPD6XYM3/xKPusXf61AqapAfRU1HFrT3EMxYicfPLwUDFGmZoc7nDUxEAAACBALjkr8py08gfWaIhPrJXlh7FIs9RdP8mIW9p0QsgW7zRFKrXixnS77d6brabM9/TGcxcRtbvwk2LQM/px8YIfJhGMoW/SP5lc7tsWvrhLjcxTNnI4PcfCbMda2RrM53oUBjd7pRpIV6K1MIFmR6yMqj0FGMHTOIgH3sWTYL6JLQd

Weiterführendes

Um PuppetDB Queries in puppet auszuführen gibt es das puppetdbquery modul. Damit kann man auch komplexe Abfragen in Puppetmodulen ausführen und mit den zurückgelieferten Daten arbeiten.

Dies ermöglicht zum Beispiel Dinge wie:

$nodes = query_nodes('Class[Apache]')
$nodes.each { |$node|
  nagios_service {
    ...
  }
}

Das Modul enthält auch ein Hiera Backend, mit dem Daten aus der PuppetDB direkt in Hiera verwendet werden können:

ntp::servers::_nodequery: 'Class[Ntp::Server]'

In diesem Beispiel wird die ntp::servers Variable mit allen Hostnamen gefüllt, die die Klasse Ntp::Server verwenden. Sollte man also einen neuen NTP Server benötigen, so muss man ihm nur die entsprechende Klasse zuweisen und alle Nodes werden den neuen Server automatisch verwenden.

Tuning

In der Standardeinstellung verwendet PuppetDB eine integrierte Datenbank. In großen Umgebungen empfiehlt es sich eine externe PostgreSQL® Datenbank einzusetzen. Nähere Informationen dazu findet man in der PuppetDB Dokumentation zur Datenbankeinrichtung

Im ersten Teil wurde Foreman installiert und konfiguriert. Damit ist es bereits möglich, Systeme mit Puppet zu konfigurieren und Reports einzusehen. Jetzt folgt die Konfiguration des Provisionings, um auch neue Systeme installieren zu können.

  1. Subnet konfigurierenUm Installationen zu ermöglichen, muss zunächst das verwendete IP-Subnet in Foreman konfiguriert werden. Dazu wird unter Infrastructure -> Subnets der Button New subnet betätigt, der Dialog ausgefüllt und bestätigt. Hierbei ist es wichtig, dass Network address und Network mask entsprechend der eigenen Infrastruktur ausgefüllt werden. Außerdem muss auf dem Reiter Proxies ein TFTP-Proxy ausgewählt werden. („foreman“).
  2. Provisioning Templates für Ubuntu konfigurierenNachdem Puppet im ersten Teil dieser Anleitung erstmals ausgeführt wurde, kennt Foreman Ubuntu 12.04.4 LTS als Betriebssystem. Dies muss nun mit verschiedenen Templates verknüpft werden. Dazu müssen unter Hosts -> Provisioning Templates nacheinander die folgenden Templates editiert werden:
    • Preseed default
    • Preseed default PXELinux
    • Preseed finish

    Dort ist jeweils unter „Association“ der Haken vor „Ubuntu 12.0.4.4“ zu aktivieren und die Änderung mit „Submit“ zu übernehmen.

  3. Architektur mit dem Betriebssystem verknüpfen:Dieser Vorgang muss dann gleich noch einmal für die System-Architekturen unter Hosts -> Architekturen wiederholt werden: Anklicken, Haken aktivieren, mit Submit betätigen.
  4. Standard-Einstellungen für Ubuntu-Installation festlegenIm Bereich „Hosts -> Operating Systems“ können die Betriebssysteme verwaltet werden, die mit Foreman installiert werden sollen.Dort ist zur Zeit nur Ubuntu bekannt, das noch mit ein paar Einstellungen versehen werden muss, um es verwenden zu können. Nach dem Anklicken sind nacheinander die gewünschten Architekturen im Reiter „Operating System“, „Preseed default“ im Reiter „Partition Table“ und „Ubuntu mirror“ unter „Installation media“ zu wählen. Schließen müssen im Reiter „Templates“ noch die Templates aus Schritt 1 für „provision“, „PXELinux“ und „finish“ gewählt werden.
  5. DHCP konfigurierenFür die automatische Ubuntu-Installation wird ein DHCP-Server für das in Schritt 1 konfigurierte Subnet benötigt.Dieser kann mit der Konfiguration
    ignore unknown-clients;

    so konfiguriert werden, dass er nur an explizit konfigurierte Systeme IP-Adressen vergibt (benötigt also nicht zwingend einen IP-Pool). Außerdem werden die folgenden Einstellungen in der subnet-Deklaration benötigt:

    next-server <IP des Foreman-Systems>;
    filename "pxelinux.0";
  6. Neuen Host provisionieren:Nachdem der DHCP-Server konfiguriert wurde, ist Foreman prinzipiell in der Lage Systeme zu provisionieren. Dafür sind pro System folgende Schritte erforderlich:
    1. Eintragen des Systems mit Mac-Adresse in die DHCP-Konfiguration:
       host <hostname> { 
         hardware ethernet <mac-adresse>;
         fixed-address <ip> 
       } 
      
    2. Anlegen eines Hosts in ForemanDies geschieht, indem unter Hosts -> All Hosts der Button New host betätigt wird.Der nachfolgende Dialog ist weitestgehend selbsterklärend. Wichtig dabei ist, dass Environment, Puppet CA und Master (foreman), MAC-Adresse und Domain auf dem Reiter Network sowie alle Einstellungen im Reiter Operating System ausgefüllt werden.
    3. Start des Systems und Wahl von PXE-Boot als Bootoption Zu guter Letzt kannn das zu installierende System gestartet und dazu bewegt werden, per PXE zu booten. Die Installation erfolgt dann vollautomatisch.

    Damit wären wir am Ende unserer zweiteiligen Anleitung. Nachdem die vorherigen Schritte befolgt wurden, besteht eine voll funktionsfähige Foreman-Installation, mit der Systeme installiert und konfiguriert werden können. Damit ist es theoretisch möglich, ein ganzes Firmennetzwerk vollautomatisch zu provisionieren – auch wenn dies natürlich weiteres Zutun erfordert.

    Für weiterführende Informationen sei auch hier wieder auf die Puppet- und Foreman-Dokumentation verwiesen, die umfassende Informationen hinsichtlich der Entwicklung von Puppet-Manifesten und der Anwendung von Foreman bereit halten. Bei Fragen stehen wir aber auch gerne zur Verfügung.

     

    Dieser Artikel wurde ursprünglich geschrieben von Patrick Schönfeld.

Konfigurationsmanagement und Monitoring sind heutzutage die wichtigsten Teile des Betriebs von größeren Computerlandschaften. Gerade beim Konfigurationsmanagement können aber leicht Fehler passieren, die Fatal für den Betrieb sein können. Ein korrektes Monitoring des Konfigurationsmanagements ist deswegen enorm wichtig.

Wenn man mit Puppet oder anderen Konfigurationsmanagement-Systemen arbeitet, kann es manchmal vorkommen, dass nach einer Änderung manche Nodes keine Konfigurationsänderungen mehr annehmen oder gar der Konfigurations-Dienst nicht mehr startet. Aus diesem Grund sollte man den Zustand der einzelnen Nodes überwachen und bei Problemen schnell handeln.

Sofern Puppet mit Icinga oder Nagios überwacht wird, wird hierfür oft ein Script verwendet, welches die Status-Datei /var/lib/puppet/state/last_run_summary.yaml auswertet. Dieses kann auf allen Puppet Nodes via NRPE aufgerufen werden, wie mein Kollege Roland beschreibt. Alternativ lässt sich das gleiche Script als lokaler check_mk Check ausführen.

Es gibt jedoch Setups, bei denen man evtl. auf NRPE verzichten möchte oder zumindest nicht den gleichen Check auf jeder Maschine ausführen möchte. In diesem Fall kommt einem PuppetDB gerne zur Hilfe. Grundsätzlich ist PuppetDB in jedem größeren Puppet Setup zu empfehlen, da diese das Cachen von Katalogen und das exportieren von Ressourcen erlaubt. Sofern die PuppetDB nicht nur Kataloge, sondern auch Reports der einzelnen Nodes speichert (via reports = store,puppetdb in der puppet.conf des Puppet Masters), lässt sich der Zustand der einzelnen Nodes mit wenigen API Aufrufen abfragen. Für die Integration dieser Aufrufe in Icinga/Nagios haben wir ein kleines Skript geschrieben: check_puppetdb_nodes.

Das Skript benötigt eine aktuelle (1.5 oder neuer) PuppetDB und einige Perl Module, die auf den meisten Systemen sowieso schon installiert sind: JSON, LWP, Date::Parse und Nagios::Plugin). Beim Aufruf fragt das Skript zunächst die PuppetDB auf localhost:8080 (oder auf einem anderen Host oder Port, wenn via -H und -p übergeben) nach der Liste der bekannten Nodes und den Zeitstempeln ihrer Kataloge. Sofern ein Katalog älter als 2 bzw 24 Stunden ist, wird WARNING bzw CRITICAL an das Monitoring zurückgeben. Andere Intervalle lassen sich mit -w und -c minutengenau einstellen.

Da ein erfolgreich kompilierter und abgerufener Katalog nicht bedeutet, dass der Puppet Agent diesen auf der Maschine auch anwenden konnte, ist es notwendig zu überprüfen, ob der Puppet Agent nach dem Durchlauf Fehler gemeldet hat. PuppetDB exportiert derartige Informationen über den Events und Event-Count Endpoint der API, hierzu muss jedoch das Speichern von Reports in der PuppetDB aktiviert sein. Für ein Monitoring reicht der Event-Count, da hierüber eine einfache Statistik des letzten Durchlaufs abrufbar ist. Standarrdmäßig wird check_puppetdb_nodes bei einer Fehleranzahl größer Null sofort CRITICAL melden, dies lässt sich jedoch mit -W und -C weiter konfigurieren.

Mit check_puppetdb_nodes lässt sich das Monitoring einer ganzen Puppet Landschaft mit Hilfe von Icinga/Nagios schnell und ressourcenschonend implementieren. Die einzige Anforderung ist eine PuppetDB mit aktivierter Speicherung von Reports, welche auch in anderen Szenarien bereits stark zu empfehlen ist.

 

Dieser Artikel wurde ursprünglich geschrieben von Evgeni Golov.

Beim Einsatz von Puppet und einer Monitoring-Infrastruktur macht es Sinn, regelmäßig zu prüfen, ob der Puppet-Agent auf allen Clients erfolgreich lief. Dies ist auf den jeweiligen Clients mit einem NRPE-Skript einfach möglich.

Puppet ermöglicht die zentrale Konfiguration auch sehr komplexer Infrastrukturen und erfreut sich einer steigenden Beliebtheit. Ebenso ist das Überwachen der Funktionalität der eigenen Infrastruktur mit Hife von Icinga üblich. So unterstützen wir mehrere Kunden beim Einsatz genau dieser kombinierten Lösung.

Da liegt es auf der Hand, die Funktionalität und den erfolgreichen Lauf von Puppet mit Hilfe des Monitoring zu überwachen. Da der Puppet-Agent auf den Clients läuft, liegt es nahe, die Überwachung dort mit Hilfe von eines NRPE-Plugins anzusetzen. Der Puppet-Anget schreibt verschiedene Status-Informationen in die Datei /var/lib/puppet/state/last_run_summary.yaml, die sich einfach prüfen lässt. Allerdings muss dabei berücksichtigt werden, dass die Yaml-Datei auch geschrieben wird, wenn der Lauf erfolglos war – es muss also auch der Inhalt der Datei geprüft werden, um zu prüfen, ob der Lauf erfolgreich war und ob der Lauf in den letzten X Minuten erfolgte und nicht schon Tage her ist.

Dazu gibt es bereits einige Tests, die aber nicht unseren Ansprüchen genügten. Nur den Zeitstempel der Datei zu prüfen reicht nicht, da wie erwähnt die Datei auch dann geschrieben wird, wenn der Puppet-Katalog nicht erfolgreich runtergeladen wird. Ein anderes Skript baut auf einer reinen Validitätsprüfung der Yaml-Struktur auf, und prüft nicht, ob Puppet eventuell einfach geraume Zeit gar nicht lief. Ein anderes Skript komplett in Bash geschrieben prüft zwar sowohl den Zeitstempel in der Datei als auch so weit möglich weitere Einträge in der Yaml-Datei, ist aber aufgrund der beschränkten Möglichkeiten von Bash nur unübersichtlich erweiterbar und kann keine echte Syntaxprüfung der Yaml-Datei vornehmen.

Vor diesem Hintergrund haben wir ein eigenes Skript entwickelt, dass von den oben genannten Lösungen inspiriert wurde, aber sowohl den Zeitstempel ausliest wie auch die Validität der Yaml-Status-Datei prüft. Es lässt sich auf der Kommandozeile testen:

# Default run
$ sudo /usr/local/lib/nagios/plugins/check_puppetagent -w 3600 -c 9000
OK: Puppet was last run 13 minutes and 21 seconds ago
 
# Last run too old
$ sudo /usr/local/lib/nagios/plugins/check_puppetagent -w 30 -c 90
CRIT: Puppet was last run 14 minutes and 30 seconds ago

Das Script steht unter der MIT-Lizenz und wurde unter Debian und Ubuntu unter Python 2.7 getestet. Es ist auch auf Monitoringexchange.org verfügbar. Für die Zukunft wäre es möglich, das Skript noch andere Inhalte der Datei auswerten zu lassen und z.B. bei einem Failcount in der Datei die Zahl der Fehler direkt an das Monitoring zurückgeben zu lassen.

 

Dieser Artikel wurde ursprünglich geschrieben von Roland Wolters.

Ein Konfigurationsmanagementsystem stellt eine immense Arbeitserleichterung dar und ist in größeren Setups unverzichtbar. Viele Kunden der credativ GmbH setzen dafür auf Puppet. Im Rahmen mehrerer Kundenprojekte sind eine Reihe generischer Puppet-Module entwickelt worden, die wir der Allgemeinheit zur Verfügung stellen möchten.

In unseren zahlreichen Kundenprojekten sind im Laufe der Zeit eine Reihe von Puppet-Modulen entstanden, die im Github-Account der credativ GmbH verfügbar sind. Die Module wurden mit einem Fokus auf eine möglichst einfache Wiederverwertung entwickelt und sind daher parametrisiert. Sie erlauben eine Konfiguration sowohl über die üblichen Puppet-Mechanismen als auch über Hiera.

Die nachfolgende Liste stellt eine Auswahl von Modulen dar, die weitestgehend ohne Anpassungen für viele Anwendungsfälle einsetzbar sind:

Alle hier beschriebenen Module wurden für die Verwendung auf Debian GNU/Linux Systemen geschrieben und machen Gebrauch von Hilfsfunktionen, die von example42 im Rahmen ihres Puppi-Moduls veröffentlicht wurden. Für eine einfachere Installation werden diese als example42lib in einem eigenen Github-Projekt bereitgestellt.

Die weiteren im Github-Account der credativ GmbH befindlichen Puppet-Module sind teilweise speziell angepasste Module für Sonderlösungen oder befinden sich noch in der Entwicklung. Bei Interesse an den Modulen oder weiteren Lösungen rund um Puppet stehen wir gerne zur Verfügung.

 

Dieser Artikel wurde ursprünglich geschrieben von Patrick Schönfeld.

Bei der Administration einer großen Zahl von Servern ist ein zentrales Konfigurations-Management irgendwann unabdingbar. Dieser Artikel beschreibt in einer ersten Einführung das in Ruby geschriebene Framework Puppet.

Einführung

Teil unseres Tagesgeschäfts ist es, beliebig große Server-Installationen zu verwalten und zu warten. Gerade bei großen Clustern heißt dies, eine Vielzahl von Maschinen mit fast identischer Konfiguration nebeneinander zu betreiben. Ohne eine zentralisierte, automatisierte Konfigurations-Verteilung ist dies kaum machbar – an dieser Stelle tritt Puppet auf den Plan.
Wie auch andere Konfigurations-Management-Werkzeuge greift Puppet auf einen zentralen Server zurück, der die Konfiguration verwaltet. Dort fragen die „Clients“ verschlüsselt die Konfiguration ab, und spielen Sie gemäß der Vorgaben des Servers ein, verändern Rechte, führen Befehle aus, etc. Die Vorteile liegen auf der Hand:

Technische Arbeitsweise

Puppet besteht aus einem zentralen Server, Puppet-Master genannt, und den Clients, genannt Nodes. Diese melden sich beim Master an, und fragen dort nach der aktuellen Konfiguration. Der Master gibt diese an die Nodes weiter – die Möglichkeiten der Anweisungen des Masters sind schier unbegrenzt:

Im Prinzip kann alles mit der Übergabe von Dateien vom Server an die Node erledigt werden, doch ist dies in komplexen Setups weder übersichtlich noch vereinfachend. Gerade die Abstraktion von System-Aufgaben (Dienste neu starten, Paket-Versionen sicherstellen, Nutzer einrichten, etc.) ohne das direkte Überschreiben von Dateien hilft ungemein beim Konfigurieren komplexer Systeme.

Installation

Für die Installation benötigt man einen zentralen Puppet-Master, der die Konfiguration verwaltet: apt-get install puppetmaster Puppet geht davon aus, dass alle beteiligten Rechner FQDNs haben, dies sollte in einem korrekt gewarteten Netzwerk aber eh der Fall sein!
Auf jedem zu verwaltenden Rechner wird ein Puppet-Client mit apt-get install puppet installiert.

Konfiguration von Puppet

Die Puppet-Nodes suchen automatisch nach dem Rechner, der auf den Namen puppet auflöst, so lange der Name korrekt auf den Hauptserver zeigt, brauchen sie nicht weiter konfiguriert werden.
Beim Puppet-Master muss der Dateiserver noch korrekt konfiguriert werden, der wie oben beschrieben Dateien an die Nodes übergeben kann. Je nach Anspruch können die Dateien dabei nah bei der weiteren Konfiguration gehalten werden, oder aber zentral in einem externen Archiv untergebracht werden. Für unser Beispiel werden wir die zu verteilenden Dateien nah bei der eigentlichen Konfiguration halten, ganz so, wie es auch im Best Practice Guide und in der Anleitung Module Configuration der Puppet-Dokumentation beschrieben wird.
Daher reicht es, in der Datei /etc/puppet/fileserver.conf folgende Konfiguration vorzunehmen:

[modules]
allow 192.168.0.1/24
allow *.credativ.de

Konfiguration der Konfiguration – Module

Um eine Server-Konfiguration durch Puppet erledigen zu lassen, ist es am Besten, diese in Aufgabenbereiche oder Themen zu unterteilen, wie zum Beispiel „ssh“, „logs“, „apache“, etc. Diese Bereiche werden in Puppet als „Module“ bezeichnet, und sind der Kern von Puppets Konfigurationsverwaltung. Den Aufbau eines Modules beschreiben wir hier anhand einer fiktiven SSH-Konfiguration und halten uns dabei eng an den Best Practice Guide.
Es sei angemerkt, dass die tatsächliche Konfiguration von SSH durch Puppet an einigen Stellen dynamischer wäre, hier wird nur ein vereinfachter Weg gezeigt.

Das SSH-Modul

Der Anspruch des SSH-Moduls ist:

  1. Das openssh-server-Paket soll in neuester Version installiert sein.
  2. Die Datei sshd_config soll der entsprechen, die im Puppet-Master hinterlegt ist.
  3. Falls die Datei sshd_config installiert wird, soll der sshd neu gestartet werden.
  4. Der Nutzer credativ soll bestimmte Dateien in seinem Verzeichnis $HOME/.ssh haben.

Um diesen Ansprüchen gerecht zu werden, erstellen wir die notwendigen Verzeichnisse des Moduls:

mkdir -p /etc/puppet/modules/ssh/manifests
mkdir -p /etc/puppet/modules/ssh/files

Der Ordner Mainfests enthält die eigentlichen Konfigurations-Anweisungen des Moduls, der Ordner files hält Dateien vor, die an die Nodes ausgeliefert werden sollen.
Die Konfigurations-Anweisungen des Modules finden sich in der Datei init.pp im Ordner manifests. Die Gruppe von Anweisungen, um die obigen Ziele 1.-4. zu erfüllen, wird dort als „Klasse“ zusammen gefasst. Die Klasse enthält selbst wiederum Untersektionen, sogenannte Types. In unserem Fall findet sich für jedes vorher definierte Ziel ein Type:

class ssh{
        package { "openssh-server":
                 ensure => latest,
        }
        file { "/etc/ssh/sshd_config":
                owner   => root,
                group   => root,
                mode    => 644,
                source  => "puppet:///ssh/sshd_config",
        }
        service { ssh:
                ensure          => running,
                hasrestart      => true,
                subscribe       => File["/etc/ssh/sshd_config"],
        }
        file { "/home/credativ/.ssh":
                path    => "/home/credativ/.ssh",
                owner   => "credativ",
                group   => "credativ",
                mode    => 600,
                recurse => true,
                source  => "puppet:///ssh/ssh",
                ensure  => [directory, present],
        }
}

jeder Type setzt eine gänzlich andere Aktion auf dem Node um:
package
Hier wird sicher gestellt, dass das Paket openssh-server in der neuesten Version installiert ist.
file
Eine Datei auf der Node wird durch die Version vom Server überschrieben und mit entsprechenden Rechten versehen.
service
Der Dienst sshd muss laufen, und wird notfalls gestartet. Falls außerdem die Datei /etc/ssh/sshd_config aktualisiert wird, wird auch der Dienst neu gestartet.
file
Hier taucht noch einmal der Type „file“ auf – es wird aber nicht eine einzelne Datei übertragen, sondern gleich ein ganzes Verzeichnis.

Damit das Modul auch korrekt arbeitet, müssen die beim Type „file“ definierten Dateien und Verzeichnisse auch unter /etc/puppet/modules/ssh/files/ zu finden sein.

Nodes und Module

Wir haben nun drei Elemente: den Puppet-Master, die Nodes, und die Module. Nun muss die Zuweisung erfolgen, welche Nodes welche Module aufrufen sollen. Dafür muss zuerst das Modul in der Datei /etc/puppet/manifests/modules.pp aktiviert werden:

import "ssh"

Die Zuweisung zu den einzelnen Nodes erfolgt in der Datei /etc/puppet/manifests/nodes.pp. Diese legt für jede Node fest, welches Modul geladen wird. Außerdem gibt es für alle nicht weiter spezifizierten Nodes einen Default-Eintrag, und zu guter Letzt können auch Einträge von anderen abgeleitet werden. Um also für alle Nodes das Modul „rsyslog“ zu laden, aber nur für die Node „external“ das Modul „ssh“, sieht der Eintrag wie folgt aus:

node default {
   include rsyslog
}
 
node 'external' inherits default {
  include ssh
}

Damit ist Puppet fertig konfiguriert, und nimmt sofort seine Arbeit auf.

Zertifikate – Sichere Kommunikation zwischen Node und Master

Die Kommunikation zwischen Master und Node verläuft verschlüsselt. Um dies zu gewährleisten, müssen Nodes auf dem Master zertifiziert werden. Dies ist möglich, nachdem ein Node das erste Mal eine Anfrage an den Master gestellt hat – der Master setzt diesen Node dann auf wait, und stellt ihm so lange keine Daten zur Verfügung. Erst, wenn die Node durch einen Admin verifiziert wurde, wird die Node frei geschaltet. Mit # puppetca --list wird die Liste der noch zu verifizierenden Nodes angezeigt, verifiziert wird mit: # puppetca --sign external.example.com Bei Bedarf kann dieser Prozess weiter verfeinert werden.

Abschließende Worte

Die hier vorgestellten Beispiele sind natürlich stark vereinfacht. Im Real-Betrieb würde die SSH-Konfiguration komplexer sein, und Schlüssel würden nicht gerade mit dem Type „file“ statisch verteilt werden. Aus diesen Beispielen lassen sich aber leicht weitere Module ableiten, und die an vielen Stellen verlinkte Konfiguration tut ihr Übriges, damit der geneigte Leser sich tiefer in die Materie einarbeiten kann.
Wir hier bei credativ haben mit Puppet mittlerweile sehr umfangreiche und sehr gute Erfahrungen gemacht, leisten an vielen Ecken Support und Beratung für Puppet und merken, wie die Nachfrage steigt. Puppet ist derzeit auf der Überholspur, und es wird spannend sein zu beobachten, wie sich der Platzhirsch cfengine angesichts dieser Konkurrenz verhält.

 

Dieser Artikel wurde ursprünglich geschrieben von Roland Wolters.