Aperçu

Formations préalables

Temps estimé

De 1 à 2h

Objectifs

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:

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:

Les fichiers à la racine d'un module:

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:

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:

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):

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

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:

https://shared-puppet-modules-group.gitlab.io/tor/

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:

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:

Dans le fichier init.pp il devrait avoir l'inclusion des 3 classes suivantes avec une ordonnance explicite:

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.

  1. Créer un fichier dans le dossier types, eg. types/reference.pp

  2. 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é.

  1. Créer un fichier ébauche: lib/facter/mymodule.rb selon le nom de votre module

  2. 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

  3. 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

  1. 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
  2. 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
    
  3. Ajouter un Rakefile:

    require 'puppetlabs_spec_helper/rake_tasks'
    defaults = [:validate, :spec]
    
  4. Setup le structure pour les tests
    1. Ajouter les dossiers nécessaires: `mkdir -p spec/fixtures/{modules,manifests}
    2. 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

  1. Connecter sur le pm-buster: vagrant ssh pm_buster

  2. 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

  3. Modifer le fichier manifests/hostmaster.pp pour introduire un erreur

  4. Re-rouler les tests

(Facultatif) Bootstrapper un module avec PDK

  1. Installer PDK: gem install pdk

  2. Créer un module:

    pdk new module modulename
  3. 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


CategoryFormation

FormationPuppetModules (last edited 2021-12-16 12:18:16 by gabriel)