Contents
- Aperçu
- Préparation des participant.e.s
- Théorie, concepts et information
- Exercices
- Rétrospective
- Information complémentaire
Aperçu
Formations préalables
Temps estimé
Entre 1h30 et 2h
Objectifs
- Connaître les différentes structures de base pour représenter les configurations d'une machine
Se familiariser avec la nature déclarative du langage
- Savoir utiliser des variables pour réutiliser certaines valeurs dans différentes ressources
- Savoir où se trouve le contenu des fichiers dans différents cas
- Connaître quelques structures de langage qui permettent d'exprimer une configuration de façon plus succinte
- Explorer les ressources communiquées entre différentes parties du catalogue d'une même machine et aussi communiquées d'une machine à d'autres
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:
Puppet utilise un langage de programmation dédié (un Domain-Specific Language (DSL) en anglais)
Le langage puppet utilise majoritairement le paradigme déclaratif, et donc ce n'est pas un langage procédural ni un langage orienté objet
Certains éléments dépendent tout de même de l'ordre d'exécution, comme les variables. Voir: https://puppet.com/docs/puppet/5.5/lang_summary.html#ordering
Le but du langage de puppet est de représenter l'état souhaité d'une machine. Donc lorsqu'on gère un fichier ou un service, on dicte la présence et le contenu de ceux-ci.
- Le langage utilise un jargon bien à lui
Les fichiers de code sont appelés des manifestes
Les objets qui représentent le contenu d'une machine sont appelées (tant ceux de base que les conteneurs plus haut niveau) sont appelés des ressources
- Les conteneurs qui peuvent être utilisés s'appellent:
classe: un conteneur qui ne peut être présent qu'une fois
type défini ("defined type"): un conteneur qui peut être présent plusieurs fois avec des valeurs différentes.
- Les conteneurs qui peuvent être utilisés s'appellent:
- Lors de l'exécution, l'agent puppet:
récupère des informations sur la machines qui sont appelés des faits (des "facts")
récupère une version compilée des instructions contenues dans les manifests qui s'appliquent directement à cet agent, qui est appelé un catalogue
Les ressources conteneurs peuvent avoir des paramètres et des types de données peuvent être utilisés pour encâdrer les types de valeurs qu'un paramètre peut prendre
Le code peut être organisé en blocs logiques réutilisables, appelés des modules
- les modules ne seront pas utilisés pendant cette formation. c'est l'objet d'une prochaine formation.
Le contenu des fichiers peut dépendre de certaines variables. Pour réaliser ça, on utilise des templates
Les ressources peuvent avoir une relation d'ordre entre elles pour forcer l'inclusion d'une certaine ressource avant une autre sur la machine.
La propriété la plus importante de l'outil c'est d'être idempotent - c'est à dire que les résultats devraient être pareils peu importe combien de fois on exécute le code.. https://fr.wikipedia.org/wiki/Idempotence#En_informatique
L'utilisation d'un langage déclaratif découle du choix de rendre cette propriété importante.
Tous les objets doivent porter des noms uniques pour un type de ressource donné. Eg. il ne peut pas avoir deux déclarations de package { 'openssh-server': } dans un catalogue compilé.
Cette formation ne couvrira pas tous les éléments du langage puppet parce qu'il y en a beaucoup trop. Pour approfondir vos connaissances, il est recommandé de lire toute la documentation sous https://puppet.com/docs/puppet/5.5/lang_summary.html et toutes les sections connexes dans le sous-titre "The Puppet Language" dans le menu à gauche.
Où est exécuté le code?
L'ordre d'exécution des choses c'est:
- L'agent démarre sur une node
Facter roule sur la node pour generer les faits. Les faits "custom" (dans /etc/facter/facts.d) sont exécutés aussi.
- Les faits sont envoyé au serveur (master) puppet: nom de la node, environnement souhaité, certificat, etc.
- Le master compile les manifestes en catalogue
- 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
- Le code des manifestes et modules est compilé avec l'aide de ces données
- Le catalogue est envoyé à l'agent
- L'agent (du côté de la node) verifie l'état des ressources défini dans le catalogue et éxécute les changements nécessaires si des différences sont remarquées
- Un rapport d'exécution est envoyé au serveur puppet
- Le serveur puppet envoie le rapport d'exécution à PuppetDB pour qu'il soit stocké
Donc sont évalués:
- sur le serveur puppet:
- fonctions
- à noter: trocla est exécuté pendant les appels de fonctions
- récupération de données de PuppetDB
- récupération des données de Hiera
- l'inclusion de la déclaration des ressources dans le catalogue
- fonctions
- sur la node:
- les facts
- les changements aux ressources qui découlent du catalogue
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:
- une application installée
- un service démarré ou non
- le contenu d'un fichier
- une entrée dans une base de données
- un compte utilisateur
- etc.
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:
un nom de type de ressoure, tout en minuscule. ici le type c'était package
- une accolade ouvrante
un nom donné à la ressource qui doit être unique pour le même type de ressource et suivi d'un deux-points. Dans plusieurs cas, le nom de la ressource représente soit le nom de ce qui doit être installé ou géré, soit le nom d'un fichier (ou une partie représentative du nom) sur disque qui sera créé)
- le nom d'une ressource doit toujours être une chaîne de texte, donc entre guillets ou apostrophes
- on peut utiliser une variable comme une partie du nom de la ressource pour générer un nom de ressource à l'aide d'une variable. voir la section plus tard dans la formation sur comment utiliser une variable à l'intérieur d'une chaîne de texte.
- on peut cependant utiliser une variable qui contient une chaîne de texte comme entièreté du nom, et dans ce cas là on n'a pas besoin d'entourer la variable de guillemets
on ne peut donc pas déclarer deux ressources du type package avec le même nom gnutls-bin: celà constitue une erreur dans puppet
- ce qui doit être unique c'est la paire du nom de type et du nom (ou identifiant) de ressource
on peut donc avoir deux ressources de types différents avec le même nom. Un exemple qui arrive souvent c'est de trouver un package et un service ayant le même nom
- le nom d'une ressource doit toujours être une chaîne de texte, donc entre guillets ou apostrophes
- une liste de paramètes, ou méta-paramètres (voir plus bas pour ce détail), avec leurs valeurs
- les paramètres doivent être séparés par un virgule. aussi, si le dernier paramètre a une virgule à la fin de sa ligne ça n'est pas un erreur: donc pour rendre les modifications plus simple il est généralement recommandé d'ajouter une virgule à la fin de la ligne d'un paramètre dans tous les cas.
- une accolade fermante
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:
- retirer manuellement les choses maintenant inutiles sur le système via SSH
modifier la ressource pour demander à puppet de retirer l'élément du système. Dans le cas du package on a fait celà en modifiant la valeur du paramètre ensure de "present" à "absent".
- s'arranger pour "purger" les ressources non déclarées dans les manifests, c'est à dire demander à puppet de retirer toutes les ressources présentes sur le système mais qui n'ont pas de déclaration de gestion dans le code puppet.
- attention, ceci ne peut pas s'appliquer dans tous les cas. Certains types de ressources, surtout ceux qui ont été créés par des utilisatrices.teurs n'ont pas de mécanisme de retrait
- pour plusieurs types de ressources comme les packages et les services c'est une très mauvaise idée de purger tout ce qui n'est pas connu par puppet: certains packages ou services font partie du système de base et il serait indésirable, voir impraticable de gérer toutes les ressources présentes sur chaque système via le code.
- un cas où cette pratique peut être utile c'est de demander à puppet d'effacer tous les fichiers inconnus dans un répertoire
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:
require: cette autre ressource devra être évaluée avant celle-ci
before: cette autre ressource sera évaluée après celle-ci
notify: esentiellement la même chose que before mais lorsqu'une modification est appliquée à cette ressource-ci, toutes les autres ressources nommés dans ce lien recevront un message de "refresh" (e.g. pour redémarrer les services ou ré-exécuter des commandes)
subscribe: essentiellement la même chose que require, mais lorsqu'une modification est appliquée à une des ressources préalables, cette ressource-ci recevra un messge de "refresh"
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:
package: une application installée via un "paquet"
file: un fichier sur disque
service: un service, ou "daemon" qui doit ou non être opérationnel
user: un compte utilisateur.trice sur la machine
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:
Après le type de conteneur, soit class ou define tout en minuscules, on retrouve tout de suite le nom de la classe ou type défini sans guillemets et avant l'accolade ouvrante.
- On peut avoir une définition de paramètres entre parentèses avant l'accolade ouvrante
dans le cas d'un type défini la déclaration de paramètres est obligatoire
- Chaque paramètre comporte, en ordre:
- un type de données (facultatif)
un nom de paramètre précédé d'un signe de $
- un signe d'égalité suivi d'une valeur par défaut (facultatif)
- une virgule pour délimiter la fin du paramètre
Ce qui se trouve entre les accolades est un bloc de code puppet, et non une liste de paramètres
- Dans ce bloc de code, on peut déclarer des ressources comme on l'a fait jusqu'à maintenant
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:
$title: correspond toujours à l'identificateur de ressource (e.g. la "string" présente après l'accolade ouvrante et avant les paramètres)
$name: par défaut contient la même valeur que $title mais peut être redéfinie via le méta-paramètre name pour contenir une valeur plus utile
on utilise généralement plus $name que $title pour la flexibilité de redéfinition ajoutée.
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.
Dans le cas d'une liste, la valeur donnée à l'opérateur [] doit être le numéro d'indice de l'élément, commençant par 0
Dans le cas d'un dictionnaire de données, la valeur données à l'opérateur [] doit être le nom de la clef de l'élément
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:
https://puppet.com/docs/puppet/5.5/lang_data_array.html#accessing-values
https://puppet.com/docs/puppet/5.5/lang_data_hash.html#accessing-values
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
$facts, un dictionnaire qui contient tous les faits déterminés par l'agent au début de l'application.
$environment: le nom de l'environnement qui est utilisé par le catalogue compilé
certaines valeurs de configuration dans le fichier puppet.conf, comme $vardir
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:
epp()
- Évaluer un template de type "Embedded Puppet" avec l'aide d'une série de variables et retourner le contenu résultant
C'est une des fonctions les plus utiles: on peut utiliser le retour de epp() comme contenu de fichier dans une ressource de type file pour créer des fichiers de configuration qui ont un contenu variable.
join()
- concaténer les valeurs d'une liste avec un séparateur
map()/reduce()/filter()
- transformer le contenu d'une liste ou d'un "hash"
warn()/fail()
- envoyer un message à l'agent ou bien faire planter intentionnellement la compilation
versioncmp()
- comparer deux strings qui contiennent des numéros de versions et déterminer laquelle est la plus élevée
Le module stdlib contient également plusieurs fonctions utiles, dont:
ensure_resources()
- créé des ressources à l'aide d'une structure de données variable (un "hash")
pick()
choisi la première valeur dans une liste qui n'a pas le type Undef
bool2str()
- retourne une de deux strings selon la valeur booléenne d'une variable
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:
<% ... %> Du code à la syntaxe du DSL puppet sera exécuté à cet endroit. Les balises et le code ne seront pas contenus dans le fichier résultant
- Le code contenu entre les deux balises peut s'étaler sur plusieurs lignes
On peut retirer les espaces avant et après la balise sur la même ligne que les balises en modifiant les balises pour ajouter un tiret -
<%- ouvre une balise de code et tous les espaces à sa gauche de la balise seront retirés du contenu du fichier
-%> ferme une balise de code et tous les espaces à droite de la balise jusqu'à la fin de la ligne ne seront pas contenus dans le fichier
<%# ... %> Le contenu de cette balise est un commentaire
<%= ... %> L'expression contenue entre ces balises doit retourner une valeur. Cette valeur sera insérée dans le contenu du fichier. On peut donc utiliser ces balises pour inscrire le contenu d'une variable dans un fichier de configuration.
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.
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.
Créer un type défini pour les instances du bot dans le module profile
- Créer la configuration pour une instance de supybot
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)
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
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.
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
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
- Créer la configuration pour une instance de supybot
Objectifs:
avoir une instance de limnoria qui roule et se connecte au canal #koumbit-test sur le réseau irc.indymedia.org
- avoir une deuxième instance de limnoria qui roule avec des configurations différentes de la précédente
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
- ressources virtuelles, ressources exportées et collecteurs
namespace: champs de disponibilité des variables
lambda, des fonctions sans noms données en paramètres à certains autres fonctions
Quand vous avez terminer l'exercise, il y une implementation exemple disponible limnoria-example.tgz