Git: créer une branche vide

Dans certains cas il peut être utile de créer une nouvelle branche ne contenant aucun fichier, par exemple pour des contenus par environnement.

Git permet de créer des branches ne dépendant d'aucun parent, ce sont des branches orphelines. Pour créer une branche orpheline il suffit de passer l'argument orphan au checkout.

git checkout --orphan BRANCHNAME

L'orphelin va néanmoins avoir préparé les fichiers de la branche source pour les commiter. Supprimez les simplement avec la commande git suivante:

git rm -rf .

Utiliser Gitlab Pages comme repository Debian

Les Gitlab Pages (et Github Pages) permettent via un processus de construction d'artefact de publier un site web au travers d'un pipeline.

Un repository Debian étant simplement constitué de fichiers statiques, nous pouvons donc faire un rapprochement entre les pipelines de build et un repository Debian.

Pour réaliser notre repository Debian hébergé sur des gitlab pages nous aurons besoin:

  • Du conteneur docker debian:stretch
  • De GPG pour la signature du repository
  • De reprepro pour créer notre repository
  • D'un repository git hébergé sur un Gitlab

Initialisation du repository

Créer un répertoire sur votre machine et initialisez le lien avec Gitlab:

mkdir debianrepo
cd debianrepo
git init
git remote add origin git@gitlab.com:nerzhul/debianrepo.git

Génération de la clef GPG

Dans un premier temps nous aurons besoin d'une clef GPG pour notre repository. Générez une clef:

gpg --gen-key

On va ensuite exporter la clef publique et la clef privée dans notre repository local:

mkdir repo_meta
gpg --armor --output repo_meta/gpg-public.key --export BDB31B849949AD389C76573EF02015F1E3A69CF2
gpg --export-secret-keys -a BDB31B849949AD389C76573EF02015F1E3A69CF2 > repo_meta/gpg-private.key

Note: exposer la clef privée dans le repository n'est pas recommandé, il est plus judicieux d'utiliser les secret variables de Gitlab pour stocker le contenu de la clef privée. Il s'agit juste de simplifier l'exercice ici

Création du fichier de définition du repository

Créez un fichier repo_distributions dans le répertoire repo_meta. Celui-ci nous servira plus tard, lors de la création du repository. Notez bien que la ligne SignWith contient le fingerprint de la clef GPG précédemment créée

Origin: Debian
Label: Debian
Codename: stretch
Architectures: amd64
Components: main
Description: Apt repository for the posterity
SignWith: BDB31B849949AD389C76573EF02015F1E3A69CF2

Création du pipeline de construction du repository

Le pipeline de repository s'appuie sur le fichier .gitlab-ci.yml de Gitlab. Voici le fichier que nous allons utiliser, nous le détaillerons juste après:

---
image: debian:stretch

stages:
  - package
  - deploy

variables:
  DEBIAN_VERSION: "stretch"
  ETCD_VERSION: 3.2.7
  RUNDECK_VERSION: 2.9.3-1

package:etcd:
  stage: package
  before_script:
    - find etcd -type d -exec chmod 755 {} \;
    - find etcd -type f -not -name postinst -exec chmod 644 {} \;
    - find etcd -type f -name postinst -exec chmod 755 {} \;
    - mkdir -p build/ dist/
    - apt-get -qy update > /dev/null
    - apt-get -qyy install wget > /dev/null
  script:
    - sed -i 's/%%ETCD_VERSION%%/'${ETCD_VERSION}'/g' etcd/DEBIAN/control
    - mkdir -p etcd/usr/bin/
    - cd build/
    - wget https://github.com/coreos/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz
    - tar xzf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz
    - cp etcd-v${ETCD_VERSION}-linux-amd64/etcd etcd-v${ETCD_VERSION}-linux-amd64/etcdctl ../etcd/usr/bin/
    - cd ..
    - dpkg-deb -b etcd/
    - mv etcd.deb dist/
  artifacts:
    when: on_success
    expire_in: 1 week
    paths:
      - dist/*

package:rundeck:
  stage: package
  before_script:
    - apt-get -qy update > /dev/null
    - apt-get -qyy install wget > /dev/null
    - mkdir dist/
  script:
    - cd dist/
    - wget http://dl.bintray.com/rundeck/rundeck-deb/rundeck-${RUNDECK_VERSION}-GA.deb
  artifacts:
    when: on_success
    expire_in: 1 week
    paths:
      - dist/*

pages:
  stage: deploy
  only:
    - master
  dependencies:
    - package:etcd
    - package:rundeck
  before_script:
    - apt-get -qy update > /dev/null
    - apt-get -qyy install reprepro > /dev/null
    - gpg --import repo_meta/gpg-private.key
  script:
    - mkdir -p public/conf
    - cp repo_meta/repo_distributions public/conf/distributions
    - cp repo_meta/gpg-public.key public/repo.key
    - cd public
    - reprepro includedeb ${DEBIAN_VERSION} ../dist/*.deb
  artifacts:
    when: on_success
    expire_in: 1 year
    paths:
      - public

Notre pipeline se décompose en 2 phases:

  • Construction des paquets du repository
  • Création et déploiement du repository

La phase de déploiement nécessite de connaître les paquets Debian construits. Par convention les paquets Debian construits seront placés dans le répertoire dist/ qui sera transféré à la phase de déploiement des Gitlab Pages.

Nous avons ici 2 paquets différents, Etcd et Rundeck qui sont 2 outils libres. Le premier ne fournit pas de paquet Debian, le second oui.

Création des paquets

Commençons par Rundeck, le plus simple.

package:rundeck:
  stage: package
  before_script:
    - apt-get -qy update > /dev/null
    - apt-get -qyy install wget > /dev/null
    - mkdir dist/
  script:
    - cd dist/
    - wget http://dl.bintray.com/rundeck/rundeck-deb/rundeck-${RUNDECK_VERSION}-GA.deb
  artifacts:
    when: on_success
    expire_in: 1 week
    paths:
      - dist/*

Dans la section __before_script__, nous mettons ce qui ne concerne pas le build, ici l'installation de wget pour télécharger le paquet.

Nous nous plaçons ensuite dans le répertoire dist et téléchargeons la version de Rundeck spécifiée dans les variables du CI, plus haut.

Les artefacts intermédiaires sont ensuite sauvegardés pour 1 semaine. Pour rappel on peut télécharger les artefacts depuis l'interface de Gitlab sur chaque pipeline.

Passons maintenant au paquet etcd:

package:etcd:
  stage: package
  before_script:
    - find etcd -type d -exec chmod 755 {} \;
    - find etcd -type f -not -name postinst -exec chmod 644 {} \;
    - find etcd -type f -name postinst -exec chmod 755 {} \;
    - mkdir -p build/ dist/
    - apt-get -qy update > /dev/null
    - apt-get -qyy install wget > /dev/null
  script:
    - sed -i 's/%%ETCD_VERSION%%/'${ETCD_VERSION}'/g' etcd/DEBIAN/control
    - mkdir -p etcd/usr/bin/
    - cd build/
    - wget https://github.com/coreos/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz
    - tar xzf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz
    - cp etcd-v${ETCD_VERSION}-linux-amd64/etcd etcd-v${ETCD_VERSION}-linux-amd64/etcdctl ../etcd/usr/bin/
    - cd ..
    - dpkg-deb -b etcd/
    - mv etcd.deb dist/
  artifacts:
    when: on_success
    expire_in: 1 week
    paths:
      - dist/*

Pour etcd nous construisons un paquet Debian depuis des binaires fournis par les développeurs. Le répertoire etcd contient l'arborescence minimale du paquet, à savoir la configuration, l'unit system et les fichiers control et postinst de Debian.

Dans l'étape __before_script__ nous avons également besoin de wget, mais également de corriger les permissions sur le répertoire etcd du repository, le Gitlab CI changeant les droits sur les fichiers. Je n'ai pas réussi à comprendre pourquoi, celles-ci sont correctes dans git.

L'étape de build se décompose ensuite de la manière suivante:

  • On modifie le fichier control du paquet afin de spécifier la version du paquet etcd
  • On télécharge l'archive etcd du release concerné, qu'on extrait
  • On copie les binaires nous intéressant dans le paquet dans /usr/bin
  • On construit le paquet avec dpkg-deb
  • On bouge le paquet dans le répertoire dist

Enfin, tout comme pour Rundeck, on archive l'artefact.

Création du repository Debian et publication

Notre dernière tâche consiste à créer le repository Debian pour l'exposer via les Gitlab Pages

pages:
  stage: deploy
  only:
    - master
  dependencies:
    - package:etcd
    - package:rundeck
  before_script:
    - apt-get -qy update > /dev/null
    - apt-get -qyy install reprepro > /dev/null
    - gpg --import repo_meta/gpg-private.key
  script:
    - mkdir -p public/conf
    - cp repo_meta/repo_distributions public/conf/distributions
    - cp repo_meta/gpg-public.key public/repo.key
    - cd public
    - reprepro includedeb ${DEBIAN_VERSION} ../dist/*.deb
  artifacts:
    when: on_success
    expire_in: 1 year
    paths:
      - public

L'étape du pipeline doit obligatoirement se nommer pages par convention de Gitlab et le livrable du repository doit se situer dans le répertoire public.

Nous ajoutons nos deux étapes de construction de paquets en dépendances, afin d'avoir les artefacts à disposition pour cette étape, puis nous installons reprepro qui va nous permettre de construire un repository Debian facilement, et nous importons la clef GPG privées dans le trousseau du conteneur de build.

La construction du repository se fait ensuite de la manière suivante:

  • Copie du fichier repo_meta/repo_distributions vers public/conf/distributions, utilisé par reprepro pour construire le repository
  • Copie de la clef GPG publique vers public/repo.key afin de la distribuer à nos clients
  • En se positionnant dans le répertoire public, inclusion de l'ensemble des fichiers deb du répertoire dist/ dans le repository

Enfin nous archivons le repository pour une durée de 1 an, afin d'assurer sa pérennité dans le temps.

Vous pouvez maintenant commiter et pousser tous les changements

Résultat

Allez maintenant sur votre repository Gitlab dans la section pipelines, vous devriez avoir un résultat similaire:

Vous savez désormais construire une repository Debian en ajoutant dynamiquement des paquets construits à partir d'un pipeline Gitlab. Je vous invite à consulter le repository ayant servi de PoC puis de référence pour cet article:

PostgreSQL: fermer toutes les connexions sur une base de données

PostgreSQL dispose d'une table d'état très utile appelée pg_stat_activity. Cette table est similaire au "SHOW PROCESSLIST" qu'on retrouve en MySQL, mais a le net avantage d'être requêtable et dispose d'informations plus précises que MySQL.

En voici un extrait:

 12407 | postgres | 51572 | 10 | postgres | | 127.0.0.1 | | 44244 | 2017-07-26 07:41:44.454929+00 | | 2017-07-26 12:48:44.474222+00 | 2017-07-26 12:48:44.474335+00 | | | idle | | | SELECT CASE WHEN pg_is_in_recovery = 'false' THEN 0 ELSE COALESCE(ROUND(EXTRACT(epoch FROM now() - pg_last_xact_replay_timestamp())), 0) END AS seconds FROM pg_is_in_recovery()<br> 349593 | db02 | 51573 | 10 | postgres | | 127.0.0.1 | | 44245 | 2017-07-26 07:41:46.305319+00 | | 2017-07-26 12:48:46.336722+00 | 2017-07-26 12:48:46.33685+00 | | | idle | | | SELECT xact_commit,xact_rollback FROM pg_stat_database WHERE datname=$1;<br> 24816 | db01 | 51575 | 10 | postgres | | 127.0.0.1 | | 44246 | 2017-07-26 07:41:46.588503+00 |

Dans certains cas, il peut être utile de couper toutes les connexions à une base de données précise (par exemple des connexions dormantes en masse). Voici une simple requête SQL à jouer sur votre PostgreSQL (9.2 et plus) permettant de couper toutes les connexions:

SELECT
pg_terminate_backend(pid) FROM
pg_stat_activity
WHERE pid <> pg_backend_pid() AND datname = 'target_database';

Cette requête va lancer un ordre de fermeture sur toutes les connexions de la base target_database excepté la connexion en cours.

Si vous souhaitez uniquement tuer les requêtes inactives vous pouvez utiliser la variante suivante:

SELECT
pg_terminate_backend(pid) FROM
pg_stat_activity
WHERE pid <> pg_backend_pid() AND datname = 'target_database' AND state = 'idle';

MySQL: tuer les requêtes trop longues

MySQL permet d'encaisser énormement de requêtes, mais dans certains cas, généralement des soucis externes (performances réseau/disque), il se peut que votre MySQL ait encaissé beaucoup de requêtes clientes qui ne soient plus légitimes et génèrent une forte charge sur votre service.

Je vous propose ci-dessous un petit script tout simple permettant de tuer toutes les requêtes supérieures à 180 secondes:

for P in $(mysql -e "show processlist"|awk '$6 > 180{print $1}'); do
mysql -e "kill $P;";
done

Archlinux: réinstaller tous les paquets

Archlinux est une distribution rolling release très stable et performante. Si vous êtes un peu casse-cou, il se peut que vous ayez cassé par inadvertance la base de vôtre système en effectuant des actions un peu avancées (noyau linux ne démarrant plus, systemd en vrac...).

Pour réinstaller tous les paquets installés sur votre système sans effort il suffit de jouer une seule commande:

sudo pacman -Sy `pacman -Q | awk '{print $1}'`

Cette commande va lister tous les paquets et les réinstaller. Attention néanmoins, si vous avez des paquets non officiels (AUR par exemple), cela ne fonctionnera pas, il faudra passer par yaourt par exemple.

Gitlab CI: Pipeline maven

Le Gitlab CI intègre depuis la version 8.8 de Gitlab la notion de pipeline. C'est une notion très à la mode permettant de pouvoir séparer son processus de build en plusieurs étapes distinctes, interdépendantes et parallélisables. Nous allons prendre ici l'exemple d'une application SpringBoot utilisant Maven et créer un pipeline de construction de l'application ayant le cheminement suivant

  • Construction de l'application
  • Tests unitaires
  • Tests de qualité de code (sonar)
  • Déploiement Nexus (SNAPSHOT)
  • Déploiement Nexus (release, branche master uniquement)

Préparation des Gitlab Runner (builders)

Dans un premier temps nous allons créer 2 répertoires partagés par nos runners pour nos builds maven

  • un répertoire .m2 qui contiendra la configuration maven standard pour nos builds
  • un répertoire tools qui contiendra notre script de release d'application Maven

mkdir -p /var/lib/gitlab-runner/.m2 /var/lib/gitlab-runner/tools On configure ensuite le runner gitlab pour utiliser en read-only le .m2 et les outils. Editez le fichier /etc/gitlab-runner/config.toml:

concurrent = 2
check_interval = 0
[[runners]]
  name = "runner01"
  url = "http://gitlab.example.org/"
  token = "99aab9a13c8efda114771f017fcf"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "debian:jessie"
    privileged = false
    disable_cache = true
    volumes = ["/cache", "/var/lib/gitlab-runner/.m2:/.m2:ro", "/var/lib/gitlab-runner/tools:/tools:ro"]
  [runners.cache]

Notre runner est pratiquement prêt, avant de le finir nous allons configurer le pipeline Maven afin que vous compreniez mieux la dernière phase. Vous pouvez voir ici 2 choses sortant du commun:

  • un répertoire /.m2 provenant de l'hôte monté en read-only. Il portera notre configuration Maven (.m2/settings.xml), avec notre URL Nexus
  • un répertoire /tools lui aussi en read-only. Il contiendra certains scripts particuliers, notamment ici notre script de release Maven et la clef SSH privée permettant d'accéder en écriture au repository

Configuration du pipeline Maven

Notre pipeline maven s'appuie sur 5 étapes:

  • Construction de l'artefact
  • Tests
  • Déploiement dans Nexus
  • Génération de release
  • Documentation

Les étapes de documentation, déploiement Nexus et de génération de release ne seront effectuées que dans certaines conditions:

  • Déploiement dans Nexus: si nous construisons un snapshot, donc, ni sur un tag git, ni sur une branche nommée releases.
  • Génération de release: uniquement lors d'un commit sur la branche releases.
  • Documentation: uniquement lors d'un tag git

Afin de s'y retrouver, je vous propose de regarder le graphe suivant Notre étape de tests est découpée en 2 tâches parallèles:

  • Tests unitaires et d'intégration
  • Tests sonar

Passons maintenant au fichier .gitlab-ci.yml que nous allons ajouter au repository:

---
image: maven:3-jdk-8
cache:
 paths:
 - $HOME/.m2/
variables:
  MAVEN_CLI_OPTS: "-s /.m2/settings.xml -B"
  SONAR_PROJECT_NAME: "project-demo"
stages:
- build
- test
- deploy
- documentation
- release

maven:build:
stage: build
environment: staging
script:
- "mvn $MAVEN_CLI_OPTS clean compile"
artifacts:
when: on_success
expire_in: 1 day
paths:
- target/

maven:package:
stage: test
environment: staging
dependencies:
- maven:build
script: "mvn $MAVEN_CLI_OPTS package"
artifacts:
when: on_success
expire_in: 1 day
paths:
- target/

maven:sonar:
stage: test
environment: staging
dependencies:
- maven:build
script: "mvn $MAVEN_CLI_OPTS verify sonar:sonar -D sonar.projectName=$SONAR_PROJECT_NAME -Dsonar.gitlab.commit_sha=$CI_BUILD_REF -Dsonar.gitlab.ref_name=$CI_BUILD_REF_NAME"
artifacts:
when: on_success
expire_in: 1 day
paths:
- target/

maven:deploy-nexus:
stage: deploy
environment: staging
except:
- tags
- releases
dependencies:
- maven:package
- maven:sonar
script: "mvn $MAVEN_CLI_OPTS deploy"
artifacts:
when: on_success
expire_in: 1 day
paths:
- target/*.jar

maven:javadoc:
stage: documentation
environment: staging
only:
- tags
dependencies:
- maven:deploy-nexus
script: "mvn $MAVEN_CLI_OPTS javadoc:javadoc"
artifacts:
when: on_success
expire_in: 1 week
paths:
- target/site/apidocs/

maven:release:
stage: release
environment: staging
only:
- releases
dependencies:
- maven:deploy-nexus
script:
- "/tools/maven-release"

Nous utilisons ici une image docker officielle contenant un builder Maven 3.3 et une JVM Java 8. Gitlab 8.16 et inférieurs ne permettant pas de pousser depuis le CI nous allons devoir ruser en utilisant un script avec une clef privée spécifique d'édition des repositories. Je reviendrai plus tard sur la méthode pour Gitlab 9.0 et supérieurs. Je vous propose de regarder le script ci-dessous:

#! /bin/bash
which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y ) > /dev/null

# Run ssh-agent (inside the build environment)
eval $(ssh-agent -s) > /dev/null

# Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
ssh-add /tools/.ssh/id_rsa > /dev/null
mkdir -p ~/.ssh
ssh-keyscan gitlab.example.org > ~/.ssh/known_hosts 2>/dev/null
[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config

git config --global user.email 'gitlab-ci@example.org'
git config --global user.name 'Gitlab CI'

# Reconfigure push repository to use SSH
CI_PUSH_REPO=`echo $CI_BUILD_REPO | perl -pe 's#.*@(.+?(\:\d+)?)/#git@\1:#'`
git remote set-url --push origin "${CI_PUSH_REPO}"

# Maven doesn't like detach HEADs, attach
git checkout -B "$CI_BUILD_REF_NAME"
mvn $MAVEN_CLI_OPTS release:clean -DskipTests=true
mvn $MAVEN_CLI_OPTS release:prepare -DskipTests=true

# prepare done, pushing tags
git push origin --tags
mvn $MAVEN_CLI_OPTS release:perform -DskipTests=true

Il s'agit ici de résoudre 2 problématiques:

  • concilier notre problème de push, en lisant la clef SSH privée de notre repository, en l'ajoutant à l'agent SSH du runner et en configurant notre commiter git.
  • ré-attacher le commit du build à sa branche, sinon Maven ne fonctionnera pas lors du release.

Enfin on pousse les tags générés par Maven et on pousse l'artefact vers Nexus. Le push du tag va déclencher un second pipeline générant automatiquement la documentation.

Conclusion

Nous avons vu ici comment utiliser efficacement le Gitlab CI pour construire une application standard Maven depuis Gitlab. Si vous utilisez Jenkins, Gitlab CI offre une alternative intéressante, notamment par des pipelines extrêmement flexibles, vous permettant de construire facilement des applications au moyen de conteneurs Docker, et donc reproductibles, avec des workspaces propres. A vous de jouer !

Ansible: variables chiffrées hors d’un vault-file

Ansible dispose d'un moyen pour chiffrer les mots de passe appelé le vault. Les vaults sont des fichiers, généralement disposés au sein de l'inventaire ansible qui sont entièrement chiffrés.

Dans un système entièrement industrialisé, on retrouve généralement ansible accompagné d'un SCM comme git afin de versionner le code et l'inventaire de l'infrastructure.L'utilisation d'un ou plusieurs vault files pour les mots de passe pose un souci, les variables chiffrées (nom et valeurs) sont tous au sein du vault et donc tous chiffrés. Lors d'un changement sur un mot de passe unitaire au sein d'une vault, le différentiel du SCM montrera un changement global de la vault, et non le changement unitaire du mot de passe.

Une fonction mal documentée existe heureusement dans Ansible et va vous permettre de retrouver un comportement similaire à hiera et eyaml-encrypt avec Puppet, il s'agit du tag !vault-encrypt renommé en !vault à partir d'Ansible 2.3.

Ce tag va vous permettre de ne chiffrer que le contenu d'une variable contenu au sein d'un fichier YAML complet non chiffré. Pour créer une variable chiffrée, on va comme pour les vaults utiliser le binaire ansible-vault.

echo -n "monpassword" | ansible-vault encrypt

Cette commande va chiffrer la chaîne monpassword avec l'utilitaire. Notez 2 choses:

  • L'option -n de la commande echo est importante auquel cas vous aurez un \n supplémentaire en fin de chaîne chiffrée
  • Si vous n'avez pas de vault-password-file spécifié dans votre configuration globale, ansible-vault vous demandera le mot de passe associé au vault chiffré, et donc la clef de chiffrement/déchiffrement.
echo -n "monpassword" | ansible-vault encrypt
Vault password:
$ANSIBLE_VAULT;1.1;AES256
65396363643837643539373866353262623930346635656566326362336635313334623239623663
3639366665656235623833633762336231393536326635370a376532663333653465366439636536
31656539633564363364666632366464613964616162376662623937656266666362663462636161
3162333838646537330a633033396635376438373036376166333938613061316539363439393662
6536

Maintenant que nous avons notre chaîne chiffrée, il n'y a plus qu'à l'utiliser quelque part dans notre inventaire ansible.

my_strong_password: !vault-encrypted |
  $ANSIBLE_VAULT;1.1;AES256
  65396363643837643539373866353262623930346635656566326362336635313334623239623663
  3639366665656235623833633762336231393536326635370a376532663333653465366439636536
  31656539633564363364666632366464613964616162376662623937656266666362663462636161
  3162333838646537330a633033396635376438373036376166333938613061316539363439393662
  6536

Vous pouvez désormais chiffrer et versionner vos passwords chiffrés proprement.

Ansible: améliorer la sortie écran

Ansible est un très bon ordonnanceur, avec une sortie relativement claire, néanmoins il se peut que vous ayez envie de l'améliorer.

La sortie standard Ansible utilise le plugin CallbackModule présent dans le répertoire lib/ansible/plugins/callback/default.py. Nous allons ici bénéficier de la possibilité de définir nos propres callback plugins et de l'héritage objet Python pour pouvoir améliorer facilement la sortie Ansible.

Configuration préliminaire

Dans un premier temps nous allons devoir établir quelques configurations Ansible dans le fichier ansible.cfg

callback_whitelist = timer
stdout_callback = customstdout
callback_plugins   = ./plugins/callback
  • La première ligne va activer le plugin timer d'Ansible, c'est un petit plugin très simple qui va en fin de playbook vous donner le temps d'exécution du playbook, pratique donc.
Playbook run took 0 days, 0 hours, 0 minutes, 37 seconds
  • La seconde ligne va définir le module customstdout comme plugin par défaut pour la sortie standard.
  • Enfin la 3ème ligne permet de définir un chemin relatif à l'exécution courante pour les callback plugins locaux à votre installation.

Création du plugin

Créez maintenant le fichier plugins/callback/customstdout.py dans votre arbre ansible courant et indiquez le contenu suivant, nous allons le détailler ensuite:

#
# Overrided version of Ansible default.CallbackModule
#
import datetime
from ansible import constants as C
from ansible.plugins.callback import default
import imp
import os

ANSIBLE_PATH = imp.find_module('ansible')[1]
DEFAULT_PATH = os.path.join(ANSIBLE_PATH, 'plugins/callback/default.py')
DEFAULT_MODULE = imp.load_source(
    'ansible.plugins.callback.default',
    DEFAULT_PATH
)

class CallbackModule(default.CallbackModule):  # pylint: disable=too-few-public-methods,no-init
    '''
    Override for the default callback module.

    Render std err/out outside of the rest of the result which it prints with
    indentation.
    '''
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'customstdout'

    def __init__(self):
        self._current_task_start = None
        super(default.CallbackModule, self).__init__()

    # Overrided functions from Ansible default.CallbackModule
    def v2_playbook_on_task_start(self, task, is_conditional):
        self._current_task_start = datetime.datetime.now()
        default.CallbackModule.v2_playbook_on_task_start(self, task, is_conditional)

    # This override show the task action where name is present. It's now the case by default
    def _print_task_banner(self, task):
        args = ''
        if not task.no_log and C.DISPLAY_ARGS_TO_STDOUT:
            args = u', '.join(u'%s=%s' % a for a in task.args.items())
            args = u' %s' % args

        if task._role and task.name and ("%s : " % task._role._role_name) not in task.name:
            task_name = "%s : %s | %s" % (task._role.get_name(), task.action.upper(), task.name)
        elif task.name:
            task_name = "%s | %s" % (task.action.upper(), task.name)
        else:
            if task._role:
                task_name = "%s : %s" % (task._role.get_name(), task.action)
            else:
                task_name = "%s" % (task.action,)

        self._display.banner(u"TASK [%s%s]" % (task_name.strip(), args))
        if self._display.verbosity >= 2:
            path = task.get_path()
            if path:
                self._display.display(u"task path: %s" % path, color=C.COLOR_DEBUG)

        self._last_task_banner = task._uuid

    def v2_runner_on_ok(self, result):

        if self._play.strategy == 'free' and self._last_task_banner != result._task._uuid:
            self._print_task_banner(result._task)

        self._clean_results(result._result, result._task.action)

        delegated_vars = result._result.get('_ansible_delegated_vars', None)
        self._clean_results(result._result, result._task.action)
        if result._task.action in ('include', 'include_role'):
            return
        elif result._result.get('changed', False):
            if delegated_vars:
                msg = "changed: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
            else:
                msg = "changed: [%s]" % result._host.get_name()
            color = C.COLOR_CHANGED
        else:
            if delegated_vars:
                msg = "ok: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
            else:
                msg = "ok: [%s]" % result._host.get_name()
            color = C.COLOR_OK

        if result._task.loop and 'results' in result._result:
            self._process_items(result)
        else:

            if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and not '_ansible_verbose_override' in result._result:
                msg += " => %s" % (self._dump_results(result._result),)
            td = datetime.datetime.now() - self._current_task_start
            msg += " (%fs)" % (td.microseconds / 1000000.0)
            self._display.display(msg, color=color)

        self._handle_warnings(result._result)

Description du plugin

Ce plugin est callback plugin dont l'objet père est default.CallbackModule, à savoir le module gérant la sortie par défaut Ansible. Grâce à l'héritage nous allons pouvoir récupérer le comportement par défaut d'Ansible et modifier les parties qui nous intéressent. Nous avons repris ici 2 fonctions du plugin pour les améliorer, l'affichage des noms de tâches (__print_taskbanner) et l'affichage du statut de changement de la tâche sur un node (_v2_runner_onok). Ces fonctions sont copiés directement depuis l'objet parent (le module default d'Ansible) et modifiées ici. Par l'héritage objet Python elles seront appelées plutôt que celles de l'objet parent.

Apports ajoutés par le module

Le premier apport est sur l'affichage du nom des tâches. En effet, lorsque vous spécifiez le module name avant d'appeler vos tâches, celui-ci remplace le nom du module utilisé. Notre amélioration va afficher le module de manière inconditionnelle, en plus du nom arbitraire attaché à la tâche en cours. Ainsi, au lieu d'avoir:

TASK [dovecot2 : Configure dovecot (conf.d files)] ******************

Vous obtiendrez:

TASK [dovecot2 : TEMPLATE | Configure dovecot (conf.d files)] ******************

C'est une fonctionnalité pratique lorsque vous lisez la sortie de vos tâches. Second apport, l'ajout du temps d'exécution des tâches par notre. Habituellement vous avez:

ok: [192.168.2.1]

Et maintenant vous aurez la sortie suivante:

ok: [192.168.2.11] (0.633523s)

Conclusion

Grâce à ce tutoriel vous savez désormais comment ajouter un callback plugin à Ansible et changer la sortie standard pour afficher la sortie comme bon vous semble.

FreeBSD: poudriere cross compiling (ARM)

Poudriere permet de réaliser du cross compiling et donc de compiler des cibles d'une autre architecture. Nous allons ici prendre l'exemple d'une architecture amd64 cross-compilant vers ARM6 pour un Raspberry PI 2.

Préparation de la jail poudrière

Tout d'abord on va télécharger une image RPI2 viable depuis le miroir FreeBSD puis la décompresser

fetch http://ftp.freebsd.org/pub/FreeBSD/releases/ISO-IMAGES/11.0/FreeBSD-11.0-RELEASE-arm-armv6-RPI2.img.xz
unxz FreeBSD-11.0-RELEASE-arm-armv6-RPI2.img.xz

On va ensuite la rendre disponible au niveau filesystem grâce à l'utilitaire mdconfig

mdconfig -a -t vnode -f FreeBSD-11.0-RELEASE-arm-armv6-RPI2.img
mount /dev/md0s2a /mnt

On va maintenant supprimer le flag pour le firstboot (inutile ici nous serons dans une jail de build) et créer le répertoire /usr/local/bin pour poudriere

rm /mnt/firstboot
mkdir -p /mnt/usr/local/bin

L'image est maintenant prête on va créer l'archive pour poudriere:

tar -cpf /tmp/FreeBSD-11.0-RELEASE-arm-armv6-RPI2.tar .

Nous pouvons maintenant créer la jail

poudriere jail -c -j 11-arm6 -m tar=/tmp/FreeBSD-11.0-RELEASE-arm-armv6-RPI2.tar -a arm.armv6 -v 11.0-RELEASE

Support de la compilation ARM

Tout d'abord on va avoir besoin de qemu-user-static pour réaliser cette cross compilation. Il a 2 avantages:

  • Il n'a pas de dépendances particulières
  • Le binaire étant statique il va permettre à poudrière de s'éxecuter correctement sans devoir copier des librairies dynamiques supplémentaires dans la jail
pkg install qemu-user-static

Ajoutez maintenant l'émulateur ARM aux interpréteurs utilisables par poudriere

binmiscctl add armv6 --interpreter "/usr/local/bin/qemu-arm-static"
      --magic
      "\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00"
      --mask
      "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff"
      --size 20 --set-enabled

Cross-compiling !

L'environnement est prêt, il ne reste plus qu'à lancer la compilation, par exemple de zsh

poudriere bulk -j 11-arm6 -p default shells/zsh
 [00:00:00] ====>> Cross-building ports for arm.armv6 on amd64 requires QEMU
 [00:00:00] ====>> Creating the reference jail... done
 [00:00:00] ====>> Mounting system devices for 11-arm6-default
 [00:00:00] ====>> Mounting ports/packages/distfiles
 [00:00:00] ====>> Using packages from previously failed build
 [00:00:00] ====>> Mounting packages from: /usr/local/poudriere/data/packages/11-arm6-default
 [00:00:00] ====>> Copying /var/db/ports from: /usr/local/etc/poudriere.d/11-arm6-options
 [00:00:00] ====>> Raising MAX_EXECUTION_TIME and NOHANG_TIME for QEMU
 [00:00:00] ====>> Copying latest version of the emulator from: /usr/local/bin/qemu-arm-static
 /etc/resolv.conf -> /usr/local/poudriere/data/.m/11-arm6-default/ref/etc/resolv.conf
 [00:00:00] ====>> Starting jail 11-arm6-default
 [00:00:01] ====>> Logs: /usr/local/poudriere/data/logs/bulk/11-arm6-default/2017-01-13_22h51m51s
 [00:00:01] ====>> Loading MOVED
 [00:00:02] ====>> Calculating ports order and dependencies
 [00:00:32] ====>> Sanity checking the repository
 [00:00:32] ====>> Checking packages for incremental rebuild needed
 [00:00:32] ====>> Deleting stale symlinks
 [00:00:32] ====>> Deleting empty directories
 [00:00:32] ====>> Cleaning the build queue
 [00:00:33] ====>> Recording filesystem state for prepkg... done
 [00:00:33] ====>> Building 36 packages using 2 builders
 [00:00:33] ====>> Starting/Cloning builders
 [00:00:35] ====>> Hit CTRL+t at any time to see build progress and stats
 [00:00:35] ====>> [01][00:00:00] Starting build of ports-mgmt/pkg

Votre jail de cross compilation est maintenant opérationnelle, vous pouvez compiler sur un vrai CPU et ne plus attendre des jours que votre Raspberry PI compile X11.

PostgreSQL: changer le owner de toutes les tables/séquences d’un schéma

Lors d'une restauration de base de données, parfois il se peut que vous ayez besoin de changer le propriétaire d'une table ou d'une séquence pour un autre user, par exemple si vous prenez une base de production pour la mettre sur votre intégration pour vos développeurs (anonymisées, bien sûr 😉 ).

Plutôt que de devoir faire fastidieusement un ALTER TABLE table par table, voici 2 requêtes SQL qui vont vous permettre de générer les SQL pour changer rapidement le owner de toutes les tables et séquences:

select 'alter table ' || relname || ' owner to mynewowner;' from pg_class join pg_namespace on pg_class.relnamespace = pg_namespace.oid where pg_namespace.nspname = 'public' and relkind='r';
select 'alter sequence ' || relname || ' owner to mynewowner;' from pg_class join pg_namespace on pg_class.relnamespace = pg_namespace.oid where pg_namespace.nspname = 'public' and relkind='S';

Résultat pour les tables

                           ?column?                             
-----------------------------------------------------------------
 alter table batch_job_execution owner to mynewowner;
 alter table purchase owner to mynewowner;
 alter table serviceprice owner to mynewowner;
 alter table account owner to mynewowner;
 alter table product owner to mynewowner;

Résultat pour les séquences:

                           ?column?                            
----------------------------------------------------------------
 alter sequence batch_job_seq owner to mynewowner;
 alter sequence batch_step_execution_seq owner to mynewowner;
 alter sequence cgufile_id_seq owner to mynewowner;

Vous n'avez plus qu'à recopier ce résultat directement ou à l'intégrer dans un script shell qui va le jouer automatiquement pour vous.

PostgreSQL: supprimer toutes les tables d’un schéma rapidement

Parfois, il peut être utile de supprimer toutes les tables d'un schéma directement, notamment si vous souhaitez réimporter un backup depuis un autre environnement. Il peut être fastidieux de faire des DROP TABLE unitaires, heureusement les schémas PostgreSQL respectent la norme SQL. Contrairement à MySQL qui mélange la notion de base de données et schéma, sur PostgreSQL les bases de données peuvent comporter plusieurs schémas. Généralement on ne regarde pas, mais il faut savoir que par défaut les tables se trouvent dans le schéma public. Nous allons exploiter la notion de schéma PostgreSQL pour procéder à ce nettoyage. Pour supprimer toutes les tables de votre schéma, entrez simplement les commandes SQL suivantes:

DROP SCHEMA public CASCADE;
CREATE SCHEMA public;

L'intérêt du mot clef cascade est ici de résoudre les suppressions bloquées par des clefs étrangères. Vous savez maintenant supprimer toutes les tables d'un schéma PostgreSQL simplement.

Gestion de LetsEncrypt sous FreeBSD avec nginx et une jail

LetsEncypt est une autorité de certification utilisant une API basée sur des appels HTTP et un client côté serveur qui va générer des tokens lus par les serveurs LetsEncrypt. Nous allons ici voir une architecture LetsEncrypt typique dans laquelle nous allons utiliser le client acme-client d'OpenBSD disponible dans les ports et les packages FreeBSD.

Installation

Dans un premier temps installez le client sur votre machine hôte (et pas votre jail web):

pkg install acme-client

Configuration de nginx

Nous allons maintenant configurer nginx afin de pouvoir interagir avec l'API letsencrypt. Créez tout d'abord le répertoire suivant dans votre jail avec l'utilisateur et le groupe nobody

mkdir /usr/local/www/.well-known && chown nobody:nobody /usr/local/www/.well-known

Ensuite configurez vos virtual hosts afin de faire pointer les appels vers /.well-known dans ce répertoire:

server {
        listen 80;
        server_name www.unix-experience.fr;
        location /.well-known {
            alias /usr/local/www/.well-known;
        }
        location / {
            return 301 https://www.unix-experience.fr$request_uri;
        }
        add_header X-Frame-Options "DENY";
        add_header Strict-Transport-Security "max-age=86400; preload";
}

Note: nous ajoutons ici le support du protocole HSTS avec un TTL d'1 journée spécifiant aux navigateurs que le site supporte SSL et qu'il faudrait passer dessus. Vous pouvez sans souci faire pointer plusieurs de vos virtual hosts dans ce même répertoire.

Configuration du client LetsEncrypt

Retournez maintenant sur votre hôte et créez le fichier /usr/local/etc/acme/domains.txt puis ajoutez y un domaine par ligne:

www.unix-experience.fr
ftp.unix-experience.fr

Créez ensuite le script ci-dessous (basé sur le script présent dans le même répertoire sous forme d'exemple) dans /usr/local/etc/acme/acme-client.sh

#!/bin/sh -e
WEBJAIL="web"
BASEDIR="/usr/local/etc/acme"
SSLDIR="/usr/local/etc/ssl/acme"
DOMAINSFILE="${BASEDIR}/domains.txt"
CHALLENGEDIR="/jails/${WEBJAIL}/usr/local/www/.well-known/acme-challenge"

[ ! -d "${SSLDIR}/private" ] && mkdir -pm700 "${SSLDIR}/private"

cat "${DOMAINSFILE}" | while read domain line ; do
   CERTSDIR="${SSLDIR}/${domain}"
   [ ! -d "${CERTSDIR}" ] && mkdir -pm755 "${CERTSDIR}"
   set +e # RC=2 when time to expire > 30 days
   acme-client -b -C "${CHALLENGEDIR}" \
               -k "${SSLDIR}/private/${domain}.pem" \
               -c "${CERTSDIR}" \
               -n -N -v \
               ${domain} ${line}
   RC=$?
   set -e
   [ $RC -ne 0 -a $RC -ne 2 ] && exit $RC
done

Ce script lit les domaines présents dans le fichier domains.txt puis il va s'occuper de créer vos clefs privées et de les faire signer par LetsEncrypt, en se basant sur le répertoire .well-known présent dans votre jail. Une fois ce script exécuté, si tout s'est passé correctement vous trouverez votre clef privée dans le répertoire /usr/local/etc/ssl/acme/private/example.org et votre clef publique et les chaînes de certificats dans le répertoire /usr/local/etc/ssl/acme/example.org. Ces répertoires sont situés sur votre hôte et non dans votre jail web. Pour déployer les scripts un script d'exemple est présent dans le répertoire /usr/local/etc/**acme, mais nous allons légèrement le modifier. Créez le fichier /usr/local/etc/acme/deploy.sh**:

#!/bin/sh

set -e

BASEDIR="/usr/local/etc/acme"
DOMAINSFILE="${BASEDIR}/domains.txt"
LEDIR="/usr/local/etc/ssl/acme"
JAILSDIR="/jails"
TARGETS="web"
cat "${DOMAINSFILE}" | while read domain line ; do
    for jail in ${TARGETS}; do
        targetdir="${JAILSDIR}/${jail}/etc/ssl"
        # Check if the certificate has changed
        [[ -z "`diff -rq ${LEDIR}/${domain}/fullchain.pem ${targetdir}/certs/${domain}.pem`" ]] && continue
        cp -L "${LEDIR}/private/${domain}.pem"   "${targetdir}/private/${domain}.pem"
        cp -L "${LEDIR}/${domain}/fullchain.pem" "${targetdir}/certs/${domain}.pem"
        chmod 400 "${targetdir}/private/${domain}.pem"
        chmod 644 "${targetdir}/certs/${domain}.pem"
        # Restart/-load relevant services
        [[ "${jail}" = "web" ]] && jexec ${jail} service nginx restart
    done
done

Ce script va copier tous les certificats associés à vos dernières négociations LetsEncrypt et va les déployer dans votre jail web puis redémarrer nginx.

Renouvellement automatique

Pour finir il ne reste plus qu'à configurer le cron ajouté par le paquet acme-client afin de configurer la tâche hebdomadaire. Pour se faire, éditez le fichier /usr/local/etc/periodic.conf et ajoutez les entrées suivantes:

# letsencrypt
weekly_acme_client_enable="YES"
weekly_acme_client_user="nobody"
weekly_acme_client_deployscript="/usr/local/etc/acme/deploy.sh"

Cela va vous permettre d'activer le cron, et de lancer le déploiement automatiquement toutes les semaines.

Conclusion

Vous pouvez désormais faire des demandes de certificats LetsEncrypt facilement et les renouveler automatiquement via un cron fourni par acme-client.