Apperçu

Formations préalables

Temps estimé

Entre 1h30 et 2h

Objectifs

Préparation des participant.e.s

Pour faire les exercices dans cette formation, nous utiliserons les machines virtuelles du projet Vagrant dans le control repository. Plus spécifiquement, les exercices seront fait sur le client puppet nommé pc_buster et à l'aide du puppetmaster dans la machine pm_buster.

Nous présumerons ici que les participant.e.s ont déjà mis en place vagrant avec libvirt et KVM durant la formation d'introduction à puppet.

Le jour avant la formation, assurez-vous que les VMs fonctionnent toujours bien. Pour démarrer la VM:

vagrant up pm_buster
vagrant up pc_buster

Théorie, concepts et information

TL;DR:

Où est exécuté le code?

puppet_agent_server_exchanges.svg

L'ordre d'exécution des choses c'est:

  1. L'agent démarre sur une node
    1. Facter roule sur la node pour generer les faits. Les faits "custom" (dans /etc/facter/facts.d) sont exécutés aussi.

  2. Les faits sont envoyé au serveur (master) puppet: nom de la node, environnement souhaité, certificat, etc.
  3. Le master compile les manifestes en catalogue
    1. Les données externes sont compilées ensemble: données du ENC (external node classifier), données de PuppetDB (eg. objets, ressources exportés), résultats d'appels de fonctions et données de Hiera
    2. Le code des manifestes et modules est compilé avec l'aide de ces données
    3. Le catalogue est envoyé à l'agent
  4. L'agent (du côté de la node) verifie l'état des ressoruces défini dans le catalogue et execute les changements nécessaires si des différences sont remarquées
  5. Un rapport d'exécution est envoyé au serveur puppet
    1. Le serveur puppet envoie le rapport d'exécution à PuppetDB pour qu'il soit stocké

Donc sont évalués:

voir: https://puppet.com/docs/puppet/5.5/subsystem_catalog_compilation.html

Representation des configurations d'une machine

Exercice: Avant de commancer, vérifiez si le package nommé gnutls-bin est déjà installé ou non sur votre VM. Il ne devrait pas être présent:

# dpkg -l gnutls-bin
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version      Architecture Description
+++-==============-============-============-=================================
un  gnutls-bin     <none>       <none>       (no description available)

Sous le user root, créez un fichier nommé gnutls.pp dans votre VM. Le fichier peut être n'importe où comme par exemple dans le répertoire home de root. Incrivez-y le contenu suivant:

package { 'gnutls-bin':
  ensure => present,
}

Demandez maintenant à puppet d'appliquer le code que vous venez de créer:

puppet apply gnutls.pp

Lisez maintenant ce que la commande vous a indiqué. Qu'est-ce que puppet a fait?

A l'aide de dpkg -l gnutls-bin vérifiez la présence du package.

Modifiez maintenant le fichier gnutls.pp et remplacez "present" par "absent" dans le fichier. Lancez puppet apply openssh.pp et observez ce que puppet fait.

Le but de Puppet est d'utiliser du code pour représenter des configurations concrètes sur un système:

Ces choses sont representées par des "Ressources"

Ressource
Objet (tant de base que des conteneurs) qui sert à déclarer la présence ou l'absence de différentes informations sur une machine.

Quand on déclare la gestion d'une ressource dans le code puppet, on le fera toujours via une forme similaire à ce qu'on a fait dans l'exercice plus haut:

Une liste de tous les types de ressources inclus de base avec puppet peut être consultée dans la documentation de puppet.

Comme vous l'avez vu dans l'exercice plus haut, on peut déclarer l'installation d'un package simplement en ajoutant la ressource pour le package. On peut également déclarer le retrait d'un package en donnant la valeur spéciale absent pour le paramètre ensure de cette ressource. Donc on peut contrôler ce qui est présent mais aussi ce qui devrait être retiré (e.g. ça peut être utile par exemple dans les cas où on veut migrer d'une application à une nouvelle qui la remplace complètement)

Exercice: Modifiez à nouveau le fichier gnutls.pp et remplacez "absent" par "present". Appliquez ensuite le code comme dans l'exercice précédent pour installer à nouveau le package.

Remplacez ensuite tout le contenu du fichier gnutls.pp par ceci:

notify { 'gnutls est déjà installé et devrait y rester!': }

Demandez maintenant à puppet d'appliquer ce fichier de code à nouveau:

puppet apply gnutls.pp

Une fois que puppet a terminé de s'exécuter, vérifiez si le package gnutls-bin est encore présent avec la même commande dpkg que dans l'exercice précédent.

Comme vous l'avez vu plus haut, s'il n'y a aucune ressource qui déclare la manière de gérer le package gnutls-bin, puppet ne le retirera pas du système s'il est déjà présent. La même chose est vraie pour tout type de ressource comme les fichiers, les utilisateurs, etc.

Et donc du même coup, si on utilise un morceau de code pour créer des fichiers ou installer des packages, puis qu'on retire ce morceau de code par la suite, les ajouts au système ne seront pas défaits automatiquement!

Un détail important à noter par rapport aux ressources c'est que si qqch n'est pas déclaré dans des manifests, par défaut ça veut simplement dire que puppet ne gère pas cette information sur la machine.

Dans ces cas là, si ce qu'on désire c'est de retirer la ressource (p-e qu'elle a été ajoutée par erreur par exemple?) il faut s'assurer de soit:

metaparameters

Toutes les ressources, qu'elles représentent directement une configuration sur une machine ou qu'elles soient simplement un conteneur, peuvent avoir certaines méta-informations attachées.

Les méta-informations les plus utiles sont celles qui définissent un ordre entre les ressources:

On les appelle metaparameter puisque ceux-ci sont présent sans qu'on ait besoin de définir leur existance.

Note: nous verrons plus en détail le rôle et l'utilisation des méta-paramètres d'ordonnancement très bientôt.

voir: https://puppet.com/docs/puppet/5.5/metaparameter.html

Les autres méta-paramètres sont intéressants mais constituent une utilisation plus avancée que l'on ne couvrira pas dans cette formation. Il est tout de même important de connaître leur existance et de savoir où trouver plus d'information par rapport à ceux-ci.

Types de base

Puppet inclue plusieurs types de ressources de base qui peuvent être utilisés pour représenter des informations concrètes sur une machine.

Quelques exemples les plus utiles:

Il y en a beaucoup plus. Voir la liste complète: https://puppet.com/docs/puppet/5.5/type.html

Exercice: En vous basant sur l'exemple de code de package dans la première section et sur la forme générale qu'une ressource doit prendre, créez un nouveau fichier fichier.pp et dedans, définissez une ressource qui déclare la gestion d'un fichier.

Rappelez-vous des détails suivant:

  • le type de ressource pour gérer un fichier s'appelle: file

  • le nom de la ressource devrait être le chemin absolu du fichier dans le système. Par exemple le nom de ressource /tmp/hello_world gèrera le fichier sous ce chemin.

  • Consultez la documentation du type de ressource (voir ici) pour trouver quels sont les paramètres dont vous avez besoin pour:

    • attribuer l'utilisateur à qui appartient le fichier. attribuez, ce fichier à l'utilisateur vagrant (ou si vous suivez une formation externe à Koumbit, l'utilisateur formation)

    • donner les permissions de lecture et d'écriture au propriétaire du fichier et de lecture au groupe (rien pour "les autres")
      • les permissions peuvent être spécifiées avec les mêmes valeurs que ce que la commande chmod accepte, donc soit numérique en mode octal soit symbolique avec des lettres pour représenter les permissions

    • s'assurer que le fichier contienne exactement le contenu: Bonjour, monde!

Vérifiez si votre code fonctionne comme prévu en demandant à puppet de l'appliquer:

puppet apply fichier.pp

NB: Il est également possible de définir de nouveaux types de base en écrivant du code ruby qui exécute les vérifications et modifications nécessaires pour représenter l'information sur la machine. Ce sujet est avancé et ne sera pas couvert lors de la formation.

Conteneurs

En plus des types de ressources de base, il serait pratique de pouvoir organiser un peu notre code, donc de grouper ensemble certaines ressources qui sont reliées d'un point de vue logique. Il serait bien aussi de pouvoir définir pour un certain groupe de ressources des "variables" qui pourraient influencer le contenu des ressources ou la manière de les gérer.

Pour répondre à ces objectifs là, Puppet offre deux types de ressources qui servent à contenir d'autre ressources.

On utilise soit des classes ou des types définis pour rassembler ensemble une ou plusieurs ressources.

Classe

Conteneur qui ne peut être défini qu'une seule fois sur une machine donnée.

Type défini (defined type)

Conteneur ayant un identifiant unique (un "nom") qui peut être défini plusieurs fois sur une même machine tant qu'il n'y a pas deux ressources du même type et ayant le même "nom".

Définition de classes et de types définis

Exercice: Créez un fichier premiere_classe.pp et inscrivez le contenu suivant:

class profile::patate (
  Boolean $epeluchee = false,
) {
  file { '/tmp/patate':
    ensure  => present,
    content => "Ma patate est épeluchée: $epeluchee",
  }
}

Créez maintenant un fichier premier_type_defini.pp et inscrivez le contenu:

define asterisk::sip::register (
  $host,
  $password,
  $port = 5666,
) {
  file { "/tmp/asterisk_sip_register_${name}":
    ensure  => present,
    content => "register => ${host}:${port}/${password}",
  }
}

Essayez maintenant d'appliquer le code de ces deux fichiers avec puppet apply pour chaque fichier.

Notez dans le code qu'on vient de créer que nous avons utilisé des variables à l'intérieur d'une chaîne de texte, donc sous la forme ${variable}. Nous verrons plus en détails les variables dans une section plus tard, mais il est tout de même intéressant de noter ici que les paramètres d'un type défini sont accessibles dans le bloc de code du type comme des variables.

C'est la même situation pour les classes également: les paramètres de classes sont accessibles comme des variables à l'intérieur du bloc de code de la classe.

La définition des conteneurs utilise une forme qui ressemble beaucoup à la déclaration de ressources. Par contre la définition a les différences suivantes:

Vous aurez déjà noté à la fin de l'exercice précédent que le code qui ne contient que des définitions de conteneurs ne produit en fait rien!

Les deux types de conteneurs, donc les classes et les types définis, doivent être définis avant qu'on puisse déclarer leur présence sur le système.

Déclaration d'une ressource de classe ou de type défini

Exercice: Ajoutez les lignes suivantes à la fin du fichier premiere_classe.pp:

include profile::patate

Puis appliquez le code de ce fichier à l'aide de puppet apply. Puppet se met maintenant à gérer le fichier qu'on a déclaré dans le conteneur!

Le mot réservé include permet de déclarer la présence de ce qu'il y a dans une classe sans spécifier la valeur d'aucun paramètre.

Exercice: Modifiez maintenant le fichier premiere_classe.pp et remplacez la ligne include profile::patate par les lignes suivantes:

class { 'profile::patate':
  epeluchee => true,
}

Puis appliquez ce code à l'aide de puppet apply. Vérifiez ensuite sur disque le contenu du fichier déclaré via la classe.

Notez que les lignes que nous venons d'ajouter au fichier sont maintenant sous la forme d'une déclaration de ressource.

Donc on spécifie le type class suivi tout de suite d'une accolade ouvrante, puis le nom de ressource correspond au nom de la classe. Cette forme nous permet de spécifier la valeur de paramètres de classe directement lors de la déclaration. Ici on change la valeur du paramètre $epeluchee, et ça a eu un impact sur le contenu du fichier!

La forme de déclaration avec include n'est qu'un raccourci pour la forme standard de déclaration de ressource, celle que nous venons de voir.

On peut toujours utiliser la forme standard de déclaration de ressource au lieu de changer de forme lorsqu'il n'y a pas de paramètre à modifier. Le raccourci est présent surtout pour des raisons historiques: les classes n'avaient pas de paramètres dans les premières versions de Puppet.

Exercice: Ajoutez les lignes suivantes à la fin du fichier premier_type_defini.pp:

asterisk::sip::register { 'fournisseur_voip':
  host     => 'founisseur.voip',
  password => 'qazwertxcv',
}

asterisk::sip::register { 'serveur_maison':
  host     => 'serveur-maison.com',
  password => 'nepasutiliserunmotdepassecommeceluici',
  port     => 5668,
}

Puis appliquez ce code à l'aide de puppet apply. Deux fichiers sont créés sur disque! Le contenu de chacun est influencé par la valeur des paramètres.

Notez que contrairement aux classes, lors de la définition du type défini nous avons comme le nom l'implique défini un nouveau type de ressource! Donc pour déclarer la gestion de cette ressource on doit utiliser le nom du type défini comme type de ressource.

A l'intérieur du bloc de code de la définition d'un type défini, on peut utiliser deux variables spéciales (ref) qui permettent de réutiliser l'identificateur unique (ou "nom") de ressource:

Dans le type défini que vous avez créé dans l'exercice plus haut, la variable $name a été utilisée dans le chemin du fichier qui est géré par la ressource. Comme l'identifiant d'une ressource doit être unique, on est ainsi assuré de gérer des fichiers différents lors de chaque déclaration de ressource de ce type défini (rappelez-vous qu'on ne peut pas gérer plusieurs ressources de fichier avec le même identifiant).

Variables

Le DSL de Puppet permet d'utiliser des variables pour contenir des valeurs lors de la compilation des manifestes.

Les noms des variables doivent commencer par un $ et une lettre minuscule. On peut utiliser des lettres, des chiffres et des bas-tirets (underscore) pour composer leur nom.

Attention par contre! Même si on les appelle des "variables", on peut leur assigner une valeur qu'une seule fois!

Elles sont donc "variables" seulement dans le sens qu'elles peuvent contenir des valeurs de différentes sources. On peut par exemple leur assigner des données qui viennent de Hiera ou bien assigner différentes valeurs selon différents embranchements conditionnels.

La raison pour que les variables ne puissent pas être redéfinies vient de la nature déclarative du langage: une chose dans ce langage ne peut être déclarée qu'une seule fois.

L'exemple suivant n'est pas valide et donne une erreur de compilation:

$x = 10;
# some time later...
$x = length($cat)
# Erreur de compilation:  Error: Evaluation Error: Cannot reassign variable '$x' (line: 3, column: 1) on node example.com

Exercice: Pour voir comment les erreurs sont affichées par puppet, copiez le contenu plus haut dans un nouveau fichier nommé erreur.pp, puis appliquez le code à l'aide de puppet apply.

Types de données

Les variables n'ont pas un type de données fixe par défaut mais prennent le type de la valeur qui leur est assignée (duck typing).

Exercice: Créez le fichier types.pp et inscrivez le contenu suivant:

$x = 10
notice(type($x))
# Notice: Integer[10, 10]

$y = "j'suis un string"
notice(type($y))
# Notice: String

$z = false
notice(type($z))
# Notice: Boolean[false]

Ajoutez à la fin du fichier une nouvelle déclaration de variable (nommez-la comme vous le voulez) qui utilise une liste. Ajoutez une ligne notice() similaire à celles déjà présentes pour afficher ce que Puppet interprète pour son type de données.

NB: une liste est entourée de crochets [] et en dedans, on peut y inscrire des données de n'importe quel type séparées par des virgules.

NB2: On avait déjà vu la ressource de type notify plus haut, dans l'exemple de package pour gnutls-bin. On utilise ici plutôt la fonction notice(). La différence entre les deux vient de l'emplacement où le code pour chacun est interprété:

  • les ressources sont compilées sur le serveur mais appliquées par les agents
  • les fonctions sont interprétées par le serveur lors de la compilation

Pour cette raison, un message émis par la fonction notice() n'apparaîtra que sur le serveur. Ici, comme on utilise puppet apply on demande à puppet de compiler le code et donc on peut voir le message.

Par contre, pour les paramètres des ressources (qui ressemblent en tout point à des variables!) il est possible de définir des types de données attendues.

On a déjà vu un exemple d'utilisation de paramètre booléen dans la classe que vous avez créée plus tôt, mais voilà un nouvel exemple abstrait:

Eg.

class ( 'example':
  Boolean $use_params = true,
) {
  # dans le code on peut s'attendre à toujours avoir une valeur booléenne
  if $use_params {
    # ....
  }
}

Pour une liste des types de données disponbiles, consultez: https://puppet.com/docs/puppet/5.5/lang_data_type.html#core-data-types

Si une variable n'a pas un type de données bien connu, on peut verifier le type de données qu'une variable contient lors de l'exécution du code (donc dans le cas d'une définition de ressources, hors des déclarations de paramètres donc dans le bloc de code).

Vous pouvez utiliser les opérateurs de comparaison des types, =~ et !~

Exercice: Ajoutez au fichier types.pp les lignes suivantes:

if $x =~ Undef {
  # Notez que $x n'est pas interprété ici, on voit $x tel quel dans le message.
  # C'est parce qu'on a utilisé des apostrophes pour délimiter la string de texte
  notice('$x est indéfini')
}
if $y !~ Array {
  notice('$y n'est pas un tableau de données')
}

Puis vérifiez quelles conditions affichent des messages en appliquant le fichier à nouveau à l'aide de puppet apply.

Modifiez le bloc if qui teste la variable $x pour tenter de faire afficher la notice à l'intérieur du bloc. Consultez la liste des types de données liée plus haut pour trouver quel nom de type utiliser.

Un détail important à se rappeler lorsqu'on assigne un type de données attendu pour un paramètre de ressource, mais aussi lorsqu'on teste le type de données d'une variable, c'est que les types de données commencent toujours par une lettre majuscule.

On utilise dans le langage Puppet des noms qui commencent par des lettres majuscules pour différencier les ressources de ce qui n'est pas des ressources (donc dans l'exemple ici, un type de données n'est pas une ressource que l'on déclare sur le système)

La fonction assert_type() peut aussi être utilisée pour faire planter la compilation du catalogue si on n'a pas le bon type de données dans une variable.

Interprétation d'une valeur de variable à l'intérieur d'une string

Pour utiliser la valeur d'une variable comme une partie d'une string, on peut utiliser la variable dans la string.

Il faut utiliser des guillemets (doubles) pour pouvoir interpréter les variables, les strings délimitées par des apostrophes (simples) contiendront le nom de la variable tel quel sans interprétation.

Par mesure de clarté il est fortement recommandé d'utiliser des accolades après le signe de dollar pour délimiter le nom du reste du contenu de la string.

Eg.

$var = "contenu"
$resultat = "ceci affiche le ${var}"
notice($resultat)
# affichera: ceci affiche le contenu

Accéder aux valeurs d'une liste ou d'un dictionnaire dans une variable

Exercice: Créer le fichier element.pp et inscrire le contenu suivant:

$liste = ["un", "deux", "horaaaa"]
notice($liste[1])
# affiche: "deux"

$dictionnaire = {
  "patate"    => 3,
  4           => "cinq",
  "sous-chef" => [
    "anna",
    "georg",
  ],
}
notice($dictionnaire["patate"])
# affiche: 3

Appliquez le fichier à l'aide de puppet apply pour voir les messages contenant les valeurs des éléments de la liste et du dictionnaire.

Ajoutez une nouvelle ligne qui utilise la fonction notice() pour afficher le 2ème élément du tableau dans l'élément "sous-chef" du dictionnaire.

Quand une variable contient une liste ou un dictionnaire, on peut utiliser l'opérateur [] pour accéder les éléments.

On peut enchaîner plusieurs opérateurs [] pour accéder des sous-éléments, donc pour "traverser" plusieurs niveaux de la structure de données.

Pour plus de détails, voir:

Variables globales disponibles par défaut

Puppet défini quelques variables globales qu'on peut utiliser dans notre code. Voici quelques unes des plus utiles

Voir: https://puppet.com/docs/puppet/5.5/lang_facts_and_builtin_vars.html

Structures conditionnelles

Comme dans tous les langages de programmation, on peut utiliser certaines expressions conditionnelles pour modifier le comportement du code selon certaines conditions.

if/elsif/else

Si la valeur de l'expression (avant l'accolade ouvrante) dans un if est vraie ("true-ish"), le block de code qui suit l'expression if (délimité par des accolades) sera executé. Si après le bloc de code du premier if, on a une expression elsif, celle-ci ne sera évaluée que si l'expression du premier if n'était pas vraie. Tout comme un if, si l'expressions qui suit (et qui précède l'accolade ouvrante) est vraie, le bloc de code qui suit (délimité par des accolades) sera exécuté. Il peut y avoir 0 ou plus blocs elsif qui suivent un if. Finalement, si une expression else vient après un bloc if ou elsif, le bloc de code qui suit le else sera exécuté si aucune des expression if ou elsif n'étaient vraies. Une expression else vient toujours en dernier après if et elsif et n'a jamais d'expression booléenne.

if $cat['fits'] {
  box { 'x':
    ensure => 'sat in',
  }
} elsif $cat['hugry'] {
  food { 'croquettes':
    ensure => present,
  }
} else {
  # Unreachable??
  visage { 'mine':
    ensure => 'astonished',
  }
}
unless

Si la valeur de l'expression qui suit l'expression unless est fausse ("false-ish"), le block de code qui suit va être executé. C'est un if à l'envers, mais qui ne peut pas avoir de elsif ou de else

unless $cat['entertained'] {
  # This is the best way to become entertained
  table_item { "object"
    ensure => "knocked off",
  }
}
case

Un des blocs de code sera exécuté selon la valeur contenue dans la variable qui suit l'expression case. Le bloc choisi sera celui qui suit la valeur correspondante. La valeur peut également être exprimée comme une expression régulière (e.g. si la valeur de la variable "match" l'expression régulière, ce bloc de code là sera choisi). Si deux valeurs correspondent à ce qu'il y a dans la variable, celle qui apparaît en premier (ligne plus haute) dans la liste sera choisie. La valeur spéciale default correspond au choix si aucune autre valeur listée n'est contenue dans la variable.

case $facts['os']['name'] {
  'Solaris':           { include role::solaris } # Apply the solaris class
  'RedHat', 'CentOS':  { include role::redhat  } # Apply the redhat class
  /^(Debian|Ubuntu)$/: { include role::debian  } # Apply the debian class
  default:             { include role::generic } # Apply the generic class
}

Voir: https://puppet.com/docs/puppet/5.5/lang_conditional.html

Loops

L'itération sur une structure de données peut être fait avec la fonction each. Celà permet d'iterer sur les listes et les dictionnaires (hashes).

Comme Puppet est un langage déclaratif, itérer permet principalement de déclarer plusieurs ressources avec des valeurs différentes selon ce qu'une liste ou un dictionnaire contiennent.

Exercice: Copier le code suivant dans un fichier loop_liste.pp:

$languages = ['en', 'fr']
$languages.each | $value | {
  # faire qqch avec $value - ici, on installe des packages selon chaque valeur de la liste
  package { "aspell-${value}":
    ensure => installed,
  }
}

Puis appliquez le fichier. Constatez ce que Puppet fait sur votre VM.

Une boucle sur une liste aura toujours un seul argument après le mot each, placé entre deux barres verticales: c'est la valeur dans le tableau à chaque position d'index du tableau. On peut nommer l'argument comme on veut. Ici on a utilisé $value mais la variable peut avoir le nom qu'on désire. Il est cependant important de faire attention à ne pas réutiliser un nom de variable qui existe déjà dans le code où on écrit la boucle.

Exercice: Copiez le contenu suivant dans un fichier loop_dictionnaire.pp:

$x = {
  'apples' => 2,
  'oranges' => 3,
  'lights' => 4,
}

$x.each | $key, $value | {
  notify { "test-${key}":
    message => "There are ${value} ${key}(s)!",
  }
}

Puis constatez de quoi les messages ont l'air.

Une boucle sur un dictionnaire aura toujours deux arguments après le mot each, encore une fois placé entre deux barres verticales: il s'agit, en ordre, de la clef puis de sa valeur, pour chaque paire de clef et valeur du dictionnaire. De la même manière que pour les listes, les deux arguments peuvent utiliser n'importe quel nom de variable.

Notez que dans les exemples plus haut, vous avez déclaré deux ressources package grâce aux deux valeurs dans la liste ainsi que trois ressources notify à l'aide des trois paires de clefs et valeurs du dictionnaire.

Fonctions communes

Voici les quelques fonctions les plus utiles:

Le module stdlib contient également plusieurs fonctions utiles, dont:

Il existe plusieurs formes d'appels pour les fonctions. Par exemple, les boucles qu'on a vues plus haut sont en fait un appel à la fonction each() mais dans une forme différente.

Les différentes façons d'appeler une fonction sont documentées dans: https://puppet.com/docs/puppet/5.5/lang_functions.html

Pour voir la liste complète des fonctions "built-in" et les définitions plus exactes: https://puppet.com/docs/puppet/5.5/function.html

La liste complète des fonctions du module stdlib: https://forge.puppet.com/modules/puppetlabs/stdlib/reference

Contenu des fichiers

Il y a 2 façons principales de définir le contenu d'un fichier: les paramètres content et source.

content
Prend une valeur de type "string" pour définir l'entièreté du contenu d'un fichier

Eg. Un string fixe,

file { '/etc/example.conf':
  content => "Hello, world\n",
}

Eg. Un string fixe, loadé à partir d'un fichier -- attention, on appelle ici une fonction et donc le fichier en question sera lu sur le serveur:

file { '/etc/example.conf':
  # This needs the file to be placed in the control repo, in 'profile/files/example.conf'
  content => file("profile/example.conf"),
  #                ^       ^- le path du fichier à partir du dossier "files" du module
  #                |- le nom du module 
}

On verra les modules seulement dans la prochaine formation. Par contre, pour rendre le chemin plus concret: le chemin utilisé plus haut 'profile/example.conf' sera transformé sur le serveur puppet en /.../chemin/vers/les/modules/profiles/files/example.conf.epp (notez l'addition du chemin vers les modules et du sous-répertoire files.

Eg. Un string généré à partir d'un template epp:

$example_vars = { 'a' => 'something', 'b'=> 'another thing' }
file { '/etc/example.conf':
  # This needs the template to be placed in the control repo, in 'profile/templates/example.conf.epp'
  content => epp('profile/example.conf.epp', $example_vars),
  #               ^       ^                 ^- hash qui défini les variables du template. ici, le template contiendra deux variables, $a et $b (qui correspondent aux clefs du hash)
  #               |       |- le path du fichier à partir du dossier "templates" du module
  #               |- le nom du module 
}

On verra les modules seulement dans la prochaine formation. Par contre, pour rendre le chemin plus concret: le chemin utilisé plus haut 'profile/example.conf.epp' sera transformé sur le serveur puppet en /.../chemin/vers/les/modules/profiles/templates/example.conf.epp (notez l'addition du chemin vers les modules et du sous-répertoire templates.

source

Un string ou une list de strings qui indique où trouver le fichier avec un URI. Dans le cas d'une liste de strings, le premier fichier trouvé sous le répertoire files sera sélectionné comme contenu du fichier.

Eg.

file { '/etc/example.conf':
  source => [
     "puppet:///modules/profile/example-${facts['fqdn']}.conf", // This takes priority, if it exists
     "puppet:///modules/profile/example.conf",
    #                   ^       ^- le path du fichier à partir du dossier "files" du module
    #                   |- le nom du module
  ],

Les URI plus haut ressemblent beaucoup au chemin qu'on donnerait à la fonction file() mais avec quelques éléments en plus au début. Les chemins plus haut sont transformés sur le serveur en /.../chemin/vers/les/modules/profiles/files/example.conf.epp (notez l'addition du chemin vers les modules et du sous-répertoire files.

Templates epp

Un template de type epp ("Embedded Puppet") utilise une syntaxe spéciale pour délimiter les expression de code puppet qui partagent les règles et types de données du DSL puppet. En dehors de ces délimiteurs, le texte sera contenu tel quel dans le fichier. Les variables dans un template epp doivent être passées dans le hash fourni comme 2ème argument de la fonction epp.

Pour informations complètes sur les templates epp: https://puppet.com/docs/puppet/6.19/lang_template_epp.html

Exercice: Créer un fichier template.pp avec le contenu suivant:

$repos = {
  "yolo" => { 
    "a" => 1,
    b => "nope",
  },
  meuh => {
    a => 0,
  },
} 

file { '/tmp/banane': 
  ensure => present,
  content => epp('banane/banane.epp', { repositories => $repos }),
}

Pour que votre template soit trouvé il vous vaut maintenant créer des répertoires:

mkdir -p /etc/puppet/code/environments/production/modules/banane/templates/

Créez maintenant un fichier /etc/puppet/code/environments/production/modules/banane/templates/banane.epp et inscrivez-y le contenu:

<% | Hash $repositories = {}
| -%>
<%# cette ligne est un commentaire. La définition au dessus liste explicitement les paramètres attendus dans l'appel à la fonction `epp`. Elle est optionnelle, mais fortement recommandée %>
---
soap:
    server_addr: 127.0.0.1
    server_port: 5391
    service_name: KGB
<%# Nous pouvons utiliser les structures et fonctions habituelles du langage puppet comme ".each" pour itérer sur une liste %>
<%# Pour retirer les espaces en début et en fin de ligne (précédant ou suivant un délimiteur), on peut ajouter '-' au délimiteur %>
repositories:
<% $repositories.each |String $name, Hash $attr| { -%>
   <%= $name -%>:
      <%- $attr.each |$key, $value| { -%>
      <%= $key -%>: <%= $value %>
   <%- } -%>
<% } -%>

Et appliquez le code du fichier template.pp avec l'aide de puppet apply. Le contenu du fichier /tmp/banane sera créé en fonction de ce que la variable $repos contient.

Dans les templates epp donc, tout ce qui n'est pas dans une balise spéciale sera contenu tel quel dans le fichier. Les balises spéciales sont:

Comme c'est noté dans l'exemple de template, on peut utiliser entre les balises de code les mêmes structures de contrôle que dans le code puppet. Donc on peut utiliser if/elsif/else, case, each. On peut également utiliser des appels de fonctions comme join() pour formatter le contenu d'une variable adéquatement pour un certain fichier.

Ordonnacement des ressources

Un manifest est compilé dans un ordre standard, avec l'évaluation des expressions dans une classe ou ressource de haut en bas.

À partir de Puppet 5, les ressources sont évaluées dans le même ordre que dans le code - avant Puppet 5, l'ordre d'évaluation n'était pas assuré d'être toujours pareil. Cependant, ce comportement n'est pas nécessairement garanti parce qu'il dépend d'un paramétrage dans la configuration Puppet.

Il est très commun que des ressources aient des interdépendences complexes, et qu'on ait donc besoin de s'assurer que ces ressources soient évaluées dans un ordre particulier (qui n'est pas nécessairement le même ordre que le code).

Un cas commun est de s'assurer que la gestion d'un service soit exécutée seulement après l'installation du package Debian qui contient ce service. Eg.

service { 'ntp':
  ensure => running,
}

package { 'ntp': ensure => present }

Dans l'exemple plus haut, si le service est démarré avant l'installation du package, on obtiendra une erreur.

Donc, on peut s'assurer que tout est bien exécuté dans le bon ordre en définissant des relations d'ordonnancement:

# code from above

Package['ntp']
-> Service['example']
->
Opérateur qui dicte que la ressource qui le précède (souvent à la ligne précédente) doit être évaluée avant celle qui suit l'opérateur.

On défini la relation après la déclaration des ressources. Comme on ne déclare pas une deuxième fois les même ressources, on leur fait simplement référence en utilisant le type de ressource avec une lettre majuscule suivi du nom de la ressource entre crochets. Cette façon d'écrire le code nous permet d'alléger le code et peut aussi nous permettre d'établir des liens d'ordonnancement différents selon les cas (à l'aide de blocs conditionnels)

Les méta-paramètres require,before,subscribe et nofify jouent le même rôle que les opérateurs "flèches".

Voir: https://puppet.com/docs/puppet/5.5/lang_relationships.html

Subscriptions / Notifications

Une autre fonctionnalité très pratique et associée à l'ordonnancement c'est l'idée qu'une ressource précédente envoie un signal (une "notification") à la ressource suivante.

Une ressource qui reçoit une notification devrait être re-evaluée (refreshed) après qu'un changement est effectué sur l'agent. C'est un mécanisme qui permet d'éviter de faire certaines modifications sur le systèmes sauf au moment où il y a un changement qui le rend nécessaire.

~>

Opérateur qui a le même résultat que -> mais qui en plus enverra une "notification" à la ressource suivant l'opérateur si la ressource précédente subit une modification sur l'agent (par exemple si un fichier est modifié). Ça permet par exemple de redémarrer un service si les fichiers de configurations sont changés.

Class['config']
~> Class['service']

Dans l'exemple plus haut, si une des ressources contenues dans la classe "config" subit une modification sur l'agent lors de l'application du catalogue, alors toutes les ressources contenues dans la classe "service" recevront une notification.

Tous les types de ressources ne peuvent pas utiliser les notifications et leur comportement lors d'une notification pourra différer.

Par exemple les ressource de type service redémareront un service lors d'une notification.

Une ressource de type exec avec le paramètre refreshonly défini avec une valeur true ne s'exécutera pas en temps normal. Par contre, elle s'exécutera lorsqu'elle reçoit une notification.

On peut également utiliser l'ancienne notation, à l'aide des méta-paramètres notify et subscribe.

Notify

défini une relation d'ordre comme avec ~> où la ressource dans laquelle le méta-paramètre est utilisé est celle qui précède la relation ~> et celle référée par le méta-paramètre notify est la ressource qui recevra la notification.

Eg. Quand on ajoute notre interface custom change, re-charger la configuration reseau

# La méthode avec la flèche "tilde":
File['/etc/network/interfaces.d/custom_interface']
~> Service['networking']

# L'utilisation du méta-paramètre "notify" lors de la définition a la même signification que l'exemple au dessus
# Attention de ne pas utiliser les deux notations en même temps.
file { '/etc/network/interfaces.d/custom_interface':
  # ...
  notify => Service['networking'],
}
Subscribe

défini une relation d'ordre comme avec ~>, mais cette fois la ressource qui contient ce méta-paramètre est celle qui recevra la notification alors que celle référée par le méta-paramètre "subscribe" est celle avant le ~> dans la relation.

Eg. Le service SSH devrait être re-evalué (refreshed) si sont fichier de configuration change

# La méthode avec la flèche "tilde"
File['/etc/ssh/sshd_config']
~> Service['sshd']

# L'utilisation du méta-paramètre "subscribe" a le même effet mais un peu comme dans un sens inverse
# Attention de ne pas utiliser les deux notations en même temps.
service { 'sshd':
  ensure    => 'running',
  subscribe => File['/etc/ssh/sshd_config'],
}

Les méta-paramètres "notify" et "subscribe" ont tendance à créer du code un peu plus mélangeant, donc on recommande d'utiliser plutôt les opérateurs flèches.

Ressources exportées

Une fonctionnalité avancée de Puppet qui devient très rapidement utile, c'est les ressources exportées. Une ressouce peut donc être créée par une machine, mais dans le but d'être exportée vers d'autres machines.

Sur la node qui exporte une ressource on utilise le préfixe @@ au nom d'une ressource pour qu'elle soit exportée:

# Quiconque importe cette clef permettra un accès SSH via le user mentionné dans la ressource
@@ssh_authorized_key { 'ichirou@machine1234':
  ensure => present,
  user   => 'ichirou',
  type   => 'ssh-ed25519',
  key    => 'AAAAC3Nz...',
}

Concrètement, ce qui se produit c'est que quand le serveur puppet compile le catalogue de la node qui exporte une ressource, la ressource sera stockée dans la base de données via PuppetDB mais rien de correspondant ne sera ajouté dans le catalogue de la node elle-même.

Ensuite, sur la node qui importe (ou collect en anglais dans la documentation de Puppet), on utilise une sytaxe particulière pour l'importation. On utilise une lettre majuscule comme première lettre de chaque élément séparé par :: du nom de la ressource (comme pour référer au type de ressource) et à droite du nom de la ressource qu'on importe, on ajoute le marqueur d'importation <<| |>>:

# Toute machine qui importe les ressource de cette manière recevront
# toutes les clefs publiques ssh qui ont été exportées par d'autres machines
Ssh_autorized_key <<| |>>

Concrètement, lorsque le serveur puppet compile le catalogue d'une node qui utilise la syntaxe de collection plus haut, les ressources seront demandées à la base de données via PuppetDB, puis seront insérées dans le catalogue de la node qui a demandé d'importer les ressources.

Notez que si une ressource exportée n'est jamais importée, celle-ci n'existera pas dans le catalogue d'une machine et donc la ressource ne sera gérée nulle part.

Comme il n'est pas toujours souhaitable de tout importer ce que toutes les machines ont exporté, on peut utiliser une fonctionnalité additionnelle: il est possible de spécifier des filtres lors de l'importation selon les valeurs des paramètres des ressources.

On peut filtrer les ressources selon n'importe quelles valeurs de paramètres.

Un exemple de filtrage pourrait être d'ajouter un tag à la ressouce lors de l'exportation:

@@ssh_authorized_key { 'irirou@machine1234':
  # ... mêmes paramètres que plus haut
  tag     => 'ceci est un tag',
}

On pourra ensuite filtrer sur la valeur du paramètre tag lors de l'importation:

# Importer seulement les ressources avec un tag d'une certaine valeur
Ssh_autorized_key <<| tag == 'ceci est un tag' |>>

On peut filtrer sur n'importe quel paramètre du type de ressource qu'on importe. Référez-vous à la documentation des ressources exportées pour plus de détails.

Ressources virtuelles

Une autre fonctionnalité avancée qui est moins commune, mais qui devient intéressante dans l'élaboration de profiles c'est les ressources virtuelles.

Une ressource virtuelle reste locale à la machine qui l'a déclarée. Par contre, celle-ci n'est pas créée tout de suite dans le catalogue lors qu'elle est rencontrée durant la compilation. Elle sera réalisée (rendue réelle) dans le catalogue seulement à l'utilisation d'une syntaxe spéciale.

Les ressources virtuelles c'est un peu le même concept que les ressources exportées, mais en conservant les informations des ressources en mémoire pendant la compilation du catalogue. Donc on ne peut jamais "importer" une ressource virtuelle sur une autre machine, ça reste toujours sur la même machine où celle-ci a été déclarée.

La syntaxe ressemble beaucoup à celle pour les ressources exportées, mais avec des symboles simples au lieu de doubles. (e.g. @ pour déclarer une ressource virtuelle, et <| |> pour réaliser les ressources virtuelles)

L'intérêt des ressources virtuelles est de permettre d'insérer à d'autres endroits dans le code la gestion d'une configuration qui serait déjà gérée ailleurs dans le code tout en évitant les doublons de ressources pour la gestion d'un même fichier.

C'est particulièrement utile pour "exporter" une configuration vers une autre application comme par exemple pour créer un check de monitoring pour l'application qu'on est en train de configurer mais dans un profile qui ne gère pas le logiciel de monitoring.

Un autre cas similaire serait la création de règles de firewall: un profile qui gère une application réseau comme un site web pourrait injecter des règles de firewall sans pour autant faire la gestion direct des fichiers de configuration du firewall.

Pour déclarer une ressource virtuelle on ajoute le préfixe @ au nom du type de ressource:

$port = 80;
@nftables::rule { "apache-port-${port}":
  ensure  => present,
  content => 'tcp dport ${port} accept',
}

Pour permettre aux autres profiles de définir des règles de firewall avec nftables, le profile nftables doit réaliser les ressources virtuelles en nommant une référence au type de ressource, donc avec une lettre majuscule comme première lettre de chaque élément du nom séparé par ::, et ensuite en ajoutant le marqueur de collection des ressources <| |>:

# Ramasser les règles de firewall des autres profiles
Nftables::Rule <| |>

Notez que que même si elle a été déclarée, si une ressource virtuelle n'est jamais réalisée, celle-ci n'existera pas dans le catalogue et donc la ressource ne sera pas gérée sur la machine client.

Dans certains cas, comme ça l'est pour notre utilisation dans le profile nftables, le délais ajouté à la création des ressources dans le catalogue ne joue pas un rôle directement pour éviter la double gestion d'une ressource. Il faut donc tout de même faire attention aux doublons.

L'imposition d'un délais de création dans ce cas là sert à rendre l'utilisation de certaines ressources optionnelles. Dans le cas du firewall, si on désactive entièrement la gestion du firewall sur une machine à l'aide d'un paramètre de profile, les règles de firewall ne seront par conséquent pas réalisées et donc aucune règle de firewall ne sera gérée.

De la même manière que pour les ressources exportées, on peut ajouter un filtre dans le marqueur de collection de ressources pour filtrer quelles ressources seront réalisées. Egalement similaire au cas des ressources exportées, on peut filtrer sur n'importe quelle valeur de paramètre de ressources.

Voir la documentation des ressources virtuelles pour plus de détails.

Exercices

Configuration d'une application

L'application Limnoria devrait rouler sous un utilisateur différent pour le séparer nettement des autres applications. Un seul utilisateur pourrait avoir plusieurs instances de bot qui roule en parallêle.

  1. Créer une classe dans le module profile pour gérer limnoria

    • la classe devrait avoir un paramètre qui permet de supprimer les instances et désinstaller le package si on n'en a plus besoin (pour ça on utilise généralement le même nom de paramètre que le méta-paramètre ensure)

    • la classe devrait avoir un paramètre qui nous permet de definir zero, une, ou plusieurs instances du bot qui roulent avec le même utilsateur
    • la class devrait créer un utilisateur et groupe pour l'application limnoria. L'utilisateur devrait être du type système avec un home quelque part dans /var/lib.

  2. Créer un type défini pour les instances du bot dans le module profile

    1. Créer la configuration pour une instance de supybot
      1. La configuration doit être placée dans dans un sous-dossier du home de l'utilisateur du bot. Le structure devrait avoir l'air de:

        # tree                                                                                                                                                                                 
        .                                                                                                                                                                                                               
        └── example                                                                                                                                                                                                        
            ├── backup # (directory)
            ├── conf # (directory)
            ├── data # (directory)
            ├── example.conf
            ├── logs # (directory)
            ├── plugins # (directory)
      1. Dans le dossier principal, ajouter un fichier de configuration pour configurer l'instance ( dans la liste plus haut, il y a un fichier d'instance nommé example.conf). Le nom du fichier devrait être le même que le nom de l'intance. Utiliser un template basé sur limnoria-example.conf

        • supybot.flush devrait être False

        • supybot.nick devrait être modifiable

      2. Ajouter un fichier dans le dossier de l'instance conf/users.conf avec un contenu similaire à ce qui suit:

        user 1
          name nicknameofowner
          ignore False
          secure False
          hashed False
          password examplepassword
          capability owner
        • le nom de l'utilisateur (sur la première ligne) et la valeur de password devraient être modifiables. la valeur de password devrait être récupérée de trocla.

        • si un utilisateur est ajouté ou modifié, le bot devrait redémarrer.
      3. En utilisant une ressource définie de type systemd::unit_file (on suppose ici que le type existe déjà -- voir le module systemd), créer un fichier unit de "service" pour le bot

        • Limnoria est normalement démarré avec supybot path/to/configfile.conf

      4. Ajouter une ressource de type service pour que l'instance du bot roule.

        • Si la configuration est modifiée, le bot devrait redémarrer pour prendre la nouvelle configuration en compte

Objectifs:

Rétrospective

Pendant un maximum de 15 minutes, les participant.e.s sont invité.e.s à partager les éléments qui ont bien ou moins bien fonctionnés et les idées qui pourraient survenir pour des manières d'améliorer le processus.

Quelques éléments qui peuvent faire partie de la rétrospective, dépendant de la grosseur du projet ou de la formation:

  • Sommaire collectif (e.g. résumé rapide en termes que tout le collectif peut comprendre)
    • Pas trop utile pour les projets vraiment simples ou les formations.

  • Chronologie des événements marquant pour le projet
    • Surtout utile pour les projet qui se sont étalés sur plusieurs jours ou plus ou bien quand beaucoup de choses se sont produites simultanément, ce qui a rendu la compréhension des influences de chaque événement complexe.

  • Les bons coups -- qu'est-ce qui a bien fonctionné et qu'on veut tenter de reproduire
  • Les problèmes
    • échecs -- avec l'aide de la chronologie (si elle a été faite), tenter de situer les échecs dans un contexte selon ce qui était connu des participant.e.s aux moments qui ont mené à l'échec
    • problèmes techniques
    • manques de ressources
    • perturbations externes
  • Une liste d'actions à court et/ou à plus long terme pour améliorer les choses telles que soulignées pendant les points précédents
    • Transférer les actions dans redmine pour qu'elles puissent être suivies!

N'hésitez pas à partager les échecs à l'extérieur de l'équipe puisqu'on peut apprendre beaucoup de ceux-ci, mais surtout évitez de les formuler comme un blâme sur la/les personne(s) ayant échoué.

Une rétrospective est surtout utile quand on la partage: ça permet aux autres d'apprendre de nos erreurs et aussi de nos idées d'améliorations. On peut par exemple envoyer une forme écrite par email, ou bien sauvegardée comme page wiki.

Information complémentaire


CategoryFormation

FormationPuppetDSL (last edited 2021-12-09 15:10:47 by gabriel)