Inleiding tot de pop

Puppet is een configuratiebeheersysteem. Het wordt gebruikt om hosts in de gewenste staat te brengen en deze staat te behouden.

Ik werk nu ruim vijf jaar met Puppet. Deze tekst is in wezen een vertaalde en opnieuw geordende compilatie van de belangrijkste punten uit de officiële documentatie, waardoor beginners snel de essentie van Puppet kunnen begrijpen.

Inleiding tot de pop

Basis informatie

Het besturingssysteem van Puppet is client-server, hoewel het ook serverloze werking met beperkte functionaliteit ondersteunt.

Er wordt gebruik gemaakt van een pull-werkingsmodel: standaard nemen clients eens per half uur contact op met de server voor een configuratie en passen deze toe. Als je met Ansible hebt gewerkt, dan gebruiken ze een ander push-model: de beheerder initieert het proces van het toepassen van de configuratie, de clients zelf passen niets toe.

Bij netwerkcommunicatie wordt gebruik gemaakt van tweeweg-TLS-encryptie: de server en client beschikken over hun eigen private sleutels en bijbehorende certificaten. Normaal gesproken geeft de server certificaten uit voor clients, maar in principe is het mogelijk om een ​​externe CA te gebruiken.

Inleiding tot manifesten

In poppenterminologie naar de poppenserver aansluiten knooppunten (knooppunten). De configuratie voor de knooppunten is geschreven in manifesten in een speciale programmeertaal - Puppet DSL.

Puppet DSL is een declaratieve taal. Het beschrijft de gewenste status van het knooppunt in de vorm van verklaringen van individuele bronnen, bijvoorbeeld:

  • Het bestand bestaat en heeft specifieke inhoud.
  • Het pakket is geïnstalleerd.
  • De dienst is begonnen.

Hulpbronnen kunnen met elkaar verbonden zijn:

  • Er zijn afhankelijkheden, deze beïnvloeden de volgorde waarin bronnen worden gebruikt.
    Bijvoorbeeld: “Installeer eerst het pakket, bewerk vervolgens het configuratiebestand en start vervolgens de service.”
  • Er zijn meldingen: als een bron is gewijzigd, worden er meldingen verzonden naar de bronnen waarop deze is geabonneerd.
    Als het configuratiebestand bijvoorbeeld verandert, kunt u de service automatisch opnieuw starten.

Bovendien heeft de Puppet DSL functies en variabelen, evenals voorwaardelijke instructies en selectors. Er worden ook verschillende sjabloonmechanismen ondersteund: EPP en ERB.

Puppet is geschreven in Ruby, dus veel van de constructies en termen zijn daaruit overgenomen. Met Ruby kun je Puppet uitbreiden - complexe logica, nieuwe soorten bronnen en functies toevoegen.

Terwijl Puppet actief is, worden manifesten voor elk specifiek knooppunt op de server in een map gecompileerd. Adresboek is een lijst met bronnen en hun relaties na het berekenen van de waarde van functies, variabelen en uitbreiding van voorwaardelijke instructies.

Syntaxis en codestijl

Hier zijn delen van de officiële documentatie die u zullen helpen de syntaxis te begrijpen als de gegeven voorbeelden niet voldoende zijn:

Hier is een voorbeeld van hoe het manifest eruit ziet:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
  # Конфигурация по сути является перечислением ресурсов и их параметров.
  #
  # У каждого ресурса есть тип и название.
  #
  # Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
  #
  # Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
  # Про разные типы ресурсов написано ниже.
  #
  # После типа в фигурных скобках пишется название ресурса, потом двоеточие,
  # дальше идёт опциональное перечисление параметров ресурса и их значений.
  # Значения параметров указываются через т.н. hash rocket (=>).
  resource { 'title':
    param1 => value1,
    param2 => value2,
    param3 => value3,
  }
}

Inspringen en regeleinden zijn geen verplicht onderdeel van het manifest, maar het wordt wel aanbevolen stijlgids. Samenvatting:

  • Inspringingen met twee spaties, tabs worden niet gebruikt.
  • Accolades worden gescheiden door een spatie; dubbele punten worden niet gescheiden door een spatie.
  • Komma's na elke parameter, inclusief de laatste. Elke parameter staat op een aparte regel. Er wordt een uitzondering gemaakt voor het geval zonder parameters en één parameter: u kunt op één regel en zonder komma schrijven (d.w.z. resource { 'title': } и resource { 'title': param => value }).
  • De pijlen op de parameters moeten zich op hetzelfde niveau bevinden.
  • Er staan ​​pijlen voor resourcerelaties voor geschreven.

Locatie van bestanden op pappetserver

Voor verdere uitleg zal ik het concept van “root directory” introduceren. De hoofdmap is de map die de Puppet-configuratie voor een specifiek knooppunt bevat.

De hoofdmap varieert afhankelijk van de versie van Puppet en de gebruikte omgevingen. Omgevingen zijn onafhankelijke configuratiesets die in afzonderlijke mappen zijn opgeslagen. Meestal gebruikt in combinatie met git, in welk geval omgevingen worden gemaakt op basis van git-takken. Dienovereenkomstig bevindt elk knooppunt zich in een of andere omgeving. Dit kan worden geconfigureerd op het knooppunt zelf, of in ENC, waarover ik in het volgende artikel zal praten.

  • In de derde versie ("oude Puppet") was de basismap /etc/puppet. Het gebruik van omgevingen is optioneel - we gebruiken ze bijvoorbeeld niet met de oude Puppet. Als er omgevingen worden gebruikt, worden deze meestal opgeslagen /etc/puppet/environments, zal de hoofdmap de omgevingsmap zijn. Als er geen omgevingen worden gebruikt, zal de hoofdmap de basismap zijn.
  • Vanaf de vierde versie (“nieuwe Puppet”) werd het gebruik van omgevingen verplicht en werd de basismap verplaatst /etc/puppetlabs/code. Dienovereenkomstig worden omgevingen opgeslagen /etc/puppetlabs/code/environments, de hoofdmap is de omgevingsmap.

Er moet een submap in de hoofdmap aanwezig zijn manifests, dat een of meer manifesten bevat die de knooppunten beschrijven. Bovendien moet er een submap zijn modules, dat de modules bevat. Welke modules dat zijn, vertel ik je even later. Bovendien kan de oude Puppet ook een submap hebben files, dat verschillende bestanden bevat die we naar de knooppunten kopiëren. In de nieuwe Puppet worden alle bestanden in modules geplaatst.

Manifestbestanden hebben de extensie .pp.

Een paar gevechtsvoorbeelden

Beschrijving van het knooppunt en de bron erop

Op het knooppunt server1.testdomain er moet een bestand worden aangemaakt /etc/issue met inhoud Debian GNU/Linux n l. Het bestand moet eigendom zijn van een gebruiker en groep root, toegangsrechten moeten zijn 644.

Wij schrijven een manifest:

node 'server1.testdomain' {   # блок конфигурации, относящийся к ноде server1.testdomain
    file { '/etc/issue':   # описываем файл /etc/issue
        ensure  => present,   # этот файл должен существовать
        content => 'Debian GNU/Linux n l',   # у него должно быть такое содержимое
        owner   => root,   # пользователь-владелец
        group   => root,   # группа-владелец
        mode    => '0644',   # права на файл. Они заданы в виде строки (в кавычках), потому что иначе число с 0 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
    }
}

Relaties tussen resources op een knooppunt

Op het knooppunt server2.testdomain nginx moet actief zijn en werken met een eerder voorbereide configuratie.

Laten we het probleem ontleden:

  • Het pakket moet worden geïnstalleerd nginx.
  • Het is noodzakelijk dat de configuratiebestanden van de server worden gekopieerd.
  • De service moet actief zijn nginx.
  • Als de configuratie wordt bijgewerkt, moet de service opnieuw worden gestart.

Wij schrijven een manifest:

node 'server2.testdomain' {   # блок конфигурации, относящийся к ноде server2.testdomain
    package { 'nginx':   # описываем пакет nginx
        ensure => installed,   # он должен быть установлен
    }
  # Прямая стрелка (->) говорит о том, что ресурс ниже должен
  # создаваться после ресурса, описанного выше.
  # Такие зависимости транзитивны.
    -> file { '/etc/nginx':   # описываем файл /etc/nginx
        ensure  => directory,   # это должна быть директория
        source  => 'puppet:///modules/example/nginx-conf',   # её содержимое нужно брать с паппет-сервера по указанному адресу
        recurse => true,   # копировать файлы рекурсивно
        purge   => true,   # нужно удалять лишние файлы (те, которых нет в источнике)
        force   => true,   # удалять лишние директории
    }
  # Волнистая стрелка (~>) говорит о том, что ресурс ниже должен
  # подписаться на изменения ресурса, описанного выше.
  # Волнистая стрелка включает в себя прямую (->).
    ~> service { 'nginx':   # описываем сервис nginx
        ensure => running,   # он должен быть запущен
        enable => true,   # его нужно запускать автоматически при старте системы
    }
  # Когда ресурс типа service получает уведомление,
  # соответствующий сервис перезапускается.
}

Om dit te laten werken, hebt u ongeveer de volgende bestandslocatie op de poppenserver nodig:

/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│   └── site.pp
└── modules/
    └── example/
        └── files/
            └── nginx-conf/
                ├── nginx.conf
                ├── mime.types
                └── conf.d/
                    └── some.conf

Brontypen

Een volledige lijst met ondersteunde resourcetypen vindt u hier in de documentatie, zal ik hier vijf basistypen beschrijven, die in mijn praktijk voldoende zijn om de meeste problemen op te lossen.

filet

Beheert bestanden, mappen, symlinks, hun inhoud en toegangsrechten.

opties:

  • naam van de bron — pad naar het bestand (optioneel)
  • pad — pad naar het bestand (als dit niet is opgegeven in de naam)
  • verzekeren - bestandstype:
    • absent - verwijder een bestand
    • present — er moet een bestand van welk type dan ook zijn (als er geen bestand is, wordt er een normaal bestand gemaakt)
    • file - regulier bestand
    • directory - map
    • link - symlink
  • content — bestandsinhoud (alleen geschikt voor gewone bestanden, kan niet samen met (bron) of doel)
  • (bron) — een link naar het pad waarvan u de inhoud van het bestand wilt kopiëren (kan niet worden gebruikt in combinatie met content of doel). Kan worden opgegeven als een URI met een schema puppet: (dan worden bestanden van de poppenserver gebruikt), en met het schema http: (Ik hoop dat het duidelijk is wat er in dit geval zal gebeuren), en zelfs met het diagram file: of als een absoluut pad zonder schema (dan wordt het bestand van de lokale FS op het knooppunt gebruikt)
  • doel — waar de symlink naar moet verwijzen (kan niet samen met content of (bron))
  • eigenaar — de gebruiker die eigenaar zou moeten zijn van het bestand
  • groep — de groep waartoe het bestand moet behoren
  • mode — bestandsrechten (als een string)
  • herhaling - maakt recursieve mapverwerking mogelijk
  • zuivering - maakt het verwijderen van bestanden mogelijk die niet in Puppet worden beschreven
  • dwingen - maakt het verwijderen van mappen mogelijk die niet in Puppet worden beschreven

pakket

Installeert en verwijdert pakketten. Kan meldingen afhandelen - installeert het pakket opnieuw als de parameter is opgegeven installeer_op_refresh opnieuw.

opties:

  • naam van de bron — pakketnaam (optioneel)
  • naam — pakketnaam (indien niet gespecificeerd in de naam)
  • leverancier - pakketbeheerder om te gebruiken
  • verzekeren — gewenste staat van de verpakking:
    • present, installed - elke geïnstalleerde versie
    • latest - nieuwste versie geïnstalleerd
    • absent - verwijderd (apt-get remove)
    • purged — verwijderd samen met configuratiebestanden (apt-get purge)
    • held - pakketversie is vergrendeld (apt-mark hold)
    • любая другая строка — de opgegeven versie is geïnstalleerd
  • installeer_op_refresh opnieuw - indien true, waarna het pakket na ontvangst van de melding opnieuw wordt geïnstalleerd. Handig voor brongebaseerde distributies, waarbij het opnieuw opbouwen van pakketten nodig kan zijn bij het wijzigen van buildparameters. Standaard false.

service

Beheert diensten. Kan meldingen verwerken - start de service opnieuw.

opties:

  • naam van de bron — te beheren dienst (optioneel)
  • naam — de dienst die beheerd moet worden (indien niet gespecificeerd in de naam)
  • verzekeren — gewenste staat van de dienst:
    • running - gelanceerd
    • stopped - gestopt
  • in staat stellen — controleert de mogelijkheid om de dienst te starten:
    • true — automatisch uitvoeren is ingeschakeld (systemctl enable)
    • mask - vermomd (systemctl mask)
    • false — automatisch uitvoeren is uitgeschakeld (systemctl disable)
  • restart - opdracht om de service opnieuw te starten
  • toestand — opdracht om de servicestatus te controleren
  • heeft opnieuw opgestart — geef aan of het service-initscript opnieuw opstarten ondersteunt. Als false en de parameter is opgegeven restart — de waarde van deze parameter wordt gebruikt. Als false en parameter restart niet gespecificeerd - de service wordt gestopt en opnieuw gestart (maar systemd gebruikt de opdracht systemctl restart).
  • heeftstatus — geef aan of het service-initscript de opdracht ondersteunt status. als false, dan wordt de parameterwaarde gebruikt toestand. Standaard true.

exec

Voert externe opdrachten uit. Als u geen parameters opgeeft creëert, alleen als, tenzij of alleen vernieuwen, wordt de opdracht elke keer uitgevoerd wanneer Puppet wordt uitgevoerd. Kan meldingen verwerken - voert een opdracht uit.

opties:

  • naam van de bron — uit te voeren opdracht (optioneel)
  • commando — het uit te voeren commando (als dit niet in de naam is gespecificeerd)
  • pad — paden waarin naar het uitvoerbare bestand moet worden gezocht
  • alleen als — als het in deze parameter gespecificeerde commando wordt aangevuld met een nulretourcode, wordt het hoofdcommando uitgevoerd
  • tenzij — als het in deze parameter gespecificeerde commando wordt aangevuld met een retourcode die niet nul is, wordt het hoofdcommando uitgevoerd
  • creëert — als het in deze parameter opgegeven bestand niet bestaat, wordt het hoofdcommando uitgevoerd
  • alleen vernieuwen - indien true, dan wordt de opdracht alleen uitgevoerd als deze exec een melding ontvangt van andere bronnen
  • CVO — map van waaruit de opdracht moet worden uitgevoerd
  • gebruiker — de gebruiker van wie de opdracht moet worden uitgevoerd
  • leverancier - hoe u de opdracht uitvoert:
    • posix — er wordt eenvoudigweg een onderliggend proces gemaakt, zorg ervoor dat u dit specificeert pad
    • schelp - het commando wordt gelanceerd in de shell /bin/sh, wordt mogelijk niet gespecificeerd pad, kunt u globbing, pipelines en andere shell-functies gebruiken. Meestal automatisch gedetecteerd als er speciale tekens zijn (|, ;, &&, || enzovoort).

cron

Beheert cronjobs.

opties:

  • naam van de bron - gewoon een soort identificatie
  • verzekeren — Crownjob-staat:
    • present - creëren als niet bestaat
    • absent - verwijder indien aanwezig
  • commando - welk commando moet worden uitgevoerd
  • milieu — in welke omgeving de opdracht moet worden uitgevoerd (lijst met omgevingsvariabelen en hun waarden via =)
  • gebruiker — vanaf welke gebruiker de opdracht moet worden uitgevoerd
  • minuut, uur, weekdag, maand, maand dag — wanneer cron moet worden uitgevoerd. Als een van deze attributen niet is gespecificeerd, zal de waarde ervan in de crontab dat zijn *.

In Marionet 6.0 cron alsof uit de doos gehaald in puppetserver, dus er is geen documentatie op de algemene site. Maar hij zit in de doos in puppet-agent, dus het is niet nodig om het apart te installeren. U kunt de documentatie ervan bekijken in de documentatie voor de vijfde versie van PuppetOf op GitHub.

Over hulpbronnen in het algemeen

Vereisten voor de uniciteit van bronnen

De meest voorkomende fout die we tegenkomen is Dubbele aangifte. Deze fout treedt op wanneer twee of meer bronnen van hetzelfde type met dezelfde naam in de map verschijnen.

Daarom schrijf ik nogmaals: manifesten voor hetzelfde knooppunt mogen geen bronnen van hetzelfde type met dezelfde titel bevatten!

Soms is het nodig om pakketten met dezelfde naam te installeren, maar met verschillende pakketbeheerders. In dit geval moet u de parameter gebruiken nameom de fout te voorkomen:

package { 'ruby-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'gem',
}
package { 'python-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'pip',
}

Andere resourcetypen hebben vergelijkbare opties om duplicatie te voorkomen − name у service, command у exec, enzovoort.

Metaparameters

Elk resourcetype heeft een aantal speciale parameters, ongeacht de aard ervan.

Volledige lijst met metaparameters in de poppendocumentatie.

Korte lijst:

  • vereisen — deze parameter geeft aan van welke hulpbronnen deze hulpbron afhankelijk is.
  • vaardigheden - Deze parameter specificeert welke bronnen afhankelijk zijn van deze bron.
  • abonneren — deze parameter specificeert van welke bronnen deze bron meldingen ontvangt.
  • de hoogte — Deze parameter specificeert welke bronnen meldingen van deze bron ontvangen.

Alle vermelde metaparameters accepteren een enkele bronlink of een reeks links tussen vierkante haakjes.

Links naar bronnen

Een bronlink is eenvoudigweg een vermelding van de bron. Ze worden vooral gebruikt om afhankelijkheden aan te geven. Als u naar een niet-bestaande bron verwijst, ontstaat er een compilatiefout.

De syntaxis van de link is als volgt: resourcetype met een hoofdletter (als de typenaam dubbele dubbele punten bevat, wordt elk deel van de naam tussen de dubbele punten met een hoofdletter geschreven), vervolgens de resourcenaam tussen vierkante haken (in het hoofdlettergebruik van de naam verandert niet!). Er mogen geen spaties voorkomen; vierkante haken worden direct na de typenaam geschreven.

Voorbeeld:

file { '/file1': ensure => present }
file { '/file2':
  ensure => directory,
  before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']

Afhankelijkheden en meldingen

Documentatie hier.

Zoals eerder vermeld zijn eenvoudige afhankelijkheden tussen hulpbronnen transitief. Wees overigens voorzichtig bij het toevoegen van afhankelijkheden: u kunt cyclische afhankelijkheden creëren, wat een compilatiefout zal veroorzaken.

In tegenstelling tot afhankelijkheden zijn meldingen niet transitief. Voor meldingen gelden de volgende regels:

  • Als de bron een melding ontvangt, wordt deze bijgewerkt. De updateacties zijn afhankelijk van het resourcetype − exec voert het commando uit, service start de dienst opnieuw, pakket installeert het pakket opnieuw. Als voor de resource geen updateactie is gedefinieerd, gebeurt er niets.
  • Tijdens één uitvoering van Puppet wordt de bron niet vaker dan één keer bijgewerkt. Dit is mogelijk omdat meldingen afhankelijkheden bevatten en de afhankelijkheidsgrafiek geen cycli bevat.
  • Als Puppet de status van een bron verandert, stuurt de bron meldingen naar alle bronnen waarop hij is geabonneerd.
  • Als een bron wordt bijgewerkt, worden er meldingen verzonden naar alle bronnen waarop deze is geabonneerd.

Omgaan met niet-gespecificeerde parameters

Als een bronparameter geen standaardwaarde heeft en deze parameter niet is opgegeven in het manifest, zal Puppet deze eigenschap in de regel niet wijzigen voor de overeenkomstige bron op het knooppunt. Als bijvoorbeeld een resource van het type filet parameter niet gespecificeerd owner, dan zal Puppet de eigenaar van het overeenkomstige bestand niet wijzigen.

Inleiding tot klassen, variabelen en definities

Stel dat we meerdere knooppunten hebben die hetzelfde deel van de configuratie hebben, maar er zijn ook verschillen - anders zouden we het allemaal in één blok kunnen beschrijven node {}. Natuurlijk kunt u eenvoudig identieke delen van de configuratie kopiëren, maar over het algemeen is dit een slechte oplossing: de configuratie groeit en als u het algemene deel van de configuratie wijzigt, zult u op veel plaatsen hetzelfde moeten bewerken. Tegelijkertijd is het gemakkelijk om een ​​fout te maken, en over het algemeen is het DRY-principe (don't repeat own jezelf) met een reden uitgevonden.

Om dit probleem op te lossen is er een ontwerp als klasse.

classes

Klasse is een benoemd blok met poppetcode. Klassen zijn nodig om code te hergebruiken.

Eerst moet de klasse worden beschreven. De beschrijving zelf voegt nergens bronnen toe. De klasse wordt beschreven in manifesten:

# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
    ...
}

Hierna kan de klasse gebruikt worden:

# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше

Een voorbeeld uit de vorige taak: laten we de installatie en configuratie van nginx naar een klasse verplaatsen:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => 'puppet:///modules/example/nginx-conf',
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    include nginx_example
}

variabelen

De klasse uit het vorige voorbeeld is helemaal niet flexibel omdat deze altijd dezelfde nginx-configuratie met zich meebrengt. Laten we het pad naar de configuratievariabele maken, waarna deze klasse kan worden gebruikt om nginx met elke configuratie te installeren.

Het kan gedaan worden variabelen gebruiken.

Let op: variabelen in Puppet zijn onveranderlijk!

Bovendien is een variabele alleen toegankelijk nadat deze is gedeclareerd, anders blijft de waarde van de variabele hetzelfde undef.

Voorbeeld van het werken met variabelen:

# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"

Marionet heeft naamruimten, en de variabelen hebben dat ook gebied van zichtbaarheid: Een variabele met dezelfde naam kan in verschillende naamruimten worden gedefinieerd. Wanneer de waarde van een variabele wordt bepaald, wordt de variabele doorzocht in de huidige naamruimte, vervolgens in de omsluitende naamruimte, enzovoort.

Voorbeelden van naamruimten:

  • globaal - variabelen buiten de klasse- of knooppuntbeschrijving gaan daarheen;
  • knooppuntnaamruimte in de knooppuntbeschrijving;
  • klassenaamruimte in de klassebeschrijving.

Om dubbelzinnigheid te voorkomen bij het benaderen van een variabele, kunt u de naamruimte opgeven in de naam van de variabele:

# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var

Laten we het erover eens zijn dat het pad naar de nginx-configuratie in de variabele ligt $nginx_conf_source. Dan ziet de klas er als volgt uit:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # здесь используем переменную вместо фиксированной строки
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    $nginx_conf_source = 'puppet:///modules/example/nginx-conf'
    include nginx_example
}

Het gegeven voorbeeld is echter slecht omdat er enige “geheime kennis” bestaat dat ergens binnen de klasse een variabele met die en die naam wordt gebruikt. Het is veel juister om deze kennis algemeen te maken: klassen kunnen parameters hebben.

Klasseparameters zijn variabelen in de klassenaamruimte. Ze worden gespecificeerd in de klasseheader en kunnen net als gewone variabelen in de klassebody worden gebruikt. Parameterwaarden worden opgegeven bij gebruik van de klasse in het manifest.

De parameter kan op een standaardwaarde worden ingesteld. Als een parameter geen standaardwaarde heeft en de waarde niet is ingesteld wanneer deze wordt gebruikt, zal dit een compilatiefout veroorzaken.

Laten we de klasse uit het bovenstaande voorbeeld parametriseren en twee parameters toevoegen: de eerste, vereist, is het pad naar de configuratie, en de tweede, optioneel, is de naam van het pakket met nginx (in Debian zijn er bijvoorbeeld pakketten nginx, nginx-light, nginx-full).

# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
  $conf_source,
  $package_name = 'nginx-light', # параметр со значением по умолчанию
) {
  package { $package_name:
    ensure => installed,
  }
  -> file { '/etc/nginx':
    ensure  => directory,
    source  => $conf_source,
    recurse => true,
    purge   => true,
    force   => true,
  }
  ~> service { 'nginx':
    ensure => running,
    enable => true,
  }
}

node 'server2.testdomain' {
  # если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
  # *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
  class { 'nginx_example':
    conf_source => 'puppet:///modules/example/nginx-conf',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

In Puppet worden variabelen getypt. Eten veel gegevenstypen. Gegevenstypen worden doorgaans gebruikt om parameterwaarden te valideren die aan klassen en definities worden doorgegeven. Als de doorgegeven parameter niet overeenkomt met het opgegeven type, treedt er een compilatiefout op.

Het type wordt onmiddellijk vóór de parameternaam geschreven:

class example (
  String $param1,
  Integer $param2,
  Array $param3,
  Hash $param4,
  Hash[String, String] $param5,
) {
  ...
}

Klassen: include classname vs class{'classname':}

Elke klasse is een soort bron klasse. Zoals bij elk ander type bron kunnen er niet twee exemplaren van dezelfde klasse op hetzelfde knooppunt voorkomen.

Als u tweemaal probeert een klasse aan hetzelfde knooppunt toe te voegen met behulp van class { 'classname':} (geen verschil, met verschillende of identieke parameters), zal er een compilatiefout optreden. Maar als u een klasse in de resourcestijl gebruikt, kunt u onmiddellijk alle parameters ervan expliciet in het manifest instellen.

Als je echter gebruikt include, dan kan de klasse zo vaak als gewenst worden toegevoegd. Het feit is dat include is een idempotente functie die controleert of een klasse aan de map is toegevoegd. Als de klasse niet in de map staat, wordt deze toegevoegd, en als deze al bestaat, doet deze niets. Maar in het geval van gebruik include U kunt tijdens de klassedeclaratie geen klasseparameters instellen. Alle vereiste parameters moeten worden ingesteld in een externe gegevensbron: Hiera of ENC. We zullen erover praten in het volgende artikel.

Definieert

Zoals in het vorige blok werd gezegd, kan dezelfde klasse niet meer dan één keer op een knooppunt aanwezig zijn. In sommige gevallen moet u echter hetzelfde codeblok met verschillende parameters op hetzelfde knooppunt kunnen gebruiken. Met andere woorden: er is behoefte aan een eigen hulpbronnentype.

Om bijvoorbeeld de PHP-module te installeren, doen we in Avito het volgende:

  1. Installeer het pakket met deze module.
  2. Laten we een configuratiebestand voor deze module maken.
  3. We maken een symbolische link naar de configuratie voor php-fpm.
  4. We maken een symbolische link naar de configuratie voor php cli.

In dergelijke gevallen is een ontwerp zoals definiëren (definiëren, gedefinieerd type, gedefinieerd resourcetype). Een Define lijkt op een klasse, maar er zijn verschillen: ten eerste is elke Define een resourcetype, geen resource; ten tweede heeft elke definitie een impliciete parameter $title, waar de resourcenaam naartoe gaat wanneer deze wordt gedeclareerd. Net als bij klassen moet eerst een definitie worden beschreven, waarna deze kan worden gebruikt.

Een vereenvoudigd voorbeeld met een module voor PHP:

define php74::module (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'installed',
  $priority = '20',
  $data = "extension=${title}.son",
  $php_module_path = '/etc/php/7.4/mods-available',
) {
  package { $php_package_name:
    ensure          => $version,
    install_options => ['-o', 'DPkg::NoTriggers=true'],  # триггеры дебиановских php-пакетов сами создают симлинки и перезапускают сервис php-fpm - нам это не нужно, так как и симлинками, и сервисом мы управляем с помощью Puppet
  }
  -> file { "${php_module_path}/${php_module_name}.ini":
    ensure  => $ensure,
    content => $data,
  }
  file { "/etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
  file { "/etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
}

node server3.testdomain {
  php74::module { 'sqlite3': }
  php74::module { 'amqp': php_package_name => 'php-amqp' }
  php74::module { 'msgpack': priority => '10' }
}

De eenvoudigste manier om de dubbele declaratiefout op te vangen is in Define. Dit gebeurt als een definitie een bron heeft met een constante naam, en er twee of meer exemplaren van deze definitie op een bepaald knooppunt voorkomen.

Het is gemakkelijk om jezelf hiertegen te beschermen: alle bronnen binnen de definitie moeten een naam hebben, afhankelijk van $title. Een alternatief is de idempotente toevoeging van bronnen; in het eenvoudigste geval is het voldoende om de bronnen die gemeenschappelijk zijn voor alle instanties van de definitie naar een aparte klasse te verplaatsen en deze klasse op te nemen in de definitiefunctie include idempotent.

Er zijn andere manieren om idempotentie te bereiken bij het toevoegen van bronnen, namelijk door functies te gebruiken defined и ensure_resources, maar ik vertel je er in de volgende aflevering over.

Afhankelijkheden en meldingen voor klassen en definities

Klassen en definities voegen de volgende regels toe aan het omgaan met afhankelijkheden en meldingen:

  • afhankelijkheid van een klasse/define voegt afhankelijkheden toe van alle bronnen van de klasse/define;
  • een klasse/define-afhankelijkheid voegt afhankelijkheden toe aan alle klasse/define-bronnen;
  • class/define notificatie waarschuwt alle bronnen van de class/define;
  • class/define-abonnement abonneert zich op alle bronnen van de class/define.

Voorwaardelijke uitspraken en selectors

Documentatie hier.

if

Alles is hier eenvoudig:

if ВЫРАЖЕНИЕ1 {
  ...
} elsif ВЫРАЖЕНИЕ2 {
  ...
} else {
  ...
}

tenzij

tenzij is een if in omgekeerde zin: het codeblok wordt uitgevoerd als de expressie onwaar is.

unless ВЫРАЖЕНИЕ {
  ...
}

geval

Ook hier is niets ingewikkelds. U kunt reguliere waarden (tekenreeksen, getallen, etc.), reguliere expressies en gegevenstypen als waarden gebruiken.

case ВЫРАЖЕНИЕ {
  ЗНАЧЕНИЕ1: { ... }
  ЗНАЧЕНИЕ2, ЗНАЧЕНИЕ3: { ... }
  default: { ... }
}

Selectoren

Een selector is een taalconstructie die lijkt op case, maar in plaats van een codeblok uit te voeren, retourneert het een waarde.

$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }

modules

Wanneer de configuratie klein is, kan deze eenvoudig in één manifest worden bewaard. Maar hoe meer configuraties we beschrijven, hoe meer klassen en knooppunten er in het manifest zijn, het groeit en het wordt lastig om mee te werken.

Daarnaast is er het probleem van hergebruik van code: als alle code in één manifest staat, is het moeilijk om deze code met anderen te delen. Om deze twee problemen op te lossen, heeft Puppet een entiteit genaamd modules.

modules - dit zijn sets klassen, definities en andere Puppet-entiteiten die in een aparte map zijn geplaatst. Met andere woorden: een module is een onafhankelijk stukje Puppet-logica. Er kan bijvoorbeeld een module zijn om met nginx te werken, en deze zal bevatten wat en alleen wat nodig is om met nginx te werken, of er kan een module zijn om met PHP te werken, enzovoort.

Modules hebben een versienummer en de afhankelijkheden van modules van elkaar worden ook ondersteund. Er is een open opslagplaats met modules - Poppen smederij.

Op de poppenserver bevinden modules zich in de modules-submap van de hoofdmap. Binnen elke module bevindt zich een standaard directoryschema: manifesten, bestanden, sjablonen, lib, enzovoort.

Bestandsstructuur in een module

De root van de module kan de volgende mappen met beschrijvende namen bevatten:

  • manifests - het bevat manifesten
  • files - het bevat bestanden
  • templates - het bevat sjablonen
  • lib — het bevat Ruby-code

Dit is geen volledige lijst met mappen en bestanden, maar voorlopig is het voldoende voor dit artikel.

Namen van bronnen en namen van bestanden in de module

Documentatie hier.

Hulpbronnen (klassen, definities) in een module kunnen niet hoe u maar wilt noemen. Bovendien is er een directe overeenkomst tussen de naam van een bron en de naam van het bestand waarin Puppet naar een beschrijving van die bron zal zoeken. Als u de naamgevingsregels overtreedt, zal Puppet de bronbeschrijving eenvoudigweg niet vinden en krijgt u een compilatiefout.

De regels zijn eenvoudig:

  • Alle bronnen in een module moeten zich in de modulenaamruimte bevinden. Als de module wordt aangeroepen foo, dan moeten alle bronnen daarin een naam krijgen foo::<anything>, of gewoon foo.
  • De bron met de naam van de module moet in het bestand staan init.pp.
  • Voor andere bronnen is het bestandsnaamschema als volgt:
    • het voorvoegsel met de modulenaam wordt verwijderd
    • alle dubbele dubbele punten, indien aanwezig, worden vervangen door schuine strepen
    • uitbreiding wordt toegevoegd .pp

Ik zal het aantonen met een voorbeeld. Laten we zeggen dat ik een module aan het schrijven ben nginx. Het bevat de volgende bronnen:

  • klasse nginx beschreven in het manifest init.pp;
  • klasse nginx::service beschreven in het manifest service.pp;
  • definiëren nginx::server beschreven in het manifest server.pp;
  • definiëren nginx::server::location beschreven in het manifest server/location.pp.

templates

U weet vast wel wat sjablonen zijn; ik zal ze hier niet in detail beschrijven. Maar ik laat het voor het geval dat link naar Wikipedia.

Hoe sjablonen te gebruiken: De betekenis van een sjabloon kan worden uitgebreid met behulp van een functie template, waaraan het pad naar de sjabloon wordt doorgegeven. Voor bronnen van het type filet gebruikt in combinatie met de parameter content. Bijvoorbeeld zoals dit:

file { '/tmp/example': content => template('modulename/templatename.erb')

Bekijk pad <modulename>/<filename> impliceert bestand <rootdir>/modules/<modulename>/templates/<filename>.

Daarnaast is er een functie inline_template — het ontvangt de sjabloontekst als invoer, niet de bestandsnaam.

Binnen sjablonen kunt u alle Puppet-variabelen in het huidige bereik gebruiken.

Puppet ondersteunt sjablonen in ERB- en EPP-formaat:

Kort over ERB

Controlestructuren:

  • <%= ВЫРАЖЕНИЕ %> — voer de waarde van de uitdrukking in
  • <% ВЫРАЖЕНИЕ %> — bereken de waarde van een uitdrukking (zonder deze in te voegen). Voorwaardelijke uitspraken (if) en lussen (each) komen hier meestal terecht.
  • <%# КОММЕНТАРИЙ %>

Expressies in ERB zijn geschreven in Ruby (ERB is eigenlijk Embedded Ruby).

Om toegang te krijgen tot variabelen uit het manifest, moet u toevoegen @ naar de variabelenaam. Om een ​​regeleinde te verwijderen dat na een controleconstructie verschijnt, moet u een afsluitende tag gebruiken -%>.

Voorbeeld van het gebruik van de sjabloon

Laten we zeggen dat ik een module schrijf om ZooKeeper te besturen. De klasse die verantwoordelijk is voor het maken van de configuratie ziet er ongeveer zo uit:

class zookeeper::configure (
  Array[String] $nodes,
  Integer $port_client,
  Integer $port_quorum,
  Integer $port_leader,
  Hash[String, Any] $properties,
  String $datadir,
) {
  file { '/etc/zookeeper/conf/zoo.cfg':
    ensure  => present,
    content => template('zookeeper/zoo.cfg.erb'),
  }
}

En het bijbehorende sjabloon zoo.cfg.erb - Dus:

<% if @nodes.length > 0 -%>
<% @nodes.each do |node, id| -%>
server.<%= id %>=<%= node %>:<%= @port_leader %>:<%= @port_quorum %>;<%= @port_client %>
<% end -%>
<% end -%>

dataDir=<%= @datadir %>

<% @properties.each do |k, v| -%>
<%= k %>=<%= v %>
<% end -%>

Feiten en ingebouwde variabelen

Vaak hangt het specifieke deel van de configuratie af van wat er momenteel op het knooppunt gebeurt. Afhankelijk van welke Debian-release is, moet u bijvoorbeeld een of andere versie van het pakket installeren. U kunt dit allemaal handmatig monitoren en manifesten herschrijven als knooppunten veranderen. Maar dit is geen serieuze aanpak; automatisering is veel beter.

Om informatie over knooppunten te verkrijgen, heeft Puppet een mechanisme dat feiten wordt genoemd. Feiten - dit is informatie over het knooppunt, beschikbaar in manifesten in de vorm van gewone variabelen in de globale naamruimte. Bijvoorbeeld hostnaam, versie van het besturingssysteem, processorarchitectuur, lijst met gebruikers, lijst met netwerkinterfaces en hun adressen, en nog veel, veel meer. Feiten zijn beschikbaar in manifesten en sjablonen als reguliere variabelen.

Een voorbeeld van werken met feiten:

notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# ресурс типа notify просто выводит сообщение в лог

Formeel heeft een feit een naam (string) en een waarde (er zijn verschillende typen beschikbaar: strings, arrays, woordenboeken). Eten reeks ingebouwde feiten. Je kunt ook je eigen schrijven. Feitenverzamelaars worden beschreven zoals functies in Ruby, of zoals uitvoerbare bestanden. Feiten kunnen ook in het formulier worden gepresenteerd tekstbestanden met gegevens op de knooppunten.

Tijdens de werking kopieert de poppenagent eerst alle beschikbare feitenverzamelaars van de pappetserver naar het knooppunt, waarna hij ze lanceert en de verzamelde feiten naar de server stuurt; Hierna begint de server met het samenstellen van de catalogus.

Feiten in de vorm van uitvoerbare bestanden

Dergelijke feiten worden in modules in de directory geplaatst facts.d. Uiteraard moeten de bestanden uitvoerbaar zijn. Wanneer ze worden uitgevoerd, moeten ze informatie uitvoeren naar standaarduitvoer in YAML- of sleutel=waarde-indeling.

Vergeet niet dat de feiten van toepassing zijn op alle knooppunten die worden beheerd door de poppet-server waarop uw module is geïmplementeerd. Zorg er daarom voor dat u in het script controleert of het systeem over alle programma's en bestanden beschikt die nodig zijn om uw feit te laten werken.

#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'

Ruby feiten

Dergelijke feiten worden in modules in de directory geplaatst lib/facter.

# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
  confine do
    Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
  end
  confine do
    File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
  end
# в блоке setcode происходит собственно вычисление значения факта
  setcode do
    hash = {}
    if (out = Facter::Core::Execution.execute('ladvdc -b'))
      out.split.each do |l|
        line = l.split('=')
        next if line.length != 2
        name, value = line
        hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
      end
    end
    hash  # значение последнего выражения в блоке setcode является значением факта
  end
end

Tekst feiten

Dergelijke feiten worden op knooppunten in de directory geplaatst /etc/facter/facts.d in oude Puppet of /etc/puppetlabs/facts.d in de nieuwe pop.

examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue

Tot de feiten komen

Er zijn twee manieren om de feiten te benaderen:

  • via het woordenboek $facts: $facts['fqdn'];
  • gebruik de feitnaam als de variabelenaam: $fqdn.

Het is het beste om een ​​woordenboek te gebruiken $facts, of nog beter, geef de globale naamruimte aan ($::facts).

Hier vindt u het relevante gedeelte van de documentatie.

Ingebouwde variabelen

Naast de feiten zijn er ook enkele variabelen, beschikbaar in de globale naamruimte.

  • vertrouwde feiten — variabelen die afkomstig zijn van het certificaat van de cliënt (aangezien het certificaat meestal op een poppet-server wordt uitgegeven, kan de agent niet zomaar zijn certificaat overnemen en wijzigen, zodat de variabelen “vertrouwd” zijn): de naam van het certificaat, de naam van de host en domein, extensies van het certificaat.
  • server-feiten —variabelen die verband houden met informatie over de server: versie, naam, IP-adres van de server, omgeving.
  • feiten van agenten — variabelen rechtstreeks toegevoegd door marionet-agent, en niet door feit — certificaatnaam, agentversie, marionetversie.
  • hoofdvariabelen - Pappetmaster-variabelen (sic!). Het is ongeveer hetzelfde als in server-feiten, plus configuratieparameterwaarden zijn beschikbaar.
  • compilervariabelen — compilervariabelen die per scope verschillen: de naam van de huidige module en de naam van de module waarin toegang werd verkregen tot het huidige object. Ze kunnen bijvoorbeeld worden gebruikt om te controleren of uw privélessen niet rechtstreeks vanuit andere modules worden gebruikt.

Toevoeging 1: hoe moet ik dit allemaal uitvoeren en debuggen?

Het artikel bevatte veel voorbeelden van poppencode, maar vertelde ons helemaal niet hoe we deze code moesten uitvoeren. Nou, ik corrigeer mezelf.

Een agent is voldoende om Puppet te laten draaien, maar in de meeste gevallen heb je ook een server nodig.

Агент

In ieder geval sinds versie XNUMX zijn puppet-agent-pakketten beschikbaar officiële Puppetlabs-repository bevatten alle afhankelijkheden (ruby en de bijbehorende edelstenen), dus er zijn geen installatieproblemen (ik heb het over op Debian gebaseerde distributies - we gebruiken geen op RPM gebaseerde distributies).

In het eenvoudigste geval is het, om de poppenconfiguratie te gebruiken, voldoende om de agent in serverloze modus te starten: op voorwaarde dat de poppencode naar het knooppunt wordt gekopieerd, start puppet apply <путь к манифесту>:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Notice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds
Notice: Hello world!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defined 'message' as 'Hello world!'
Notice: Applied catalog in 0.01 seconds

Het is natuurlijk beter om de server in te stellen en agenten op de knooppunten in daemon-modus uit te voeren - dan zullen ze eens per half uur de configuratie toepassen die van de server is gedownload.

U kunt het push-werkmodel imiteren - ga naar het knooppunt waarin u geïnteresseerd bent en begin sudo puppet agent -t. Sleutel -t (--test) bevat eigenlijk verschillende opties die afzonderlijk kunnen worden ingeschakeld. Deze opties omvatten het volgende:

  • niet in daemon-modus draaien (standaard start de agent in daemon-modus);
  • afsluiten na het toepassen van de catalogus (standaard blijft de agent werken en past hij de configuratie eens per half uur toe);
  • schrijf een gedetailleerd werklogboek;
  • wijzigingen in bestanden weergeven.

De agent heeft een werkingsmodus zonder wijzigingen - u kunt deze gebruiken als u niet zeker weet of u de juiste configuratie heeft geschreven en wilt controleren wat de agent precies zal veranderen tijdens de werking. Deze modus wordt ingeschakeld door de parameter --noop op de opdrachtregel: sudo puppet agent -t --noop.

Bovendien kunt u het foutopsporingslogboek van het werk inschakelen - daarin schrijft de pop over alle acties die hij uitvoert: over de bron die hij momenteel verwerkt, over de parameters van deze bron, over welke programma's hij start. Natuurlijk is dit een parameter --debug.

Server

Ik zal in dit artikel niet ingaan op de volledige installatie van de pappetserver en het implementeren van code; ik wil alleen zeggen dat er kant-en-klaar een volledig functionele versie van de server is die geen aanvullende configuratie vereist om met een klein aantal te kunnen werken. knooppunten (bijvoorbeeld maximaal honderd). Een groter aantal knooppunten vereist afstemming - standaard lanceert de poppenserver niet meer dan vier werkers, voor betere prestaties moet je hun aantal verhogen en vergeet niet de geheugenlimieten te verhogen, anders zal de server het grootste deel van de tijd afval verzamelen.

Code-implementatie - als je het snel en gemakkelijk nodig hebt, kijk dan (op r10k)[https://github.com/puppetlabs/r10k], voor kleine installaties zou dit voldoende moeten zijn.

Addendum 2: Coderingsrichtlijnen

  1. Plaats alle logica in klassen en definities.
  2. Bewaar klassen en definities in modules, niet in manifesten die knooppunten beschrijven.
  3. Gebruik de feiten.
  4. Maak geen ifs op basis van hostnamen.
  5. Voel je vrij om parameters voor klassen en definities toe te voegen - dit is beter dan impliciete logica verborgen in de hoofdtekst van de klasse/define.

Waarom ik dit aanbeveel, zal ik in het volgende artikel uitleggen.

Conclusie

Laten we eindigen met de introductie. In het volgende artikel vertel ik je over Hiera, ENC en PuppetDB.

Alleen geregistreerde gebruikers kunnen deelnemen aan het onderzoek. Inloggen, Alsjeblieft.

Er is zelfs veel meer materiaal - ik kan artikelen schrijven over de volgende onderwerpen, stemmen over waar u graag over zou willen lezen:

  • 59,1%Geavanceerde poppenconstructies - wat dingen van het volgende niveau: loops, mapping en andere lambda-expressies, bronnenverzamelaars, geëxporteerde bronnen en communicatie tussen hosts via Puppet, tags, providers, abstracte gegevenstypen.13
  • 31,8%“Ik ben de beheerder van mijn moeder” of hoe we bij Avito vrienden hebben gemaakt met verschillende poppet-servers van verschillende versies, en, in principe, het gedeelte over het beheren van de poppet-server.7
  • 81,8%Hoe we poppencode schrijven: instrumentatie, documentatie, testen, CI/CD.18

22 gebruikers hebben gestemd. 9 gebruikers onthielden zich van stemming.

Bron: www.habr.com