Introduzione à Puppet

Puppet hè un sistema di gestione di cunfigurazione. Hè adupratu per purtà l'ospiti à u statu desideratu è mantene stu statu.

Aghju travagliatu cù Puppet da più di cinque anni. Stu testu hè essenzialmente una compilazione tradutta è riordinata di punti chjave da a documentazione ufficiale, chì permetterà à i principianti di capisce rapidamente l'essenza di Puppet.

Introduzione à Puppet

Infurmazione basica

U sistema operatore di Puppet hè cliente-servitore, ancu s'ellu sustene ancu l'operazione senza servitore cù funziunalità limitata.

Un mudellu di operazione di pull hè utilizatu: per automaticamente, una volta ogni meza ora, i clienti cuntattate u servitore per una cunfigurazione è l'applicà. Se avete travagliatu cù Ansible, allora usanu un mudellu push differente: l'amministratore principia u prucessu di applicà a cunfigurazione, i clienti stessi ùn anu micca applicà nunda.

Durante a cumunicazione di a rete, a criptografia TLS bidirezionale hè utilizata: u servitore è u cliente anu e so chjavi privati ​​​​è i certificati currispondenti. Di genere, u servitore emette certificati per i clienti, ma in principiu hè pussibule aduprà una CA esterna.

Introduzione à i manifesti

In a terminologia Puppet à u servitore di pupi cunnette nodi (nodi). A cunfigurazione per i nodi hè scritta in i manifesti in una lingua di prugrammazione speciale - Puppet DSL.

Puppet DSL hè una lingua dichjarazione. Descrive u statu desideratu di u node in forma di dichjarazioni di risorse individuali, per esempiu:

  • U schedariu esiste è hà un cuntenutu specificu.
  • U pacchettu hè stallatu.
  • U serviziu hà cuminciatu.

E risorse ponu esse interconnesse:

  • Ci sò dipendenze, affettanu l'ordine in quale i risorse sò utilizati.
    Per esempiu, "prima installate u pacchettu, dopu edità u schedariu di cunfigurazione, dopu inizià u serviziu".
  • Ci sò notifiche - se una risorsa hà cambiatu, manda notificazioni à e risorse abbonate.
    Per esempiu, se u schedariu di cunfigurazione cambia, pudete riavvia automaticamente u serviziu.

Inoltre, u Puppet DSL hà funzioni è variàbili, è ancu dichjarazioni cundiziunali è selettori. Diversi meccanismi di mudelli sò ancu supportati - EPP è ERB.

Puppet hè scrittu in Ruby, cusì parechji di e custruzzioni è i termini sò pigliati da quì. Ruby permette di espansione Puppet - aghjunghje una logica cumplessa, novi tipi di risorse, funzioni.

Mentre Puppet hè in esecuzione, i manifesti per ogni nodu specificu nantu à u servitore sò compilati in un repertoriu. catalogo hè una lista di risorse è e so rilazioni dopu avè calculatu u valore di funzioni, variàbili è espansione di e dichjarazioni cundiziunali.

Sintassi è codestyle

Eccu rùbbriche di a documentazione ufficiale chì vi aiuterà à capisce a sintassi se l'esempi furniti ùn sò micca abbastanza:

Eccu un esempiu di ciò chì pare u manifestu:

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

L'indentazione è i rotture di linea ùn sò micca una parte necessaria di u manifestu, ma ci hè un cunsigliu guida di stile. Riassuntu:

  • Indentazioni di dui spazii, tabulazioni ùn sò micca aduprate.
  • I ricci ricci sò separati da un spaziu; i dui punti ùn sò micca siparati da un spaziu.
  • Commas dopu ogni paràmetru, cumpresu l'ultimu. Ogni paràmetru hè nantu à una linea separata. Una eccezzioni hè fatta per u casu senza paràmetri è un paràmetru: pudete scrive nantu à una linea è senza una virgola (ie. resource { 'title': } и resource { 'title': param => value }).
  • E frecce nantu à i paràmetri deve esse à u listessu livellu.
  • Frecce di relazione di risorsa sò scritte davanti à elli.

Locu di i schedari nantu à pappetserver

Per più spiegazione, aghju introduttu u cuncettu di "directory root". U repertoriu radicale hè u cartulare chì cuntene a cunfigurazione Puppet per un node specificu.

U cartulare radicali varieghja secondu a versione di Puppet è l'ambienti utilizati. L'ambienti sò gruppi indipendenti di cunfigurazione chì sò almacenati in cartulari separati. Di solitu usatu in cumbinazioni cù git, in quale casu l'ambienti sò creati da rami git. Per quessa, ogni node hè situatu in un ambiente o un altru. Questu pò esse cunfiguratu nantu à u node stessu, o in ENC, chì parleraghju in u prossimu articulu.

  • In a terza versione ("vecchio Puppet") u repertoriu di basa era /etc/puppet. L'usu di l'ambienti hè facultativu - per esempiu, ùn avemu micca usatu cù u vechju Puppet. Se l'ambienti sò usati, sò generalmente almacenati in /etc/puppet/environments, u repertoriu radicali serà u cartulare di l'ambiente. Se l'ambienti ùn sò micca usati, u repertoriu radicale serà u cartulare di basa.
  • Partendu da a quarta versione ("New Puppet"), l'usu di l'ambienti hè diventatu ubligatoriu, è u cartulare di basa hè statu spustatu /etc/puppetlabs/code. Per quessa, l'ambienti sò almacenati /etc/puppetlabs/code/environments, u repertoriu root hè u cartulare di l'ambiente.

Ci deve esse un subdirectory in u cartulare radicali manifests, chì cuntene unu o più manifesti chì descrizanu i nodi. Inoltre, deve esse un subdirectory modules, chì cuntene i moduli. Vi dicu ciò chì i moduli sò un pocu dopu. Inoltre, u vechju Puppet pò ancu avè un subdirectory files, chì cuntene diversi schedari chì copiemu à i nodi. In u novu Puppet, tutti i schedari sò posti in moduli.

I schedarii Manifest anu l'estensione .pp.

Un paru di esempi di cummattimentu

Descrizzione di u node è risorsa nantu à questu

Nantu à u node server1.testdomain un schedariu deve esse creatu /etc/issue cun cuntenutu Debian GNU/Linux n l. U schedariu deve esse pussede da un utilizatore è gruppu root, i diritti d'accessu deve esse 644.

Scrivemu un manifestu:

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 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
    }
}

Relazioni trà e risorse nantu à un node

Nantu à u node server2.testdomain nginx deve esse in esecuzione, travagliendu cù una cunfigurazione preparata prima.

Decomponemu u prublema:

  • U pacchettu deve esse stallatu nginx.
  • Hè necessariu chì i schedarii di cunfigurazione esse copiati da u servitore.
  • U serviziu deve esse in esecuzione nginx.
  • Se a cunfigurazione hè aghjurnata, u serviziu deve esse riavviatu.

Scrivemu un manifestu:

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 получает уведомление,
  # соответствующий сервис перезапускается.
}

Per fà questu, avete bisognu di circa a seguente locu di u schedariu nantu à u servitore puppet:

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

Tipi di risorse

Una lista cumpleta di i tipi di risorse supportati pò esse truvata quì in a documentazione, quì descriveraghju cinque tippi basi, chì in a mo pratica sò abbastanza per risolve a maiò parte di i prublemi.

schedariu

Gestisce i schedari, i cartulari, i ligami simbolichi, u so cuntenutu è i diritti d'accessu.

Parametri:

  • nome di risorsa - percorso à u schedariu (opcional)
  • strada - percorso à u schedariu (se ùn hè micca specificatu in u nome)
  • guarantiscenu - tipu di schedariu:
    • absent - sguassà un schedariu
    • present - ci deve esse un schedariu di ogni tipu (se ùn ci hè micca un schedariu, un schedariu regulare serà creatu)
    • file - schedariu regulare
    • directory - annuariu
    • link - ligame simbolicu
  • cuntenutu - cuntenutu di u schedariu (adatta solu per i schedarii regulari, ùn pò micca esse usatu inseme cù surghjenti o mira)
  • surghjenti - un ligame à u percorsu da quale vulete copià u cuntenutu di u schedariu (ùn pò esse usatu inseme cù cuntenutu o mira). Pò esse specificatu cum'è un URI cù un schema puppet: (poi i schedari da u servore pupi sarà usatu), è cù u schema http: (Spergu chì hè chjaru ciò chì succede in questu casu), è ancu cù u diagramma file: o cum'è una strada assoluta senza schema (poi u schedariu da u FS lucale nantu à u node serà utilizatu)
  • mira - induve u ligame simbolicu duverà puntà (ùn pò micca esse usatu inseme cù cuntenutu o surghjenti)
  • u patrone - l'utilizatore chì deve pussede u schedariu
  • gruppu - u gruppu à quale u schedariu deve appartene
  • modu - permessi di fugliale (cum'è una stringa)
  • recurse - permette a trasfurmazioni di repertorii recursivi
  • purge - permette di sguassà i fugliali chì ùn sò micca descritti in Puppet
  • forza - permette di sguassà cartulari chì ùn sò micca descritti in Puppet

U pacchettu

Installa è sguassate i pacchetti. Capace di trattà e notificazioni - reinstalla u pacchettu se u paràmetru hè specificatu reinstall_on_refresh.

Parametri:

  • nome di risorsa - nome di u pacchettu (opcional)
  • Cognome - nome di u pacchettu (se ùn hè micca specificatu in u nome)
  • Cumpagnu - gestore di pacchetti da aduprà
  • guarantiscenu - statu desideratu di u pacchettu:
    • present, installed - ogni versione installata
    • latest - l'ultima versione installata
    • absent - sguassatu (apt-get remove)
    • purged - sguassatu cù i schedarii di cunfigurazione (apt-get purge)
    • held - a versione di u pacchettu hè chjusa (apt-mark hold)
    • любая другая строка - a versione specifica hè stallata
  • reinstall_on_refresh - sì true, dopu avè ricevutu a notificazione, u pacchettu serà reinstallatu. Utile per distribuzioni basate in fonti, induve a ricustruzzione di pacchetti pò esse necessariu quandu cambia i paràmetri di creazione. Default false.

sirvizziu

Gestisce i servizii. Capace di processà e notificazioni - riavvia u serviziu.

Parametri:

  • nome di risorsa - serviziu da gestisce (opcional)
  • Cognome - u serviziu chì deve esse amministratu (se ùn hè micca specificatu in u nome)
  • guarantiscenu - u statu desideratu di u serviziu:
    • running - lanciata
    • stopped - firmò
  • attivati - cuntrolla a capacità di inizià u serviziu:
    • true - l'autorun hè attivatu (systemctl enable)
    • mask - dissimulatu (systemctl mask)
    • false - l'autorun hè disattivatu (systemctl disable)
  • restituisce - cumanda per riavvia u serviziu
  • statutu - cumanda per verificà u statu di serviziu
  • hà ripartitu - indica se u serviziu initscript supporta u riavviu. Se false è u paràmetru hè specificatu restituisce - u valore di stu paràmetru hè utilizatu. Se false è paràmetru restituisce micca specificatu - u serviziu hè firmatu è cuminciatu à riavvia (ma systemd usa u cumandamentu systemctl restart).
  • hasstatus - indicà se u serviziu initscript supporta u cumandamentu status... Sì false, allura u valore di u paràmetru hè utilizatu statutu. Default true.

exec

Esegue cumandamenti esterni. Sè vo ùn specificà paràmetri crea, solu si, salvu o rinfrescante, u cumandimu serà eseguitu ogni volta chì Puppet hè esercitu. Capace di processà e notificazioni - esegue un cumandamentu.

Parametri:

  • nome di risorsa - cumanda da esse eseguita (opcional)
  • cummandu - u cumandimu per esse eseguitu (se ùn hè micca specificatu in u nome)
  • strada - percorsi in quale circà u schedariu eseguibile
  • solu si - se u cumandamentu specificatu in stu paràmetru cumpletu cù un codice di ritornu zero, u cumandamentu principale serà eseguitu
  • salvu - se u cumandamentu specificatu in stu paràmetru cumpletu cù un codice di ritornu non-zero, u cumandamentu principale serà eseguitu
  • crea - se u schedariu specificatu in stu paràmetru ùn esiste micca, u cumandamentu principale serà eseguitu
  • rinfrescante - sì true, allura u cumandamentu serà eseguitu solu quandu questu exec riceve notificazione da altre risorse
  • cwd - repertoriu da quale eseguisce u cumandamentu
  • Fammi - l'utilizatore da quale eseguisce u cumandamentu
  • Cumpagnu - cumu fà u cumandamentu:
    • pusix - un prucessu di u zitellu hè simplicemente creatu, assicuratevi di specificà strada
    • conchiglia - u cumandamentu hè lanciatu in a cunchiglia /bin/sh, ùn pò micca esse specificatu strada, Pudete aduprà globbing, pipi è altre funziunalità shell. Di solitu rilevatu automaticamente s'ellu ci sò caratteri speciali (|, ;, &&, || eccetera).

CRON

Cuntrolla i cronjobs.

Parametri:

  • nome di risorsa - solu un tipu d'identificatore
  • guarantiscenu - statu di curona:
    • present - creà s'ellu ùn esiste micca
    • absent - sguassate s'ellu esiste
  • cummandu - chì cumanda per curriri
  • ambiente - in quale ambiente per eseguisce u cumandamentu (lista di variabili di l'ambiente è i so valori via =)
  • Fammi - da quale utilizatore per eseguisce u cumandamentu
  • minutu, ora, ghjornu di a settimana, mese, ghjornu di u mese - quandu curriri cron. Se qualchissia di sti attributi ùn hè micca specificatu, u so valore in u crontab serà *.

In Puppet 6.0 CRON comu si cacciatu da a scatula in puppetserver, cusì ùn ci hè micca documentazione nantu à u situ generale. Ma ellu hè in a scatula in puppet-agent, cusì ùn hè micca bisognu di stallà separatamente. Pudete vede a documentazione per questu in a documentazione per a quinta versione di Puppet, o nantu à GitHub.

Riguardu à e risorse in generale

Requisiti per l'unicità di e risorse

L'errore più cumuni chì scontru hè Dichjarazione duplicata. Stu errore si trova quandu dui o più risorse di u listessu tipu cù u listessu nome appariscenu in u cartulare.

Dunque, scriveraghju di novu: manifesti per u stessu node ùn deve micca cuntene risorse di u stessu tipu cù u stessu titulu!

Calchì volta ci hè bisognu di stallà pacchetti cù u listessu nome, ma cù diversi gestori di pacchetti. In questu casu, avete bisognu di utilizà u paràmetru nameper evità l'errore:

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

Altri tipi di risorse anu opzioni simili per aiutà à evità a duplicazione - name у sirvizziu, command у exec, eccetera.

Metaparametri

Ogni tipu di risorsa hà qualchi paràmetri spiciali, indipendentemente da a so natura.

Lista completa di meta-parametri in a documentazione di Puppet.

Lista corta:

  • esse dumandate - stu paràmetru indica da quali risorse dipende sta risorsa.
  • nanzu - Stu paràmetru specifica quale risorse dependenu di sta risorsa.
  • abbunà - stu paràmetru specifica da quali risorse sta risorsa riceve notificazioni.
  • avvisà - Stu paràmetru specifica quale risorse ricevenu notificazioni da questa risorsa.

Tutti i metaparametri listati accettanu o un ligame di risorsa unicu o un array di ligami in parentesi quadrate.

Ligami à e risorse

Un ligame di risorsa hè solu una menzione di a risorsa. Sò usati principalmente per indicà e dipendenze. Riferimentu à una risorsa inesistente pruvucarà un errore di compilazione.

A sintassi di u ligame hè a siguenti: tipu di risorsa cù una lettera maiuscola (se u nome di u tipu cuntene dui punti, allora ogni parte di u nome trà i dui punti hè capitalizata), allura u nome di risorsa in parentesi quadrate (u casu di u nome). ùn cambia micca!). Ùn deve esse micca spazii; i parentesi quadrate sò scritti subitu dopu à u nome di u tipu.

Esempiu:

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

Dipendenze è notificazioni

Documentazione quì.

Comu diciatu prima, e dependenzii simplici trà e risorse sò transitivi. A propositu, attentu à aghjunghje dependenzii - pudete creà dependenzii ciclichi, chì pruvucarà un errore di compilazione.

A cuntrariu di e dipendenze, e notificazioni ùn sò micca transitivi. E seguenti regule s'applicanu à e notificazioni:

  • Se a risorsa riceve una notificazione, hè aghjurnata. L'azzioni di l'aghjurnamentu dipendenu da u tipu di risorsa - exec esegue u cumandamentu, sirvizziu riavvia u serviziu, U pacchettu reinstalla u pacchettu. Se a risorsa ùn hà micca definitu una azzione di aghjurnamentu, allora nunda ùn succede.
  • Durante una corsa di Puppet, a risorsa hè aghjurnata micca più di una volta. Questu hè pussibule perchè e notificazioni includenu dipendenze è u graficu di dependenza ùn cuntene micca ciculi.
  • Se Puppet cambia u statu di una risorsa, a risorsa manda notificazioni à tutte e risorse abbonate.
  • Se una risorsa hè aghjurnata, manda notificazioni à tutte e risorse abbonate.

Manipulazione di parametri micca specificati

Comu regula, se qualchì paràmetru di risorsa ùn hà micca un valore predeterminatu è questu paràmetru ùn hè micca specificatu in u manifestu, Puppet ùn cambia micca sta pruprietà per a risorsa currispundente in u node. Per esempiu, se una risorsa di tipu schedariu paràmetru micca specificatu owner, allura Puppet ùn cambierà micca u pruprietariu di u schedariu currispundente.

Introduzione à e classi, variabili è definizione

Suppone chì avemu parechji nodi chì anu a listessa parte di a cunfigurazione, ma ci sò ancu differenze - altrimenti pudemu descriverà tuttu in un bloccu. node {}. Di sicuru, pudete simpricimenti cupià parti identiche di a cunfigurazione, ma in generale questa hè una mala suluzione - a cunfigurazione cresce, è se cambiate a parte generale di a cunfigurazione, avete da edità a stessa cosa in parechji posti. À u listessu tempu, hè faciule fà un sbagliu, è in generale, u principiu DRY (ùn ripetite micca) hè statu inventatu per una ragione.

Per risolve stu prublema ci hè un tali disignu cum'è класс.

Classes

Class hè un bloccu chjamatu di codice poppet. E classi sò necessarii per riutilizà u codice.

Prima, a classa deve esse descritta. A descrizzione stessu ùn aghjunghje micca risorse in ogni locu. A classa hè descritta in i manifesti:

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

Dopu questu, a classe pò esse usata:

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

Un esempiu di u compitu precedente - movemu a stallazione è a cunfigurazione di nginx in una classe:

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
}

Variabili

A classa da l'esempiu precedente ùn hè micca flexible in tuttu perchè sempre porta a stessa cunfigurazione nginx. Facemu u percorsu à a variabile di cunfigurazione, allora sta classa pò esse usata per installà nginx cù qualsiasi cunfigurazione.

Si pò fà usendu variabili.

Attenzione: e variabili in Puppet sò immutabili!

Inoltre, una variàbile pò esse accessu solu dopu chì hè stata dichjarata, altrimente u valore di a variàbile serà undef.

Esempiu di travaglià cù variàbili:

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

Puppet hà spazii di nomi, è e variàbili, per quessa, anu zona di visibilità: Una variàbile cù u stessu nome pò esse definitu in spazii di nomi diffirenti. Quandu si risolve u valore di una variàbile, a variàbile hè cercata in u spaziu di nome attuale, dopu in u spaziu di nomi chì cuntene, è cusì.

Esempi di namespace:

  • glubale - variabili fora di a classa o a descrizzione di u nodu vanu quì;
  • node namespace in a descrizzione di u node;
  • namespace class in a descrizzione di classa.

Per evità l'ambiguità quandu accede à una variabile, pudete specificà u spaziu di nome in u nome di variàbile:

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

Accettamu chì a strada per a cunfigurazione nginx si trova in a variàbile $nginx_conf_source. Allora a classe sarà cusì:

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
}

In ogni casu, l'esempiu datu hè male perchè ci hè qualchì "cunniscenza secreta" chì in qualchì locu in a classa una variabile cù un tali è un nome hè utilizatu. Hè assai più curretta per fà sta cunniscenza generale - e classi ponu avè parametri.

Paràmetri di classi sò variàbili in u spaziu di nomi di classi, sò specificati in l'intestazione di classi è ponu esse utilizati cum'è variabili regulari in u corpu di classi. I valori di i paràmetri sò specificati quandu si usa a classa in u manifestu.

U paràmetru pò esse stabilitu à un valore predeterminatu. Se un paràmetru ùn hà micca un valore predeterminatu è u valore ùn hè micca stabilitu quandu s'utilice, pruvucarà un errore di compilazione.

Parametremu a classa da l'esempiu di sopra è aghjunghje dui parametri: u primu, necessariu, hè u percorsu à a cunfigurazione, è u sicondu, facultativu, hè u nome di u pacchettu cù nginx (in Debian, per esempiu, ci sò pacchetti. 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, i variàbili sò tipati. Manghja parechji tippi di dati. Tipi di dati sò tipicamente usati per cunvalidà i valori di i paràmetri passati à e classi è definizione. Se u paràmetru passatu ùn currisponde à u tipu specificatu, un errore di compilazione accade.

U tipu hè scrittu immediatamente prima di u nome di u paràmetru:

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

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

Ogni classa hè una risorsa di tipu burghisìa. Cum'è cù qualsiasi altru tipu di risorsa, ùn pò micca esse dui casi di a listessa classa nantu à u stessu node.

Se pruvate d'aghjunghje una classa à u stessu node duie volte usendu class { 'classname':} (senza differenza, cù paràmetri diffirenti o idèntici), ci sarà un errore di compilazione. Ma s'è vo aduprate una classa in u stilu di risorsa, pudete subitu subitu esplicitu tutti i so paràmetri in u manifestu.

Tuttavia, si usa include, allura a classa pò esse aghjunta quante volte chì vulete. U fattu hè chì include hè una funzione idempotente chì verifica se una classa hè stata aghjunta à u cartulare. Se a classa ùn hè micca in u cartulare, l'aghjunghje, è s'ellu esiste digià, ùn face nunda. Ma in casu di usu include Ùn pudete micca stabilisce i paràmetri di classi durante a dichjarazione di classi - tutti i paràmetri richiesti devenu esse stabiliti in una fonte di dati esterna - Hiera o ENC. Avemu da parlà di elli in u prossimu articulu.

Definisce

Comu dissi in u bloccu precedente, a listessa classa ùn pò esse presente in un node più di una volta. In ogni casu, in certi casi, avete bisognu di pudè utilizà u listessu bloccu di codice cù diversi paràmetri nantu à u stessu node. In altri palori, ci hè bisognu di un tipu di risorsa propria.

Per esempiu, per installà u modulu PHP, facemu i seguenti in Avito:

  1. Installa u pacchettu cù stu modulu.
  2. Creemu un schedariu di cunfigurazione per stu modulu.
  3. Creemu un ligame simbolicu à a cunfigurazione per php-fpm.
  4. Creemu un ligame simbolicu à a cunfigurazione per php cli.

In tali casi, un disignu cum'è definisce (definitu, tipu definitu, tipu di risorsa definitu). A Definizione hè simile à una classa, ma ci sò differenzi: prima, ogni Definizione hè un tipu di risorsa, micca una risorsa; secondu, ogni definizione hà un paràmetru implicitu $title, induve u nome di risorsa va quandu hè dichjaratu. Cum'è in u casu di e classi, prima deve esse descrittu una definizione, dopu chì pò esse usata.

Un esempiu simplificatu cù un modulu per 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' }
}

U modu più faciule per catturà l'errore di dichjarazione Duplicate hè in Definite. Questu succede se una definizione hà una risorsa cù un nome constantu, è ci sò dui o più casi di sta definizione nantu à un node.

Hè faciule per pruteggiri da questu: tutte e risorse in a definizione deve avè un nome secondu $title. Una alternativa hè l'addizione idempotente di risorse; in u casu più simplice, hè abbastanza per spustà e risorse cumuni à tutti i casi di a definizione in una classa separata è include sta classa in a definizione - funzione. include idempotenti.

Ci hè altre manere di ottene l'idempotenza quandu aghjunghje risorse, vale à dì l'usu di funzioni defined и ensure_resources, ma vi dicu in u prossimu episodiu.

Dipendenze è notificazioni per classi è definizione

E classi è e definizioni aghjunghjenu e seguenti regule per a gestione di dipendenze è notificazioni:

  • a dependenza nantu à una classa / definisce aghjunghje dipendenze nantu à tutte e risorse di a classa / definisce;
  • una dependenza di classi / definisce aghjunghje dipendenze à tutte e risorse di classi / definisce;
  • class/define notificazione notifica tutte e risorse di a classa / definisce;
  • L'abbonamentu class/define abbona à tutte e risorse di a classa/definisce.

Dichjarazioni cundiziunali è selettori

Documentazione quì.

if

Hè simplice quì:

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

salvu

salvu chì hè un se in reverse: u bloccu di codice serà eseguitu se l'espressione hè falsa.

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

casu

Ùn ci hè nunda complicatu ancu quì. Pudete aduprà valori regulari (strings, numeri, etc.), espressioni regulare, è tipi di dati cum'è valori.

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

Selettori

Un selettore hè una custruzzione di lingua simile à case, ma invece di eseguisce un bloccu di codice, torna un valore.

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

Moduli

Quandu a cunfigurazione hè chjuca, pò esse facilmente guardatu in un manifestu. Ma i più cunfigurazioni chì descrivimu, più classi è nodi ci sò in u manifestu, cresce, è diventa inconveniente per travaglià.

Inoltre, ci hè u prublema di reutilizazione di codice - quandu tuttu u codice hè in un manifestu, hè difficiule di sparte stu codice cù l'altri. Per risolve questi dui prublemi, Puppet hà una entità chjamata moduli.

Moduli - Quessi sò insemi di classi, definizione è altre entità Puppet posti in un repertoriu separatu. In altri palori, un modulu hè un pezzu indipendente di a logica Puppet. Per esempiu, pò esse un modulu per travaglià cù nginx, è cuntene ciò chì è solu ciò chì hè necessariu per travaglià cù nginx, o pò esse un modulu per travaglià cù PHP, è cusì.

I moduli sò versionati, è e dipendenze di i moduli l'una di l'altru sò ancu supportati. Ci hè un repositoriu apertu di moduli - Puppet Forge.

Nant'à u servitore puppet, i moduli sò situati in u subdirectory moduli di u repertoriu radicali. Dentru ogni modulu ci hè un schema di cartulare standard - manifesti, schedarii, mudelli, lib, etc.

Struttura di u schedariu in un modulu

A radica di u modulu pò cuntene i seguenti cartulari cù nomi descrittivi:

  • manifests - cuntene manifesti
  • files - cuntene i schedari
  • templates - cuntene mudelli
  • lib - cuntene u codice Ruby

Questa ùn hè micca una lista cumpleta di cartulari è schedarii, ma hè abbastanza per questu articulu per avà.

Nomi di risorse è nomi di schedari in u modulu

Documentazione quì.

Risorse (classi, definizione) in un modulu ùn pò micca esse chjamatu cum'è vo vulete. Inoltre, ci hè una currispundenza diretta trà u nome di una risorsa è u nome di u schedariu in quale Puppet cercarà una descrizzione di quella risorsa. Se violate e regule di nomenclatura, Puppet simpricimenti ùn truverà micca a descrizzione di risorse, è uttene un errore di compilazione.

E regule sò semplici:

  • Tutte e risorse in un modulu devenu esse in u spaziu di nomi di u modulu. Se u modulu hè chjamatu foo, allura tutte e risorse in questu deve esse chjamatu foo::<anything>, o solu foo.
  • A risorsa cù u nome di u modulu deve esse in u schedariu init.pp.
  • Per altre risorse, u schema di nomenclatura di u schedariu hè a siguenti:
    • u prefissu cù u nome di u modulu hè scartatu
    • tutti i punti doppiu, s'ellu ci hè, sò rimpiazzati cù slashes
    • estensione hè aghjuntu .pp

Dimustraraghju cù un esempiu. Diciamu chì scrivu un modulu nginx. Contene e seguenti risorse:

  • класс nginx descrittu in u manifestu init.pp;
  • класс nginx::service descrittu in u manifestu service.pp;
  • definisce nginx::server descrittu in u manifestu server.pp;
  • definisce nginx::server::location descrittu in u manifestu server/location.pp.

Modelli

Di sicuru, tù stessu sapete ciò chì i mudelli sò; Ùn li descriveraghju micca in dettaglio quì. Ma l'aghju lasciatu in casu ligame à Wikipedia.

Cumu aduprà mudelli: U significatu di un mudellu pò esse allargatu cù una funzione template, chì hè passatu u percorsu à u mudellu. Per risorse di tipu schedariu usatu inseme cù u paràmetru content. Per esempiu, cusì:

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

Vede u percorsu <modulename>/<filename> implica u schedariu <rootdir>/modules/<modulename>/templates/<filename>.

Inoltre, ci hè una funzione inline_template - riceve u testu di u mudellu cum'è input, micca u nome di u schedariu.

In i mudelli, pudete aduprà tutte e variabili Puppet in u scopu attuale.

Puppet supporta i mudelli in formati ERB è EPP:

Brevemente nantu à ERB

Strutture di cuntrollu:

  • <%= ВЫРАЖЕНИЕ %> - inserisci u valore di l'espressione
  • <% ВЫРАЖЕНИЕ %> - calculà u valore di una espressione (senza inserisce). L'affirmazioni cundiziunali (se) è i loops (ognuna) sò generalmente quì.
  • <%# КОММЕНТАРИЙ %>

L'espressioni in ERB sò scritte in Ruby (ERB hè in realtà Embedded Ruby).

Per accede à variàbili da u manifestu, avete bisognu di aghjunghje @ à u nome variabile. Per caccià un ruttura di linea chì appare dopu à una custruzzione di cuntrollu, avete bisognu di utilizà un tag di chjude -%>.

Esempiu di usu di u mudellu

Diciamu chì scrivu un modulu per cuntrullà ZooKeeper. A classa rispunsevuli di creà a cunfigurazione s'assumiglia à questu:

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'),
  }
}

È u mudellu currispundenti zoo.cfg.erb - Allora :

<% 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 -%>

Fatti è Variabili integrati

Spessu a parte specifica di a cunfigurazione dipende di ciò chì succede oghje nantu à u node. Per esempiu, secondu a versione di Debian, avete bisognu di installà una o una altra versione di u pacchettu. Pudete monitorà tuttu questu manualmente, riscrivendu i manifesti se i nodi cambianu. Ma questu ùn hè micca un approcciu seriu; l'automatizazione hè assai megliu.

Per ottene infurmazioni nantu à i nodi, Puppet hà un mecanismu chjamatu fatti. Facts - questu hè infurmazione nantu à u node, dispunibule in manifesti in a forma di variàbili ordinali in u spaziu di nomi glubale. Per esempiu, u nome di l'ospitu, a versione di u sistema operatore, l'architettura di u processatore, a lista di l'utilizatori, a lista di l'interfaccia di a rete è i so indirizzi, è assai, assai più. I fatti sò dispunibuli in manifesti è mudelli cum'è variabili regularmente.

Un esempiu di travaglià cù fatti:

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

Formalmente parlante, un fattu hà un nome (stringa) è un valore (diverse tipi sò dispunibuli : strings, arrays, dizionari). Manghja serie di fatti integrati. Pudete ancu scrive u vostru propiu. I cullettori di fatti sò descritti cum'è funzioni in Ruby, o cum'è i fugliali eseguibili. I fatti ponu ancu esse presentati in a forma schedarii di testu cù dati nantu à i nodi.

Durante u funziunamentu, l'agente di pupi prima copia tutti i cullizzioni di fatti dispunibuli da u pappetserver à u node, dopu chì li lancia è manda i fatti cullati à u servitore; Dopu questu, u servitore principia a compilazione di u catalogu.

Fatti in forma di schedari eseguibili

Tali fatti sò posti in moduli in u cartulare facts.d. Di sicuru, i schedari deve esse eseguibili. Quandu sò eseguiti, anu da trasmette l'infurmazioni à l'output standard in u formatu YAML o chjave = valore.

Ùn vi scurdate chì i fatti s'applicanu à tutti i nodi chì sò cuntrullati da u servitore poppet à quale u vostru modulu hè implementatu. Dunque, in u script, fate cura di verificà chì u sistema hà tutti i prugrammi è i schedarii necessarii per u vostru fattu per travaglià.

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

fatti Ruby

Tali fatti sò posti in moduli in u cartulare 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

Fatti di testu

Tali fatti sò posti nantu à i nodi in u cartulare /etc/facter/facts.d in vechju Pupa o /etc/puppetlabs/facts.d in u novu Puppet.

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

Arrivà à i fatti

Ci hè dui modi per avvicinà i fatti:

  • attraversu u dizziunariu $facts: $facts['fqdn'];
  • usendu u nome di fattu cum'è u nome di variabile: $fqdn.

Hè megliu aduprà un dizziunariu $facts, o ancu megliu, indicà u spaziu di nomi globale ($::facts).

Eccu a sezione pertinente di a documentazione.

Variabili integrati

In più di i fatti, ci hè ancu alcune variabili, dispunibule in u spaziu di nomi glubale.

  • fatti di fiducia - variàbili chì sò pigliati da u certificatu di u cliente (siccomu u certificatu hè generalmente emessu in un servitore poppet, l'agente ùn pò micca solu piglià è cambià u so certificatu, cusì i variàbili sò "fiducii"): u nome di u certificatu, u nome di u certificatu. host è duminiu, estensioni da u certificatu.
  • fatti di u servitore -variabili ligati à l'infurmazioni nantu à u servitore - versione, nome, indirizzu IP di u servitore, ambiente.
  • fatti agenti - Variabili aghjuntu direttamente da puppet-agent, è micca da factor - nome di certificatu, versione di l'agente, versione puppet.
  • variabili maestru - Variabili Pappetmaster (sic!). Hè circa u listessu cum'è in fatti di u servitore, più i valori di parametri di cunfigurazione sò dispunibili.
  • variabili di compilatore — Variabili di compilatore chì sò diffirenti in ogni scopu: u nome di u modulu attuale è u nome di u modulu in quale l'ughjettu attuale hè statu accessu. Puderanu esse aduprati, per esempiu, per verificà chì e vostre classi private ùn sò micca aduprate direttamente da altri moduli.

Addition 1: cumu per eseguisce è debug tuttu questu?

L'articulu cuntene assai esempi di codice di pupi, ma ùn ci hà micca dettu à tuttu cumu per eseguisce stu codice. Ebbè, mi curreghju.

Un agentu hè abbastanza per eseguisce Puppet, ma per a maiò parte di i casi avete ancu bisognu di un servitore.

Agente

Almenu da a versione 5, pacchetti puppet-agent da repository ufficiale di Puppetlabs cuntenenu tutte e dipendenze (ruby è e gemme currispundenti), cusì ùn ci sò micca difficultà di stallazione (parlu di distribuzioni basate in Debian - ùn usemu micca distribuzioni basate in RPM).

In u casu più simplice, per utilizà a cunfigurazione di pupi, hè abbastanza per lancià l'agente in modu senza servitore: basta chì u codice di pupi hè copiatu à u node, lanciate. 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

Hè megliu, sicuru, per stallà u servitore è eseguisce l'agenti nantu à i nodi in modu daemon - dopu una volta ogni meza ora applicà a cunfigurazione scaricata da u servitore.

Pudete imite u mudellu push di u travagliu - andate à u node chì site interessatu è cumincià sudo puppet agent -t. Chjave -t (--test) in realtà include parechje opzioni chì ponu esse attivate individualmente. Queste opzioni includenu i seguenti:

  • ùn eseguite micca in modu daemon (per default l'agente principia in modu daemon);
  • chjusu dopu à applicà u catalogu (per difettu, l'agente cuntinueghja à travaglià è applicà a cunfigurazione una volta ogni meza ora);
  • scrive un logu di travagliu detallatu;
  • mostra cambiamenti in i schedari.

L'agente hà un modu operativu senza cambiamenti - pudete aduprà quandu ùn site micca sicuru chì avete scrittu a cunfigurazione curretta è vulete verificà ciò chì esattamente l'agente cambierà durante l'operazione. Stu modu hè attivatu da u paràmetru --noop nantu à a linea di cummanda: sudo puppet agent -t --noop.

Inoltre, pudete attivà u log di debugging di u travagliu - in questu, puppet scrive nantu à tutte l'azzioni chì eseguisce: nantu à a risorsa chì hè attualmente trasfurmata, nantu à i paràmetri di sta risorsa, nantu à quali prugrammi lancia. Di sicuru, questu hè un paràmetru --debug.

Servidor

Ùn cunsideraghju micca a cunfigurazione completa di u pappetserver è l'implementazione di u codice in questu articulu; Diceraghju solu chì fora di a scatula ci hè una versione cumpletamente funziunale di u servitore chì ùn hà micca bisognu di cunfigurazione supplementu per travaglià cù un picculu numeru di nodi (per dì, finu à centu). Un nùmeru più grande di nodi necessitarà sintonizzazioni - per difettu, puppetserver lancia micca più di quattru travagliadori, per un rendimentu più grande avete bisognu di aumentà u so numeru è ùn vi scurdate di aumentà i limiti di memoria, altrimenti u servitore raccoglierà a basura a maiò parte di u tempu.

Implementazione di codice - se ne avete bisognu rapidamente è facilmente, allora fighjate (à r10k) [https://github.com/puppetlabs/r10k], per picculi installazioni deve esse abbastanza.

Addendum 2: Linee di codificazione

  1. Pone tutta a logica in classi è definizione.
  2. Mantene e classi è definizioni in moduli, micca in manifesti chì descrizanu i nodi.
  3. Aduprate i fatti.
  4. Ùn fate micca ifs basatu nantu à i nomi di host.
  5. Sentite liberu di aghjunghje parametri per classi è definizioni - questu hè megliu cà a logica implicita oculta in u corpu di a classa / definisce.

Spiegheraghju perchè ricumandemu di fà questu in u prossimu articulu.

cunchiusioni

Finitemu cù l'intruduzioni. In u prossimu articulu vi dicu circa Hiera, ENC è PuppetDB.

Solu l'utilizatori registrati ponu participà à l'indagine. Firmà lu, per piacè.

In fatti, ci hè assai più materiale - possu scrive articuli nantu à i seguenti temi, votate nantu à ciò chì vi interessarebbe leghje:

  • 59,1%Custruzzioni di pupi avanzati - qualchì merda di u prossimu livellu: loops, mapping è altre espressioni lambda, cullezzione di risorse, risorse esportate è cumunicazione inter-ospiti via Puppet, tags, fornitori, tipi di dati astratti.13
  • 31,8%"Sò l'amministratore di a mo mamma" o cumu in Avito avemu fattu amici cù parechji servitori poppet di diverse versioni, è, in principiu, a parte di l'amministrazione di u servitore poppet.7
  • 81,8%Cumu scrive u codice di pupi: strumentazione, ducumentazione, teste, CI/CD.18

22 utilizatori anu vutatu. 9 utilizatori si sò astenuti.

Source: www.habr.com