14 Commits

Author SHA1 Message Date
5c82486422 [article] utilisation de JsonPath en Python 2020-07-05 00:33:08 +02:00
f13c69075f [artice] add TL;DR: 2020-06-09 22:16:44 +02:00
7060732b5c [article] On insert-ordered by default dict comparative advantages 2020-06-09 21:50:12 +02:00
cc664b5918 [article] add examples to dynamic enum 2020-06-05 19:57:33 +02:00
feee74665a add stuff to readme 2020-06-04 11:21:31 +02:00
656d093c87 rename 2 posts 2020-06-04 11:07:29 +02:00
57026c7fa5 [article] python dynamic enum 2020-06-02 21:00:13 +02:00
001420ea8d [article] fusermount umount sshfs 2020-05-30 16:50:47 +02:00
354abbdee8 fix svg test2 2020-05-18 16:50:29 +02:00
09e4032a62 fix svg test 2020-05-18 16:48:12 +02:00
a0d7eb3fbf fix images 2020-05-15 17:03:30 +02:00
e093d83600 change date 2020-05-15 17:01:07 +02:00
e2dfdd3d81 finish wireguard article 2020-05-15 16:59:20 +02:00
6286b2eee8 this was crazy 2020-05-15 14:58:53 +02:00
9 changed files with 1008 additions and 2 deletions

View File

@@ -9,7 +9,7 @@
### En-tête
L'en-tête contient les métodonnées de l'article et se présente sous la forme suivante :
L'en-tête contient les métadonnées de l'article et se présente sous la forme suivante :
```
---
@@ -52,9 +52,15 @@ Pour vérifier la cohérence des articles, des scripts sont disponibles dans le
Utilisés sans arguments, ces scripts vérifient l'ensemble des articles. Avec un argument, les scripts ne vérifient que le post dont l'argument est le nom de fichier.
__Il est très fortement recommandé d'exécuter ces scripts de vérification avant chaque commit ou push.__ Pour cela, les [hooks mis à disposision par git](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) peuvent vous être utiles.
## Publication
Pour publier, il suffit de pousser.
Pour publier, il suffit de pousser sur la branche `master`.
Comme seule la branche `master` est affichée sur le site, il est tout à fait possible d'utiliser des branches distinctes à la guise des auteurs (par exemple, pour enregistrer et versionner ses brouillons).
__Note :__ Dans un futur proche, une instance de test du blog sera déployée, et le contenu affiché correspondra à la branche `test`.
# Pour l'administrateur système

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created by diasvg.py -->
<svg width="14.250cm" height="12.000cm" viewBox="31.811 27.910 46.061 39.910"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="31.861" y="27.960" width="14.150" height="11.900" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="31.861" y="27.960" width="14.150" height="11.900" fill="none" stroke="#FFFFFF" stroke-width="0.100" />
<rect x="33.530" y="28.893" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="33.530" y="28.893" width="2.034" height="1.525" fill="none" stroke="#000000" stroke-width="0.050" />
<rect x="33.750" y="29.113" width="1.593" height="1.051" fill="#000000" stroke="none" stroke-width="0"/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="33.805,30.418 34.852,30.418 34.852,30.655 33.860,30.655 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="33.805,30.418 34.852,30.418 34.852,30.655 33.860,30.655 "/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="34.852,30.418 35.288,30.418 35.233,30.655 34.852,30.655 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="34.852,30.418 35.288,30.418 35.233,30.655 34.852,30.655 "/>
<rect x="34.923" y="30.489" width="0.095" height="0.095" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="34.923" y="30.489" width="0.095" height="0.095" fill="none" stroke="#000000" stroke-width="0.025" />
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="34.343,30.655 34.750,30.655 34.750,30.774 34.954,30.774 34.954,30.893 34.140,30.893 34.140,30.774 34.343,30.774 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="34.343,30.655 34.750,30.655 34.750,30.774 34.954,30.774 34.954,30.893 34.140,30.893 34.140,30.774 34.343,30.774 "/>
<text x="34.547" y="31.554" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Alice</text>
<rect x="42.307" y="28.893" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="42.307" y="28.893" width="2.034" height="1.525" fill="none" stroke="#000000" stroke-width="0.050" />
<rect x="42.528" y="29.113" width="1.593" height="1.051" fill="#000000" stroke="none" stroke-width="0"/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="42.583,30.418 43.629,30.418 43.629,30.655 42.638,30.655 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="42.583,30.418 43.629,30.418 43.629,30.655 42.638,30.655 "/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="43.629,30.418 44.066,30.418 44.011,30.655 43.629,30.655 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="43.629,30.418 44.066,30.418 44.011,30.655 43.629,30.655 "/>
<rect x="43.701" y="30.489" width="0.095" height="0.095" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="43.701" y="30.489" width="0.095" height="0.095" fill="none" stroke="#000000" stroke-width="0.025" />
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="43.121,30.655 43.528,30.655 43.528,30.774 43.731,30.774 43.731,30.893 42.918,30.893 42.918,30.774 43.121,30.774 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="43.121,30.655 43.528,30.655 43.528,30.774 43.731,30.774 43.731,30.893 42.918,30.893 42.918,30.774 43.121,30.774 "/>
<text x="43.324" y="31.554" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Bob</text>
<rect x="37.919" y="36.040" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="37.919" y="36.040" width="2.034" height="1.525" fill="none" stroke="#000000" stroke-width="0.050" />
<rect x="38.139" y="36.260" width="1.593" height="1.051" fill="#000000" stroke="none" stroke-width="0"/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="38.194,37.566 39.241,37.566 39.241,37.803 38.249,37.803 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="38.194,37.566 39.241,37.566 39.241,37.803 38.249,37.803 "/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="39.241,37.566 39.677,37.566 39.622,37.803 39.241,37.803 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="39.241,37.566 39.677,37.566 39.622,37.803 39.241,37.803 "/>
<rect x="39.312" y="37.637" width="0.095" height="0.095" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="39.312" y="37.637" width="0.095" height="0.095" fill="none" stroke="#000000" stroke-width="0.025" />
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="38.732,37.803 39.139,37.803 39.139,37.921 39.342,37.921 39.342,38.040 38.529,38.040 38.529,37.921 38.732,37.921 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="38.732,37.803 39.139,37.803 39.139,37.921 39.342,37.921 39.342,38.040 38.529,38.040 38.529,37.921 38.732,37.921 "/>
<text x="38.936" y="38.702" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Carol</text>
<line x1="35.285" y1="31.095" x2="38.052" y2="35.600" stroke="#000000" stroke-width="0.100" />
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="35.089,30.775 35.563,31.070 35.285,31.095 35.137,31.332 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="35.089,30.775 35.563,31.070 35.285,31.095 35.137,31.332 "/>
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="38.248,35.920 37.773,35.625 38.052,35.600 38.199,35.363 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="38.248,35.920 37.773,35.625 38.052,35.600 38.199,35.363 "/>
<line x1="39.820" y1="35.600" x2="42.586" y2="31.095" stroke="#000000" stroke-width="0.100" />
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="39.623,35.920 39.672,35.363 39.820,35.600 40.098,35.625 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="39.623,35.920 39.672,35.363 39.820,35.600 40.098,35.625 "/>
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="42.782,30.775 42.734,31.332 42.586,31.095 42.308,31.070 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="42.782,30.775 42.734,31.332 42.586,31.095 42.308,31.070 "/>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created by diasvg.py -->
<svg width="14.250cm" height="12.000cm" viewBox="31.811 27.910 46.061 39.910"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<g transform="scale(2)">
<rect x="31.861" y="27.960" width="14.150" height="11.900" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="31.861" y="27.960" width="14.150" height="11.900" fill="none" stroke="#FFFFFF" stroke-width="0.100" />
<rect x="33.530" y="28.893" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="33.530" y="28.893" width="2.034" height="1.525" fill="none" stroke="#000000" stroke-width="0.050" />
<rect x="33.750" y="29.113" width="1.593" height="1.051" fill="#000000" stroke="none" stroke-width="0"/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="33.805,30.418 34.852,30.418 34.852,30.655 33.860,30.655 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="33.805,30.418 34.852,30.418 34.852,30.655 33.860,30.655 "/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="34.852,30.418 35.288,30.418 35.233,30.655 34.852,30.655 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="34.852,30.418 35.288,30.418 35.233,30.655 34.852,30.655 "/>
<rect x="34.923" y="30.489" width="0.095" height="0.095" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="34.923" y="30.489" width="0.095" height="0.095" fill="none" stroke="#000000" stroke-width="0.025" />
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="34.343,30.655 34.750,30.655 34.750,30.774 34.954,30.774 34.954,30.893 34.140,30.893 34.140,30.774 34.343,30.774 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="34.343,30.655 34.750,30.655 34.750,30.774 34.954,30.774 34.954,30.893 34.140,30.893 34.140,30.774 34.343,30.774 "/>
<text x="34.547" y="31.554" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Alice</text>
<rect x="42.307" y="28.893" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="42.307" y="28.893" width="2.034" height="1.525" fill="none" stroke="#000000" stroke-width="0.050" />
<rect x="42.528" y="29.113" width="1.593" height="1.051" fill="#000000" stroke="none" stroke-width="0"/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="42.583,30.418 43.629,30.418 43.629,30.655 42.638,30.655 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="42.583,30.418 43.629,30.418 43.629,30.655 42.638,30.655 "/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="43.629,30.418 44.066,30.418 44.011,30.655 43.629,30.655 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="43.629,30.418 44.066,30.418 44.011,30.655 43.629,30.655 "/>
<rect x="43.701" y="30.489" width="0.095" height="0.095" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="43.701" y="30.489" width="0.095" height="0.095" fill="none" stroke="#000000" stroke-width="0.025" />
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="43.121,30.655 43.528,30.655 43.528,30.774 43.731,30.774 43.731,30.893 42.918,30.893 42.918,30.774 43.121,30.774 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="43.121,30.655 43.528,30.655 43.528,30.774 43.731,30.774 43.731,30.893 42.918,30.893 42.918,30.774 43.121,30.774 "/>
<text x="43.324" y="31.554" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Bob</text>
<rect x="37.919" y="36.040" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="37.919" y="36.040" width="2.034" height="1.525" fill="none" stroke="#000000" stroke-width="0.050" />
<rect x="38.139" y="36.260" width="1.593" height="1.051" fill="#000000" stroke="none" stroke-width="0"/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="38.194,37.566 39.241,37.566 39.241,37.803 38.249,37.803 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="38.194,37.566 39.241,37.566 39.241,37.803 38.249,37.803 "/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="39.241,37.566 39.677,37.566 39.622,37.803 39.241,37.803 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="39.241,37.566 39.677,37.566 39.622,37.803 39.241,37.803 "/>
<rect x="39.312" y="37.637" width="0.095" height="0.095" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="39.312" y="37.637" width="0.095" height="0.095" fill="none" stroke="#000000" stroke-width="0.025" />
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="38.732,37.803 39.139,37.803 39.139,37.921 39.342,37.921 39.342,38.040 38.529,38.040 38.529,37.921 38.732,37.921 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="38.732,37.803 39.139,37.803 39.139,37.921 39.342,37.921 39.342,38.040 38.529,38.040 38.529,37.921 38.732,37.921 "/>
<text x="38.936" y="38.702" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Carol</text>
<line x1="36.076" y1="29.893" x2="41.795" y2="29.893" stroke="#000000" stroke-width="0.100" />
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="35.701,29.893 36.201,29.643 36.076,29.893 36.201,30.143 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="35.701,29.893 36.201,29.643 36.076,29.893 36.201,30.143 "/>
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="42.170,29.893 41.670,30.143 41.795,29.893 41.670,29.643 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="42.170,29.893 41.670,30.143 41.795,29.893 41.670,29.643 "/>
<line x1="35.285" y1="31.095" x2="38.052" y2="35.600" stroke="#000000" stroke-width="0.100" />
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="35.089,30.775 35.563,31.070 35.285,31.095 35.137,31.332 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="35.089,30.775 35.563,31.070 35.285,31.095 35.137,31.332 "/>
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="38.248,35.920 37.773,35.625 38.052,35.600 38.199,35.363 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="38.248,35.920 37.773,35.625 38.052,35.600 38.199,35.363 "/>
<line x1="39.820" y1="35.600" x2="42.586" y2="31.095" stroke="#000000" stroke-width="0.100" />
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="39.623,35.920 39.672,35.363 39.820,35.600 40.098,35.625 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="39.623,35.920 39.672,35.363 39.820,35.600 40.098,35.625 "/>
<polygon fill="#000000" stroke="none" stroke-width="0.100" points="42.782,30.775 42.734,31.332 42.586,31.095 42.308,31.070 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.100" points="42.782,30.775 42.734,31.332 42.586,31.095 42.308,31.070 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created by diasvg.py -->
<svg width="22.150cm" height="10.850cm" viewBox="39.700 17.400 61.850 28.250"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<line x1="51.700" y1="17.450" x2="60.800" y2="17.450" stroke="#000000" stroke-width="0.100" stroke-dasharray="0.10,0.10" stroke-linejoin="round"/>
<line x1="51.700" y1="28.200" x2="60.800" y2="28.200" stroke="#000000" stroke-width="0.100" stroke-dasharray="0.10,0.10" stroke-linejoin="round"/>
<path stroke="#000000" fill="none" stroke-width="0.100" stroke-dasharray="0.10,0.10" stroke-linejoin="round" d ="M 51.700,17.450 A 1.000,1.000 0 0,0 50.700,18.450 "/>
<path stroke="#000000" fill="none" stroke-width="0.100" stroke-dasharray="0.10,0.10" stroke-linejoin="round" d ="M 61.800,18.450 A 1.000,1.000 0 0,0 60.800,17.450 "/>
<line x1="50.700" y1="18.450" x2="50.700" y2="27.200" stroke="#000000" stroke-width="0.100" stroke-dasharray="0.10,0.10" stroke-linejoin="round"/>
<line x1="61.800" y1="18.450" x2="61.800" y2="27.200" stroke="#000000" stroke-width="0.100" stroke-dasharray="0.10,0.10" stroke-linejoin="round"/>
<path stroke="#000000" fill="none" stroke-width="0.100" stroke-dasharray="0.10,0.10" stroke-linejoin="round" d ="M 50.700,27.200 A 1.000,1.000 0 0,0 51.700,28.200 "/>
<path stroke="#000000" fill="none" stroke-width="0.100" stroke-dasharray="0.10,0.10" stroke-linejoin="round" d ="M 60.800,28.200 A 1.000,1.000 0 0,0 61.800,27.200 "/>
<line x1="40.750" y1="19.925" x2="49.700" y2="19.925" stroke="#000000" stroke-width="0.100" stroke-linejoin="round"/>
<line x1="40.750" y1="25.725" x2="49.700" y2="25.725" stroke="#000000" stroke-width="0.100" stroke-linejoin="round"/>
<path stroke="#000000" fill="none" stroke-width="0.100" stroke-linejoin="round" d ="M 40.750,19.925 A 1.000,1.000 0 0,0 39.750,20.925 "/>
<path stroke="#000000" fill="none" stroke-width="0.100" stroke-linejoin="round" d ="M 50.700,20.925 A 1.000,1.000 0 0,0 49.700,19.925 "/>
<line x1="39.750" y1="20.925" x2="39.750" y2="24.725" stroke="#000000" stroke-width="0.100" stroke-linejoin="round"/>
<line x1="50.700" y1="20.925" x2="50.700" y2="24.725" stroke="#000000" stroke-width="0.100" stroke-linejoin="round"/>
<path stroke="#000000" fill="none" stroke-width="0.100" stroke-linejoin="round" d ="M 39.750,24.725 A 1.000,1.000 0 0,0 40.750,25.725 "/>
<path stroke="#000000" fill="none" stroke-width="0.100" stroke-linejoin="round" d ="M 49.700,25.725 A 1.000,1.000 0 0,0 50.700,24.725 "/>
<rect x="49.663" y="23.486" width="1.990" height="0.791" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="49.663" y="23.486" width="1.990" height="0.791" fill="none" stroke="#FFFFFF" stroke-width="0.100" />
<rect x="50.279" y="21.406" width="0.789" height="1.842" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="50.279" y="21.406" width="0.789" height="1.842" fill="none" stroke="#000000" stroke-width="0.080" />
<rect x="50.358" y="21.516" width="0.632" height="0.211" fill="none" stroke="#000000" stroke-width="0.010" />
<rect x="50.358" y="21.727" width="0.632" height="0.211" fill="none" stroke="#000000" stroke-width="0.010" />
<rect x="50.358" y="21.937" width="0.632" height="0.211" fill="none" stroke="#000000" stroke-width="0.010" />
<rect x="50.358" y="22.148" width="0.632" height="0.211" fill="none" stroke="#000000" stroke-width="0.010" />
<rect x="50.358" y="22.400" width="0.395" height="0.126" fill="none" stroke="#000000" stroke-width="0.010" />
<ellipse cx="50.950" cy="22.421" rx="0.028" ry="0.028" fill="#00FF00" stroke="none" /><ellipse cx="50.950" cy="22.421" rx="0.028" ry="0.028" fill="none" stroke="#000000" stroke-width="0.010" /><ellipse cx="50.950" cy="22.506" rx="0.028" ry="0.028" fill="#FFFF00" stroke="none" /><ellipse cx="50.950" cy="22.506" rx="0.028" ry="0.028" fill="none" stroke="#000000" stroke-width="0.010" /><rect x="50.792" y="22.442" width="0.095" height="0.084" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="50.792" y="22.442" width="0.095" height="0.084" fill="none" stroke="#000000" stroke-width="0.010" />
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 50.411,22.695 L 50.411,23.156 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 50.542,22.695 L 50.542,23.156 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 50.674,22.695 L 50.674,23.156 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 50.805,22.695 L 50.805,23.156 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 50.937,22.695 L 50.937,23.156 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 51.068,22.695 L 51.068,23.156 "/>
<polygon fill="#999999" stroke="none" stroke-width="0.010" points="50.121,23.406 50.279,23.090 50.279,23.248 51.068,23.248 51.068,23.090 51.279,23.406 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.010" points="50.121,23.406 50.279,23.090 50.279,23.248 51.068,23.248 51.068,23.090 51.279,23.406 "/>
<text x="50.700" y="24.052" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Carol</text>
<rect x="41.562" y="21.387" width="0.789" height="1.842" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="41.562" y="21.387" width="0.789" height="1.842" fill="none" stroke="#000000" stroke-width="0.080" />
<rect x="41.641" y="21.497" width="0.632" height="0.211" fill="none" stroke="#000000" stroke-width="0.010" />
<rect x="41.641" y="21.708" width="0.632" height="0.211" fill="none" stroke="#000000" stroke-width="0.010" />
<rect x="41.641" y="21.918" width="0.632" height="0.211" fill="none" stroke="#000000" stroke-width="0.010" />
<rect x="41.641" y="22.129" width="0.632" height="0.211" fill="none" stroke="#000000" stroke-width="0.010" />
<rect x="41.641" y="22.382" width="0.395" height="0.126" fill="none" stroke="#000000" stroke-width="0.010" />
<ellipse cx="42.233" cy="22.403" rx="0.028" ry="0.028" fill="#00FF00" stroke="none" /><ellipse cx="42.233" cy="22.403" rx="0.028" ry="0.028" fill="none" stroke="#000000" stroke-width="0.010" /><ellipse cx="42.233" cy="22.487" rx="0.028" ry="0.028" fill="#FFFF00" stroke="none" /><ellipse cx="42.233" cy="22.487" rx="0.028" ry="0.028" fill="none" stroke="#000000" stroke-width="0.010" /><rect x="42.075" y="22.424" width="0.095" height="0.084" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="42.075" y="22.424" width="0.095" height="0.084" fill="none" stroke="#000000" stroke-width="0.010" />
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 41.694,22.676 L 41.694,23.137 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 41.825,22.676 L 41.825,23.137 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 41.957,22.676 L 41.957,23.137 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 42.088,22.676 L 42.088,23.137 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 42.220,22.676 L 42.220,23.137 "/>
<path stroke="#000000" fill="none" stroke-width="0.010" d="M 42.352,22.676 L 42.352,23.137 "/>
<polygon fill="#999999" stroke="none" stroke-width="0.010" points="41.404,23.387 41.562,23.071 41.562,23.229 42.352,23.229 42.352,23.071 42.562,23.387 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.010" points="41.404,23.387 41.562,23.071 41.562,23.229 42.352,23.229 42.352,23.071 42.562,23.387 "/>
<text x="41.983" y="24.034" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Dave</text>
<rect x="57.844" y="18.481" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="57.844" y="18.481" width="2.034" height="1.525" fill="none" stroke="#000000" stroke-width="0.050" />
<rect x="58.064" y="18.701" width="1.593" height="1.051" fill="#000000" stroke="none" stroke-width="0"/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="58.119,20.007 59.166,20.007 59.166,20.244 58.174,20.244 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="58.119,20.007 59.166,20.007 59.166,20.244 58.174,20.244 "/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="59.166,20.007 59.602,20.007 59.547,20.244 59.166,20.244 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="59.166,20.007 59.602,20.007 59.547,20.244 59.166,20.244 "/>
<rect x="59.237" y="20.078" width="0.095" height="0.095" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="59.237" y="20.078" width="0.095" height="0.095" fill="none" stroke="#000000" stroke-width="0.025" />
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="58.657,20.244 59.064,20.244 59.064,20.362 59.268,20.362 59.268,20.481 58.454,20.481 58.454,20.362 58.657,20.362 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="58.657,20.244 59.064,20.244 59.064,20.362 59.268,20.362 59.268,20.481 58.454,20.481 58.454,20.362 58.657,20.362 "/>
<text x="58.861" y="21.143" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Alice</text>
<rect x="57.844" y="24.281" width="2.034" height="1.525" fill="#B3B3B3" stroke="none" stroke-width="0"/>
<rect x="57.844" y="24.281" width="2.034" height="1.525" fill="none" stroke="#000000" stroke-width="0.050" />
<rect x="58.064" y="24.501" width="1.593" height="1.051" fill="#000000" stroke="none" stroke-width="0"/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="58.119,25.807 59.166,25.807 59.166,26.044 58.174,26.044 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="58.119,25.807 59.166,25.807 59.166,26.044 58.174,26.044 "/>
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="59.166,25.807 59.602,25.807 59.547,26.044 59.166,26.044 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="59.166,25.807 59.602,25.807 59.547,26.044 59.166,26.044 "/>
<rect x="59.237" y="25.878" width="0.095" height="0.095" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<rect x="59.237" y="25.878" width="0.095" height="0.095" fill="none" stroke="#000000" stroke-width="0.025" />
<polygon fill="#B3B3B3" stroke="none" stroke-width="0.050" points="58.657,26.044 59.064,26.044 59.064,26.162 59.268,26.162 59.268,26.281 58.454,26.281 58.454,26.162 58.657,26.162 "/>
<polygon fill="none" stroke="#000000" stroke-width="0.050" points="58.657,26.044 59.064,26.044 59.064,26.162 59.268,26.162 59.268,26.281 58.454,26.281 58.454,26.162 58.657,26.162 "/>
<text x="58.861" y="26.943" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Bob</text>
<rect x="51.218" y="22.270" width="1.640" height="0.394" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<text x="51.218" y="22.584" fill="#000000" text-anchor="start" font-size="0.42" font-family="monospace" font-style="normal" font-weight="400">
10.0.0.3</text>
<rect x="56.109" y="19.257" width="1.640" height="0.394" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<text x="57.749" y="19.572" fill="#000000" text-anchor="end" font-size="0.42" font-family="monospace" font-style="normal" font-weight="400">
10.0.0.1</text>
<rect x="56.124" y="25.147" width="1.640" height="0.394" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<text x="57.764" y="25.462" fill="#000000" text-anchor="end" font-size="0.42" font-family="monospace" font-style="normal" font-weight="400">
10.0.0.2</text>
<rect x="47.793" y="22.270" width="2.255" height="0.394" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<text x="50.048" y="22.584" fill="#000000" text-anchor="end" font-size="0.42" font-family="monospace" font-style="normal" font-weight="400">
192.168.0.1</text>
<rect x="42.482" y="22.271" width="2.255" height="0.394" fill="#FFFFFF" stroke="none" stroke-width="0"/>
<text x="42.482" y="22.585" fill="#000000" text-anchor="start" font-size="0.42" font-family="monospace" font-style="normal" font-weight="400">
192.168.0.2</text>
<text x="45.225" y="25.574" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Réseau interne</text>
<text x="56.250" y="28.049" fill="#000000" text-anchor="middle" font-size="0.80" font-family="sans" font-style="normal" font-weight="400">
Réseau VPN</text>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,82 @@
---
title: Avantages et inconvénients des dictionnaires ordonnés
date: 2020-06-09
author: motius
template: post
tags: python,dict,hashmap,développement
---
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](https://stackoverflow.com/questions/39980323/are-dictionaries-ordered-in-python-3-6), 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](https://www.youtube.com/watch?v=p33CVV29OG8), 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](https://docs.python.org/3/library/collections.html#collections.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).
```python
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](https://xkcd.com/1172/) 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

View File

@@ -0,0 +1,258 @@
---
title: Créer un enum Python dynamiquement
date: 2020-06-02
author: motius
template: post
tags: python,enum,type,développement
---
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 :
```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 :
```python
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 :
```python
>>> 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 ?
```python
# 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](https://docs.python.org/3/library/functions.html#type "Documentation Python3 sur type"), 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 :
```python
# 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 :
```python
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 :
```python
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 :
```python
@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.
```python
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 :
```python
# 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.
```python
# 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 :
```python
# 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

View File

@@ -0,0 +1,60 @@
---
title: Démonter un disque distant après une erreur réseau
date: 2020-05-30
author: motius
template: post
tags: sshfs,réseau,umount,fusermount
---
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 :
```bash
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 :
```bash
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 :
```bash
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 :
```bash
# 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

View File

@@ -0,0 +1,176 @@
---
title: Explorer des logs ELK avec JsonPath
date: 2020-07-04
author: motius
template: post
tags: python,JSON,log,ELK,développement
---
Bonjour !
Aujourd'hui, je veux vous parler d'une bibliothèque Python3 :
[`jsonpath2`](https://pypi.org/project/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](https://fr.wikipedia.org/wiki/Elk#Sigle), 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 :
```bash
# 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`
qu'il se trouve dans mon JSON, je peux le faire ainsi :
```python
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 :
```python
pattern = "$.._source.message"
```
Par rapport au motif précédent, le seul nouveau caractère spécial est :
4. `.` : 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 600000 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 :
```python
pattern = "$..[?(@._source.exception)]._source.message"
```
Il y a pas mal de nouveautés par rapport aux exemples précédents :
5. `@` : il s'agit de l'élément couramment sélectionné
6. `[]` : permet de définir un prédicat ou d'itérer sur une collection
7. `?()` : 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) :
- [un article](https://goessner.net/articles/JsonPath/) présentant le filtrage
JSON d'après l'équivalent XPath pour XML ;
- [le lien vers PyPI de la bibliothèque](https://pypi.org/project/jsonpath2/)
- [le lien GitHub de la bibliothèque](https://github.com/pacifica/python-jsonpath2/)
- [le lien vers une implémentation JavaScript populaire](https://www.npmjs.com/package/jsonpath)
de JsonPath.
`jsonpath2` utilise le générateur de parseur [ANTLR](https://www.antlr.org/),
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 :
8. `()` : 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

View File

@@ -0,0 +1,196 @@
---
title: Wireguard, le VPN sauce KISS
date: 2020-05-15
author: raspbeguy
template: post
tags: tutoriel,vpn,kiss,sécurité,réseau
---
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](https://www.zx2c4.com/), l'auteur de [pass](https://www.passwordstore.org/), 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](https://www.wireguard.com/) 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](/posts/parcs-informatiques-partie-1-lan-to-lan) et [ici](/posts/quand-debian-me-gonfle-stretch-et-openvpn). 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](https://lists.zx2c4.com/pipermail/wireguard/2020-March/005206.html). Un [patch pour le noyau OpenBSD](https://lists.zx2c4.com/pipermail/wireguard/2020-May/005427.html) 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
![](%assets_url%/wireguard-le-vpn-sauce-kiss/wg-p2p.svg)
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
![](%assets_url%/wireguard-le-vpn-sauce-kiss/wg-p2p.svg)
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 :
```ini
[Interface]
PrivateKey = 6JcAuA98HpuSqfvOaZjcwK5uMmqD2ue/Qh+LRZEIiFs=
Address = 10.0.0.1/16
[Peer]
PublicKey = htjM/99P5Y0z4cfolqPfKqvsWb5VdLP6xMjflyXceEo=
AllowedIPs = 10.0.0.2/32
```
Pour Bob :
```ini
[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 :
```ini
[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 :
```ini
[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.
![](%assets_url%/wireguard-le-vpn-sauce-kiss/wg-rw.svg)
Carol a donc une paire de clef :
* Clef privée : `8NnK2WzbsDNVXNK+KOxffeQyxecxUALv3vqnMFASDX0=`
* Clef publique : `u8MYP4ObUBmaro5mSFojD6FJFC3ndaJFBgfx3XnvDCM=`
La configuration de Carol ressemble alors à ceci :
```ini
[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 :
```ini
[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.