Contents
- Aperçu
- Préparation des participant.e.s
- Préparation de la part de l'animateur.trice
-
Théorie, concepts et information
- Où est-ce qu'on retrouve les modules?
- Structure des répertoires d'un module
- Emplacement spécifique pour que les classes et types définis soient chargés automatiquement
- Comment faire pour qu'un module soit utilisé sur un serveur
- Quelques patterns de code recommandés
- Documentation du code
- Exécuter les tests unitaires d'un module
- Exercices
- Rétrospective
- Information complémentaire
Aperçu
Formations préalables
- Une notion de Ruby, spécifiquement pour les exercices:
- Création d'un fact custom
Gestion du setup du module (surtout pour gérer le contenu du fichier Rakefile du module)
- Ecriture de test unitaire
Temps estimé
De 1 à 2h
Objectifs
- Avoir une bonne idée de la structure (e.g. la hiérarchie de répertoires et de fichiers) d'un module puppet
- Se familiariser avec l'emplacement "auto-load" des fichiers manifests
- Créer un alias de type de données pour un certain module
- Déployer un "fact custom" via un module
- Ajouter et rouler un tests rspec de base dans un module
Préparation des participant.e.s
Pour faire les exercices dans cette formation, nous travailleront à l'aide du serveur puppet et d'un client qui devra appliquer les changements.
Donc toutes les commandes des exercices devront être faites sur le serveur puppet à moins que les exercices ne spécifient de les exécuter sur le client.
Pour les formations internes à Koumbit:
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
Pour les formations externes à Koumbit:
Nous travailleront tout le monde en même temps sur la VM serveur.
Pour coordonner les changements, nous nous rejoigneront dans une session ssh partagée et chaque participant.e pendant les commandes chacun.e à son tour.
Une fois connecté via ssh, utilisez les commandes suivantes pour devenir administrateur puis rejoindre la session partagée:
sudo -i tmux attach -t formation
Préparation de la part de l'animateur.trice
La VM serveur aurait dû être créée par l'organisatrice.teur lors de la FormationPuppetIntroduction, sinon suivre les étapes dans FormationPuppetIntroduction#Pr.2BAOk-paration_de_la_part_de_l.27animateur.trice pour la mettre en place. Les VMs client devraient déjà être connectées au serveur puppet (e.g. certificat client signé).
Les accès au serveur puppet devraient être préparés, soit via un mot de passe partagé, soit via une clef ssh pour chaque participant.e.
La session tmux devrait être créée sous le user root avant la formation:
tmux new -t formation
Théorie, concepts et information
TL;DR:
- Un module puppet c'est un bloc réutilisable d'une personne à l'autre. Donc c'est la manière donc les gens partagent le code pour l'installation et la configuration de différents composantes de systèmes.
- Un module contient le plus souvent du code puppet -- des manifests
On peut aussi y trouver des facts, des fonctions, des types de données et des types de ressources
- Il peut également y avoir des tâches pour automatiser certaines actions ponctuelles avec Puppet Bolt
- Pour pouvoir partager un module avec d'autres personnes, il faut essayer de rendre le code le plus générique et paramétrisable possible
Un site web, forge.puppet.com, centralise le partage de modules. C'est donc un bon endroit pour chercher si d'autres personnes ont déjà codé le nécessaire pour la configuration d'un service. puppet peut également installer les modules qui se trouvent sur ce site de manière automatisée avec la commande puppet module install $foo (n.b. Koumbit utilise cependant un outil différent pour la gestion des dépendances: librarian-puppet. Nous verrons plus de détails sur cet outil et son utilité dans la prochaine formation)
- Le plus souvent, un module partagé au grand public (soit sur forge, ou juste github/gitlab/autre repository git public) a comme point de focus soit:
installer et configurer un seul service (e.g. un daemon, ou bien simplement un outil qu'on peut utiliser sur la ligne de commande)
minimalement configurer un framework du système comme le firewall ou bien les configuration du kernel (par exemple sysctl) et ensuite rendre disponible certaines ressources et/ou fonctions qui permettent à d'autres modules qui gèrent des "services" de configurer le système d'une manière spécifiquement utile pour le service en question.
- rendre disponible certaines extensions plus génériques au langage puppet (e.g. facts, fonctions, types de données) que d'autres modules peuvent ensuite réutiliser
Un module peut dépendre de d'autres modules pour certaines fonctionnalités comme des fonctions, des types de données ou de ressources ou des facts.
On peut déclarer les dépendances à d'autres modules dan le fichier metadata.json, ce qui permet d'automatiser l'installation des dépendances.
C'est généralement fortement recommandé de ne pas ajouter de dépendances à un module qui configure un service autre que celui dont votre module fait l'objet (i.e. un module apache ne devrait pas déprendre d'un autre module nagios puisque nagios est tout un autre service à part entière et il existe plusieurs alternatives à nagios pour ajouter les fonctionnalités de surveillance des services)
- Un module doit utiliser une structure de sous-répertoires précis puisque les fichiers correspondants y sont trouvés et chargés automatiquement.
On peut utiliser l'outil rspec-puppet pour écrire des tests unitaires en ruby
L'outil puppet-strings permet d'utiliser des commentaires formattés d'une certaine façon pour créer de la documentation technique directement à partir du code.
Où est-ce qu'on retrouve les modules?
Par défaut, les modules sont dans un répertoire nommé modules sous le répertoire d'un environnement.
En travaillant avec un control repository, tout ce qui se trouve dans le control repository correspond à un environnement. Donc vous trouverez les modules sous le réperotire modules du control repository.
Il est également possible d'ajouter des répertoires qui contiennent des modules. Cet ajout se fait dans le fichier environment.conf à la racine d'un environnement -- donc à la racine du control repository.
Exercice: Inspecter le fichier environment.conf de l'environnement production sur le serveur puppet pour voir quels sont les noms de répertoires de modules.
cd /etc/puppet/code/environments/production cat environment.conf
Dans le cas de Koumbit, on y retrouve la ligne suivante qui ajoute le répetoire site en plus de modules:
modulepath = modules:site
Koumbit a décidé de placer sous site les modules nommés role, profile et test. Les deux premiers modules servent à organiser le code à l'aide d'un modèle d'abstraction entre la liste des technologies qui remplissent un rôle, et les logiciels et configurations que permettent d'avoir une technologie fonctionnelle (profile). Le modèle d'abstraction ne sera présenté que dans la prochaine formation sur l'organisation du control repository -- mais vous pouvez retenir pour maintenant que role et profile sont des modules puppet.
Exercice: Sur le serveur puppet, investiguez l'emplacement des répertoires de modules pour l'environnement nommé production:
puppet config print modulepath --environment production
Structure des répertoires d'un module
Un module puppet doit utiliser une certaine structure de sous-répertoires pour placer certains éléments et certains fichiers qui remplissent une fonction utile. Un module a cette disposition là:
. ├── data/ ├── examples/ ├── facts.d/ ├── files/ ├── functions/ ├── hiera.yaml ├── lib/ │ ├── facter/ │ └── puppet/ ├── LICENSE ├── manifests/ ├── metadata.json ├── Rakefile ├── README.md ├── spec/ ├── tasks/ ├── templates/ └── types/
Tous les sous-répertoires sont facultatifs et peuvent n'être créés que lorsqu'on a besoin de placer au moins un fichier dans ceux-ci. La majorité du temps, on aura au moins un répertoire manifests, et souvent on accompagnera ça aussi de files et templates.
En ordre de l'arbre plus haut, chaque répertoire a la fonction:
data: contient des fichiers de stockage hiera (le plus souvent en format YAML)
examples: simplement des manifests qui doivent montrer comment utiliser le module. complément au fichier README
facts.d: contient des scripts de facts custom qui sont déployés automatiquement
files: là où on trouve les fichiers quand on utilise le paramètre source des ressources de type file
functions: contient des fonctions écrites en DSL puppet
hiera.yaml: la configuration pour les données hiera en module (data-in-module). Eg., pour définir une hierachie spécifique ou l'emplacement des données pour le module
lib/facter: définitions de facts custom en ruby
lib/puppet: définitions de nouveaux types de ressources en ruby
manifests: contient le code puppet écrit en DSL
spec: tests unitaires
tasks: tâches pour l'outil bolt (puppet lui-même n'utilise pas vraiment ça)
templates: là qu'on trouve les templates utilisés par les fonctions epp() et template()
types: définitions d'alias de types de données
Les fichiers à la racine d'un module:
metadata.json contient de l'information sur le module et ses dépendances et permet de faciliter son installation. Nous verrons un peu plus en détails ce ficheir dans la section #metadata.json
Rakefile est optionnel et sert surtout à faciliter certaines tâches comme l'exécution des tests unitaires.
Il peut y avoir d'autres fichiers facultatifs également, soit pour ajouter plus de documentation ou pour soutenir certains outils en plus qui aident avec la gestion du code, comme puppet-development-kit.
voir la documentation de puppet sur l'organisation des modules: https://puppet.com/docs/puppet/5.5/modules_fundamentals.html
Exercice: Télécharger un module pour en inspecter son contenu.
Nous inspecterons ici le module suivant: https://forge.puppet.com/modules/puppet/nginx
puppet module install puppet-nginx
Note: le nom du module ici est formé du nom du compte sur forge.puppet.com qui a publié le module (ici le compte s'appelle puppet), d'un tiret, puis du nom du module sous ce compte.
Vous pouvez remarquer que cette commande a installé trois modules! Il s'agit du module demandé et de ses dépendances (donc le module nginx utilise les fonctionnalités des deux autres modules pour fonctionner).
Inspectez le contenu du module nginx:
ls /etc/puppet/code/environments/production/modules/nginx
Vous pouvez également inspecter le contenu de chaque sous-répertoire pour constater quel type de fichier est contenu dans chacun.
metadata.json
Le format du fichier metadata.json est défini dans la documentation upstream et peut être validé à l'aide de l'outil metadata-json-lint (disponible comme packagé dans debian à partir de bullseye)
Les champs les plus important du fichier sont:
le nom du module. Le format de ce champs là doit être $username-$modulename où le nom d'utilisateur correspond à l'utilisateur qui a publié le module sur forge.puppet.com (par exemple LeLutin-asterisk correspond à https://forge.puppet.com/modules/LeLutin/asterisk)
la version. on veut le plus possible utiliser des numéros de versions qui suivent le semantic versioning
- les dépendances. il s'agit d'une liste de dictionnaires de données. chaque module est spécifié par un des dictionnaires de données. pour chaque dépendance on doit avoir:
une clef name qui défini le nom d'un module sous le même format que le champs de nom de module plus haut
une clef version_requirement qui défini les limites de versions utilisables de la dépendance. sa valeur est généralement sous la forme d'un ensemble de versions avec les symboles <, <=, > et >=.
Un exemple complet de fichier metadata.json illustre mieux son format:
{
"name": "duritong-munin",
"version": "0.0.4",
"author": "duritong",
"summary": "Puppet module for Munin monitoring",
"license": "Apache License, Version 2.0",
"source": "git://github.com/duritong/puppet-munin.git",
"project_page": "https://github.com/duritong/puppet-munin",
"issues_url": "https://github.com/duritong/puppet-munin/issues",
"description": "Munin is a performance monitoring system which creates nice RRD graphs and has a very easy plugin interface",
"dependencies": [
{"name":"puppetlabs/concat","version_requirement":">= 1.1.0"},
{"name":"puppetlabs/stdlib","version_requirement":">= 3.2.0"},
{"name":"duritong/openbsd","version_requirement":">= 0.0.1"}
]
}
Emplacement spécifique pour que les classes et types définis soient chargés automatiquement
Puppet utilise beaucoup de chargement automatique de code selon l'emplacement des fichiers. Le chargement automatique dépend de l'emplacement des fichiers: il faut savoir où placer les fichiers de code et comment les nommer pour que ceux-ci soient pris en compte automatiquement par puppet.
Par example, pour qu'une classe puppet soit bien chargée, la classe doit être présente sous le dossier manifests/. En plus, le nom du fichier devrait concorder avec le nom de la classe.
Exception: le fichier init.pp peut contenir la classe qui porte le nom du module. On utilise généralement le nom du module comme nom de classe comme point d'entrée dans le code du module -- donc si on veut utiliser le module nginx on utilisera généralement sa class nginx.
Pour une classe nommée apache dans le module apache, le fichier manifets/init.pp devrait contenir:
class apache {
...
}
Toutes les autres classes devraient avoir le nom du module comme premier élément de leur nom. Donc pour une classe nommée apache::module, le fichier manifests/module.pp devrait contenir:
class apache::module {
...
}
On peut également organiser nos classes en sous-répertoires pour mieux classifier les sous-fonctionnalités reliées. Pour faire ça, on ajoute des éléments au nom de la classe après le nom du module qui correspondent aux répertoires qu'on doit parcourir pour trouver le fichier. Le nom du fichier devrait correspondre au dernier élément à droite. Donc pour une classe nommée apache::module::ldap, le fichier manifests/module/ldap.pp devrait contenir:
class apache::module::ldap {
...
}
Les mêmes règles s'appliquent telles quelles pour les types définis, donc pour leur nom, leur emplacement et le nom du fichier qui les contient.
Organisation des autres répertoires du module
Les aliases de types de données doivent aller dans le dossier types/ et les fichiers sous ce répertoire sont du code puppet et sont nommés en suivant les mêmes règles que pour les classes et les types définis.
Les répertoires suivant peuvent être organisés comme il convient le mieux pour le module particulier:
Les templates doit aller dans le dossier templates
Lorsqu'on utilisera un template, son chemin aura automatiquement le bon nom de répertoire d'ajouté après le nom de module spécifié. donc par exemple epp('fail2ban/jail.conf.epp') correspondra au fichier templates/jail.conf.epp sous le module fail2ban.
Les fichiers doit aller dans le dossier files
Le même phénomène que pour les templates s'applique aux fichiers statiques mais dans le répertoire files. Donc si on spécifie à puppet source => 'puppet:///modules/fail2ban/filters/nftables-multi.conf', puppet utilisera alors le fichier files/filters/nftables-multi.conf sous le module fail2ban`.
Pour tout ce qui est écrit en ruby direct, le chemin devra utiliser le mécanisme d'auto-chargement de code de ruby (qui ressemble énormément à celui de puppet, donc les fichiers devraient avoir le même nom que la classe/fonction qui est définie):
Fonctions dans lib/puppet/functions
Types dans lib/puppet/types
Facts dans lib/facter/
Comment faire pour qu'un module soit utilisé sur un serveur
Une fois que vous avez placé le module au bon endroit, et que les fichiers dans le modules sont bien placés pour le chargement automatique, toutes les définitions de classes et de types définis du modules deviennent maintenant disponibles.
Donc pour utiliser un module à ce moment il s'agit tout simplement de déclarer une classe ou une ressource d'un type défini.
Exercice: Configurez nginx sur votre client puppet.
En premier lieu il faut déclarer la classe du module dans le bon fichier pour la node (le client) en question:
cd /etc/puppet/code/environments/production/manifests # Vous pouvez utiliser n'importe quel éditeur de texte disponible au lieu de vim ## pour les formations externes, remplacez "pc_buster.test" par le nom de domaine complet (fqdn) de votre VM client vim pc_buster.test.pp ## ajoutez à l'intérieur du bloc "node" une déclaration de classe pour la classe nommée "nginx".
Ensuite sur votre client puppet, lancez l'agent puppet pour appliquer les changements que vous venez d'apporter:
puppet agent -t
De retour sur le serveur puppet, modifiez le fichier hiera de votre client:
## pour les formations externes, remplacez "pc_buster.test" par le nom de domaine complet (fqdn) de votre VM client vim ../data/pc_buster.test.yaml # Ajoutez les clefs nginx::confd_purge et nginx::server_purge et donnez la valeur true aux deux
Finalement, sur votre client puppet, lancez à nouveau l'agent pour constater les changements que la modification dans hiera a eue sur votre client:
puppet agent -t
Quelques patterns de code recommandés
Le point d'entrée principal, p-e des defined types en plus
On cherche généralement, lors de l'écriture d'un module, à concentrer la gestion des paramètres pour la configuration de l'application le plus possible dans init.pp quand ça a du sens. Pour prévoir des fonctionnalités additionnelles mais facultatives on pourra créer des classes et types définis additionnels. Bien entendu, on peut également toujours séparer le code d'une fonctionnalité en plus petits blocs, donc dans des classes et types définis additionnels, simplement pour améliorer la lisibilité et la maintenance du code.
En général un bon objectif est de pouvoir le plus simplement utiliser le module. L'utilisation de base d'un module pourrait ressembler à:
include mymodule
Le snippet ci-haut devrait automatiquement gérer l'installation du logiciel lui-même mais également de ses dépendences fortes (ce qui est absolument nécessaire pour que le logiciel puisse fonctionner), ainsi que la configuration minimale et également démarrer le(s) service(s) s'il y en a.
Bien entendu, malgré l'utilisation simpliste plus haut, on pourra utiliser hiera pour spécifier les valeurs des paramètres de la classe pour définir la configuration précise pour notre installation. Donc comme exemple dans un fichier YAML on pourrait spécifier la valeur du paramètre some_feature:
mymodule::some_feature: true
Une autre méthode serait de donner explicitement les valeurs des paramètres lors de la déclaration de gestion de cette classe ailleurs dans le code:
class { 'mymodule':
some_feature => true,
}
Note: En forçant la valeur d'un paramètre dans le code comme ci-haut, ce paramètre ne pourra pas être modifié via hiera.
Couper le travail du module en sous-classes et ordonnancer au niveau plus haut
Un pattern fréquemment utilisé pour diviser le travail est de subdiviser le travail en les étapes: "Installation -> Configuration -> Service".
Cette methode est utilisée pour mieux répartir le travail en blocs logiques et également pour aider à planifier l'ordonnancement de haut niveau des ressources déclarées par le module.
Donc le fichier init.pp aura généralement cette allure:
class mymodule (
# params...
) {
contain mymodule::install
contain mymodule::config
contain mymodule::service
Class['mymodule::install']
-> Class['mymodule::config']
~> Class['mymodule::service']
# Notez dans la ligne juste au dessus la flèche zigzag pour que des changements de configuration
# aient comme effet de "notifier" le service, et donc le redémarrer pour rendre les changements
# de configuration effectifs immédiatement
}
Ici on introduit l'utilisation du mot clef contain pour remplacer include. Celui-ci permet de faire l'inclusion d'une classe mais s'assure en même temps que si l'on impose un ordre sur la classe dans laquelle on utilise contain, donc dans cet exemple-ci mymodule, tout ce qui est contenu par les classes install, config et service devra être évalué dans le même ordre que la relation imposée sur mymodule.
Le mot-clef include a la fâcheuse tendance à ne pas imposer l'évaluation des classes incluses dans d'autres classes.
Voir les détails upstream pour le mot-clef contain et son utilité.
Donc pour un exemple concret, lors de l'utilisation suivante de la class mymodule, la ressource exec pourrait être évaluée avant l'installation et la configuration du logiciel sous mymodule si l'on avait utilisé include mymodule::... dans la définition de la classe mymodule
class profile::someprofile {
include mymodule
exec { '/usr/local/bin/populate_database.sh':
# Si la définition de mymodule utilisait include au lieu de contain,
# l'ordre du exec ne serait pas garanti d'être après l'installation
# et la configuration des choses de mymodule.
require => Class['mymodule'],
}
Vieille méthode d'ordonnacement de haut niveau au niveau du module
Dans les vielles versions de puppet, une autre syntaxe était utilisée pour faire le même job que le mot-cléf contain: anchor.
C'est encore visible dans des modules tel que mysql.
Voici un example raccourci de mysql/manifests/server.pp:
class mysql (
# ... params
) {
...
anchor { 'mysql::server::start': }
anchor { 'mysql::server::end': }
if $restart {
Class['mysql::server::config']
~> Class['mysql::server::service']
}
Anchor['mysql::server::start']
-> Class['mysql::server::config']
-> Class['mysql::server::install']
-> Class['mysql::server::managed_dirs']
-> Class['mysql::server::installdb']
-> Class['mysql::server::service']
-> Class['mysql::server::root_password']
-> Class['mysql::server::providers']
-> Anchor['mysql::server::end']
}
L'utilisation des ressources de type anchor n'est plus nécessaire dans puppet 4 et plus. On recommande donc d'utiliser la méthode montrée précédemment pour l'ordonnancement lors de l'écriture de nouveaux modules.
Privilégier l'utilisation de hiera pour les valeurs par défaut des paramètres
À partir de hiera 5, chaque module peut avoir un répertoire data/ qui contient des données hiera. L'utilisation de hiera directement dans un module est généralement appelé data-in-module.
Dans ce répertoire data/, on peut définir les valeurs par défaut des paramètres des classes et types définis du module.
NB: Il est également possible de modifier l'emplacement et la hierarchie pour les données dans un fichier hiera.yaml à la racine du module. L'emplacement par défaut est plus facile à trouver, mais il peut être pratique de définir sa propre hiérarchie pour le module si on veut par exemple obtenir des valeurs par défaut différentes pour des distributions linux différentes.
Pour l'utiliser, ça peut être assez simple: ajouter un fichier common.yaml dans le dossier data du module.
Par exemple, dans le module dovecot data/common.yaml contient:
---
lookup_options:
# Usually you want to REPLACE (not merge) package lists.
'^dovecot::(.*::)?package_name$':
merge:
strategy: first
# Do NOT deep merge poolmon config to allow replacing default values.
"dovecot::poolmon_config":
merge:
strategy: hash
'^dovecot::.*':
merge:
strategy: deep
dovecot::config_path: '/etc/dovecot'
...
Si on veut ajouter des hierarchies pour le module, il est possible de les définir dans hiera.yaml à la racine du module. Eg,
---
version: 5
defaults:
datadir: 'data'
data_hash: 'yaml_data'
hierarchy:
- name: 'Operating System Family'
path: '%{facts.os.family}.yaml'
- name: 'common'
path: 'common.yaml'
Celà nous permet d'ajouter un fichier yaml data/Debian.yaml avec certaines données spécifique à Debian.
Par exmeple dans le module dovecot, on a data/Debian.yaml qui contient:
---
dovecot::package_name: ['dovecot-core']
...
Exercice: Installez le module oxc-dovecot sur le serveur pour inspecter les données dans le module.
puppet module install oxc-dovecot cd /etc/puppet/code/environments/production/modules/dovecot cat hiera.yaml cat data/common.yaml cat data/Debian.yaml cat data/FreeBSD.yaml
Référez-vous à la formation Hiera pour les méthodes de recherches de données qui s'appliquent également dans les fichiers d'un module.
Ancien style: classe de "params"
Avant que le pattern data-in-module existe (e.g. utiliser hiera à l'intérieur d'un module -- comme dans la section précédente), un pattern de code fréquemment utilisé était d'ajouter au module une classe nommée "params" pour gérer les valeurs par défaut des paramètres.
On peut encore présentement (2020) voir ce style dans plusieurs modules qu'on utilise dont apache et php.
Ce pattern utilisait l'héritage de classes pour rendre les valeurs disponibles à la classe principale tout en permettant d'avoir des valeurs par défaut qui pouvaient dépendre de la valeur d'un fact ou d'une configuration.
Le pattern a été largement abandonné en faveur de "data-in-module" pour avoir une séparation encore plus claire entre le code et les valeurs (ce qui était déjà l'objectif du pattern "params" mais qui conservait quand même les valeurs dans un fichier de code).
Eg. manifests/apache.pp:
class apache::params {
$ensure = 'present'
$service_enable = 'enable'
$service_ensure = 'running'
if $facts['osfamily'] == 'RedHat' {
$service_name = 'httpd'
}
elsif $facts['osfamily'] == 'Debian' {
$service_name = 'apache2'
}
}
Puis dans init.pp:
class apache (
Enum['present', 'absent'] $ensure = $apache::params::ensure,
Enum['enable', 'disable'] $service_enable = $apache::params::service_enable,
Enum['running', 'stopped'] $service_ensure = $apache::params::service_ensure,
String[1] $service_name = $apache::params::service_name,
) inherits apache::params {
...
}
N.B Le namespacing dans $apache::params::ensure
On cherche dans la classe apache::params le value du variable ensure
Celà marche pour 2 raisons: la classe apache "hérite" de apache::params et ces deux classes sont dans le même module (apache).
Lorsque vous écrirez de nouveaux modules, il est fortement recommandé d'utiliser le pattern "data-in-module" plutôt que celui-ci. Par contre comme on le retrouve encore dans des modules qui n'ont pas été changé pour utiliser le nouveau pattern, c'est intéressant de le connaître.
Documentation du code
Dans puppet, il est possible et souhaité de créer de la documentation dans le code lui même. C'est fait avec une syntaxe de commentaire special "puppet strings", qui est une extension du module Ruby "strings".
En général les commentaires vont au début d'un fichier, au dessus de la définition des classes, ressources, fonctions et types.
Cette documentation de code peut être transformée en pages HTML qu'on peut alors consulter pour mieux comprendre le code.
Koumbit exporte et publie toute la documentation en HTML sur https://docs.koumbit.net/puppet/production -- ce site est privé et accessible seulement pour l'équipe sysadmin de Koumbit.
Description sommaire pour les classes et types définis
Dans les fichiers de définitions de classes et types définis on veut toujours commencer avec une sommaire bref de la fonctionnalité apportée. Le sommaire commence par le marqueur @summary.
Après la ligne de sommaire, on peut ajouter des descriptions plus détaillées.
On peut également ajouter des encadrés pour faire sortir certaines notes. Le marqueur @note peut être utilisé pour ça. On doit normalement incrémenter de deux espaces les lignes après la première ligne de la note.
On peut aussi indiquer que certaines fonctionnalités ne sont pas encore implémentées à l'aide du marquer `@note@. De la même manière, les lignes suivant la première doivent être incrémentées de deux espaces.
Par exemple:
# @summary Create a group for Koumbit admins and add local users
#
# Those users are meant to have administrative access to all of the servers.
# We create local users instead of relying on LDAP for this particular set of
# users so that if our LDAP setup ever has issues, we can still login to
# machines and perform operations to make them functional again.
#
# @note IMPORTANT: Please keep in mind that this profile needs to be the first
# in ordering when used in roles. Since this profile manages the root user
# and lots of things seem to create implicity dependencies to the root user,
# placing this profile anywhere later than this might get you an error about
# a dependency cycle.
#
# @todo This approach still needs to be verified! We've had some annoying
# issues trying to access systems while LDAP was down: this is caused by the
# fact that everything on the system starts timing out and the whole system
# is slowed down to a crawl.
#
Examples de l'utilisation d'une classe ou d'un type défini
Une des choses le plus utiles est d'avoir des exemples d'utilisations pour les cas majeures d'utilisation.
On peut utiliser le marqueur @example pour celà. La première ligne contient une brève description de l'exemple puis toutes les lignes subséquentes, incrémentées de deux espaces, forment le code en exemple:
# @example Using this profile. All data should be fetched from hiera
# include profile::admins
#
# @example Dataset of users. Create one and delete one.
# ---
# # This can be placed in common.yaml for Koumbit's sysadmins, or it could be
# # added to any node yaml to create admins for clients.
# profile::admins::users:
# phil:
# ensure: present
# uid: 9000
# bob:
# ensure: absent
# uid: 3333
Documentation des paramètres
Pour chaque paramètre d'une classe ou d'un type défini il est utile d'ajouter de la documentation.
Normalement les valeurs par défaut ainsi que le type de données sont pris directement du code. Il s'agit donc surtout de documenter qu'est ce qui change lorsqu'on l'utilise et dans quels cas c'est utile de modifier sa valeur.
Le marqueur @param est utilisé pour documenter un paramètre. La première ligne contient le nom du paramètre qu'on documente puis les lignes subséquentes, incrémentées de deux espaces, forment le texte de documentation du paramètre:
# @param root_password
# The root user's password: applied only if manage_root_password is true.
# This can take any form that is accepted for this field in the `/etc/shadow`
# file. If you want to set a password, you will likely want to set this
# parameter to a salted hash.
#
# By default, the password for the root user is disabled ('!') if the machine
# is an VPS. This makes it 'slightly' harder to root the machine. Note however
# that somebody could still mount the drives during a reboot to change the
# root password and takeover the machine, unless the drives are encrypted.
#
# If the machine is a physical device (is_virtual evaluates to false), a root
# password is generated in the key ${facts['fqdn']}_root_password using trocla
# and set. After provisioning a new physical machine, please go in to trocla
# and remove the plain text password, saving it into the password manager.
#
Générer des pages HTML avec la documentation du code
Une fois que notre code est documenté, on peut générer des pages HTML qu'on peut alors utiliser comme site web de documentation technique.
Pour générer les pages HTML:
cd /etc/puppet/code/environments/production/modules/formation puppet strings . ls docs
Exercice: Visiter un site web de documentation générée par puppet-strings pour voir ça ressemble à quoi:
Exécuter les tests unitaires d'un module
Chaque module peut contenir des tests unitaires pour prévenir les régressions et les bugs inutiles introduits pendant une modification.
Une abstraction par dessus la librairie rspec de ruby a été créée spécialement pour puppet: rspec-puppet. Cette librairie là permet de décrire (avec du code ruby formatté d'une façon spéciale) des scénarios d'utilisation de classes et de types définis avec certaines valeurs de paramètres, et ensuite de vérifier si certaines ressources attendues se retrouvent dans un catalogue compilé avec cet appel de classe ou de type défini.
Les tests unitaires d'un module se trouvent sous le répertoire spec/.
Pour exécuter les tests unitaires d'un module, il faut avoir les outils nécessaires d'installés sur notre ordinateur:
puppet
rspec-puppet
rake
bundle
Sur debian, il est possible de les installer avec les commandes suivantes:
sudo apt install puppet ruby-rake bundler
On peut généralement utiliser la commande suivante, en étant placé au répertoire "le plus haut" dans la hiérarchie du module (e.g. le répertoire du module lui-même, pas un sous-répertoire) pour les exécuter:
Avec bundler:
bundle exec rake spec
Sans bundler:
rake spec
Pour certains modules il est possible de rouler rspec spec directement, mais souvent soit bundle exec rake spec soit rake spec sont nécessaires pour mettre en place tout les dépendences.
Un truc qu'on peut utiliser pour éviter d'avoir à installer plein d'outils sur notre propre ordinateur c'est d'utiliser le serveur puppet de développement local, pm_buster.
L'écriture de tests unitaires pour un module fera l'objet d'une autre formation.
Exercices
Créer un nouveau module
Choisir une utilitaire ou application que t'aimerais gerer avec puppet. Des applications exemples incluent: un serveur web (apache, nginx, etc.), mariadb.
Créer un nouveau module manuellement dans le dossier site du control-repo. Le module devrait avoir minimalement ces fichiers:
README.md
metadata.json
LICENSE
manifests/init.pp
Dans le fichier init.pp il devrait avoir l'inclusion des 3 classes suivantes avec une ordonnance explicite:
install
config
service (si l'application roule comme une service)
La commande metadata-json-lint doit passer.
Créer un alias de type de données
Voici un example, mais si votre module a une donnée qui doit prendre une forme précise vous pouvez l'utiliser plutôt que l'example.
On a une structure de données qu'on aimerait faire une certaine validation dessous pour empêcher qu'utilisateur ou utilisatrice oublie les champs obligatoires. Voici un exemple:
$reference = {
'title' => 'Art of Fermentation',
'author' => 'Katz'
'page' => 326,
'comment' => '',
}
Le title, author et page sont obligatoire, mais le comment ne l'est pas. Si la référence est un paramètre à la class, ça serait longue de le définir tout dans le type ou trop flou eg. Hash. Si on veut faire référence au type dans plusieurs endroits, eg. les templates on doit re-écrire la déscription du type là aussi.
Pour faciliter la maintenant et le checking on peut définir le type dans un endroit (type alias) et l'utiliser par la suite dans le paramètres et templates.
Créer un fichier dans le dossier types, eg. types/reference.pp
Définir le type en puppet, avec le mot cléf type. Eg.
type Mymodule::Reference = Hash[String, Struct[{ # put your field here }] ]
Ensuite, dans la paramètre de votre class, vous pouvez l'utilisez, eg.
class mymodule (
Mymodule::Reference $references = {},
) {
...
}
Créer un fact custom
Pour l'application que vous avez installé dans l'exercise précédante, écrire un fact custom pour la version de l'application qui est installé.
Créer un fichier ébauche: lib/facter/mymodule.rb selon le nom de votre module
Ajouter une version "non-structured", eg. "myapp_version" en ajoutant une fonction ruby. Pour aider voici une ébauche:
Facter.add("myapp_version") do setcode do # Put your code here end end
La fonction %x{...} peut être utiliser en ruby pour execture une commande shell
- Ajouter une version "structure" du fact, eg. "myapp" qui inclus un cléf "version" en ajoutant une fonction ruby.
Le module json peut être inclus pour faciliter l'output.
La fonction JSON.generate prend un hash ruby pour créer l'output
Ecrire un test de base rspec: verifier que vos modules compile
Setup de base
Installer les dépendences pour rouler les tests:
sudo su apt install ruby-rspec rake ruby-bundler # À cause d'une lacune dans le package puppet de Debian, 'puppet' et 'hiera' ne sont pas disponibles comme gem même si les packages sont installés gem install puppet -v 5.5.10 gem install hiera -v 3.2.0 gem install facter -v 3.11.0
Ajouter un Gemfile de base, eg.
gem 'puppet' gem 'rake' group :test do gem 'metadata-json-lint' gem 'puppetlabs_spec_helper' gem 'rspec-puppet-facts' end
Ajouter un Rakefile:
require 'puppetlabs_spec_helper/rake_tasks' defaults = [:validate, :spec]
- Setup le structure pour les tests
- Ajouter les dossiers nécessaires: `mkdir -p spec/fixtures/{modules,manifests}
Ajouter un classe "helper" spec/spec_helper.rb:
require 'puppetlabs_spec_helper/module_spec_helper' require 'rspec-puppet' fixture_path = File.join(File.dirname(File.expand_path(__FILE__)), 'fixtures') RSpec.configure do |c| c.module_path = File.join(fixture_path, 'modules') c.manifest_dir = File.join(fixture_path, 'manifests') c.manifest = File.join(fixture_path, 'manifests', 'site.pp') c.environmentpath = File.join(Dir.pwd, 'spec') end
Ecriture d'un test
Les tests pour les classes vont dans des fichiers Ruby tel que spec/classes/mymodule__classname_spec.rb
Un contenu tres basique a l'air de:
require 'spec_helper'
describe 'mymodule::classname' do
# If you need to set params
# let(:params) {
# {
# 'a' => 'something'
# }
# }
# If you need to set some facts
# let(:facts} {
# {
# :operatingsystem => 'Debian'
# }
# }
# A compilation test
it { is_expected.to compile.with_all_deps }
end
Pour rouler les tests, à partir de la racine du module:
rake spec
ou
bundle exec rake spec
Rouler les tests rspec d'un module
Ici on présume que les packages et gems sont téléchargés à partir du exercise précédante
Connecter sur le pm-buster: vagrant ssh pm_buster
Rouler les tests: bundle exec rake spec ou rake spec
bundle exec roule la commande avec les versions dans le Gemfile.lock
N.B. normalement on peut aussi rouler rspec spec, mais la commande rake spec fait certaines choses pour mettre les dependences en place pour tester
Modifer le fichier manifests/hostmaster.pp pour introduire un erreur
- Re-rouler les tests
(Facultatif) Bootstrapper un module avec PDK
Installer PDK: gem install pdk
Créer un module:
pdk new module modulename
- Répondre aux questions
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
Module fundamentals: https://puppet.com/docs/puppet/5.5/modules_fundamentals.html
Beginner's guide to modules: https://puppet.com/docs/puppet/5.5/bgtm.html#concept-1345
Rspec puppet: https://rspec-puppet.com/
Info sur Puppet PDK: https://puppet.com/docs/pdk/1.x/pdk.html
Merge behaviours for hiera data: https://puppet.com/docs/puppet/5.5/hiera_automatic.html#merge-behaviors
Where do modules go? https://puppet.com/docs/puppet/5.5/dirs_modulepath.html
- Formations suivantes: