• At chevron_right

      Explorer des logs ELK avec JsonPath

      motius · pubsub.gugod.fr / atomtest · Friday, 3 July, 2020 - 22:00 · 5 minutes

    Bonjour ! Aujourd'hui, je veux vous parler d'une bibliothèque Python3 : jsonpath2. Un peu de contexte Il s'agit d'une petite bibliothèque de code qui permet de filtrer des données au format JSON. Vous me direz, on peut déjà faire ça avec des petites fonctions utilitaires, quelques coups de liste en ...

    Bonjour !

    Aujourd'hui, je veux vous parler d'une bibliothèque Python3 : jsonpath2.

    Un peu de contexte

    Il s'agit d'une petite bibliothèque de code qui permet de filtrer des données au format JSON. Vous me direz, on peut déjà faire ça avec des petites fonctions utilitaires, quelques coups de liste en compréhension. Il y a bien sûr un grand nombre de choses que la bibliothèque ne permet pas de faire, on y reviendra, mais concentrons-nous d'abord sur ce qu'elle permet, et les avantages qu'elle procure.

    Pour cela, je vous propose tout simplement de vous présenter l'exemple que j'ai eu à traiter.

    Un exemple

    Supposez que vous ayez comme moi une application qui écrive un journal d'exécution au format JSON, dont les entrées sont relevées périodiquement par un ELK, et que vous ayez à analyser une journée complète, ce qui vous donne environ 50 Mo de logs compressés en gzip, et 700 Mo une fois décompressés. Vous rentrez ça dans un interpréteur ipython et bam, 3 Go de RAM supplémentaires utilisés. (Dans les cas où vous utilisez beaucoup de mémoire dans ipython, rappelez-vous que celui-ci stocke tout ce que vous taper dans des variables nommées _i1, _i2... et les résultats de ces opérations dans les variables correspondantes _1, _2, ce qui peut faire que votre interpréteur consomme une très grande quantité de mémoire, pensez à la gérer en créant vous-même des variables et en les supprimant avec del si nécessaire. Mais je m'égare.)

    Il peut y avoir plusieurs raisons qui font que ces logs ne seront pas complètement homogènes :

    • vous avez plusieurs applications qui fontionnent en microservices ;
    • les messages comportant des exceptions ont des champs que les autres messages plus informatifs n'ont pas ;
    • etc.

    Toujours est-il que pour analyser ce JSON, vous pouvez être dans un beau pétrin au moment où vous vous rendez compte que chacune des petites fonctions utilitaires que vous écrivez doit :

    • gérer un grand nombre de cas ;
    • gérer des cas d'erreur ;
    • être facilement composable, même pour les cas d'erreur.

    Je ne dis pas que ce soit infaisable, et il m'est arriver de le faire ainsi pour certaines actions plutôt qu'en utilisant JsonPath.

    Pour l'installation, c'est comme d'habitude dans le nouveau monde Python :

    # dans un virtualenv Python3
    pip install jsonpath2

    Cas pratiques

    Exemple de code n°1

    Par exemple, si je souhaite obtenir toutes les valeurs du champ message où qu'il se trouve dans mon JSON, je peux le faire ainsi :

    import json
    from jsonpath2.path import Path as JsonPath
    with open("/path/to/json_file/file.json", mode='r') as fr:
        json_data = json.loads(fr.read())
    pattern = "$..message"
    ls_msg = [
        match.curent_value
        for match in JsonPath.parse_str(pattern).match(json_data)
    ]

    La variable qui nous intéresse ici, c'est pattern. Elle se lit ainsi :

    1. $ : racine de l'arbre analysé
    2. .. : récursion sur tous les niveaux
    3. message : la chaîne de caractères recherchés

    Avantage

    Le premier avantage que l'on voit ici, c'est la possibilité de rechercher la valeur d'un champ quelle que soit la profondeur de ce champ dans des logs.

    Exemple de code n°2

    On peut aussi raffiner la recherche. Dans mon cas, j'avais une quantité de champs "message", mais tous ne m'intéressaient pas. J'ai donc précisé que je souhaitais obtenir les champs "message" seulement si le champ parent est, dans mon cas, "_source" de la manière suivante :

    pattern = "$.._source.message"

    Par rapport au motif précédent, le seul nouveau caractère spécial est :

    1. . : permet d'accéder au descendant direct d'un champ.

    Avantage

    L'autre avantage qu'on vient de voir, c'est la possibilité de facilement rajouter des contraintes sur la structure de l'arbre, afin de mieux choisir les champs que l'on souhaite filtrer.

    Exemple de code n°3

    Dans mon cas, j'avais besoin de ne récupérer le contenu des champ "message" que si le log sélectionné était celui associé à une exception, ce qui correspondait à environ 1% des cas sur à peu près 600 000 entrées.

    Le code suivant me permet de sélectionner les "message" des entrées pour lesquelles il y a un champ "exception" présent :

    pattern = "$..[?(@._source.exception)]._source.message"

    Il y a pas mal de nouveautés par rapport aux exemples précédents :

    1. @ : il s'agit de l'élément couramment sélectionné
    2. [] : permet de définir un prédicat ou d'itérer sur une collection
    3. ?() : permet d'appliquer un filtre

    Avantage

    On peut facilement créer un prédicat simple pour le filtrage d'éléments, même lorsque l'élément sur lequel on effectue le prédicat n'est pas le champ recherché in fine.

    Au sujet de jsonpath2

    Si vous êtes intéressé par le projet, je vous mets à disposition les liens suivants (ils sont faciles à trouver en cherchant un peu sur le sujet) :

    jsonpath2 utilise le générateur de parseur ANTLR, qui est un projet réputé du Pr. Terence Parr.

    Inconvénients

    Parmi les prédicats qu'on peut faire, on peut tester si une chaîne de caractères est égale à une chaîne recherchée, mais les caractères qu'on peut mettre dans la chaîne recherchée sont assez limités : je n'ai pas essayé de faire compliqué, seulement de rechercher des stacktraces Python ou Java, qui ont peu de caractères spéciaux.

    Il paraît qu'on peut effectuer des filtrages plus puissants avec une fonctionnalité supplémentaire que je n'ai pas présentés parce que je n'ai pas pris le temps de l'utiliser :

    1. () : s'utilise afin d'exécuter des expressions personnalisées.

    J'espère que tout ceci pourra vous être utile. Je vous recommande notamment de tester vos motifs sur un petit jeu de données, on peut facilement faire des bêtises et consommer beaucoup de mémoire et pas mal de temps sans cela.

    Joyeux code !

    Motius

    • At chevron_right

      Avantages et inconvénients des dictionnaires ordonnés

      motius · pubsub.gugod.fr / atomtest · Monday, 8 June, 2020 - 22:00 · 5 minutes

    Bonjour ! Aujourd'hui, je veux vous parler des dictionnaires en Python, et notamment de leur — relativement — nouvelle propriété d'ordre. TL;DR: les considérations concernant les dictionnaires ordonnés tourne autour des performances. On oublie de mentionner que cela facilite le débogage, mais que ce...

    Bonjour !

    Aujourd'hui, je veux vous parler des dictionnaires en Python, et notamment de leur — relativement — nouvelle propriété d'ordre.

    TL;DR: les considérations concernant les dictionnaires ordonnés tourne autour des performances. On oublie de mentionner que cela facilite le débogage, mais que cela peut aussi cacher un bogue dans l'implémentation d'un algorithme, raison pour laquelle j'ai écrit le fragment de code ci-dessous.

    Un peu de contexte

    Si l'on en croit ce thread StackOverFlow, les clefs d'un dictionnaire dans Python3 depuis sa version 3.6 sont de facto ordonnées dans l'implémentation CPython (dans l'ordre d'insertion), et Python3 dans sa version 3.7 standardise cet état de fait, ce qui veut dire que les autres implémentations (PyPy, Jython...) devront s'aligner afin de correctement implémenter le nouveau standard, pour assurer la compatibilité du code entre "interpréteurs". (Je mets le lien StackOverFlow parce qu'il en contient d'autres vers la liste de diffusion de courriel, etc.)

    Si vous vous demandez ce qui a amené à cet état de fait, je vous recommande la vidéo suivante, par le curieux Raymond Hettinger.

    Avec ça, vous aurez des éléments pour évaluer la pertinence des dictionnaires ordonnées en Python.

    Notez, pour ceux qui n'ont pas regardé la documentation, que Python3 met à disposition un OrderedDict déjà disponible dans les versions antiques, antédiluviennes, je veux dire celles pré-3.4 (oui, je trolle, mais à peine).

    Le problème

    Voyons un peu le contexte des deux problèmes qui m'ont amené à écrire cet article.

    Scénario 1

    J'étais en train de déboguer un logiciel, appelons-le Verifikator, qui faisait des appels API afin de vérifier des données en bases. Il se trouve que les résultats de Verifikator étaient aléatoires. De temps en temps, il retournait les bons résultats, i.e. il indiquait que certaines données en base étaient invalides, et de temps en temps, Verifikator n'indiquait pas d'erreur, alors qu'on pouvait vérifier a la mano qu'il y avait effectivement une erreur en base. Pour faire simple, la raison pour laquelle Verifikator n'était pas déterministe, c'est qu'il dépendait de l'ordre d'un dictionnaire dont la donnée provenait de l'API de la base de données. Vous comprenez que j'exagère quand je dit que Verifikator n'était pas déterministe, il l'est heureusement, puisqu'il s'agit d'un système informatique, et qu'on néglige les rayons cosmiques et les bogues système.

    Je me permet de rappeler à ceux d'entre vous qui s'étonnent de ce comportement erratique de Verifikator que j'étais en train de le déboguer (qui plus est, j'avais seulement participé à sa conception, pas à son implémentation).

    Verifikator tourne en Python3.5, si l'on avait utilisé une version supérieure, on n'aurait pas eu ce problème, notamment, Verifikator aurait soit systématiquement planté, soit systématiquement fonctionné. Ç'eût été mieux afin de pouvoir localiser le bug par dichotomie, je vous avoue que mon état de santé mentale se dégradait à vu d'œil lorsque j'ai commencé à observer ce comportement aléatoire, en cherchant à localiser les lignes fautives par dichotomie.

    Je reviens un instant sur cette histoire de dichotomie pour bien faire sentir à quel point c'est fatiguant. Imaginez un peu, vous essayez de déterminer un point A où le programme est dans un état valide et un point B dans lequel l'état est invalide, puis vous regardez un nouveau point C "au milieu", afin de savoir si ce point C est le nouveau point valide A' ou bien le nouveau point B' pour l'itération suivante. Le problème, c'est que parfois vous pensez que l'état du programme est valide à ce point C, mais que ce n'est pas vrai en général. Vous continuez donc à itérer entre C et B, alors que le problème se trouve entre A et C.

    Vous comprenez pourquoi j'apprécie fortement le fait que les dictionnaires soient ordonnés pour le débogage.

    Mais. Comme vous imaginez, il y a un mais. Parce qu'il n'y a pas que des avantages aux dictionnaires ordonnés. C'est le sujet du second scénario.

    Scénario 2

    Dans d'autres circonstances, j'ai déjà écrit — j'étais l'auteur du code cette fois-là, contrairement à Verifikator — un algorithme qui ne fonctionnait que si le dictionnaire sur lequel il tournait était ordonné. J'utilisais Python3.6 à l'époque, et par conscience professionnelle, j'ai compilé les versions 3.4 à 3.7 de Python afin de vérifier que les tests étaient corrects avec ces interpréteurs. Quelle ne fut pas ma surprise quand je m'aperçus que ce n'était point le cas. Ce n'était même pas le nouvel Python3.7 encore en bêta qui posait problème, mais les version 3.4 et 3.5. J'ai donc réécrit cet algorithme afin qu'il ne dépende plus de l'ordre du dictionnaire d'entrée (très simplement en construisant un OrderedDict à partir du dictionnaire et d'une liste correctement triée).

    Conséquences

    À l'époque, je n'avais pas rajouté de code pour tester les fonctions de service du second programme, je m'assurai qu'il fonctionnait avec les 4 versions mineures de Python3 pour lesquelles je développais le programme.

    J'ai fait les choses différemment cette fois, puisque j'ai codé cette fonction, qui randomise les clefs d'un dictionnaire plat (fonction non récursive sur d'éventuels dictionnaires en valeurs du dictionnaire passé en argument).

    import random as rnd
    def shuffle_dict_keys(d: dict) -> dict:
        """shuffle the keys of a dictionary for testing purposes now that
        Python dictionaries are insert-ordered. Does not compute inplace.
        Does not work recursively.
        """
        res = {}
        l = list(d)
        rnd.shuffle(l)
        for k in l:
            res[k] = d[k]
        return res

    J'avais en tête cette planche XKCD en écrivant ce code. Non pas que je préfère l'ancien comportement, mais que pouvoir y souscrire de manière optionnelle me permet d'avoir des tests de meilleure qualité, et qu'il a donc fallu queje trouve un contournement afin de retrouver l'ancien comportement dans les cas de tests.

    Conclusion

    De manière générale, je suis assez content que les dictionnaires soient ordonnés, mais je ne m'attendais pas à rencontrer de tels écueils, notamment puisque la majorité des conversations que j'avais lues sur le sujet s'attardaient sur les performances de cette nouvelle implémentation, et non sur ce genre de considérations.

    Joyeux code !

    Motius

    • At chevron_right

      Créer un enum Python dynamiquement

      motius · pubsub.gugod.fr / atomtest · Monday, 1 June, 2020 - 22:00 · 8 minutes

    Bonjour ! Aujourd'hui, je vous propose un micro tutoriel pour créer un enum dynamiquement en Python. Je vous accorde qu'on ne fait pas ça tous les jours, c'est d'ailleurs la première fois que je suis tombé sur ce cas. Un peu de contexte En général, un enum, ça ressemble à ça en Python : class MonEnu...

    Bonjour !

    Aujourd'hui, je vous propose un micro tutoriel pour créer un enum dynamiquement en Python. Je vous accorde qu'on ne fait pas ça tous les jours, c'est d'ailleurs la première fois que je suis tombé sur ce cas.

    Un peu de contexte

    En général, un enum, ça ressemble à ça en Python :

    class MonEnumCustom(enum.Enum):
        """docstring
        """
        VALIDATION  = "VALIDATION"
        SEND        = "SEND"

    Si l'enum contient un grand nombre de valeurs, chacune correspondant à un état, on peut aisément les formater avec un programme, afin d'écrire un enum comme ci-dessus. Ça fait simplement un enum comme ci-dessus, mais en plus gros :

    class MonEnumCustom(enum.Enum):
        """docstring
        """
        VALIDATION  = "VALIDATION"
        SEND        = "SEND"
        COMMIT      = "COMMIT"
        REVIEW      = "REVIEW"
        ... # une centaine d'états supplémentaires disponibles

    Mais que faire quand on récupère les valeurs dynamiquement depuis un appel à une API, qui retourne l'ensemble des états possibles sous forme d'une collection, comme ceci :

    >>> ls_remote_states = fetch_all_states(...)
    >>> print(ls_remote_states)
    [
        "VALIDATION",
        "SEND",
        "COMMIT",
        "REVIEW",
        ...
    ]

    et que l'on souhaite créer un enum à partir de ladite collection afin d'écrire du code qui dépendra de valeurs d'états prise dans l'enum comme ci-dessous ?

    # pseudo-code
    ls_remote_states = fetch_all_states(...)
    MonEnumCustom = create_enum_type_from_states(ls_remote_states)
    
    # dans le code
    if state is MonEnumCustom.VALIDATION:
        print("En cours de validation")
    elif state is MonEnumCustom.COMMIT:
        ...
    else:
        raise

    C'est le sujet de la discussion qui suit, qui va présenter les options disponibles, et quelques considérations quant à leurs usages.

    Le scénario

    Je souhaitais pouvoir m'interfacer avec un service qui définit un grand nombre (un peu plus d'une centaine) de constantes qu'il me renvoie sous forme d'une collection de chaînes de caractères. Trois choix s'offraient ainsi à moi :

    • utiliser les constantes telles quelles dans une collection (list, dict...)

      Avantages :

      • rapide & facile

      Inconvénients :

      • sémantiquement pauvre
      • la liste des états valides n'est jamais écrite dans le code
    • faire un copié collé et du formatage (avec vim, c'est facile et rapide)

      Avantages :

      • rapide & facile
      • l'intégralité des valeurs connues de l'enum peuvent être lues dans le programme à la seule lecture du code

      Inconvénients :

      • duplique un enum dont je ne suis pas responsable
    • générer dynamiquement l'enum

      Avantages :

      • résout les inconvénients des solutions précédentes
      • évite la duplication de code inutile. Il arrive assez souvent que l'on doive définir un même enum à plusieurs endroits dans un système informatique, par exemple en base de donnée dans Postgre, puis en Java / Python, et enfin en Typescript si vous faites du développement sur toute la stack. Mais dans ce cas-ci, j'aurais redéfini un enum sans que mon code ne soit la source d'autorité sur celui-ci, ce qui contrevient au principe du Single Source of Truth, c'est donc plus gênant que le simple problème de duplication de code.
      • Plus facile à adapter lors de l'évolution de l'enum. Ce dernier point est vrai dans mon cas où j'ai l'assurance que l'API ne fera qu'augmenter les états et n'invalidera jamais un état existant. Dans le cas contraire, il faudra changer les fonctions qui utilisent les états définis dans l'enum, avec ou sans la génération d'enum automatique.

      Inconvénients :

      • à nouveau, la liste des états valides n'est jamais écrite dans le code
      • Un tantinet plus long, surtout si tout ne va pas sur des roulettes du premier coup.

    Et si je mentionne les roulettes, c'est que vous imaginez bien que ça n'a pas marché du premier coup (sinon je n'écrirais pas cet article).

    La théorie

    Selon la documentation officielle, il suffit de passer 3 paramètres à la fonction type afin de créer une classe dynamiquement. Si vous voulez mon avis, on est là dans la catégorie des fonctionnalités de Python vraiment puissantes pour le prototypage (à côté de eval et exec, même si ces derniers devraient être quasiment interdits en production).

    Chouette, me dis-je en mon fort intérieur, ceci devrait marcher :

    # récupération du dictionnaire des valeurs
    d_enum_values: dict = fetch_all_states(...)
    # création dynamique de l'enum
    MonEnumCustom = type("MonEnumCustom", (enum.Enum,), d_enum_values)

    La pratique

    Ne vous fatiguez pas, ça ne marche pas. La solution que j'ai trouvée est un contournement dégoûtant (dû justement au module enum de Python).

    Je contourne les problèmes de création d'enum à l'aide d'un mapper qui ressemble à ça :

    class EnumMappingHelper(dict): # héritage pour contourner type
        def __init__(self, mapping: dict = None):
            self._mapping = mapping
    
        def __getitem__(self, item):
            if item == "_ignore_": # contournement d'enum
                return []
            return self._mapping[item]
    
        def __delitem__(self, item):
            return item # contournement d'enum
    
        def __getattr__(self, key): # Accès au clefs de l'enum par attribut
            return self._mapping[key]
    
        @property
        def _member_names(self): # contournement d'enum
            return dict(self._mapping)
    
    enum_mapping = EnumMappingHelper(d_enum_values)
    MonEnumCustom = type("MonEnumCustom", (enum.Enum,), enum_mapping)

    Je m'en sors donc en torturant un peu un classe personnalisée que j'appelle EnumMappingHelper afin que celle-ci se comporte d'une façon qui soit acceptable à la fois pour la fonction type et pour le module enum.

    Je peux utiliser les variantes de mon enum ainsi :

    if state is MonEnumCustom.VALIDATION:
        print("En cours de validation")
    elif state is MonEnumCustom.COMMIT:
        print("Validé")
    elif state is MonEnumCustom.SEND:
        print("Aucune modification possible")
    elif ...:
        ...
    else:
        raise UnknownEnumStateValue("Code is not in sync with API")

    Un petit plus... avec un mixin

    Je dispose bien entendu d'une fonction qui permet de mapper un état sous forme de chaîne de caractères à la valeur représentée dans l'enum de référence. Moralement et en très très gros, elle ressemble à ça :

    @functools.lru_cache
    def map_str_to_custom_enum(s: str) -> MonEnumCustom:
        """docstring
        """
        return dict((v.value, v) for v in MonEnumCustom)[s]

    L'idée étant donc de faire le mapping inverse de celui fourni par 'enum, i.e. de générer automatiquement et efficacement une variante d'enum à partir de sa représentation sous forme de chaîne de caractères.

    Sauf qu'en réalité, la fonction n'est pas du tout implémentée ainsi. À la place, j'utilise un mixin, ce qui me permet d'avoir la même fonctionnalité sur tous les enum. La fonction ci-dessus est remplacée par une méthode de classe. Le cache lru_cache est remplacé par un attribut de classe de type dictionnaire, ce qui évite toutes sortes d'inconvénients.

    class EnumMixin:
        _enum_values = {}
        def __init__(self, *args, **kwargs):
            self.__class__._enum_values[args[0]] = self
    
        @classmethod
        def convert_str_to_enum_variant(cls, value: str):
            cls._enum_values
            return cls._enum_values(value, object())

    Avec le mixin de conversion, le code de génération d'enum devient :

    # inchangé
    enum_mapping = EnumMappingHelper(d_enum_values)
    # MonEnumCustom hérite en premier du mixin EnumMixin
    MonEnumCustom = type("MonEnumCustom", (EnumMixin, enum.Enum), enum_mapping)

    Gestion de version de l'API pour les variantes de l'enum

    Enfin en ce qui concerne la gestion de version de l'API, il est possible de rajouter un assert dans le code qui vérifie toutes les variantes de l'enum, afin de lever une exception le plus tôt possible et de ne pas faire tourner du code qui ne serait pas en phase avec la version de l'API utilisée.

    # je définis la variable suivante en copiant directement les valeurs
    # récupérées par un appel à l'API au moment du développement :
    ls_enum_variantes_copie_statiquement_dans_mon_code = [
        "VALIDATION",
        "SEND",
        "COMMIT",
        "REVIEW",
        ... # et une centaine d'états supplémentaires
    ]
    # si la ligne suivante lance une AssertionError, l'API a mis à jour
    # les variantes de l'enum
    assert fetch_all_states(...) == ls_enum_variantes_copie_statiquement_dans_mon_code

    Dans mon cas, je suis plus souple, car ayant la garantie que les variantes publiées de mon enum ne seront pas dépréciées par de nouvelles versions de l'API, je vérifie simplement cette assertion :

    # idem
    ls_enum_variantes_copie = [
        "VALIDATION",
        "SEND",
        "COMMIT",
        "REVIEW",
        ... # et une centaine d'états supplémentaires
    ]
    # si la ligne suivante lance une AssertionError, l'API n'a pas honoré son
    # contrat de ne pas déprécier les variantes de l'enum.
    assert set(fetch_all_states(...)).issuperset(set(ls_enum_variantes_copie))

    Notez que si je souhaite être strict et n'autoriser aucun changement des variantes de l'API, alors il est préférable de copier coller les variantes directement dans le code et de comparer ces valeurs à l'exécution. Le code en est rendu plus lisible. Mais ce n'est pas le cas de figure dans lequel je me trouve.

    Conclusion

    Seule la solution utilisant un copier coller permettait de voir la liste des valeurs de l'enum dans le code. Je combine les avantages de cette solution et de la troisième que j'ai implémentée en copiant les valeurs prises par l'enum dans la docstring de sa classe et en mentionnant la version du service distant associé. Cela me permet de combiner les avantages de toutes les solutions à l'exception, bien sûr, d'un peu de temps passé.

    L'inconvénient inattendu de ce code, c'est celui de devoir créer une classe d'aide dont le seul but soit de forcer un comportement afin d'obtenir le résultat escompté. Si vous avez des suggestions pour améliorer ce bazar, je prends.

    Joyeux code !

    Motius

    • At chevron_right

      Démonter un disque distant après une erreur réseau

      motius · pubsub.gugod.fr / atomtest · Friday, 29 May, 2020 - 22:00 · 1 minute

    Bonjour ! Aujourd'hui, un micro tutoriel d'administration système pour gérer des problèmes de déconnexion du réseau. J'utilise sshfs afin de monter un disque distant. Ça me permet d'avoir de la synchronisation de données sur un seul disque dur de référence, qui reste solidement attaché à un serveur....

    Bonjour !

    Aujourd'hui, un micro tutoriel d'administration système pour gérer des problèmes de déconnexion du réseau.

    J'utilise sshfs afin de monter un disque distant. Ça me permet d'avoir de la synchronisation de données sur un seul disque dur de référence, qui reste solidement attaché à un serveur. Il m'est donc inutile de brancher et débrancher le disque, et de le déplacer.

    Ce programme s'utilise ainsi :

    sshfs user@remote:/chemin/du/répertoire/distant /mnt/chemin/du/répertoire/local

    Note : il vous faudra avoir les droits d'écriture sur le répertoire local sur lequel vous montez le disque distant.

    Et pour le démonter, j'utilise :

    fusermount -u /mnt/chemin/du/répertoire/local

    Cette commande peut échouer pour plusieurs raisons, notamment s'il y a toujours un programme qui utilise le disque distant. Ça peut être aussi bête que d'essayer de démonter le répertoire depuis un shell (bash, zsh) dont le répertoire courant est dans l'arborescence sous le disque distant.

    Le message d'erreur ressemble à ceci :

    fusermount: failed to unmount /mnt/chemin/du/répertoire/local: Device or resource busy

    Il suffit d'utiliser la commande lsof (list open files) pour savoir quels programmes utilisent encore le disque distant de cette manière :

    lsof | grep /mnt/chemin/du/répertoire/local

    Mais ! mais mais, il peut arriver que la connexion réseau se coupe (ou que votre serviteur déplace son ordinateur hors de portée du Wi-Fi, il lui arrive d'être distrait...)

    Dans ce cas, on peut essayer de recourir à umount en tant qu'utilisateur root, si on lui passe les bonnes options, que voici :

    # Lazy unmount. Detach the filesystem from the file hierarchy now, and clean up all
    # references to this filesystem as soon as it is not busy anymore.
    umount -l /mnt/chemin/du/répertoire/local 
    
    # StackOverflow suggère aussi :
    # https://stackoverflow.com/questions/7878707/how-to-unmount-a-busy-device
    umount -f /mnt/chemin/du/répertoire/local
    # mais la page de manuel indique que cette option est utile pour les disques NFS.

    Joyeux code !

    Motius

    • At chevron_right

      Wireguard, le VPN sauce KISS

      raspbeguy · pubsub.gugod.fr / atomtest · Thursday, 14 May, 2020 - 22:00 · 9 minutes

    Vous vous souvenez la dernière fois que je vous ai parlé de Wireguard ? Moi non plus, je n'en ai jamais parlé, et c'est un scandale. Ça fait pourtant presque un an que je l'utilise à titre personnel et c'est clairement un de mes outils système préféré toutes catégories confondues. Il y a eu beaucoup...

    Vous vous souvenez la dernière fois que je vous ai parlé de Wireguard ? Moi non plus, je n'en ai jamais parlé, et c'est un scandale. Ça fait pourtant presque un an que je l'utilise à titre personnel et c'est clairement un de mes outils système préféré toutes catégories confondues.

    Il y a eu beaucoup d'articles sur Wireguard, beaucoup de tests, beaucoup de comparatifs. Il faut dire que ce n'est pas passé inaperçu, à tel point que même le grand Linus en a dit du bien.

    Mais vous savez bien que je ne suis pas du genre à vanter les prouesses d'un outil pour la seule raison qu'il est à la mode. Sinon je ferais du Docker, de nombreux frameworks Javascript, du Big Data et d'autres digitaleries marketeuses.

    Sans plus tarder, je vais donc vous faire l'affront de vous présenter un outil qui est déjà bien couvert.

    Je suis tombé sur Wireguard il y a quelques années en me baladant sur le site de Jason Donenfeld, l'auteur de pass, un gestionnaire de mot de passe que j'utilisais déjà. De quoi, je vous en ai jamais parlé non plus ? Franchement c'est impardonnable.

    Présentation

    Wireguard est un VPN. Un VPN, pour rappel, c'est le fait de mettre plusieurs machines plus ou moins éloignées géographiquement sur un même réseau abstrait, et bien entendu de manière sécurisée. Les VPN sont très utilisés de nos jour, surtout depuis le début du confinement et l'explosion du télétravail. Je vous ai déjà parlé d'OpenVPN par exemple ici et ici. Pour moi, avant Wireguard, OpenVPN était le VPN le plus pratique à déployer. Cependant il a plusieurs défauts :

    • La sécurité des transferts est faible voire trouée, notament à cause de l'usage de la bibliothèque OpenSSL.
    • Les performances sont loin d'être optimales.
    • Plusieurs implémentations différentes mênent à des comportement différents selon les plateformes.
    • Le volume de code est extravagant.

    D'autres VPN existent et sont un peu plus performants et plus sûrs, comme IPsec. Mais, concernant IPsec, son utilisation est un peu mystique si j'ose dire, et niveau volume de code, c'est encore pire. Mais alors, d'où vient cette malédiction des VPN ? Pas vraiment d'explication si ce n'est que le code est assez vieux, ce qui d'habitude mène à un outil solide et à l'épreuve des bugs, alors qu'ici cela a apporté des couches de codes cancéreuses et la nécrose qui l'accompagne.

    Wireguard est donc un nouveau VPN qui est, malgré son aproche moderne et son chiffrement dernier cri, le premier à miser sur la maintenabilité et le minimalisme de son code. Cette stratégie vise à faciliter les audits de sécurité (afin que tout le monde sache que c'est un protocole sûr) et la réduction de la surface d'attaque de personnes malveillantes (moins de code = moins de failles).

    Autre point très important, et c'est pour moi le point clef, c'est la simplicité d'utilisation pour les administrateurs systèmes. En gros, une connexion Wireguard pourra être manipulées avec les outil réseau standards disponibles sur UNIX. La simplicité se retrouve aussi dans la configuration. L'établissement d'une liaison Wireguard est pensée pour être aussi simple qu'une connexion SSH. Il s'agit en effet d'un simple échange de clef publiques. Donenfeld dit s'inspirer de la simplicité des outils OpenBSD.

    Le protocole étant été prévu comme intégré directement au noyau, il s'est présenté sous la forme d'un module kernel jusqu'à sa fusion directe dans le noyau Linux le 30 mars dernier. Un patch pour le noyau OpenBSD est en cours de peaufinage et fera d'OpenBSD le deuxième système d'exploitation intégrant Wireguard nativement.

    Topologie

    Contrairement à OpenVPN qui par défaut a une vision client/serveur, c'est à dire que tous les membres du réseau privé gravitent autour d'un unique point d'accès, Wireguard laisse une grande liberté de manœuvre et considère tous les membres du VPN comme des pairs indépendants : chaque nœud possède une liste de pair à qui il va parler directement. Par example, imaginons un réseau privé de trois machines Alice, Bob et Carol. Le plus efficace serait que chacune des trois machines connaisse toutes les autres, c'est à dire qu'elle ait deux pairs correspondant aux deux autres machines :

    • Pairs d'Alice :
      • Bob
      • Carol
    • Pairs de Bob :
      • Alice
      • Carol
    • Pairs de Carol :
      • Alice
      • Carol

    L'ennui, c'est que si on veut ajouter une nouvelle machine Dave en respectant cette topologie, il faut récupérer les infos de toutes les autres machines (Alice, Bob et Carol) pour dresser la liste de pairs de Dave, et ensuite on doit ajouter Dave à la liste de pairs d'Alice, Bob et Carol.

    De plus, pour établir une liaison entre deux pairs, il faut un point d'accès connu pour au moins un des pairs (une IP et un port UDP). Si on ne connait pas les points d'accès d'Alice et Bob, ou alors si Alice et Bob n'ont pas besoin de se parler entre eux et s'intéressent uniquement à Carol, alors Alice et Bob n'ont besoin que de Carol dans leurs liste de pairs. Carol doit avoir Alice et Bob dans ses pairs. C'est le scénario connu sous le nom de roadwarrior : Alice et Bob sont des pairs mobiles et Carol est une passerelle vers un autre réseau dont les pairs mobiles ont besoin. Dans le cas d'usage du télétravail, Alice et Bob sont des salariés chez eux et Carol est la passerelle du parc informatique de l'entreprise.

    • Pairs d'Alice :
      • Carol
    • Pairs de Bob :
      • Carol
    • Pairs de Carol :
      • Alice
      • Bob

    Ainsi, quand on voudra ajouter Dave dans la topologie, il suffira que Dave ait connaissance de Carol dans sa liste de pairs, et il faudra dire à Carol d'ajouter Dave à ses pairs.

    Ne vous méprenez pas, dans la topologie roadwarrior, le fait que les pairs mobiles ne soient pas les uns dans les listes de pairs des autres ne signifie pas obligatoirement qu'il ne pourront pas communiquer entre eux. Simplement, ils devront communiquer en passant par la passerelle (qui devra être préparée à cet effet) au lieu de communiquer directement comme dans le cas où tout le monde connait tout le monde.

    Paire de clefs

    Je vous ai parlé d'échanges de clefs sauce SSH. Pour établir une laison entre deux pairs, il faut que chacun des pairs génère une paire de clef.

    wg genkey | tee private.key | wg pubkey > public.key

    wg genkey va générer une clef privée et wg pubkey va créer la clef publique correspondante.

    Ensuite, les deux pairs doivent s'échanger leur clefs publiques par les moyens qu'ils estiment les plus adéquats (vérification en personne, mail, SMS, télégramme, fax...).

    Disons qu'Alice et Bob souhaitent devenir pairs l'un de l'autre. Chacun se crée une paire de clef.

    • Pour Alice :
      • Clef privée : 6JcAuA98HpuSqfvOaZjcwK5uMmqD2ue/Qh+LRZEIiFs=
      • Clef publique : gYgGMxOLbdcwAVN8ni7A17lo3I7hNYb0Owgp3nyr0mE=
    • Pour Bob :
      • Clef privée : yC4+YcRd4SvawcfTmpa0uFiUnl/5GR1ZxxIHvLvgqks=
      • Clef publique : htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=

    Alice et Bob vont ensuite s'échanger leurs clefs publiques.

    Notez comme la tête d'une clef privée ressemble à celle d'une clef privée. C'est tentant de confondre les deux. Mais ne le faîtes pas, ce serait mauvais pour votre karma.

    Ensuite Alice et Bob vont constituer leurs fichiers de configuration, à placer dans /etc/wireguard/wg0.conf. Le fichier n'est pas obligé de s'appeler wg0.conf, il doit juste se terminer par .conf.

    Pour Alice :

    [Interface]
    PrivateKey = 6JcAuA98HpuSqfvOaZjcwK5uMmqD2ue/Qh+LRZEIiFs=
    Address = 10.0.0.1/16
    
    [Peer]
    PublicKey = htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=
    AllowedIPs = 10.0.0.2/32

    Pour Bob :

    [Interface]
    PrivateKey = yC4+YcRd4SvawcfTmpa0uFiUnl/5GR1ZxxIHvLvgqks=
    Address = 10.0.0.2/16
    
    [Peer]
    PublicKey = gYgGMxOLbdcwAVN8ni7A17lo3I7hNYb0Owgp3nyr0mE=
    AllowedIPs = 10.0.0.1/32

    Trois remarques :

    • Seule la clef publique permet de différencier les pairs. Il n'y a pas de champs pour un nom ou un éventuel commentaire.
    • L'IP ou la plage IP définie dans AllowedIPs correspond à toutes les adresses IP cibles des paquets qui seront envoyées à ce pair, et à toutes les adresses IP sources des paquets susceptibles d'être reçus par ce pair. On en reparle plus tard.
    • En l'état, le VPN ne pourra pas marcher : ni Alice ni Bob ne sais où trouver l'autre pair. Il faut qu'au moins un des deux pairs ait un point d'accès, comme nous l'avons expliqué plus haut. S'il est décidé qu'Alice communique son point d'accès, Alice devra ajouter un champ ListenPort à ta rubrique Interface, et Bob ajoutera un champ Endpoint à la déclaration du pair correspondant à Alice.

    Pour Alice, sa configuration devient :

    [Interface]
    PrivateKey = 6JcAuA98HpuSqfvOaZjcwK5uMmqD2ue/Qh+LRZEIiFs=
    Address = 10.0.0.1/16
    ListenPort = 51820
    
    [Peer]
    PublicKey = htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=
    AllowedIPs = 10.0.0.2/16

    Pour Bob :

    [Interface]
    PrivateKey = yC4+YcRd4SvawcfTmpa0uFiUnl/5GR1ZxxIHvLvgqks=
    Address = 10.0.0.2/16
    
    [Peer]
    PublicKey = gYgGMxOLbdcwAVN8ni7A17lo3I7hNYb0Owgp3nyr0mE=
    AllowedIPs = 10.0.0.1/32
    Endpoint = alice.example.com:51820

    Routage des pairs

    La signification du champ AllowedIPs est un peu subtile, car elle concerne les deux sens de circulation des paquets. C'est à la fois utilisé pour filtrer les paquets arrivant pour vérifier qu'ils utilisent une IP attendue et pour router les paquets sortants vers ce pair.

    On est pas obligé de ne mettre que l'adresse VPN du pair. D'ailleurs, notament dans le scénario roadwarrior, il faut que les machines mobiles configurent le pair correspondant à la passerelle d'accès avec un champ AllowedIPs correspondant au réseau VPN entier, par exemple 10.0.0.0/16.

    Reprenons notre scénario roadwarrior avec Alice et Bob en pair mobile et Carol en passerelle d'accès. On définit le réseau VPN 10.0.0.0/16. D'autre part, disons que le réseau interne auquel Carol doit servir de passerelle est en 192.168.0.0/16 et contient une machine Dave.

    Carol a donc une paire de clef :

    • Clef privée : 8NnK2WzbsDNVXNK+KOxffeQyxecxUALv3vqnMFASDX0=
    • Clef publique : u8MYP4ObUBmaro5mSFojD6FJFC3ndaJFBgfx3XnvDCM=

    La configuration de Carol ressemble alors à ceci :

    [Interface]
    PrivateKey = 8NnK2WzbsDNVXNK+KOxffeQyxecxUALv3vqnMFASDX0=
    Address = 10.0.0.1/16
    ListenPort = 51820
    
    [Peer] # Alice
    PublicKey = gYgGMxOLbdcwAVN8ni7A17lo3I7hNYb0Owgp3nyr0mE=
    AllowedIPs = 10.0.0.1/32
    
    [Peer] # Bob
    PublicKey = htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=
    AllowedIPs = 10.0.0.2/16

    Celle d'Alice et Bob ne contiennent d'un seul pair correspondant à Carol et ressemblant à ceci :

    [Peer]
    PublicKey = u8MYP4ObUBmaro5mSFojD6FJFC3ndaJFBgfx3XnvDCM=
    AllowedIPs = 10.0.0.0/16, 192.168.0.0/16
    Endpoint = vpn.example.com:51820

    La valeur d'AllowedIPs signifie que des paquets en 10.0.0.0/16 et 192.168.0.0/16 vont arriver en provenance de Carol, et que les paquets vers ces même plages IP seront acheminés vers ce pair. On retrouve alors bien le fait que si Alice désire parler à Bob, elle ne le pourra le faire qu'en passant par Carol. Mais ce genre de besoin est rare en roadwarrior.

    Dans un futur article j'aborderai la configuration d'une telle passerelle et les subtilités de routages VPN.

    • At chevron_right

      Repérage dans les dataclasses Python

      motius · pubsub.gugod.fr / atomtest · Monday, 11 May, 2020 - 22:00 · 2 minutes

    Bonjour ! Aujourd'hui un tout petit tutoriel pour parler esthétique et sémantique dans le développement Python. Comme vous le savez sûrement, Python a introduit dans sa version 3.7 le nouveau module dataclasses qui permet de réduire la verbosité dans la création de classes. Pour rappel, cela permet ...

    Bonjour !

    Aujourd'hui un tout petit tutoriel pour parler esthétique et sémantique dans le développement Python.

    Comme vous le savez sûrement, Python a introduit dans sa version 3.7 le nouveau module dataclasses qui permet de réduire la verbosité dans la création de classes. Pour rappel, cela permet de transformer quelque chose comme ça :

    class Chat:
        def __init__(self,
                     taille: float = None,
                     âge: int = None, # oui oui, c'est légal, déclarez π = math.pi aussi
                     couleur: str = None,
                     vivant: bool = None, # Schrödinger
                    ):
            self.taille = taille
            self.âge = âge
            self.couleur = couleur
            self.vivant = vivant

    en ça :

    @dataclasses.dataclass
    class Chat:
        taille: float
        âge: int
        couleur: str
        vivant: bool

    avec éventuellement plein de paramètres dans le décorateur dataclass, que je vous encourage à aller lire dans la doc Python. On devine assez facilement ce que font les options :

    - init=True
    - repr=True
    - eq=True
    - order=False
    - unsafe\_hash=False
    - frozen=False

    et les conséquences de leurs valeurs par défaut, mais il y a quelques subtilités qui mérite un peu de lecture.

    Ce sur quoi je souhaitais mettre l'accent dans cet article, c'est l'existence de la méthode __post_init__ dans le module. En effet, cette méthode est très pratique, et je trouve qu'elle permet facilement de distinguer deux types d'usages de dataclass.

    En général, les classes qui n'utilisent pas cet méthode sont plus souvent des vraies dataclass, au sens classes qui contiennent de la données, comparable à une struct en C ou au Records java à venir.

    A contrario, celles qui utilisent cette méthode peuvent parfois être des classes qui retiennent un état, par exemple pour de la configuration, mais celles-ci peuvent faire des choses bien plus avancées dans cette méthode qui est appelée automatiquement après la méthode init qui, je le rappelle, est gérée par le module dataclasses.

    Par conséquent, si vous rencontrez une dataclass, cela peut être pour deux raisons :

    • utiliser une nouvelle fonctionalité Python déclarative et plus élégante ;
    • créer un objet contenant principalement de la donnée.

    Connaître cette distinction sémantique et l'avoir en tête permet une analyse statique du code à la lecture plus rapide, je suis tombé sur une occurrence de ce phénomène et la partage donc.

    Joyeux code !

    Motius

    • At chevron_right

      Édito #11 : Refonte en profondeur

      raspbeguy · pubsub.gugod.fr / atomtest · Sunday, 10 May, 2020 - 22:00 · 2 minutes

    Aujourd'hui, je mets en production le nouveau site, plus simple, plus beau, plus léger. Ça faisait longtemps que j'avais envie de refaire le site. Lorsque je l'ai créé au cours de l'été 2015, je ne connaissait pas grand chose en administration système ni en bonnes pratiques pour entretenir un site w...

    Aujourd'hui, je mets en production le nouveau site, plus simple, plus beau, plus léger. Ça faisait longtemps que j'avais envie de refaire le site.

    Lorsque je l'ai créé au cours de l'été 2015, je ne connaissait pas grand chose en administration système ni en bonnes pratiques pour entretenir un site web. Je suis donc plongé dans le pière de Wordpress. Cette solution est très bien pour avoir un résultat rapide sans toucher au code une seule fois. C'est une solution que je continuerai de recommander à des gens qui n'y connaissent rien, à la condition qu'ils soient accompagnés par des experts Wordpress. Et je ne le suis pas.

    J'avais perdu tout contrôle sur le site, qui étais devenu une sorte de tumeur. J'ai décider d'amputer avant la phase terminale.

    Vous le savez, au cours de mes tribulations, j'ai affirmé mon goût pour les solutions KISS. Je me suis donc mis à la recherche d'un CMS simple à entretenir, dont je comprends le fonctionnement (et un peu le code), mais aussi dont je puisse me passer avec un minimum d'effort si je décide de trouver encore mieux. J'ai donc opté pour PicoCMS.

    PicoCMS, comme son nom l'indique, est un CMS minimaliste. Il a le bon goût de travailler sans base de données : les articles sont stockés sous la forme de simples fichiers Markdown. Il est donc très facile de versionner le contenu du site avec git. D'autre part le langage Markdown est plus ou moins standard et fonctionne avec des tonnes d'autres CMS et générateurs de sites statiques. S'il me prends l'envie de changer du jour au lendemain pour passer à Pelican ou Hugo par example, ce sera très facile.

    Pour effectuer la migration de Wordpress à PicoCMS, cela n'a pas été très aisé. La conversion du format base de donnée au Markdown a été assez pénible. Il existe un script assez dégoutant qui fait la plupart du travail. Mais hélas il n'est pas exhaustif et j'ai du repasser sur beaucoup de points manuellement, à coup de find, sed, grep, notamment pour reprendre la liste des tags et les dates. L'intégration des images était aussi à revoir complètement, et les extraits de code, à cause d'une extention Wordpress bizarre (qui me semblait cool à l'époque) a été bâclée.

    Une fois les article à peu près propres, je suis tombé sur un thème très intéressant mettant en avant les tags. Et je suis assez satisfait du résultat.

    En attendant le prochain changement majeur, je vais danser sur la tombe de Wordpress.

    • At chevron_right

      La virtu pour les nuls : le stockage

      raspbeguy · pubsub.gugod.fr / atomtest · Sunday, 26 January, 2020 - 23:00 · 24 minutes

    Mise en garde : cet article va être très long, car j'ai pensé qu'il serait intéressant d'exposer ma démarche complète pour la conception de mon infrastructure, et de présenter aussi non seulement ce que j'ai mis en place, mais également ce que je n'ai pas retenu. En plus on va parler de plein de tec...

    Mise en garde : cet article va être très long, car j'ai pensé qu'il serait intéressant d'exposer ma démarche complète pour la conception de mon infrastructure, et de présenter aussi non seulement ce que j'ai mis en place, mais également ce que je n'ai pas retenu. En plus on va parler de plein de technos différentes donc vous n'êtes pas obligé de tout lire si vous voulez. D'un autre côté vous êtes toujours libre de faire ce que vous voulez donc je sais pas de quoi je me mêle.

    L'introduction reste générale, objective, et pose les contraintes, la première partie expose la solution choisie, et la deuxième partie aborde la mise en œuvre.

    Comme tout ingénieur système sain d'esprit, j'aime bien les infrastructures qui ont de la gueule. Par là, j'entends une infra qui soit à l'épreuve de trois fléaux :

    • Le temps : Les pannes étant choses fréquentes dans le métier, les différents éléments de l'infrastructure, tant matériels que stratégiques, doivent pouvoir être facilement et indépendamment ajoutés, remplacés ou améliorés.
    • L'incompétence : Les opérateurs étant des êtres humains plus ou moins aguerris, le fonctionnement de l'infrastructure doit être le plus simple possible et sa maintenance doit réduire au maximum les risques d'erreurs de couche 8.
    • L'utilisation : Personne ne doit être en mesure d'utiliser l'infrastructure d'une façon ou dans un objectif non désiré par ses concepteurs. C'est valable pour certains utilisateurs finaux qui tenteraient d'exploiter votre œuvre dans un but malveillant, aussi bien que d'éventuels changements d'équipe qui ne comprend rien à rien (cf. les dangers de l'incompétence).

    Risques de Corona

    J'aurais pu aussi préciser que votre infrastructure doit également être à l'épreuve du coronavirus, mais pas grand risque de ce côté là, quoiqu'il ne faille pas confondre avec la bière Corona, qui, si renversée négligemment sur un serveur, peux infliger des dégâts regrettables.

    Concrètement, dans le cadre d'un cluster de virtualisation, ça se traduit par quelques maximes de bon sens :

    • On fait en sorte qu'une VM puisse être exécutée sur n'importe quel hyperviseur.
    • N'importe quel opérateur doit être en mesure d'effectuer des opérations de base (et de comprendre ce qu'il fait) sans corrompre le schmilblick, à toute heure du jour ou de la nuit, à n'importe quel niveau de sobriété, et s'il est dans l'incapacité physique, il doit pouvoir les expliquer à quelqu'un qui a un clavier et qui tient debout.
    • La symphonie technologique (terme pompeux de mon cru pour désigner les différentes technos qui collaborent, s'imbriquent et virevoltent avec grâce au grand bal des services) doit s'articuler autour de technos et protocoles indépendants, remplaçables, robustes et s'appuyant les uns sur les autres.

    Que de belles résolutions en ce début d'année, c'est touchant. Alors pour retomber sur terre, on va parler du problème épineux de l'unicité des instances de VM.

    Et bien oui, le problème se pose, car dans un cluster, on partage un certain nombre de ressources, ça tombe sous le sens. Dans notre cas, il s'agit du système de stockage. Comme on l'a déjà évoqué, chaque nœud du cluster doit pouvoir exécuter n'importe quelle VM. Mais le danger est grand, car si on ne met pas en place un certain nombre de mécanismes de régulation, on risque d'exécuter une VM à plusieurs endroit à la fois, et c'est extrêmement fâcheux. On risque de corrompre d'une part les systèmes de fichiers de la VM concernée, ce qui est déjà pénible, mais en plus, si on effectue des modifications de la structure de stockage partagé à tort et à travers, on risque d'anéantir l’ensemble du stockage du cluster, ce qui impactera l'ensemble du service. C'est bien plus que pénible.

    La solution de stockage

    La virtualisation

    L'outil de gestion de machines invitées que je préfère est libvirt, développé par les excellents ingénieurs de Redhat. À mon sens c'est le seul gestionnaire de virtualisation qui a su proposer des fonctionnalités très pratiques sans se mettre en travers de la volonté de l'utilisateur. Les commandes sont toujours implicites, on sait toujours ce qui est fait, la documentation est bien remplie, les développeurs sont ouverts, et surtout, libvirt est agnostique sur la gestion des ressources, c'est à dire qu'il manipule des concepts simples qui permettent à l'opérateur de le coupler à ses propres solutions avec une grande liberté.

    Voici une liste d'autres solutions, qui ne me correspondent pas :

    • Proxmox : Il s'agit d'une de ces solutions qui misent sur le tout-en-un, et donc contraire à l'approche KISS. Cela dit, son approche multi-utilisateurs est très intuitive surtout quand on a pas de compétences poussées en virtualisation, du fait de la présence d'une interface web intuitive. Il s'agit d'une solution satisfaisante lorsqu'on souhaite utiliser un unique nœud mais dès lors qu'on construit un cluster, Proxmox fait intervenir des élément très compliqués qui peuvent vite se retourner contre vous lors de fausses manipulations qui peuvent vous sembler bénignes. De ce fait, sa difficulté d'utilisation et de maintenance augmente de façon spectaculaire, et c'est ce contraste que je lui reproche.
    • oVirt : Il s'agit d'une solution également développée par Redhat, avec interface web conne proxmox et beaucoup (peut-être trop) de fonctionnalités, et qui utilise libvirt pour faire ses opérations. Cependant, comme j'ai du mal à voir ce qui est fait sous le capot, j'ai décidé de ne pas l'utiliser.
    • Virtualbox : Du fait de l'omniprésence et de la disponibilité des tutoriels sur Virtualbox, cela aurait semblé être une bonne solution. Mais il y a plusieurs problèmes : en premier lieu cette solution a été conçue principalement autour de son interface graphique, et son interface en ligne de commande est assez pauvre. Ensuite, par Virtualbox on parle d'une part de l'outil de gestion de VM, mais également du moteur d'exécution lui même. Ce moteur nécessite l'installation d'un module tiers dans le noyau du système, et je préfère éviter cette pratique.
    • VMware : Non là je trolle. #Sapucépalibre

    Sous linux, la solution la plus logique pour faire de la virtualisation est QEMU + KVM. Comme je l'ai évoqué, il existe aussi le moteur Virtualbox que j'ai exclu pour les raisons que vous connaissez. On aurait pu aussi s'éloigner de la virtualisation au sens propre et se tourner vers des solutions plus légères, par exemple la mise en conteneurs de type LXC ou Docker.

    Les conteneurs sont de petits animaux très gentils avec un petit chapeau sur la tête pour pouvoir se dire bonjour. Et ils sont très, très intelligents.

    Les conteneurs sont très utiles lorsqu'il s'agit de lancer des programmes rapidement et d'avoir des ressources élastiques en fonction de l'utilisation. L'avantage des conteneurs est qu'ils sont très réactifs en début et en fin de vie, pour la simple raison qu'ils ne possèdent pas de noyau à lancer et donc ses processus sont directement lancés par l'hôte. En revanche, je compte lancer des machines virtuelles complètes, dotées de fonctions propres, parfaitement isolées de l'hôte, et qui vont durer pendant de longues périodes. Pour des services qui doivent durer, les conteneurs n'apportent rien d'autre que des failles de sécurité. Mais bon, c'est à la mode, vous comprenez...

    Bon c'est entendu, partons sur libvirt ne demande pas grand chose pour lier une machine virtuelle à son stockage, ce qui fait qu'il est compatible avec beaucoup de façons de faire. En particulier, si votre stockage se présente sous la forme d'un périphérique en mode block dans votre /dev, libvirt saura en faire bon usage.

    Le partitionnement

    Au plus haut niveau pour le stockage des VM, je me suis tourné naturellement vers LVM. C'est la solution par excellence quand on cherche à gérer beaucoup de volumes modulables et élastiques, avec de très bonnes performances. Besoin d'un nouveau volume ? Hop, c'est fait en une commande. Le volume est trop petit ? Pas de problème, LVM peut modifier la taille de ses partitions sans avoir à s'inquiéter de la présence d'autres partitions avant ou après, LVM se débrouille comme un chef.

    LVM s'articule autour de trois notions :

    • Les LV (logical volumes) : Les partitions haut niveau qui seront utilisées par les VM.
    • Les VG (volumes groups) : un VG est une capacité de stockage sur laquelle on va créer des LV.
    • Les PV (physical volumes) : Les volumes qui sont utilisés pour créer des VG. Un VG peut utiliser plusieurs PV, en addition ou en réplication, de façon similaire au RAID.

    Par défaut, un VG est local, c'est à dire conçu pour n'être utilisé que sur une seule machine. Ce n'est pas ce qu'on souhaite. On souhaite un VG commun à tous les nœuds, sur lequel se trouvent des LV qui, on le rappelle, ne sont utilisés que par un seul nœud à la fois. LVM nous permet de créer un tel VG partagé, en ajoutant un système de gestion de verrous.

    Là encore, il y a beaucoup de façons de faire. Moi, je cherchais un moyen qui ne m'impose pas de mettre en place des technos qui sont très mises en valeur dans la littérature mais sur lesquelles je me suis cassé les dents, à l'installation comme au dépannage. Typiquement, je refusais catégoriquement de donner dans du Corosync/Pacemaker. J'ai déjà joué, j'ai perdu, merci, au revoir. Un jour peut-être vous vanterai-je dans un article les bienfaits de Corosync lorsque j'aurai compris le sens de la vie et obtenu mon prix Nobel de la paix. Ou bien depuis l'au-delà lors du jugement dernier lorsque les bons admins monterons au ciel et les mauvais descendront dans des caves obscures pour faire du Powershell ou se faire dévorer par les singes mutants qui alimentent en entropie les algorithmes de Google. En attendant, j'y touche pas même avec un bâton.

    Une technique qui m'a attiré par sa simplicité et son bon sens fait intervenir la notion de sanlock : plutôt que de mettre en place une énième liaison réseau entre les différents nœuds et de paniquer au moindre cahot, on part du principe que de toute façon, quelle que soit la techno de stockage partagé utilisé, les nœuds ont tous en commun de pouvoir lire et écrire au mème endroit. Donc on va demander à LVM de réserver une infime portion du média pour y écrire quelles ressources sont utilisées, et avec quelles restrictions. Ainsi, les nœuds ne se marchent pas sur les câbles. Moi ça m'allait bien.

    Une photo de groupe de l'équipe admin de mon ancien travail. On venait de changer de locaux.

    D'ailleurs, à ce niveau de l'histoire, je dois dire que je ne suis pas peu fier. En effet, pour mettre au point cette infrastructure, j'ai été inspiré en grande partie par l'infrastructure utilisée dans une de mes anciennes entreprises, dans laquelle j'avais la fonction de singe savant, et je ne comprenais pas (et ne cherchais pas à comprendre) le fonctionnement de mon outil de travail. Après mon départ, quand j'ai cherché à comprendre, j'ai entretenu une correspondance courriel avec un de mes anciens référents techniques, qui a conçu cette infra et pour qui j'ai toujours eu beaucoup de respect.

    Je lui avais demandé des infos sur le sanlocking, ce à quoi il m'a répondu :

    Quand j'ai testé le sanlocking ça avait fonctionné, mais je suis pas allé plus loin. Mais ici on gère vraiment le locking à la main.

    En effet, les VG de stockages étaient de simples VG locaux. Les métadonnées étaient individuellement verrouillées localement sur chaque nœud, et dès lors qu'on voulait provisionner une nouvelle VM, on devait lancer un script qui déverouillait les métadonnées sur le nœud de travail. Du coup, rien n'empêchait un autre nœud de faire la même chose, au risque d'avoir des modifications concurrentes et la corruption de l'univers. Donc, on était obligé de hurler à la cantonade dans l'open space dès qu'on touchait à quoi que ce soit. Si on avait de l'esprit, on aurait pu parler de screamlock à défaut de sanlock. De la même façon, on risquait à tout moment de lancer des VM à deux endroits à la fois !

    En fait on passe petit à petit sur oVirt pour gérer ce cas là. Aujourd'hui sur notre cluster KVM "maison", rien ne nous empêche de démarrer x fois la même machine sur plusieurs hôtes et de corrompre son système de fichiers 😕️ C'est une des raisons de la bascule progressive vers oVirt.

    Donc je suis fier de ne pas avoir eu à passer à oVirt pour faire ça. Libvirt est suffisamment simple et ouvert pour nous laisser faire ce qu'on veut et c'est ce que j'aime.

    Le partage

    Quel PV doit-on choisir pour notre cluster ? Il nous faut un machin partagé, d'une manière ou d'une autre. J'ai envisagé au moins trois solutions :

    • Un LUN iSCSI depuis un SAN (aka. la solution optimale) : Un SAN est un serveur de stockage mettant à disposition des volumes (appelés LUN). Le stockage est donc géré par des appareils dédiés, qui se fichent complètement de la façon dont on utilise ses volumes ni sur combien de nœuds. Le SAN ne sait pas si ses volumes contiennent des systèmes de fichiers, et il ne bronchera pas du tout si vous corrompez leur contenu parce que vous êtes pas fichu de vous organiser. Le soucis avec les SAN, c'est leur coût qui fait souvent dans les cinq chiffres...
    • Une réplication DRBD multimaster (aka. la solution du pauvre) : Si vous êtes comme moi et que les brousoufs ne surpopulent pas votre compte en banque, vous pouvez vous limiter à utiliser le stockage local de vos nœuds, que vous mettez en réplication multimater DRBD. De cette façon, chaque modification du volume sera instantanément répercuté sur l'autre nœud. L'avantage de cette solution est que le stockage est local, donc pour les accès en lecture on ne génère pas de trafic réseau. D'autre part, on a de facto une réplication de l'ensemble des données en cas de panne d'un nœud. Le soucis avec DRBD, c'est qu'il ne supporte, pour l'instant, que deux nœuds. D'un autre côté, comme on est pauvre, on a difficilement plus de deux nœuds de toute façon, mais cela empêche une mise à l'échelle horizontale dans l'immédiat. On peut se consoler en planifiant qu'on pourra passer sans trop d'efforts à la solution SAN quand les finances seront propices.
    • D'autres protocoles plus lourds comme Ceph qui vous promettent la lune et des super fonctions haut niveau (aka. la mauvaise solution) : Attention danger, on rentre dans l'effroyable empire des machines à gaz : on sait pas trop ce que ça fait, ça bouffe des ressources sans qu'on sache pourquoi, et quand ça foire, si on a pas passé sa vie à en bouffer, on peut rien faire. Déjà, Ceph est une solution mise en avant par Proxmox, donc il y a déjà matière à se méfier. En plus, les fameuses fonctions haut niveau, comme le partitionnement, le modèle qu'on a choisit nous pousse à le gérer avec autre chose, ici LVM. Donc merci, mais non merci.

    Notez que peu importe la solution que vous choisirez, comme il s'agit de stockage en réseau, il est fortement recommandé d'établir un réseau dédié au stockage, de préférence en 10 Gbps. Et malheureusement, les cartes réseau et les switchs 10 Gbps, c'est pas encore donné.

    Schéma récapitulatif

    La mise en œuvre

    Lors de la phase de conception, on est parti du besoin haut niveau pour descendre au contraintes matérielles. Pour la mise en œuvre, on va cheminer dans l'autre sens.

    DRBD

    Comme je vous l'ai avoué, d'un point de vue budget infrastructure, j'appartiens à la catégorie des pauvres. J'ai donc du me rabattre sur la solution DRBD.

    Sur chacun de mes deux nœuds, je dispose de volumes de capacité identique, répondant au nom de /dev/sdb.

    Après avoir installé DRBD, il faut créer sur chacun des nœuds la ressource associés à la réplication, qu'on va nommer r0.

    # /etc/drbd.d/r0.res
    
    resource r0 {
        meta-disk internal;
        device /dev/drbd0;
        disk /dev/sdb;
    
        syncer { rate 1000M; }
            net {
                    allow-two-primaries;
                    after-sb-0pri discard-zero-changes;
                    after-sb-1pri discard-secondary;
                    after-sb-2pri disconnect;
            }
        startup { become-primary-on both; }
    
        on node1.mondomaine.net { address 10.0.128.1:7789; }
        on node2.mondomaine.net { address 10.0.128.2:7789; }
    }

    Quelques explications :

    • /dev/sdb est le périphérique de stockage qui va supporter DRBD.
    • /dev/drbd0 sera le volume construit par DRBD
    • Les noms des nœuds doivent correspondre au nom complet de l'hôte (défini dans /etc/hostname) et ne pointe pas obligatoirement vers l'adresse IP précisée, qui est souvent dans un réseau privé dédié au stockage.

    Une fois que c'est fait, on peut passer à la création de la ressource sur les deux machines :

    drbdadm create-md r0
    systemctl enable --now drbd

    DRBD va ensuite initier une synchronisation initiale qui va prendre pas mal de temps. L'avancement est consultable en affichant le fichier /proc/drbd. À l'issue de cette synchronisation, vous constaterez que l'un des nœuds est secondaires. On arrange ça par la commande drbdadm primary r0 sur les deux hôtes (normalement uniquement sur l'hôte secondaire mais ça mange pas de pain de le faire sur les deux).

    LVM

    On a besoin d'installer le paquet de base de LVM ainsi que les outils pour gérer les verrous.

    apt install lvm2 lvm2-lockd sanlock

    La page du manuel de lvmlockd est extrêmement bien ficelée mais je sens que je vais quand même devoir détailler sinon ce serait céder à la paresse. On va donc reprendre les étapes de la même façon que la documentation.

    On prends conscience que c'est bien le type sanlock qu'on veut utiliser. L'autre (dlm) est presque aussi nocive que Corosync.

    Dans le fichier /etc/lvm.conf on modifie :

    use_lvmlockd = 1
    locking_type = 1

    Sur la version de LVM de la Debian actuelle (Buster), le paramètre locking_type est obsolète et n'a pas besoin d'être précisé.

    Ne pas oublier de donner un numéro différent à chaque hôte dans le fichier /etc/lvm/lvmlocal.conf dans la rubrique local dans le paramètre host_id.

    On active sur chaque hôte les services qui vont bien :

    systemctl enable --now wdmd sanlock lvmlockd

    On crée le VG partagé virtVG (sur un seul nœud) :

    vgcreate --shared virtVG /dev/drbd0

    Puis enfin on active le VG sur tous les hôtes :

    vgchange --lock-start

    Vous pouvez dès à présent créer des LV. Pour vérifier que votre VG est bien de type partagé, vous pouvez taper la commande vgs -o+locktype,lockargs. Normalement vous aurez un résultat de ce genre :

      VG     #PV #LV #SN Attr   VSize   VFree   LockType VLockArgs    
      virtVG   1   2   0 wz--ns 521,91g 505,66g sanlock  1.0.0:lvmlock

    Dans la colonne Attr, le s final signifie shared, et vous pouvez voir dans la colonne LockType qu'il s'agit bien d'un sanlock.

    Pour créer un LV de 8 Go qui s'appelle tintin :

    node1:~# lvcreate -L 8g virtVG -n tintin
      Logical volume "tintin" created.
    node1:~# lvs
      LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
      prout  virtVG -wi------- 8,00g                                                    
      test   virtVG -wi------- 8,00g                                                    
      tintin virtVG -wi-a----- 8,00g

    Le a de la commande Attr indique que le volume est activé. C'est sur cette notion d'activation qu'on va pouvoir exploiter le verrou. Pour en faire la démonstration, rendez-vous sur un autre hôte (on n'a qu'un seul autre hôte comme on est pauvre).

    node2:~# lvs
      LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
      prout  virtVG -wi------- 8,00g                                                    
      test   virtVG -wi------- 8,00g                                                    
      tintin virtVG -wi------- 8,00g

    Notez que tintin n'est pas activé ici, et on a d'ailleurs tout fait pour que l'activation n'ait lieu que sur un seul nœud à la fois. Voyons ce qui se passe si on essaye de l'activer :

    node2:~# lvchange -ay virtVG/tintin
      LV locked by other host: virtVG/tintin
      Failed to lock logical volume virtVG/tintin.

    Cessez les applaudissements, ça m'embarrasse. Donc, pour l'utiliser sur le deuxième nœud il faut et il suffit de le désactiver sur le premier.

    node1:~# lvchange -an virtVG/tintin
    node1:~# lvs
      LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
      prout  virtVG -wi------- 8,00g                                                    
      test   virtVG -wi------- 8,00g                                                    
      tintin virtVG -wi------- 8,00g

    Et sur le deuxième on retente :

    node2:~# lvchange -ay virtVG/tintin
    node2:~# lvs
      LV     VG     Attr       LSize Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
      prout  virtVG -wi------- 8,00g                                                    
      test   virtVG -wi------- 8,00g                                                    
      tintin virtVG -wi-a----- 8,00g

    Magie. Quand un volume est activé, libre à vous d'en faire un système de fichier, de le monter, de faire un dd dessus... Tous ce dont vous aurez besoin c'est du chemin du périphérique /dev/virtVG/tintin. Le plus probable dans le cadre de la virtualisation est qu'il va y être déployé une table de partition MBR ou GPT, auquel cas il faudra, si un jour vous souhaitez explorer ce LV autrement que depuis la VM, lire sa table de partition avec kpartx.

    Virtualisation

    On arrive presque à l'issue de la chaîne des technos. En fait, dès maintenant, libvirt est capable de créer des VM. Mais par contre, en l'état, vous devrez vous charger manuellement de l'activation des LV. C'est un peu fastidieux, surtout dans un milieu industriel.

    D'un point de vue conceptuel, deux possibilités s'offrent à nous :

    • On définit toutes les VM sur tous les hôtes (et bien sûr on ne démarre des VM que sur un seul hôte à la fois).
    • On définit sur un hôte uniquement les VM que cet hôte a le droit d'exécuter pour l'instant

    J'ai choisi la deuxième option pour des raisons de clarté, mais je n'exclue pas de passer à la première option dans le futur pour une raison ou une autre.

    Il faut ensuite adopter une stratégie d'activation des LV. On peut en choisir entre ces deux là :

    • Un hôte active les LV de toutes les VM qui sont définies sur lui.
    • Un hôte active uniquement les LV des VM en cours d'exécution sur lui.

    J'ai choisi la deuxième stratégie, car la première est incompatible avec l'option de définir toutes les VM sur tous les hôtes.

    Selon cette deuxième stratégie, il y a trois opérations de base sur les VM qui nécessitent d'intervenir sur l'activation des LV :

    • La mise en service
    • L'arrêt
    • La migration (le transfert d'une VM d'un hôte à un autre)

    Pour la mise en service, il faut activer le volume (et donc poser un verrou) avant que la machine ait commencé à s'en servir. Pour l'arrêt, il faut désactiver le volume (et donc libérer le verrou) après que la machine ait fini de s'en servir. Pour la migration... c'est plus compliqué.

    Pour la migration à froid, c'est à dire pendant que la machine est à l'arrêt, c'est facile : il faut activer le volume sur la machine cible avant la migration et le désactiver après. Pour la migration à chaud, c'est à dire pendant que la VM est en marche, je vous propose d'étudier la suite des états de la VM et du LV, avec en gras les événements et en italique les périodes :

    1. Avant l'instruction de migration : la VM est en marche sur l'hôte source, donc le LV associé doit être activé sur l'hôte source.
    2. Instruction de migration
    3. Entre l'instruction de migration et la migration effective : la VM est en marche sur l'hôte source, donc le LV associé doit être activé sur l'hôte source ; l'hôte cible vérifie qu'il peut accéder au LV, donc le LV doit être activé sur l'hôte cible.
    4. Migration effective
    5. Après la migration effective : la VM est en marche sur l'hôte cible, donc le LV associé doit être activé sur l'hôte cible.

    Vous le voyez bien : à l'étape 3, il est nécessaire que le LV de la VM soit activé à la fois sur la source et sur la cible. Et là, ce pour quoi on s'est battu depuis le début de cet article vole en éclat, et notre beau château de carte est désintégré par une bourrasque de vent.

    Mais non, il reste de l'espoir, car je ne vous ai pas encore tout dit.

    En fait, par défaut, les verrous des LV sont exclusifs, mais il est aussi possible de poser un verrou partagé :

    lvchange -asy virtVG/tintin

    Lorsqu'un LV est activé avec un verrou partagé, n'importe quel autre hôte a la possibilité d'activer le même LV avec un verrou partagé. Il faudra attendre que tous les hôtes aient désactivé le LV (et donc libéré leurs verrous partagés) pour que le LV puisse à nouveau être activé avec un verrou exclusif.

    Bien entendu, si on laisse un verrou partagé sur plusieurs hôtes pendant une longue période, il est probable qu'un opérateur négligeant finisse par commettre l'irréparable en lançant la VM en question alors qu'elle est déjà en marche ailleurs...

    C'est pourquoi il faut absolument que les périodes d'usage des verrous partagés soient réduites au nécessaire, c'est à dire pendant la migration.

    Heureusement, libvirt vous donne la possibilité de mettre en place des crochets, c'est à dire des scripts qui vont être automatiquement appelés par libvirt au moment où il lance ces opérations. L'emplacement du crochet dont on a besoin est /etc/libvirt/hooks/qemu.

    Nouveau problème : en l'état actuel, en ce qui concerne la migration, libvirt invoque le crochet uniquement sur l'hôte cible, et uniquement avant la migration effective. Nous avons besoin qu'il invoque le crochet à trois autres reprises :

    • Avant la migration effective, sur l'hôte source, pour permettre de muter le verrou exclusif en verrou partagé
    • Après la migration effective, sur l'hôte source et l'hôte, pour permettre de libérer les deux verroux partagés, dans le cas d'une migration à froid

    Dans le cas d'une migration à chaud, libvirt notifie de toute façon, à la source, l'arrêt de la VM et donc la libération du verrou quel qu'il soit, et sur la cible, le démarrage de la VM et donc la pose d'un verrou exclusif qui écraserait un éventuel verrou partagé.

    J'ai remonté le problème aux ingénieurs de Redhat en charge de libvirt, ils sont en train de développer la solution (suivez le fil sur la mailing list). Je publierai mon script dès que ce problème sera réglé.

    À propos de la configuration de libvirt, j'ai un dernier conseil qui me vient à l'esprit. Il ne sert pas à grand chose de configurer le VG en temps que pool de volume, au sens de libvirt. En vérité, on peut très bien démarrer une VM même si son volume est en dehors de tout pool défini dans libvirt. En plus, si le pool est en démarrage automatique, libvirt va activer tous les LV quand il va démarrer, ce qui va provoquer une erreur dans le cas d'un redémarrage d'un hôte dans un cluster en service.

    Le seul avantage de définir un pool auquel je peux penser est que libvirt peut créer des LV sans commande LVM auxiliaire au moment de la création d'une VM. La belle affaire. Personnellement je provisionne déjà mes VM avec un outil auxiliaire qui s'appelle virt-builder, faisant partie de la très utile suite utilitaire libguestfs. Je vous la recommande, elle fera peut être l'objet d'un futur article.

    J'ai déjà beaucoup trop parlé et ça m'a saoulé, alors maintenant je vais la fermer et dormir un peu.

    • At chevron_right

      La virtu pour les nuls : faire un bridge

      raspbeguy · pubsub.gugod.fr / atomtest · Saturday, 10 August, 2019 - 22:00 · 5 minutes

    Je vous avais déjà fait part de ma fascination pour la virtualisation. Et c'est vrai. J'aime la virtualisation. Bon Dieu ce que je l'aime. Aaaaaah. Diantre j'adore ça. Ça me donne envie de sortir dans la rue et de faire des trucs stupides tellement je kiffe ça. Bref. Plus rationnellement et plus déc...

    Je vous avais déjà fait part de ma fascination pour la virtualisation. Et c'est vrai. J'aime la virtualisation. Bon Dieu ce que je l'aime. Aaaaaah. Diantre j'adore ça. Ça me donne envie de sortir dans la rue et de faire des trucs stupides tellement je kiffe ça.

    Bref.

    Plus rationnellement et plus décemment, la virtualisation (ou l'isolation de contexte, alias les conteneurs, mais j'ai plus de mal avec ça) est le moyen le plus efficace pour avoir des services propres, maintenables et sûrs. C'est une méthode de travail excellente et très intéressante.

    La virtualisation nécessite que la machine hôte mette à disposition des machines invitées un certain nombre de ressources : processeurs, mémoire, interfaces... Notamment, nos machines virtuelles ont la plupart du temps besoin de réseau.

    Il existe au moins deux façons de faire du réseau avec des machines virtuelles, une bonne et une mauvaise une facile et une pratique :

    • La méthode facile, c'est de mettre toutes les VMs d'un hyperviseur dans un réseau NATé, dont l'hyperviseur est la passerelle.
    • La méthode pratique, c'est de passer un pont (bridge) liant l'interface physique de l'hôte avec les interface virtuelles des invités.

    Maintenant un brin d'explication.

    • La méthode NAT est facile car elle est déployée par défaut par la plupart des hyperviseurs. Elle ne demande aucune modification du réseau de l'hyperviseur. Toutes les requêtes des invités semblent donc émaner de l'hôte, de la même manière que dans votre réseau local dans votre maison, toutes vos machines passent leurs requêtes à votre box-routeur. Cela pose un problème de taille lorsque les invités ont besoin d'être accessible de l'extérieur, ce qui nécessite de gérer des règles d'ouvertures de ports (et donc deux machines ne peuvent pas écouter sur le même port, vu de l'extérieur). D'autre part, il arrive souvent que l'hôte soit lui même derrière un NAT (par exemple si vous ne disposez que d'une seule IP publique pour votre foyer comme c'est le cas dans 99% des connexions internet des particuliers). Placer donc un deuxième NAT géré par votre hôte apporte donc un niveau de complexité à votre infrastructure réseau.
    • La méthode Bridge est pratique car, d'un point de vue réseau, vos invitées sont sur le même niveau que votre hôte. Outre le fait que vous avez autre chose à faire de votre vie que de gérer un double NAT et les redirections qui s'ensuivent, ça rend les invités en quelque sortes indépendants de leurs hôtes. Par exemple, imaginons que vous avez deux hôtes H1 et H2 qui font tourner un invité chacun, G1 sur H1 et G2 sur H2. Un client tiers n'a pas besoin de savoir sur quel hôte se situe G1 et G2 pour les contacter car ils ont tous les deux des IPs indépendantes. D'autre part, si vous devez passer H1 en maintenance, il vous suffit de migrer G1 sur H2 sans autre forme de procès, ce sera complètement transparent pour votre infrastructure (pour peu que votre routage de flux soit flexible).eth0

    Disons donc que pour faire tourner une distribution desktop ponctuellement pour tester un programme, vous n'avez probablement pas besoin de cet article et vous pouvez très bien vivre avec la solution NAT. Mais pour un parc de machine en production, il est presque toujours indispensable de passer par la solution Bridge.

    Mais au fait dis moi Jamy, c'est quoi un bridge ? Et bien Fred, un bridge ça veut dire pont en anglais, et comme son nom l'indique, ça va permettre de lier des interfaces réseau. Ici, le but est de lier les interfaces virtuelles des invités à l'interface physique de la machine. Cela implique que l'interface physique sera partagée par plusieurs machines.

    Mettons nous dans la tête d'un hôte dont on a pas encore configuré le bridge. Disons que son interface réseau physique s'appelle eth0. Donc, par défaut et de manière naturelle, cette interface va porter son IP et il ne va pas se poser plus de question que ça. Nous, on veut changer ça. On va créer un bridge, appelons le br0, qui va être le maître de eth0, que sera donc son esclave (ne me regardez pas comme ça, ce sont là les termes officiels, je vous assure). On configurera notre plateforme de virtualisation préférée (libvirt en ce qui me concerne, proxmox pour d'autres, vmware pour les pêcheurs) pour lui dire d'utiliser br0 comme périphérique réseau. Alors, lorsqu'on lancera un invité, son interface virtuelle sera esclave de br0. Quant à l'hôte, l'interface qui utilisera à présent pour se connecter au réseau sera br0.

    Si votre hôte est sous CentOS, on peut configurer un nouveau bridge, ici en adressage statique sur mon réseau privé derrière mon routeur, en créant le fichier /etc/sysconfig/network-scripts/ifcfg-br0 contenant ceci :

    DEVICE=br0
    TYPE=Bridge
    BOOTPROTO=static
    DNS1=8.8.8.8
    DNS2=8.8.4.4
    GATEWAY=192.168.1.1
    IPADDR=192.168.1.10
    NETMASK=255.255.255.0
    ONBOOT=yes

    Pour un hôte sous Debian, on aura ce bloc dans /etc/network/interfaces :

    iface br0 inet static
            bridge_ports eth0
            address 192.168.1.10
            broadcast 192.168.1.255
            netmask 255.255.255.0
            gateway 192.168.1.1

    Il ne faut alors pas oublier de changer la configuration de eth0 pour qu'aucune IP ne lui soit associé. Cette interface n'aura d'autre rôle que de transmettre et recevoir les paquets bruts à notre bridge, qui se chargera de dispatcher les paquets à ses esclaves où à l'hôte.

    C'est tout pour aujourd'hui. Allez chauffe Marcel, envoie le générique de fin.