fetch every old content

This commit is contained in:
Fred Pauchet 2018-08-02 14:56:09 +02:00
parent 5d0b0f7525
commit 334ce3314d
230 changed files with 7515 additions and 0 deletions

5
articles/a-propos.md Normal file
View File

@ -0,0 +1,5 @@
Title: A propos
Créateur de bugs depuis 1985 (et pour longtemps encore); un peu d'admin-sys, devops, bouquins et autres trucs intéressants.
Ceci est juste un petit ensemble de ce qui sortirait par écrit si on me faisait une trépanation du cerveau. Il contient un ensemble de brouillons, d'idées qui me passent par la tête et pour lesquels je me dis "faudra un jour que j'en fasse un article". J'y reviens parfois, je les fais évoluer, vivre (et parfois mourir).

View File

@ -0,0 +1,33 @@
---
Title: Good Old Games
Date: 2014-08-28
Slug: good-old-games
---
Suite au test de Divinity: Original Sin dans le Canard PC de mi-juillet, résumé par un simple "9/10: Achetez ce jeu", je me suis mis en quête de la version idéale, celle qui permet de grapiller quelques piécettes, qui offre quelques petits bonus.
Le jeu est évidemment disponible **via** Steam. Avec un peu de recherche, on peut souvent trouver le même contenu sur d'autres plateformes. C'est généralement le cas d'amazon.co.uk, qui propose les versions boîtes pour (souvent) moins cher que la version Steam. Ne trouvant rien (en stock) aux Royaumes-Unis, j'ai regardé en France: [une version boîte pour 39,90€](http://www.amazon.fr/Focus-Divinity-Original-Sin/dp/B008AF1XLW/ref=sr_1_1?ie=UTF8&qid=1407157174&sr=8-1&keywords=divinity+original+sin)! Les frais de port sont offerts, le jeu est en stock, ... En même temps, si c'est pour conserver une boite et ne rien faire d'autre que l'exposer, cela ne m'intéresse pas.
De fil en aiguille, je suis (re)tomber sur goodoldgames.com, qui propose quelques jeux récents au prix officiel. Divinity: Original Sin est disponible à 39.90€, avec une petite note expliquant ceci:
> Gamers who are paying in EUR and GBP are generally charged more for the games that they buy than gamers in the US - this is called regional pricing. We will adamantly continue to fight for games with flat worldwide pricing, but if we cannot deliver it (as is the case with this game), we will make up most of the difference out of our own pocket by providing you with a mix of $9.99 and $5.99 game codes, which you can use to redeem games from our site. In the near future, once we have such functionality implemented, we will give you store credit instead, which you will then be able to use towards any purchase and cover the price of it in full, or partially. **If you paid 36.99 EUR, you will get one $9.99 code. If you paid 39.99 EUR, you will get one $5.99 and one $9.99 code. If you paid 29.99 GBP, you will get two $5.99 codes. As for the DLC, if you paid in EUR or GBP, you will get one $5.99 code from us**.
En gros, en payant un jeu à 39.90€, on reçoit également un bon d'achat un achat d'une valeur de $5.99, ainsi qu'un deuxième bon d'une valeur de $9.99. Après le traditionnel passage à la caisse, je me rends compte également que la clé Steam n'est pas inclue dans l'achat. Etonnant, puisque c'est une pratique courante que d'acheter un jeu sur une plateforme, et de recevoir simplement une clé pour le télécharger *via* Steam.
Commençons par les désavantages:
* On ne profite pas de la mise-à-jour automatique du contenu
* Ni de la plateforme de communication intégrée à Steam (celle qui fait que votre petit cousin vous fait "kikoo" pendant que vous lâchez la tourelle sur une armée de robots dans BD2).
* Ni des sauvegardes automatiques (s'il y en a); celles qui vous disent que votre contenu n'est pas synchronisé avec le **cloud**, et qu'au final, vous venez de perdre les deux dernières heures de jeu.
Et parmi les avantages:
* Purée mais c'est sans DRM! Ca existe encore, ça?!
* La plateforme de téléchargement de GoG permet de choper toutes les parties du jeu sans avoir à cliquer sur chacune d'entre elles.
* C'est sans DRM. Je l'avais déjà dit?
En furetant un peu, on peut aussi découvrir quelques jeux qui en valent réellement la peine :
* [Botanicula](http://www.gog.com/game/botanicula)
* [Tetrobot](https://www.gog.com/game/tetrobot_and_co)
* [Fallout 2](https://www.gog.com/game/fallout_2)

View File

@ -0,0 +1,19 @@
---
Title: "La vérite sur l'affaire Harry Quebert"
Date: 2014-09-04
Writer: "Joël Dicker"
Image: book/joel_dicker/harry-quebert-cover.jpg
Slug: the-truth-about-harry-quebert
---
Pour être honnête, la couverture n'inspirait absolument pas confiance. Cela sentait les vacances, avec des couleurs pastels, un petit village champêtre, son église, un joli ciel bleu... Tout pour faire penser à une belle histoire d'amour copiée du dernier Marc Levy. En plus de cela, les 860 pages de mon édition de poche laissaient franchement penser que je le lâcherais avant même la fin de la première partie.
Perdu.
Passé les deux premiers chapitres, tout se met en place et s'accélère. De nouveaux évènements interviennent après chaque vague émotionnelle, on est immergé de plein gré dans une histoire qui nous dépasse, où chaque détail à son importance et où l'auteur semble prendre un malin plaisir à embrouiller volontairement grâce à une mise en abîme temporelle savament orchestrée.
On s'accroche, on s'identifie aux personnages. Chacun d'entre eux cache quelque chose. On le sait, on le sent, mais on n'arrive pas réellement à mettre la main sur ce qui cloche. On sait que cela va déraper, sans jamais réellement mettre le pourquoi. Ou alors pour mieux glisser quelques pages plus loin.
Chaque détail va mettre la puce à l'oreille; on se met méfie de tout le monde, sans réellement y croire vu la quantité de pages restantes. Celle-ci se réduit de plus en plus, jusqu'au dénouement final, celui qu'on attendait, qu'on soupçonnait peut-être, mais qui s'avera finalement libérateur tant les dénouements, changements de rhytme et rebondissements sont fréquents. C'est une ôde à l'amour, au fonctionnement des instincts humains primaires. On passe de la répulsion à la compréhension en quelques pages seulement: l'auteur intrpsecte. On s'attache et se détache des personnages comme on tourne les pages: les nouveaux évènements apportent de la fraîcheur, des réponses et de nouvelles interrogations. Après l'avoir lu, on a un peu l'impression que chaque page, dialogue ou introduction y a sa place, que chaque information ou discussion aura une suite et on fait attention à chaque miette que l'auteur aura la bonté de laisser derrière lui et qu'il aura dispersé parmi toutes ces phrases et paragraphes. On se prend finalement au jeu du roman, en espérant en avoir encore un peu par après, en espérant que la prochaine page ne contiendra pas l'un de ces clichés ou facilités autorisées par l'écriture.
D'habitude, une histoire se subit: on se laisse porter par le flot des dialogues, à attendre qu'un évènement se passe. Ici, on se surprend à inventer et imaginer la suite, à porter des soupçons, à espérer qu'un évènement se produise. C'est là aussi tout le désespoir ce roman, c'est qu'il se passe simultanément dans le passé et dans le présent. Le présent fait place à un réel suspens. Ce qui arrive actuellement, la pression mise par certains personnages, la rancune tranmise par d'autres sont réellement ressenties, mais laissent place à d'autres sentiments une fois évaporées. Le passé est passé, on en connait la fin dès les premières pages, mais on se prend quand même à espérer qu'il soit faux ou partiellement incorrect, que des indices laissent à penser que... peut-être qu'on se trompe? Peut-être que tout ça ne s'est jamais passé? Peut-être que l'auteur rêve tout ce qui lui arrive, que tout cela n'est jamais arrivé, et que la réalité n'est pas celle qu'on lit?

View File

@ -0,0 +1,11 @@
---
Title: Door Kickers
Date: 2014-12-17
Slug: door-kickers
---
Des bons jeux, il y en a de moins en moins. Entre les suites et redites des jeux AAA annuels, et à part la Wii U et sa mablette, on a un peu l'impression que les consoles et ordinateurs next-gen s'enfoncent sans se renouveler réellement.
Door Kickers fout un bon coup de latte à ce principe. Original, il l'est, malgré qu'il se base sur des concepts existants depuis un bon bout de temps maintenant: on dirige une équipe de SWAT sur-entraînée, et on doit zigouiller du terroriste/sauver des otages (voire les deux, si on est bon). On joue en vue du dessus, les lignes de vue sont réalistes à souhait et prennent bien les obstacles rencontrés (vos p'tits gars tirent tous seuls, pour peu que l'ennemi soit dans leur champ de vision). Chaque mission peut être réalisée en prenant son temps, mais des challenges (réussir la mission endéans un certain temps, ne pas utiliser la pause stratégique, ...) permettent d'acquérir des étoiles. Ces étoiles permettent alors d'améliorer son équipement ou les capacités de son équipe.
Voila pour le concept. Chaque mission peut se réussir en quelques minutes, mais vous risquez d'y passer pas mal de temps pour réussir tous les défis. A tout cela, on ajoute un super bon éditeur de niveaux, des mods à développer (et à télécharger), des statistiques *alakon*, et on obtient un super bon jeu pour pas trop cher (c'est bientôt les fêtes de fin d'année, non ?).

View File

@ -0,0 +1,7 @@
---
Title: True Detective
Date: 2015-01-13
Slug: true-detective
---
Lent et psychologique. Je crois que si je devais qualifier cette série de deux adjectifs, ce seraient ceux-là. Dès le début, on se sent envahis par une ivresse relaxante, mis sous pression et transporté par un scénario digne du jeu des acteurs. Woody Harrelson est génial dans son rôle de père foireux et mari jaloux, dont le boulot de flic prend trop de temps sur la vie de famille. Il est le parfait complément de Matthew McConaughey, sociopathe, homme brisé et

View File

@ -0,0 +1,11 @@
---
Title: Le Soleil des Scorta
Date: 2015-02-09
Writer: "Laurent Gaudé"
image: book/soleil-scorta.jpg
Slug: le-soleil-des-scorta
---
> Le Soleil des Scorta est un roman écrit par Laurent Gaudé publié chez Actes Sud le 1er août 2004 et ayant remporté le prix Goncourt la même année, constituant le premier prix prestigieux obtenu par les éditions Actes Sud. Ce roman a également reçu le prix du roman populiste et le prix du jury Jean-Giono en 2004.
Source: [Wikipedia](https://fr.wikipedia.org/wiki/Le_Soleil_des_Scorta)

View File

@ -0,0 +1,12 @@
---
Title: "Le problème Spinoza"
Date: 2015-02-20
Image: book/le-probleme-spinoza.jpg
Slug: le-probleme-spinoza
---
Nous sommes en juin, j'ai pris du retard dans mes écrits. Tant pis. Ce sera de mémoire, épicétou !
De ce dont je me rappelle, c'est que ce livre donnait deux aperçus contradictoire sur la religion juive; d'une part, la vision de Spinoza quant aux obligations de la relagion, son exclusion de la communauté et ses écrits. De l'autre, [...] faisant partie des jeunesses Hitlériennes, persuadé de son bon droit, de la nécessité de se débarasser des Juifs. Ses professeurs lui demanderont un travail sur Spinoza, afin de relativiser ses idéaux face à un Juif.
*A priori*, cet article restera en brouillon :)

View File

@ -0,0 +1,13 @@
---
Title: "Un homme effacé"
Date: 2015-03-06
Slug: un-homme-efface
---
> "Quoiqu'il arrive, il faut vous faire à l'idée que vous ne ressortirez pas *blanchi* du tribunal. C'est une illusion de croire ça. On ne ressort pas blanchi d'un procès comme celui-ci. Soit on en ressort sali, soit on n'en ressort pas du tout."
Je ne pense pas qu'il soit déontologiquement propice de cataloguer un livre parlant de pédo-pornographie comme "touchant", mais c'est le seul qui me soit venu à l'esprit après avoir tourné sa dernière page.
L'auteur y parle d'une méprise: d'un professeur, petit-fils de célébrité, qui est accusé d'avoir téléchargé un petit millier de photos impliquant bébés, enfants et dégradations sexuelles. Son avocat lui conseille de plaider coupable. Déjà pas terrible à la base, sa vie lui échappe un peu plus, sans qu'il ne puisse se rattacher à quoi que ce soit qu'il connaisse: le moindre de ses actes passés devient un prétexte, son entourage lui fait bénéficier de la présomption de culpabilité et aucun de ses collègues ne lève le petit doigt pour lui tendre la main.
Au final, un bon pastiche sur la une des média, la froideur d'un système judiciaire, l'hypocrisie des relations humaines et les profondeurs dans lesquelles l'esprit humain peut parfois sombrer.

View File

@ -0,0 +1,18 @@
---
Title: "Les proies"
Date: 2015-05-16
Summary: "Un huit-clos durant la guerre de sécession."
Slug: les-proies
Image: book/les-proies.jpg
Writer: "Thomas Cullinan"
---
Une brique de 600 pages en "écrit-tout-petit" complètement malsaine, où une pensionnaire ramène un soldat blessé de l'armée adverse. Les pensionnaires vont se monter les unes sur les autres pour se rapprocher du nouveau venu, qui va tremper dans les discussions sordides et les coups montés foireux.
> Le 6 mai 1864, la forêt de la Wilderness est le théâtre de l'une des plus effroyables batailles de la guerre de Sécession. L'orage d'acier que déchaîne ce jour-là l'artillerie rebelle de Robert Lee, à laquelle répond celle du général de l'Union Ulysses Grant, embrase sans distinction arbres et fantassins. Malgré ses blessures, un caporal nordiste réussit à s'échapper du brasier et trouve refuge dans un pensionnat pour jeunes filles confédéré. Mais l'intrusion soudaine d'un mâle vient perturber la vie de recluses, pétrie de valeurs puritaines et de pulsions refoulées, des huit femmes qu'abrite encore l'institution. Objet de tous les fantasmes, le soldat va s'employer à les incarner avec un art consommé de la manipulation, jusqu'à une nuit où tout bascule. Dès lors, la haine sera sa seule maîtresse, la vengeance l'unique motivation de ses anges gardiens.
Source: [20minutes.fr](http://www.20minutes.fr/livres/1151927-20130514-20130508-les-proies-thomas-cullinan-chez-passage-nord-ouest-albi-france).
Pour l'anecdote (toujours du même site):
> En 1971, saisi par l'originalité de ce roman que les critiques littéraires américains qualifient de psychological sexual novel, Don Siegel le porte à l'écran, offrant enfin à Clint Eastwood un rôle d'exception aux antipodes de son image habituelle de gunfighter triomphant.

View File

@ -0,0 +1,15 @@
---
Title: "Little Bird"
Date: 2015-05-30
Writer: Craig Johnson
Illustration: book/craig_johnson/little-bird.jpeg
Slug: craig-johnson-little-bird
---
J'ai découvert Craig Johnson un peu au pif. Mon papou qui me sort un petit "Tiens, cela pourrait te plaire..." juste avant que je ne mette les voiles avec le schtroumpf. Mes voyages en métro commençaient à s'allonger, j'avais envie d'une histoire courte, prenante et dynamique. Il faut dire aussi qu'après avoir lu [Les proies]({{< relref "thought/book/2015-05-16-les-proies.md" >}}) (600 pages de bonne humeur dans un huit clos sanglant), j'avais besoin d'un truc frais, expéditif, drôle, ouvert, aéré, avec des grands espaces, des poneys qui courent et un arc-en-ciel.
Il ne pouvait pas mieux tomber.
> Après vingt-quatre années au bureau du shérif du comté d'Absaroka, Walt Longmire aspire à finir sa carrière en paix. Ses espoirs s'envolent quand on découvre le corps de Cody Pritchard près de la réserve cheyenne. Deux ans auparavant, Cody avait été un des quatre adolescents condamnés avec sursis pour le viol d'une jeune Indienne, un jugement qui avait avivé les tensions entre les deux communautés. Aujourd'hui, il semble que quelqu'un cherche à venger la jeune fille. Alors que se prépare un violent blizzard, Walt devra parcourir les vastes étendues du Wyoming sur la piste d'un assassin déterminé à parvenir à ses fins. Avec ce premier volet des aventures du shérif mélancolique et désabusé, Walt Longmire, Craig Johnson s'impose d'emblée parmi les plus grands.
Du grand, du très bon, de l'indispensable. A lire. Vite.

View File

@ -0,0 +1,15 @@
---
Title: Le camp des morts
Date: 2015-06-16
Writer: Craig Johnson
Illustration: book/craig_johnson/le-camp-des-morts.jpg
Slug: craig-johnson-le-camp-des-morts
---
Je continue mon exploration du far west contemporain avec le shérif Walt Longmire. Après avoir englouti le premier tome de ses aventures, *Little Bird*, je me suis lancé dans le deuxième tome.
> "Lorsque Mari Baroja est empoisonnée à la maison de retraite de Durant, Wyoming, le shérif Walt Longmire se trouve embarqué dans une enquête qui le ramène cinquante ans en arrière. Il se plonge alors dans le passé mystérieux de cette femme et dans celui de son mentor, le shérif Lucian Connally à la poigne légendaire. Tandis que l'histoire douloureuse de la victime trouve peu à peu une résonance dans le présent, d'autres meurtres se mettent sur le chemin des deux shérifs. Aidé par son ami de toujours, l'Indien Henri Standing Bear, son adjointe au langage fleuri et un nouveau venu séduisant, le shérif mélancolique et désabusé se lance à la poursuite de l'assassin à travers les Hautes Plaines enneigées."
Moins prenant que le premier, il n'en reste pas moins excellent dans son histoire, ses blagues vaseuses, sa violence et ses personnages hauts en noirceur. Le caractère des personnages déjà croisés y est approfondi et de nouveaux font leur apparition, le tout agrémenté des réfléxions du shérif ou de son pote de toujours, Henry Standing Bear. Lucian est un peu plus mis en avant dans cet épisode, et devient un élément clé pour l'enquête.
Du bon, du très bon (quoiqu'un cran en dessous du premier), bref un *must read*.

View File

@ -0,0 +1,11 @@
---
Title: L'indien blanc
Date: 2015-06-28
Illustration: book/craig_johnson/l-indien-blanc.jpg
Writer: Craig Johnson
Slug: craig-johnson-l-indien-blanc
---
> Walt Longmire, le shérif du comté d'Absaroka, n'a pas pour habitude de s'éloigner de ses terres familières du Wyoming. Quand il décide d'accompagner son vieil ami Henry Standing Bear à Philadelphie, où vit sa fille Cady, il ne se doute pas que son séjour va prendre une tournure tragique. Agressée pour une raison inconnue, Cady se retrouve dans le coma, première victime d'une longue liste, et Walt doit se lancer sur la piste d'un vaste réseau de trafiquants de drogue. Commence alors une longue errance urbaine sous la surveillance d'un mystérieux Indien blanc. Ce nouveau volet des aventures de Walt Longmire nous entraîne dans une course-poursuite haletante au coeur de la Cité de l'amour fraternel.
L'indien blanc est la troisième tome des aventures du shérif Longmire. Il reste un cran en dessous du premier (et du quatrième!), mais toujours mieux que le deuxième :)

View File

@ -0,0 +1,12 @@
---
Title: Le premier jour du reste de ma vie
Date: 2015-08-06
Summary: "Un road-movie/book sur la crise de la quarantaine."
Slug: le-premier-jour-du-reste-de-ma-vie
---
Lorsque son mari fête ses 40 ans, l'héroïne le plaque pour aller vivre un tour du monde en croisière.
Bon allez, zou, sans faire durer le suspens: je ne suis pas arrivé au bout. Le livre fait passer un bon moment, tout se passe plus ou moins bien et les moments négatifs remontent généralement la pente assez vite. On y trouve toujours quelqu'un peut sauver la situation.
Bon pour le moral, mais manque de meurtres.

View File

@ -0,0 +1,10 @@
---
Title: Demain est un autre jour
Date: 2015-12-10
Image: book/demain-est-un-autre-jour.jpg
Slug: demain-est-un-autre-jour
---
> À la mort de sa mère, Brett Bohlinger pense quelle va hériter de lempire de cosmétique familial. Mais, à sa grande surprise, elle ne reçoit quun vieux papier jauni et chiffonné : la liste des choses quelle voulait vivre, rédigée lorsquelle avait 14 ans. Pour toucher sa part dhéritage, elle aura un an pour réaliser tous les objectifs de cette life list... Mais la Brett daujourdhui na plus rien à voir avec la jeune fille de lépoque. Enseigner ? Elle na aucune envie dabandonner son salaire confortable pour batailler avec des enfants rebelles. Un bébé ? Andrew, son petit ami avocat, nen veut pas. Entamer une vraie relation avec un père trop distant ? Les circonstances ne sy prêtent guère. Tomber amoureuse ? Cest déjà fait, grâce à Andrew, à moins que... Malgré tout, Brett va devoir quitter sa cage dorée pour tenter de relever le défi. Et elle est bien loin d'imaginer ce qui l'attend.
Je l'ai lu il y a presque un an, maintenant, donc les détails ne sont plus très frais. L'histoire est parfois un peu cliché, il y a quelques retournements de situation, et il est dans l'ensemble très agréable à lire :).

View File

@ -0,0 +1,11 @@
---
Title: Paul à Québec
Date: 2016-11-21
Slug: paul-a-quebec
Cover: ""
Status: draft
---
Un petit film familial tout mignon (et un peu dramatique quand même), tiré d'une bande dessinée et adapté au cinéma.
> To be completed ;)

View File

@ -0,0 +1,10 @@
---
Title: Le magasin des suicides
Summary: How to get away with self-murder
Date: 2017-07-31
Category: Livres
---
> Imaginez un magasin où l'on vend depuis dix générations tous les ingrédients possibles pour se suicider. Cette petite entreprise familiale prospère dans la tristesse et l'humeur sombre jusqu'au jour abominable où surgit un adversaire impitoyable : la joie de vivre...
Cela se lit super-facilement, c'est bourré d'humour noir et d'idées que Franquin ne renierait pas et l'histoire ne stagne pratiquement pas. Le thème est cependant usé jusqu'à la corde (ahah). De là à le conseiller, bouaif... Sans plus.

View File

@ -0,0 +1,6 @@
---
Title: Edelweiss
Writers: Lucy Mazel, Cédric Mayen
Date: 2017-08-02
---

View File

@ -0,0 +1,11 @@
---
Title: Surveillance-deux-points-slash-slash
Status: draft
Tags: vie privée, surveillance, gafam, framasoft
---
Bon, à vrai dire, je n'avais pas spécialement envie de lire ce livre. Je passe déjà beaucoup de temps à m'auto-former, à lire des trucs techniques, philosophiques ou informatiques. Pas que le sujet ne m'intéresse pas, juste qu'avant même de l'avoir lu, je savais déjà que je devrai en parler autour de moi: à mes proches, mes amis et mes collègues. Certains d'entre eux sont déjà sensibilisés, les autres pas du tout. J'entends souvent (et j'admets) qu'il est pratique d'avoir toutes ses informations centralisées, de ne pas avoir à chercher telle ou telle donnée car elle est déjà présente ou pré-remplie, mais cette série de services gratuits cache
> Si c'est gratuit, c'est toi le produit.
http://blog.loicg.net/autour-dun-cafe/2016/10/10/privacy.html

View File

@ -0,0 +1,52 @@
---
Title: Breadcrumb en CSS
Date: 2013-03-08
Tags: css, html
Slug: css-breadcrumb
---
En vrac, un petit style pour définir facilement un breadcrumb en css:
```css
ul.breadcrumb
{
margin-bottom: 20px;
padding: 10px;
background-color: rgb(245, 245, 245);
border: 1px solid rgb(227, 227, 227);
}
ul.breadcrumb li
{
display: inline;
}
ul.breadcrumb li:after
{
content: " » ";
}
ul.breadcrumb li:last-child:after
{
content: "";
}
```
L'idée est super simple: il suffit de définir une liste non-ordonnée et ses éléments de manière classique, et définir la classe **'breadcrumb'** sur l'élément *ul*. Le **display: inline;** sur les éléments *li* les affichera sur une seule ligne (obviously), et les pseudo-éléments **:after** et **:last-child:after** définiront la séparation.
```html
<ul class="breadcrumb">
<li>Mon premier element</li>
<li>Le second element</li>
<li>Le troisieme</li>
<li>Et le dernier.</li>
</ul>
```
Le résultat final donnera quelque chose comme :
```
Mon premier element » Le second element » Le troisieme » Et le dernier.
```
Attention que ceci pourrait ne pas fonctionner sur certains navigateurs (testé sur IE > 7, FF, Chrome et Safari quand même)...

View File

@ -0,0 +1,98 @@
---
Title: Ramasse-miettes
Date: 2013-05-23
Tags: python, sql, breadcrumb, dev
Slug: breadcrumbs-folly
---
Pour faire suite à [un premier article](|filename|../css/breadcrumb-css.md) et toujours sur le même thème, voici une petite série d'idées d'implémentation pour mettre un ramasse-miettes en place.
En SQL, le plus simple est d'avoir une table qui a une référence vers elle-même. Un petit exemple ci-dessous en T-SQL:
```sql
CREATE TABLE Entity (
[Id] int IDENTITY(1,1) NOT NULL PRIMARY KEY,
[ParentId] int NULL FOREIGN KEY REFERENCES Entity(Id),
[Label] nvarchar(255) NOT NULL
)
```
That's it. Par contre, cela ne donne quasiment aucune information, puisque pour construire l'arborescence sur cinq niveaux, il va falloir faire cinq requêtes distinctes: une première pour choper l'entité ayant un identifiant en particulier, ensuite choper son parent, puis choper le parent du parent, ... Jusqu'à arriver à la racine, pour laquelle le champ ParentId sera nul.
Une autre possibilité est de construire une fonction tabulaire qui, pour un identifiant donné, va construire la liste et la retourner dans une table temporaire, en y ajoutant un indice de position, la racine ayant la position zéro.
```sql
CREATE FUNCTION [dbo].[BreadCrumb]
(
@currentid int
)
RETURNS @Tab TABLE (Id int, pos int, ParentId int, Label nvarchar(255))
AS
Begin
Declare @parentid int;
Declare @label nvarchar(max);
Declare @entitytypeid int;
Declare @i int;
Set @parentid = @currentid;
Set @i = 0;
While(@parentid is not null)
Begin
set @currentid = @parentid;
(Select @parentid = ParentId, @label = Entity.Label
From Entity Where Entity.Id = @currentid);
INSERT INTO @Tab (id, pos, label, parentid)
VALUES (@currentid, @i, @label, @parentid)
set @i = @i + 1;
End
return;
END;
```
Comme expliqué ci-dessus, le résultat retourne une table qui contient quelques colonnes (l'id, le parentid, le label et la position), et permet de chipoter un peu parmi les liens de l'arborescence, puisqu'il suffit de filer un identifiant en paramètre à la fonction pour ressortir toute l'arborescence. Magique (ou presque).
Une autre approche est de passer par un ORM. Pour l'exemple (et parce que "c'est le plus mieux" ©), ça sera l'ORM de Django. Pour l'exemple, j'ai créé une simple classe 'Album', chaque album pouvant contenir plusieurs sous-albums. Le principe est le même: pour retracer l'arborescence d'un élément, il faut remonter jusqu'à la racine, afin de pouvoir tracer le ramasse-miettes.
```python
class Album(models.Model):
name = models.CharField(verbose_name='Nom', max_length=50)
slug = models.SlugField(max_length=50, blank=False, editable=True)
description = models.CharField(verbose_name='Description', max_length=255, null=True, blank=True)
parent = models.ForeignKey('self', null=True, blank=True)
created_at = models.DateTimeField(verbose_name='Date de création', auto_now_add=True)
updated_at = models.DateTimeField(verbose_name='Date de modification', auto_now=True)
def __unicode__(self):
return self.name
def illustration(self):
""" returns an illustration for the album """
pictures = self.picture_set.all()
if len(pictures) > 0:
return pictures[0].picture
else:
return None
def parentbreadcrumb(self):
return self.breadcrumb()[:-1]
def breadcrumb(self):
""" Returns the breadcrumb for the current album """
if self.parent == None:
return (self,)
return self.parent.breadcrumb() + (self,)
def path(self):
"""
returns the path where the album structure should be stored
"""
return os.sep.join((x.name for x in self.breadcrumb()))
```
(Avec quelques méthodes en plus pour faire joli).

View File

@ -0,0 +1,137 @@
---
Title: Exécuter du code Powershell au travers d'un service WCF
Date: 2013-07-16
Slug: powershell-code-execution-through-wcf-service
Tags: powershell, wcf, dotnet, code, runspace
---
[PowerShell](https://fr.wikipedia.org/wiki/Windows_PowerShell) permet d'écrire et d'exécuter des scripts avec un modèle objet plutôt complet, et se basant sur le framework .Net. En gros, cela remplace (avantageusement) les scripts `.bat`, tout en se rapprochant de ce que les shells Unix permettent de faire depuis 20 ans.
Les dépendances PowerShell étant spécifiques à la machine hôte (et aux librairies qui y sont installées, forcément), impossible d'accéder à certaines fonctionnalités depuis n'importe quel poste client. C'est notamment le cas pour tout ce qui concerne les actions liées à l'Active Directory. Pour bypasser ce mécanisme, je fais en sorte que ce soit la machine sur laquelle les outils d'administration AD sont installés qui exécute les scripts PowerShell. J'accède ensuite au lancement et à la gestion de ces scripts au travers d'un service [WCF](https://fr.wikipedia.org/wiki/Windows_Communication_Foundation) déployé sur cette même machine, au travers d'IIS.
La difficulté principale est donc d'écrire les scripts PowerShell, de leur passer des paramètres et de récupérer le résultat.
## Interface et client WCF
Le service WCF ne présente pas vraiment de difficulté, puisqu'il ne fait qu'exposer les paramètres et les différentes méthodes au travers du protocole existant. Une interface de management (`[ServiceContract]`) expose toutes les méthodes disponibles. Celles-ci sont taggées par un attribut `[OperationContract]`.
```csharp
[ServiceContract]
public interface IManagement
{
[OperationContract]
Result CreateAdAccount(int uid, string samaccountname, int? regimental, string displayname, string name, string firstname, string service, string defaultPassword);
[OperationContract]
Result CreateHomeFolder(string samAccountName);
[OperationContract]
Result CreateEmailAddress(string samAccountName, string emailAddress);
[OperationContract]
Result GetADUser(string samAccountName);
}
```
Côté client, on appellera ces différentes méthodes grâce à une instance d'un client WCF (après l'avoir ajouté dans les dépendances du projet):
```csharp
var client = new ADServiceReference.ManagementClient();
client.GetADUser('james_bond');
/* ... */
```
## Script PowerShell
On construit ensuite un script `get-aduser.ps1` que l'on place dans le répertoire `Scripts`:
```powershell
<#
Get-ADUser et ses propriétés, sur base du sam account name.
#>
param(
[Parameter(Mandatory = $true)] [string] $SAMAccount
)
Import-Module ActiveDirectory
$user = Get-ADUser -Identity $SAMAccount -Properties *
$MaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.TotalDays
$passwordExpirationDate = $user.PassWordLastSet + $MaxPasswordAge
Write-Output "DisplayName:", $user.DisplayName
Write-Output "Password last set: ", $user.PasswordLastSet
Write-Output "Password Expiration: ", $passwordExpirationDate
Write-Output ""
Write-Output "Member of: ", $user.MemberOf
Write-Output ""
Write-Output "when changed:", $user.whenChanged
Write-Output "When created:", $user.whenCreated
```
## Runspace .Net
Retour au code .Net, dans l'implémentation des méthodes du service WCF: on construit un `runspace` en lui passant les paramètres qui collent avec ce que le script attend. La première étape est de forcer le chargement des modules et des *snapins* PowerShell. On ouvre ensuite le pipeline pour y enquiller les paramètres, avant de lancer l'exécution proprement dite:
```csharp
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.ServiceModel.Activation;
public class Management : IManagement
{
public Result GetADUser(string samAccountName)
{
string SCRIPT_PATH = @"Scripts/get-aduser.ps1";
StringBuilder stringBuilder = new StringBuilder();
try
{
InitialSessionState initial = InitialSessionState.CreateDefault();
PSSnapInException snapinException = new PSSnapInException();
initial.ImportPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out snapinException);
initial.ImportPSModule(new string[] { "ActiveDirectory" });
using (Runspace runspace = RunspaceFactory.CreateRunspace(initial))
{
runspace.ApartmentState = System.Threading.ApartmentState.STA;
runspace.ThreadOptions = PSThreadOptions.UseNewThread;
runspace.Open();
Pipeline pipeline = runspace.CreatePipeline();
Command cmd = new Command(System.Web.HttpContext.Current.Server.MapPath(SCRIPT_PATH));
cmd.Parameters.Add(new CommandParameter("SAMAccount", samAccountName));
pipeline.Commands.Add(cmd);
var result = pipeline.Invoke();
foreach (var res in result)
{
stringBuilder.AppendLine(res.ToString());
}
ManageErrors(pipeline);
}
}
catch (Exception ex)
{
stringBuilder.Append(ex.ToString());
return new Result() { Status = "NOK", Content = stringBuilder.ToString() };
}
return new Result() { Status = "OK", Content = stringBuilder.ToString() };
}
}
```

View File

@ -0,0 +1,143 @@
---
Title: (re)group by, ou comment grouper facilement des données dans un template Django
Date: 2013-07-26
Slug: django-regroup-by
Tags: django, groupby, dev
---
Un truc hyper sympa en Django, c'est de pouvoir construire un dictionnaire à la volée dans un template. A partir du modèle de l'application, on peut facilement regrouper un ensemble d'éléments sur base d'un champ particulier lors de l'affichage d'un template.
Pour l'exemple ci-dessous, je définis plusieurs modules à exécuter et une ou plusieurs planifications pour chacun d'entre eux. Ces planifications peuvent être programmées une fois par mois (au début, au milieu ou à la fin du mois), à un moment de la semaine ou quotidiennement. Pour l'affichage, une manière simple de faire serait la suivante:
```django
{% for mod in modules %}
<h2>{{ mod }}</h2>
<p class="description info">{{ mod.description }}</p>
<h2>Planifications</h2>
<ul class="ulbox">
{% for plan in mod.planification_set.all %}
<li>Planifié à {{ p.time }} ({{ p.frequency }})</li>
{% endfor %}
</ul>
{% endfor %}
```
On aurait tous les modules qui s'afficheraient les uns après les autres (ça tombe bien, c'est ce qu'on veut), mais les planifications ne seraient pas regroupées. On aura vite le cas suivant:
```
Module 1
Planifié à 18h (quotidiennement)
Planifié à 18h (lundi)
Planifié à 10h (au début du mois)
Planifié à 10h (quotidiennement)
```
Pour l'affichage, j'aimerais en fait que toutes les planifications soient regroupées par fréquence, puis par moment d'exécution, pour avoir la visualisation suivante:
```
Module 1
Quotidiennement
Planifié à 10h
Planifié à 17h
Lundi
Planifié à 18h
Au début du mois
Planifié à 10h
```
C'est là que la méthode `regroup` de Django peut nous aider.
Supposons que nous ayons le modèle suivant :
```python
class Module(models.Model):
label = models.CharField(max_length=255, editable=False, db_index=True)
description = models.CharField(max_length=255, blank=True, editable=False)
def __unicode__(self):
return unicode(self.label)
class Planification(models.Model):
FREQUENCY = (
('1x par jour', ((0, 'Quotidiennement'),)),
('1x par semaine', (
(1, 'Lundi'),
(2, 'Mardi'),
(3, 'Mercredi'),
(4, 'Jeudi'),
(5, 'Vendredi'),
(6, 'Samedi'),
(7, 'Dimanche')
)),
('1x par mois', (
(-1, 'Au début du mois'),
(-15, 'Au milieu du mois'),
(-30, 'A la fin du mois')
)),)
MONTH_LENGTH=[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
module = models.ForeignKey('Module')
time = models.TimeField(default='18:00:00')
frequency = models.IntegerField(choices=FREQUENCY, verbose_name="Fréquence de planification")
class Meta:
""" Le petit paramètre qui permet de trier directement nos planifications par fréquence, puis par moment d'exécution """
ordering = ['frequency', 'time',]
def __unicode__(self):
return u'[%s] %s, %s' % (self.module, self.get_frequency_display(), self.time)
def __self__(self):
return '[%s] %s, %s' % (self.module, self.get_frequency_display(), self.time)
def __repr__(self):
return '[%s] %s, %s' % (self.module, self.get_frequency_display(), self.time)
```
Ce n'est pas trop compliqué, il y a juste une `ForeignKey` planquée quelque part entre la classe `Module` et la classe de `Planification` et qui permet de faire le lien entre ces deux classes. Dans le template:
1. on va d'abord boucler sur tous les modules présents,
2. puis on va appeler le groupement sur les planifications qui y sont associées.
3. On va ensuite boucler non seulement sur les différentes clés (la méthode `regroup` construit un **dictionnaire** et pas une simple liste), puis sur chacune des valeurs proposées pour cette clé:
```django
{% for mod in modules %}
<h2>{{ mod }}</h2>
<p class="description info">{{ mod.description }}</p>
{% regroup mod.planification_set.all by get_frequency_display as plans_by_freq %}
{% if plans_by_freq %}
<h2>Planifications</h2>
<ul class="ulbox">
{% for plan in plans_by_freq %}
<li><b>{{ plan.grouper }}</b>
<ul>
{% for p in plan.list %}
<li>{{ p.time }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
```
Explications dans l'ordre:
* La méthode `regroup <bidule> by <chose>` permet de construire le dictionnaire sur base du champ souhaité
* Le `{% if plans_by_freq %}` n'est pas obligatoire, il est juste là pour ne pas afficher le titre s'il n'y a aucune planification associée
* `{% for plan in plans_by_freq %}` boucle sur toutes les clés/valeurs
* `{{ plan.grouper }}` permet d'afficher la valeur de la clé de tri
* `{% for p in plan.list %}` permet de boucler sur toutes les valeurs associées à la clé courante.
Django va alors trier ce dictionnaire avec les paramètres par défaut définis au niveau de la classe. C'est pour cette raison que j'avais ajouté un tri dans la classe `Planification`:
```python
class Meta:
ordering = ['frequency', 'time']
```
Petite astuce en plus: si on veut utiliser l'affichage d'un champ `CHOICE` plutôt que la valeur de l'option, il suffit d'utiliser la méthode `get_*field*_display` dans le template. Bref, c'est magique.

View File

@ -0,0 +1,92 @@
---
Title: Composition avec MEF
Date: 2014-03-30
Slug: composition-with-mef
Tags: framework, composition, code, dev
---
[MEF](http://msdn.microsoft.com/en-us/library/dd460648%28v=vs.110%29.aspx) est un framework permettant d'étendre facilement des applications. Il embarque tout un ensemble de méthodes pour permettre de charger dynamiquement des contextes et composants plus petits, utiles pour un utilisateur, en créant par exemple un mécanisme de *plugins*. L'idée est donc de définir un **chargeur** (*loader*) et de définir une interface qui sera respectée par tous les plugins. Au chargement de l'application, le loader s'occupera de récupérer tous les petits morceaux éparpillés (dans une assembly, dans un répertoire, ...) et de les lancer. L'étape de récupération est appelée "composition".
Tout d'abord, il faut bien entendu installer MEF (*via* [Nuget](https://www.nuget.org/packages/Microsoft.Composition)) et pouvoir inclure les namespaces suivants (disponibles dans la DLL `System.ComponentModel.Composition`):
```csharp
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
```
L'utilisation que j'en fais est assez simple: j'ai un ensemble de traitement à exécuter dans un ordre défini; chaque traitement est défini dans une classe (voire dans un projet séparé), et expose des propriétés au travers d'une interface, que j'ai appelée `IDataLoader`. Cette interface ressemble à ceci:
```csharp
using System.ComponentModel.Composition;
[InheritedExport]
public interface IDataLoader
{
void Start();
int Priority { get; }
string Type { get; }
event EventHandler OnMessageSent;
event EventHandler OnPercentageChange;
}
```
Chaque traitement (*plugin*) va hériter de cette interface, et va implémenter une méthode `Start()`, ainsi que la propriété `Priority` qui sera utilisée par la suite. L'attribut `InheritedExport` indique que les classes héritant de cette interface devront également être reprises lors de la composition.
## Création des modules
Pour créer un nouveau module, il suffit de définir une nouvelle classe qui hérite de l'interface `IDataLoader` définie ci-dessus:
```csharp
public class Module1 : IDataLoader
{
public void Start()
{
// do something
}
public string Type
{
get { return this.GetType().ToString(); }
}
public int Priority
{
get { return 0; }
}
}
```
## Composition
La composition en elle-même se fait au travers d'un catalogue (le bidule qui répertorie toutes les classes à instancier) et d'une liste de modules. C'est une liste qui contient des instances de l'interface définie ci-dessus. Dans la classe "hôte" (le fichier `main.cs` par exemple):
```csharp
[ImportMany]
public List<IDataLoader> LoadedModules { get; set; }
private void Compose()
{
catalog = new DirectoryCatalog(".");
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
}
```
Le catalogue, c'est simplement une classe qui hérite de `ComposablePartCatalog` (dans le namespace `System.ComponentModel.Composition.Primitives`). Parmi les types existants, on a par exemple:
* DirectoryCatalog, qui va pêcher les classes parmi les assemblies posées dans un répertoire particulier
* AssemblyCatalog pour ne prendre que les classes situées dans une assembly spécifique (genre `var catalog = AssemblyCatalog(System.Reflection.Assembly.GetExecutingAssembly());`)
* ...
Ci-dessus, j'utilise un `new DirectoryCatalog(".")`, qui va parcourir le répertoire courant pour y récupérer les classes intéressantes. On instancie ensuite un `container`, qui va inventorier toutes ces classes, puis on appelle la méthode `ComposeParts` sur le container pour que l'objet courant (`this`) soit initialisé avec les bidules trouvés dans le catalogue.
Mon interface s'appelle `IDataLoader`, il existe donc une `List<IDataLoader>` dans l'objet courant, qui sera remplie avec toutes les classes héritant de cette interface (vous vous rappelez de l'attribut `InheritedExport` sur l'interface?). L'attribut `ImportMany` indique au container de charger autant de classes qu'il trouvera.
Après la composition, les instances sont dispos au travers de la liste déclarée ci-dessus. Dans l'exemple, après que les *plugins* aient été chargés, on les parcourt dans une liste triée par priorité, et on appelle la méthode `Start()`:
```csharp
foreach (var module in LoadedModules.OrderBy(p => p.Priority))
{
module.Start();
}
```

View File

@ -0,0 +1,67 @@
---
Title: Git
Date: 2014-08-02
Status: draft
Tags: git, vcs
---
Au plus j'utilise Git, au moins je peux m'en passer. J'ai commencé à utiliser un outil de contrôle de sources pendant un travail de groupe. Le but était de développer une petite application Web en SmallTalk, sous VisualWorks. Les assistants avaient déployé un service SVN, sur lequel on pouvait se connecter et déployer notre code source. Plus tard, durant mon parcours pro, on a également déployé un serveur SVN. On l'utilisait finalement de manière trèèès basique: quelques hooks, un serveur Trac, les commits de manière centralisée (et se gueuler dessus parce que quelqu'un avait foiré sa partie) et quelques tags.
Par facilité, tous nos projets se trouvaient dans le même repository: plus besoin de jouer sur les dépendances, tout était au même endroit. Et puisqu'on jouait très peu avec les branches, forcément, ça clashait régulièrement.
Plus tard, je suis passé avec un collègue sur Git. On bossait chacun sur des projets différents, mais on avait quand même sérieusement besoin de pouvoir centraliser le code source. Finalement, c'est moins le côté partage de sources et plus le côté "ça m'a permis de gagner du temps" qui aura été bénéfique sur le long terme. Aujourd'hui encore, je bosse pratiquement uniquement avec Git. Mes textes sont sur Git, mes sources aussi. J'ai un compte Github (que j'utilise trop peu), un serveur perso, ... Et je jongle de plus en plus avec les branches, et bénis la facilité avec laquelle l'appli permet leur gestion.
Je vous épargnerai la création d'un nouveau repository... quoique... Bon allez, on recommence à zéro.
Pour initialiser un dépôt, il suffit de taper un petit `git init` dans le répertoire souhaité. Cela créera un répertoire `.git`, contenant toutes les informations nécessaires, notamment la configuration (dans le fichier `.git/config`).
une fois que c'est fait, tout reste en local. Aucun serveur distant n'est configuré. Pour y remédier, on va ajouter un `git remote add origin <url du serveur>`. Ceci permettra de synchroniser avec le serveur distant: il est dès lors très facile de définir plusieurs hôtes, de synchroniser des branches et fichiers depuis l'une de ces sources, et de les amener vers une autre. C'est notamment comme cela que fonctionne les *pull requests* sur Github:
1. une personne (A) crée un repository;
2. une deuxième (B) le clone et crée un deuxième repository basé sur le premier.
3. A ce stade, les deux sont décorellés. Par contre, si B ajoute le premier repository comme hôte distant, il lui sera possible de se synchroniser sur le "maître".
Par exemple, je clone le repository `orf/simple`. Ceci crée un nouveau repository sous mon compte, baptisé de la même manière (`grimbox/simple`). Pour commencer à travailler sur ce code source, je vais le rappatrier en local :
git clone https://github.com/grimbox/simple.git
Par défaut, la fonction `clone` ajoute l'hôte sous la dénomination `origin`. Pour garder la synchronisation avec le repertoire maitre, je vais également l'ajouter sous la dénomination `upstream` (mais on pourrait l'appeler patate que ce serait pareil. C'est juste une convention).
git remote add upstream https://github.com/orf/simple.git
Ensuite, je vais créer une nouvelle branche dans mon code source, y travailler, et soumettre une pull request.
```
git checkout -b new_branch
...
git add .
git commit -m "new functionality"
git push origin new_branch
```
En résumé:
* On part d'un repertoire existant
* On le clone
* On conserve le lien vers le répertoire originel
* On bosse comme un acharné pour sortir une nouvelle fonctionnalité indispensable qui révolutionnera le monde
* On add/commit/push cette nouvelle fonctionnalité, et on soumet une pull request (ou pas, si on préfère garder le fork tel quel).
Si on souhaite récupérer le contenu du répertoire maître, on va commencer par en récupérer le contenu grâce à un `fetch upstream`, puis on va merger ce contenu dans notre branche master à nous.
```
git fetch upstream
git checkout master
git merge upstream/master
```
Ca a peut-être l'air un chouia indigeste pour une première fois, mais on s'y adapte parfaitement. Les branches permettent réellement de faire tout et n'importe quoi, de garder une branche stable, une branche de développement, de partir sur de nouvelles branches pour de nouvelles fonctionnalités, puis de tout fusionner par la suite.
Des exemples pratiques, j'en ai quelques uns: j'ai dernièrement dû travailler sur plusieurs fonctionnalités simultanément. Plutôt que de travailler uniquement sur la branche maître, j'ai créé une première branche `development`, qui sert de base (et de réceptacle) pour toute nouvelle fonctionnalité. J'ai donc commencé à travailler sur un nouveau module d'import pour une application. Cela a mené à la branche `dev-ug`, dans laquelle je fusionne régulièrement la branche de développement principale, pour récupérer les derniers hotfixes par exemple.
J'ai également dû travailler sur une nouvelle fonctionnalité pour la création de comptes dans l'Active Directory: une nouvelle branche `dev-ad`. Et vu que la validation de mes modifications aura presque pris un mois, cela m'aura permis de continuer à travailler sur d'autres points, de faire évoluer mon code, sans dépendre du bon vouloir (et des vacances) de mes collègues adorés. Une fois ma validation en poche, je suis revenu sur la branche `development`, sur laquelle j'ai fusionné la branche `dev-ad`, après quoi j'ai fusionné ces modifications dans la branche maître.
De nouveau, cela permet de conserver une "porte de sortie", un endroit d'où on peut sortir un patch de derrière les fagots si l'on se rend compte qu'un bug a été trouvé: on revient sur la branche maître, on crée une branche `hotfix`, on développe le sparadrap et on le ramène dans la branche maître. L'idéal est évidemment de rapatrier également ce patch vers les autres branches... Mais vu que la finalité est de tout ramener à la branche maître, le patch finira bien par être présent partout. Attention à la présence de la branche `development`, qui permet quand même de faire un gros test de non-régression avant de passer en production.
Voir aussi :
* [Github flow aliases](http://haacked.com/archive/2014/07/28/github-flow-aliases pour les alias et quelques idées de flux).
* [Inverser les derniers commits avec Git](http://sametmax.com/inverser-les-derniers-commits-avec-git/)

View File

@ -0,0 +1,43 @@
---
Title: Compatibilité Python 2.x/3.x
Date: 2014-08-27
Slug: python-six-compat-2.x-3.x
Tags: python, six, dev, 2to3
---
En adaptant une appli qui tourne sur du Python 2.7+ vers du Python 3.4, on remarque qu'il y a [beaucoup de nouveautés](https://docs.python.org/3/whatsnew/3.0.html). Préparez-vous à adapter votre code pour que cela fonctionne :-)
Parmi les quelques nouveautés:
* La déclaration `print` devient une fonction; oubliez donc tous vos `print 'bidule'`: on passe à `print('bidule')` maintenant !
* La fonction `raw_input` disparait au profit de `input`.
* ...
Petit exemple avec la fonction `raw_input` qui devient `input`. Une manière de faire est la suivante:
```python
def _default_input_func(*args, **kwargs):
"""
Check if we can use `input()` from python3.
If not, fallback to `raw_input()`.
"""
func = None
try:
func = input(*args, **kwargs)
except NameError:
func = raw_input(*args, **kwargs)
return func
```
On utilise alors cette nouvelle fonction `_default_input_func(*args, **kwargs)` quand on attend un *input* de l'utilisateur. Bref, c'est sympa, mais cela ne fonctionne que si une fonction est renommée entre deux versions majeures de l'interpréteur.
Une autre manière de faire pour que votre code tourne autant sur Python 2.7 que sur Python 3.x est de passer par [six](https://pypi.python.org/pypi/six) (*2 to 3 equals 6*, oui, simplement). En gros, l'idée est de passer par des fonctions *tampons*, qui feront la redirection vers la **bonne** méthode ou fonction (en fonction de la version de l'interpréteur). Au niveau du code par contre, on appelle directement la bonne fonction définie dans le package `six`.
Pour revenir à l'exemple ci-dessus, on pourrait importer la fonction `input` du package `six` et l'utiliser, indépendamment de la version de l'interpréteur utilisée.
```python
from six.moves import input
```
En gros, tout se passe grâce à deux flags: `six.PY2` et `six.PY3`, qui indiquent quelle version est utilisée. Cela revient plus ou moins à notre super fonction `_default_input_func`, sauf que c'est géré, maintenu et centralisé. La liste des fonctions uniformisées est disponible ici: [https://pythonhosted.org/six/](https://pythonhosted.org/six/).

View File

@ -0,0 +1,119 @@
---
Title: Ruby Version Manager
Language: Ruby
Tags: dev, ruby, environment
---
Il y a quelques jours, j'ai voulu essayer [Jekyll](http://jekyllrb.com/), un générateur de site statique, au même titre que [Pelican](http://docs.getpelican.com/en/3.5.0/quickstart.html), [Nikola](http://getnikola.com/) ou [Hyde](http://ringce.com/hyde). Il propose plusieurs avantages, notamment sa compatibilité avec [GitHub Pages](https://help.github.com/articles/using-jekyll-with-pages/) ou sa grande flexibilité.
En regardant le site Web, l'installation ressemble à ceci:
``` shell
~ $ gem install jekyll
~ $ jekyll new my-awesome-site
~ $ cd my-awesome-site
~ $ jekyll serve
```
Dans la réalité, et pour un béotien en Ruby (moi :-)), on en est loin du compte. `ruby` et `gem` sont bien installés sur ma machine, mais l'installation a déjà planté deux fois avec un message d'erreur un-peu-pas-tout-à-fait-super-très-très compréhensible... L'utilisation des commandes ci-dessus a finalement bien installé ce qu'il fallait, mais impossible de retrouver les binaires. Etant adepte de `virtualenv` et `pew`, je ne désespère pas et trouve [RVM](https://rvm.io/), afin de créer un environnement virtuel dans lequel je pourrais déployer Jekyll.
Pour commencer, il faut ajouter la clé GPG et téléchargez RVM avec les commandes suivantes:
``` shell
$ gpg --keyserver hkp://keys.gnupg.net --recv-keys D39DC0E3
$ curl -sSL https://get.rvm.io | bash -s stable
```
Ce script fraîchement téléchargé installe les binaires pour l'utilisateur courant, dans le répertoire `~/.rvm`. Pour le désinstaller, il suffira de supprimer ce répertoire.
Le chemin vers les binaires est ajouté au fichier `~/.profile`. Comme expliqué dans la documentation (au moment de l'installation), ajouter un `source ~/.profile` à la fin de votre fichier `~/.bash_profile` pour en profiter sans avoir à tout recopier.
Une [cheat sheet](http://cheat.errtheblog.com/s/rvm) est disponible. Le but ici est de paramétrer RVM pour l'utiliser de la même manière que l'on utiliserait `virtualenv`.
Installation de Ruby
--------------------
RVM ne permet pas d'utiliser la version utilisée par le système. Il s'agit en fait d'un gestionnaire de versions pour Ruby (comme l'indique l'acronyme...).
Pour définir la version de ruby qui sera utilisée par RVM, commencez par la commande `rvm install ruby`, afin de récupérer automatiquement la dernière version en date.
``` shell
$ rvm install ruby
Searching for binary rubies, this might take some time.
No binary rubies available for: arch/libc-2.20/x86_64/ruby-2.2.0.
Continuing with compilation. Please read 'rvm help mount' to get more information on binary rubies.
Checking requirements for arch.
Requirements installation successful.
Installing Ruby from source to: /home/fred/.rvm/rubies/ruby-2.2.0, this may take a while depending on your cpu(s)...
[...]
Install of ruby-2.2.0 - #complete
Ruby was built without documentation, to build it run: rvm docs generate-ri
```
Pour finir, on définit la version utilisée par RVM avec la commande `rvm use 2.2.0`. Vérifiez que la version de Ruby correspond à celle initialisée ci-dessus grâce à `ruby -v`. Pour revenir à la version de base, il faut utiliser `rvm use system`. Vous pouvez également vérifier quelle binaire est exécuté avec `which ruby`, qui devrait vous retourner quelque chose comme `/home/fred/.rvm/rubies/ruby-2.2.0/bin/ruby` au lieu de `/usr/bin/ruby`.
Il est également possible d'installer une version particulière de Ruby, en utilisant la commande `rvm install 2.1.0`. Cela installera (par exemple) la version 2.1.0 dans le répertoire `~/.rvm/rubies/ruby-2.1.0/bin/ruby` et pourra être appelée grâce à `rvm use 2.1.0`.
Pour revenir à la version de base, il faut utiliser `rvm use system`. Vous pouvez également vérifier quelle binaire est exécuté avec `which ruby`, qui devrait vous retourner quelque chose comme `/home/fred/.rvm/rubies/ruby-2.2.0/bin/ruby` au lieu de `/usr/bin/ruby`.
Création d'un gemset
--------------------
La création d'un nouvel environnement virtuel se fait grâce à la commande `rvm gemset create project_name`. Toutes les commandes liées à ces environnements sont gérées par le préfixe `rvm gemset`, suivi de la commande souhaitée:
``` bash
rvm gemset create project_name # create a gemset
rvm gemset use project_name # use a gemset in this ruby
rvm gemset list # list gemsets in this ruby
rvm gemset delete project_name # delete a gemset
rvm 2.1.0@other_project_name # use another ruby and gemset
rvm 2.2.0@_project --create --rvmrc # use and create gemset & .rvmrc
```
Jekyll
------
Maintenant que toutes les étapes ci-dessus ont été réalisées, on peut passer à l'installation de Jekyll: `gem install jekyll`.
Pour info, j'ai rencontré un problème lié au runtime ExecJS:
``` bash
home/fred/.rvm/gems/ruby-2.2.0/gems/liquid-2.6.1/lib/liquid/htmltags.rb:43: warning: duplicated key at line 46 ignored: "index0"
/home/fred/.rvm/gems/ruby-2.2.0/gems/execjs-2.2.2/lib/execjs/runtimes.rb:51:in `autodetect': Could not find a JavaScript runtime. See https://github.com/sstephenson/execjs for a list of available runtimes. (ExecJS::RuntimeUnavailable)
[...]
```
L'installation du paquet `nodejs` a résolu le problème... et j'ai finalement pu créer un nouveau site. Par défaut, ce site est pratiquement vide.
Le paramétrage du site se fait au travers du fichier `_config.yml`, notamment pour les valeurs de titre, l'email de l'auteur, la description, les noms d'utilisateurs Twitter et GitHub, ...
Pour aller plus loin:
* [Créer un nouveau post](http://jekyllrb.com/docs/posts/)
* [Ajouter une page](http://jekyllrb.com/docs/pages/)
* [Le reste de la documentation](http://jekyllrb.com/docs/home/)
Octopress
---------
[Octopress](http://octopress.org/) est un framework basé sur Jekyll. L'avantage par rapport à ce dernier est que la configuration par défaut propose quelque chose de très rapidement fonctionnel (plus rapidement qu'avec Jekyll en tout cas).
Cloner le dépôt Git, puis installer les dépendances. Installez ensuite le thème par défaut, et commencez à écrire :
``` bash
### Clone du dépôt
git clone git://github.com/imathis/octopress.git octopress
cd octopress
### Installation des dépendances (toujours sur base de notre Ruby installé avec RVM)
gem install bundler
bundle install
### Installation du thème par défaut
rake install
### Création d'un nouveau post
rake new_post["My new blog post"]
### Création d'une nouvelle page
rake new_page["My new page"]
```
Les articles se trouvent dans le répertoire `_posts`, avec la structure `YYYY-MM-DD-my-new-blog-post`. Pour prévisualiser votre nouveau site, utilisez `rake preview` et rendez-vous à l'url `http://localhost:4000`.

View File

@ -0,0 +1,37 @@
---
Title: Dépendances sur une clé étrangère
Date: 2015-01-13
Slug: sql-foreign-key-dependencies
Tags: sql, oracle
---
Dans un schéma relationnel, des champs d'une table A peuvent référencer l'enregistrement d'une table B grâce à une référence vers l'un de ses champs. Grâce au schéma de la table qui spécifie quels champs sont des clés étrangères, on peut facilement interroger les contraintes associées aux tables au travers d'une requête SQL, afin d'obtenir les relations inverses (*ie. quels sont les tables qui référencent telle autre table?*). Parmi les types de contraintes, on a [ceux-ci](http://docs.oracle.com/cd/B19306_01/server.102/b14237/statviews_1037.htm#i1576022):
* C : Vérification d'une contrainte sur la table, par exemple que tel champ n'est pas nul.
* P : Clé primaire
* U : Clé unique
* R : Clé étrangère
* V : Vérification sur une vue
* O : Lecture seule, sur une vue
La requête de base sur Oracle ressemble à ceci:
```sql
Select
a.table_name "Referenced Table", b.table_name "Referenced By", a.*, b.*
From all_constraints a
Left Outer Join all_constraints b on a.constraint_name = b.r_constraint_name
Order By a.table_name
```
Comme on ne fixe aucune condition, cela liste toutes les contraintes pour toutes les tables.
* Le champ "Referenced Table" indique la table sur laquelle les contraintes s'applique
* Le champ "Referenced By" indique la table référençant la première lorsqu'elle existe.
* `a.constraint_type` indiquera le type de contrainte sur la table référencée
* `b.constraint_type` sera d'office de type `R`
Sources
-------
* [StackOverflow (comme d'habitude)](http://stackoverflow.com/questions/2509512/how-to-find-foreign-key-dependencies-pointing-to-one-record-in-oracle)

View File

@ -0,0 +1,35 @@
---
Title: The Zen Of Python
Date: 2015-02-23
Slug: the-zen-of-python
Tags: zen, python, dev
---
Deux petites choses hyper importantes à savoir en Python (même que cela a sauvé la vie de Chuck Norris. Deux fois.): `import this` et `import antigravity`.
```
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
```
A vous d'essayer `import antigravity`.

View File

@ -0,0 +1,67 @@
---
Title: Rendu partiel (et asynchrone) avec ASP MVC & jQuery
Date: 2015-08-17
Slug: partial-render-using-asynchronous-js
Tags: dev, code, jquery, js
---
En faisant du chargement asynchrone, on donne l'impression à l'utilisateur que la page se charge plus rapidement qu'elle ne se charge réellement: le serveur répond d'abord une première fois avec le squelette (et le contenu) de la page racine, puis une fois que cette page est complètement chargée, on va récupérer le reste des données (équivalent du `$(document).ready(function(...) { ... });`.
Au niveau des contrôleurs, on va définir une première méthode (qui retourne un `ActionResult`) et qui envoie les données vers une vue pour l'affichage de la page principale. On définit ensuite une deuxième méthode, qui retourne un objet de type `PartialViewResult` (mais qui fonctionne grosso-modo de la même manière):
```csharp
public ActionResult Details(int? id)
{
/* fetch objects */
return View(...);
}
public PartialViewResult GetAdUser(string samAccountName)
{
/* fetch objects */
return PartialView(...);
}
```
Dans la page principale (celle qui correspond à la méthode **/Details**), il faut:
* Inclure jQuery
* Ecrire un petit morceau de code pour charger les données une fois que la page est complètement chargée
## Inclure jQuery
```html
<script src="/Assets/js/jquery-1.8.2.js" type="text/javascript"></script>
```
Oui. C'est tout.
## Le morceau de code
En fait, on doit d'abord définir l'emplacement où le résultat sera stocké. Soit avec une classe, soit avec un identifiant. Par exemple:
```html
<div class="partialContent" data-url="/GetAdUser?samAccountName=fred">
Loading...
</div>
```
Le `Loading...` peut être remplacé par une petite image qui s'agite, ça rendra très très bien. Dans cet exemple, on va utiliser l'attribut `class` pour identifier les éléments devant être appelés après le chargement de la page, et l'attribut `data-url` pour initier l'URL à contacter.
Finalement, le code Javascript permettant d'opérer la magie est le suivant:
```html
<script type="text/javascript">
$(document).ready(function(e) {
$(".partialContent").each(function (index, item) {
var url = $(item).data("url");
if (url && url.length > 0)
$(item).load(url);
});
});
</script>
```
En gros, pour chaque élément du sélecteur `$(".partialContent")`, on récupère la valeur de l'attribut `data-url` et on charge le contenu de cette action en dessous de l'élément actuel. Ici, le résultat va donc remplacer le `Loading...`.

View File

@ -0,0 +1,37 @@
---
Title: Dapper .Net
Date: 2015-08-18
Tags: dev, dotnet, dapper, db, orm
Slug: dapper-dot-net
---
[Dapper](https://github.com/StackExchange/dapper-dot-net) est un *wrapper* pour le framework .Net qui étend, par des extensions de méthodes, les fonctionnalités présentes dans le *namespace* `System.Data.Client` pour la connexion à une base de données. En gros, on reste avec du code dans lequel on ouvre gentiment ses connexions et dans lequel on fait ses requêtes. *à l'ancienne*, mais en ajoutant un ensemble de fonctions 'achement utiles, comme par exemple `Query<T>` qui va faire correspondre les colonnes de la requête avec les champs d'une classe. On est un niveau plus bas qu'un ORM, mais un niveau plus haut qu'un client ADO.Net classique. On garde donc la main sur la construction de nos requêtes... Ce qui a ses avantages et inconvénients :)
En plus de cela, il existe le projet [Dapper.Contrib](https://github.com/StackExchange/dapper-dot-net/tree/master/Dapper.Contrib), dans lequel on trouve notamment la classe `SqlBuilder`, qui permet d'améliorer la construction des requêtes. Son fonctionnement est un chouia touchy la première fois, mais fonctionne particulièrement bien au second tour :-) Tout fonctionne en fait sur base de templates et de clauses: on définit une requête de base dans laquelle des chaînes de caractères particulières seront remplacées par les clauses:
```csharp
using Dapper;
SqlBuilder builder = new SqlBuilder();
var template = builder.AddTemplate(
@"Select * From Table /**where**/");
builder.Where("EntityAttribute.[Key] = 'Route'");
builder.Where("EntityAttributeValue.EntityId = @entityid", new { entityid = entityId });
using (SqlConnection sqlConnection = new SqlConnection(ConfigurationManager.ConnectionStrings["RPS"].ConnectionString))
{
var result = sqlConnection.Query<string>(template.RawSql, template.Parameters).Select(s => int.Parse(s));
}
```
En résumé, on crée un `SqlBuilder` et un `template` *via* la méthode `AddTemplate`. Puis on applique un ensemble de clauses *via* la méthode `Where`, sur notre nouveau `builder`.
Le résultat sera ensuite généré en ouvrant une connection classique grâce à une nouvelle instance de `SqlConnection`, ouverte grâce à une chaîne de connexion disponible dans le `ConfigurationManager`. Ensuite, on utilisera la méthode `Query<T>` dont on parlait ci-dessus pour récupérer toutes les occurences de la db, en lui passant le template et les clauses en paramètres.
Bref, cela demande un peu de réflexion par rapport à une connexion classique, cela demande un peu plus de travail qu'un (micro-)ORM, mais cela permet également d'avoir un énorme contrôle sur les requêtes exécutées. Et après avoir essayé plusieurs ORM pour le framework .Net, j'en reviens toujours à cette solution-ci. En terme de performances, c'est sans équivalent (bien que je ne doute pas que le contraire existe :)).
Sources:
* [Dapper Tutorial Part II](https://liangwu.wordpress.com/2012/08/20/dapper-net-tutorial-ii/)
* [SqlBuilder.cs](https://github.com/StackExchange/dapper-dot-net/blob/63460c60e92caadb53542dbde10221958b6630d6/Dapper.SqlBuilder/SqlBuilder.cs)

View File

@ -0,0 +1,28 @@
---
Title: PEP8
Date: 2015-08-19
Tags: pep8, flake8, python, code quality
---
Le langage [Python](https://www.python.org/) fonctionne avec un système d'améliorations basées sur des propositions: les [PEP](https://www.python.org/dev/peps/), ou "Python Enhancement Proposal". Chacune d'entre elles doit être approuvée par le [Benevolent Dictator For Life](https://www.python.org/dev/peps/pep-0001/#python-s-bdfl).
Celle qui m'intéresse pour la suite de cet article est la [PEP-8](https://www.python.org/dev/peps/pep-0008/), ou "Style Guide for Python Code". Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, ... En bref, elle décrit comment écrire du code proprement pour que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Sur cette base, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: [pep8](https://pypi.python.org/pypi/pep8). Pour l'installer, passez par `pip`. Lancez ensuite la commande `pep8` suivie du chemin à analyser (`.`, le nom d'un répertoire, le nom d'un fichier `.py`, ...). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options `--statistics -qq`.
```python
$ pep8 . --statistics -qq
7 E101 indentation contains mixed spaces and tabs
6 E122 continuation line missing indentation or outdented
8 E127 continuation line over-indented for visual indent
23 E128 continuation line under-indented for visual indent
3 E131 continuation line unaligned for hanging indent
12 E201 whitespace after '{'
13 E202 whitespace before '}'
86 E203 whitespace before ':'
```
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez [pyflakes](https://pypi.python.org/pypi/pyflakes): il analaysera vos sources à la recherche d'erreurs (imports inutilsés, méthodes inconnues, etc.
Et finalement, si vous voulez grouper les deux, il existe [flake8](https://pypi.python.org/pypi/flake8). Sur base la même interface que `pep8`, vous aurez en plus des avertissements concernant le code source :)

View File

@ -0,0 +1,107 @@
---
Title: Un p'tit parser de fichiers en C#
Date: 2015-10-01
Slug: a-little-csharp-files-parser
Tags: dev, dotnet, csv, fichier, parser
---
Pour faciliter l'analyse de fichiers `.csv`, j'ai écrit un petit ensemble de classes (loin d'être parfait, mais qui a le mérite de plutôt bien fonctionner pour mon utilisation). Cet analyseur ne traite pas les cas où un délimiteur se trouve perdu au beau milieu d'un champ (ce qui est quand même la base, je sais bien), il faudrait donc améliorer la partie *lecture du fichier* pour qu'elle crache un tableau de chaînes de caractères et ce sera bon (genre [ici](www.filehelpers.net) ou [](https://github.com/JoshClose/CsvHelper)).
## Les interfaces de construction
L'interface `IBuilder` est juste l'interface dont dépendent les classes de représentation des *records*. Si un fichier représente des personnes, on définira ensuite une classe `People` qui implémente l'interface ci-dessous (et en particulier la méthode `Build(string[] content);`).
```csharp
public interface IBuilder<T>
{
T Build(string[] content);
}
public class BuilderException : Exception
{
public BuilderException(string message) : base(message) { }
}
```
Ensuite, on a une interface `IManager`, pour définir la manière dont sont gérés les *records*. Plusieurs lignes du fichier lu peuvent par exemple représenter une et une seule personne (et plusieurs de ses contrats, par exemple). Cela permet d'avoir un gestionnaire qui va stocker chaque personne dans une clé, et qui renverra la liste des éléments connus au travers de la méthode `Items()`.
```csharp
public interface IManager<T> where T : new()
{
/// <summary>
/// Ajoute un élément à la liste interne.
/// </summary>
/// <param name="item">L'élément à ajouter.</param>
void Add(T item);
/// <summary>
/// Retourne tous les éléments de la liste.
/// </summary>
/// <returns>Une instance de liste générique de type T.</returns>
List<T> Items();
}
```
Et finalement, la méthode `Read()`, qui lit le fichier passé en paramètre (plutôt un tableau contenant les lignes, en fait) et qui retourne la liste des éléments de type `T` au travers du gestionnaire (implémentant l'interface `IManager`), s'il existe.
```csharp
public List<T> Read<T>(string[] lines,
char[] delimiters = null,
bool processFirstLine = false,
Managers.IManager<T> manager = null)
where T : Builders.IBuilder<T>, new() // implements a default ctor and IBuilder
{
if (delimiters == null || delimiters.Length == 0)
delimiters = new char[2] { ';', '\t' };
List<T> list = new List<T>();
for (int i = 0; i < lines.Count(); i++)
{
if (!processFirstLine && i == 0) // avoid line zero, as it contains titles
continue;
string line = lines[i];
try
{
string[] array = line.Split(delimiters, StringSplitOptions.None);
// checks at least one item is not empty or white spaces
if (array.All(c => String.IsNullOrWhiteSpace(c)))
continue;
// init element with default ctor
T elem = new T();
// call Builder method with string array
elem.Build(array);
if (manager != null)
{
manager.Add(elem);
}
else
{
// add item to returned list
list.Add(elem);
}
}
catch (Builders.BuilderException buildex)
{
System.Diagnostics.Debug.WriteLine(buildex.ToString());
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
}
}
if (manager != null)
return manager.Items();
return list;
}
```
Du coup, les parties à revoir seraient le `split` sur la ligne, qui n'est franchement pas top, et la gestion des exceptions, puisque dans l'exemple ci-dessus, on ne log strictement rien.

View File

@ -0,0 +1,278 @@
---
Title: todo.txt avec Django
Date: 2015-10-10
Slug: todo.txt-avec-django
Tags: django, todo, dev
---
Un petit début d'implémentation de [todo.txt](http://todotxt.com/) avec [Django](https://www.djangoproject.com/).
```python
# app/models.py
from datetime import date
import re
from django.db import models
from django.contrib.auth.models import User
class Project(models.Model):
"""Décrit un projet.
Attributes:
name (str): Le nom du projet en cours.
"""
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Context(models.Model):
"""Décrit un contexte.
Attributes:
name (str): Le nom du contexte.
"""
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Task(models.Model):
"""Décrit quelque chose qui doit être fait :)
Attributes:
description (str): La description complète.
projects (Project): La liste des projets qui sont liés à la tâche en cours.
contexts (Context): La liste des contextes qui sont liés à la tâche en cours.
priority (str): Le niveau de priorité (A, B, C, ...)
deadline (date): L'échéance à laquelle la tâche doit avoir été effectuée.
creator (User): L'utilisateur qui a créé la tâche.
completed (boolean): Indique si la tâche est terminée ou pas.
completion_date (date): La date à laquelle la tâche a été clôturée.
"""
description = models.CharField(max_length=2000)
projects = models.ManyToManyField('Project')
contexts = models.ManyToManyField('Context')
priority = models.CharField(max_length=1, null=True)
deadline = models.DateField(null=True)
creator = models.ForeignKey(User, null=True)
completed = models.BooleanField(default=False)
completion_date = models.DateTimeField(null=True)
@property
def iscomplete(self):
"""Vérifie que la tâche est terminée.
La méthode se base sur la présence d'un `x` au début de la ligne.
"""
return self.description.startswith('x ')
@staticmethod
def create(description):
"""Crée une nouvelle tâche, en se basant sur sa description.
Pour rappel, les spécifications de `todo.txt` sont dispos `ici <http://todotxt.com/>`_.
"""
t = Task()
t.description = description
t.priority = t.__buildpriority()
t.date = t.__builddate()
t.completed = t.iscomplete
t.save()
for project_name in t.buildprojects():
p, insert_result = Project.objects.get_or_create(name=project_name)
t.projects.add(p)
for context_name in t.buildcontexts():
c, insert_result = Context.objects.get_or_create(name=context_name)
t.contexts.add(c)
return t
def complete(self, adddate=False):
"""Clôture une tâche, en y ajoutant soit la date de clôture, soit simplement un `x`.
"""
if not self.iscomplete:
if adddate:
today = date.today().strftime('%Y-%m-%d') + ' '
self.description = 'x ' + today + self.description
else:
self.description = 'x ' + self.description
self.completed = True
self.save()
def __buildpriority(self):
"""Retourne la priorité de la tâche, en se basant sur sa description.
La méthode regarde si la tâche débute par une lettre majuscule [A-Z], suivie d'un espace.
"""
re_result = re.findall(r'^\([A-Z]\) ', self.description)
if re_result:
return re_result[0].strip()
return None
def __builddate(self):
"""Récupère l'échéance depuis la description.
Selon les spécifications, l'échéance se trouve *après* la définition de la priorité.
On recherche donc une chaîne de caractères représentée par une date au format YYYY-MM-DD.
"""
re_result = re.findall(r'^(\([A-Z]\) )?[0-9]{4}-[0-9]{2}-[0-9]{2}',
self.description)
if re_result:
return date(2011, 9, 9)
return None
def __str__(self):
"""Retourne la description de la tâche."""
return self.description
def __buildvars(self, char):
"""Retourne tous les mots précédés du paramètre `char` dans la description.
Example:
self.description = "blabla @truc @machin #chose"
print(self.__buildvars('@'))
>>>> ['truc', 'machin']
print(self.__buildvars('#'))
>>>> ['chose']
"""
return [x[1:] for x in re.findall(r'[%s]\w+' % (char,), self.description)]
def buildprojects(self):
"""Récupère tous les projets associés à la tâche en cours."""
return self.__buildvars('+')
def buildcontexts(self):
"""Récupère tous les contextes associés à la tâche en cours."""
return self.__buildvars('@')
```
Et les tests qui vont bien:
```python
# app/tests.py
from datetime import date
from django.test import TestCase
from potatoe.models import Task
class TaskTestCase(TestCase):
def setUp(self):
pass
def test_build_task_projects(self):
"""Récupère les projets liés à une tâche."""
t = Task.create("(A) Todo rps blablab +RPS +SharePoint")
projects = [str(p) for p in t.projects.all()]
self.assertIn('RPS', projects)
self.assertIn('SharePoint', projects)
def test_build_task_contexts(self):
"""Récupère tous les contextes liés à une tâche."""
t = Task.create("(B) Todo bidule @brol @machin +RPS")
contexts = [str(c) for c in t.contexts.all()]
self.assertIn('brol', contexts)
self.assertIn('machin', contexts)
self.assertNotIn('RPS', contexts)
def test_priorities(self):
"""
Rule 1: If priority exists, it ALWAYS appears first.
The priority is an uppercase character from A-Z enclosed
in parentheses and followed by a space.
"""
t = Task.create("Really gotta call Mom (A) @phone @someday")
self.assertIsNone(t.priority)
t = Task.create("(b) Get back to the boss")
self.assertIsNone(t.priority)
t = Task.create("(B)->Submit TPS report")
self.assertIsNone(t.priority)
def test_dates(self):
"""
Rule 2: A tasks creation date may optionally appear
directly after priority and a space.
If there is no priority, the creation date appears first.
If the creation date exists, it should be in the format YYYY-MM-DD.
"""
t = Task.create("2011-03-02 Document +TodoTxt task format")
self.assertEqual(t.deadline, date(2011, 3, 2))
t.description = "(A) 2011-03-02 Call Mom"
self.assertEqual(t.deadline, date(2011, 3, 2))
t.description = "(A) Call Mom 2011-03-02"
self.assertIsNone(t.deadline, None)
def test_contexts_and_projects(self):
"""
Rule 3: Contexts and Projects may appear anywhere in the line
after priority/prepended date.
"""
t = Task.create("(A) Todo rps blablab +RPS +SharePoint")
projects = [str(p) for p in t.projects.all()]
self.assertIn('RPS', projects)
self.assertIn('SharePoint', projects)
t = Task.create("(A) Todo rps blablab @phone @email")
contexts = [str(c) for c in t.contexts.all()]
self.assertIn('phone', contexts)
self.assertIn('email', contexts)
def tearDown(self):
pass
class TestCompleteTasks(TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_complete_without_date(self):
t = Task.create("Some task @machin @brol +chose")
t.complete()
self.assertTrue(t.iscomplete)
self.assertTrue(t.description.startswith('x'))
t = Task.create("xylophone lesson")
self.assertFalse(t.iscomplete)
t.description = "X 2012-01-01 Make resolutions"
self.assertFalse(t.iscomplete)
t.description = "(A) x Find ticket prices"
self.assertFalse(t.iscomplete)
def test_complete_with_date(self):
t = Task.create("Some task @machin @brol #chose")
t.complete(True)
value = 'x ' + date.today().strftime('%Y-%m-%d')
beginstr = t.description[0:12]
self.assertEqual(value, beginstr)
```

View File

@ -0,0 +1,146 @@
Scaleway
========
Utilisation de `Sysbench <https://wiki.gentoo.org/wiki/Sysbench>`_ pour les tests.
Les trois serveurs comparés sont les suivants:
* Un VPS Cloud chez OVH: 4Go de Ram, single-core et SSD de 20Go.
* Un C1 chez Scaleway: 2Go de RAM, quad-core ARMv7 et SSD de 50Go.
* Un Kimsufi KS-1: Atom @ 1.86Ghz (2 coeurs, 4 threads), 2Go de RAM et 500 de disque.
Disk speed
----------
On utilise la commande ``dd`` (http://www.stevefortuna.com/check-disk-speed-quickly-and-easily-in-linux/)
pour simuler une écriture sur disque. Chez OVH, le serveur tourne à **514Mo/s**, tandis qu'il plafonne à **115Mo/s** sur le C1.
Idem pour la vitesse de lecture: chez OVH, on atteint les **2.3Go/s**, tandis que le C1 se voit aux alentours de **130Mo/s**.
Sur le KS-1, on tombe sur les valeurs suivantes: **143Mo/s** en écriture, **145Mo/s** en lecture.
Processeur
----------
On commence par installer ``sysbench`` et on le lance sur les trois machines ``sysbench --test=cpu --cpu-max-prime=20000 run``:
VPS Cloud
.. code-block:: shell
Maximum prime number checked in CPU test: 20000
Test execution summary:
total time: 29.8127s
total number of events: 10000
total time taken by event execution: 29.8107
per-request statistics:
min: 2.66ms
avg: 2.98ms
max: 6.78ms
approx. 95 percentile: 3.33ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 29.8107/0.00
Sur le C1
.. code-block:: shell
Maximum prime number checked in CPU test: 20000
Test execution summary:
total time: 686.8824s
total number of events: 10000
total time taken by event execution: 686.8633
per-request statistics:
min: 68.62ms
avg: 68.69ms
max: 76.86ms
approx. 95 percentile: 68.73ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 686.8633/0.00
Sur le KS-1
.. code-block:: shell
Maximum prime number checked in CPU test: 20000
Test execution summary:
total time: 209.6545s
total number of events: 10000
total time taken by event execution: 209.6493
per-request statistics:
min: 20.94ms
avg: 20.96ms
max: 34.01ms
approx. 95 percentile: 20.98ms
Threads fairness:
events (avg/stddev): 10000.0000/0.00
execution time (avg/stddev): 209.6493/0.00
4 Threads
---------
Et la même chose en spécifiant 4 threads (pour profiter des 4 coeurs du processeur ARM): ``sysbench --test=cpu --cpu-max-prime=20000 --num-threads=4 run``.
Sur VPS:
.. code-block:: shell
Test execution summary:
total time: 27.9756s
total number of events: 10000
total time taken by event execution: 111.8676
per-request statistics:
min: 2.50ms
avg: 11.19ms
max: 33.53ms
approx. 95 percentile: 15.28ms
Threads fairness:
events (avg/stddev): 2500.0000/1.22
execution time (avg/stddev): 27.9669/0.01
Et sur le C1:
.. code-block:: shell
Maximum prime number checked in CPU test: 20000
Test execution summary:
total time: 171.3275s
total number of events: 10000
total time taken by event execution: 685.2335
per-request statistics:
min: 68.46ms
avg: 68.52ms
max: 109.33ms
approx. 95 percentile: 68.54ms
Threads fairness:
events (avg/stddev): 2500.0000/1.22
execution time (avg/stddev): 171.3084/0.01
Sur le KS-1:
.. code-block:: shell
Maximum prime number checked in CPU test: 20000
Test execution summary:
total time: 86.2656s
total number of events: 10000
total time taken by event execution: 345.0025
per-request statistics:
min: 23.36ms
avg: 34.50ms
max: 78.49ms
approx. 95 percentile: 34.55ms
Threads fairness:
events (avg/stddev): 2500.0000/1.87
execution time (avg/stddev): 86.2506/0.01

View File

@ -0,0 +1,71 @@
---
Title: SOLID principle
Date: 2016-02-09
Slug: solid-principles
Tags: pattern, dev, code
---
* S : SRP (Single Responsibility
* O : Open closed
* L : LSP (Liskov Substitution)
* I : Interface Segregation
* D : Dependency Inversion
## Single Responsibility Principle
Le principe de responsabilité unique définit que chaque concept ou domaine d'activité ne s'occupe que d'une et d'une seule chose. En prenant l'exemple d'une méthode qui communique avec une base de données, ce ne sera pas à cette méthode à gérer l'inscription d'une exception à un emplacement quelconque. Cette action doit être prise en compte par une autre classe (ou un autre concept), qui s'occupera elle de définir l'emplacement où l'évènement sera enregistré (base de données, Graylog, fichier, ...).
Cette manière d'organiser le code ajoute une couche d'abstraction (ie. "I don't care") sur les concepts, et centralise tout ce qui touche à type d'évènement à un et un seul endroit. Ceci permet également de centraliser la configuration pour ce type d'évènements, et augmenter la testabilité du code.
## Open Closed
Un des principes essentiels en POO est l'héritage de classes et la surcharge de méthodes: plutôt que de partir sur une série de comparaisons pour définir le comportement d'une instance, il est parfois préférable de définir une nouvelle sous-classe, qui surcharge une méthode bien précise. Pour l'exemple, on pourrait ainsi définir trois classes:
* Une classe `Customer`, pour laquelle la méthode `GetDiscount` ne renvoit rien;
* Une classe `SilverCustomer`, pour laquelle la méthode revoit une réduction de 10%;
* Une classe `GoldCustomer`, pour laquelle la même méthode renvoit une réduction de 20%.
Si on rencontre un nouveau type de client, il suffit alors de créer une nouvelle sous-classe. Cela évite d'avoir à gérer un ensemble conséquent de conditions dans la méthode initiale, en fonction d'une autre variable (ici, le type de client).
En anglais, dans le texte : "Putting in simple words the “Customer” class is now closed for any new modification but its open for extensions when new customer types are added to the project.". En résumé: on ferme la classe `Customer` à toute modification, mais on ouvre la possibilité de créer de nouvelles extensions en ajoutant de nouveaux types [héritant de `Customuer`].
## Liskov Substitution
Le principe de substitution fait qu'une classe B qui hérite d'une classe A doit se comporter de la même manière que cette dernière. Il n'est pas question que la classe B n'implémente pas certaines méthodes, alors que celles-ci sont disponibles pour A.
> [...] if S is a subtype of T, then objects of type T in a computer program may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T), without altering any of the desirable properties of that program (correctness, task performed, etc.). (Source: [Wikipédia](http://en.wikipedia.org/wiki/Liskov_substitution_principle)).
> Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S, where S is a subtype of T. (Source: [Wikipédia aussi](http://en.wikipedia.org/wiki/Liskov_substitution_principle))
Ce principe s'applique à tout type de polymorphisme, et même aux langages de type *duck typing*: "when I see a bird that quacks like a duck, walks like a duck, has feathers and webbed feet and associates with ducks—Im certainly going to assume that he is a duck" (Source: [Wikipedia (as usual)](http://en.wikipedia.org/wiki/Duck_test)). Pour le cas émis ci-dessus, ce n'est donc parce qu'une classe a besoin **d'une méthode** définie dans une autre classe qu'elle doit forcément en hériter. Cela bousillerait le principe de substitution (et par la même occasion le *duck test*).
## Interface Segregation
Ce principe stipule qu'un client ne peut en aucun cas dépendre d'une méthode dont il n'a pas besoin. Plus simplement, plutôt que de dépendre d'une seule et même (grosse) interface présentant un ensemble conséquent de méthodes, il est proposé d'exploser cette interface en plusieurs (plus petites) interfaces. Ceci permet aux différents clients de n'utiliser qu'un sous-ensemble précis d'interfaces, répondant chacune à un besoin particulier.
L'exemple par défaut est d'avoir une interface permettant d'accéder à des éléments. Modifier cette interface pour permettre l'écriture impliquerait que toutes les applications ayant déjà accès à la première, obtiendraient (par défaut) un accès en écriture, ce qui n'est pas souhaité/souhaitable.
Pour contrer ceci, on aurait alors une première interface permettant la lecture, tandis qu'une deuxième (héritant de la première) permettrait l'écriture. On aurait alors le schéma suivant :
* A : lecture
* B (héritant de A) : lecture (par A) et écriture.
## Dependency inversion
Dans une architecture conventionnelle, les composants de haut-niveau dépendant directement des composants de bas-niveau. Une manière très simple d'implémenter ceci est d'instancier un nouveau composant. L'inversion de dépendances stipule que c'est le composant de haut-niveau qui possède la définition de l'interface dont il a besoin, et le composant de bas-niveau qui l'implémente.
Le composant de haut-niveau peut donc définir qu'il s'attend à avoir un `Publisher` pour publier du contenu vers un emplacement particulier. Plusieurs implémentation de cette interface peuvent alors être mise en place:
* Une publication par SSH
* Une publication par FTP
* Une publication
* ...
L'injection de dépendances est un patron de programmation qui suit le principe d'inversion de dépendances.
## Sources
* [Understanding SOLID principles on CodeProject](http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp)
* [Software Craftmanship](http://en.wikipedia.org/wiki/Software_craftsmanship)
* [Dependency Injection is NOT the same as dependency inversion](http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/)
* [Injection de dépendances](http://en.wikipedia.org/wiki/Dependency_injection)

View File

@ -0,0 +1,44 @@
---
Title: Définition du lien actif avec Django et un templatetag
Date: 2016-02-19
Slug: django-definition-lien-actif-menu
Tags: django, templatetag, dev, web
---
Pour que l'utilisateur ne se perde pas trop dans l'application, il est nécessaire qu'il ait des repères correctement définis. Que le menu principal ait toujours la même structure, et que le lien dans le menu sur lequel il vient de cliquer soit mis en gras, par exemple. Avec Django, on peut utiliser les [templatetags](https://docs.djangoproject.com/en/1.10/ref/templates/), de la manière suivante:
1. On définit un nouveau `templatetag` dans un fichier `tools/menuitems.py`. Il doit prendre le contexte actuel en paramètre (`takes_context=True`) et regarder si le lien qu'on lui demande de construire est identique à l'URL actuelle.
2. Ajoutez l'application `tools` (ou quel que soit le nom que vous lui aurez donné) dans les `INSTALLED_APPS`, dans le fichier de configuration.
3. Dans le template, il suffit d'importer le package défini ci-dessus et d'utiliser la nouvelle fonction lorsqu'on construit un lien.
## Définition du template tag
```python
# tools/menuitems.py
from django import template
register = template.Library()
@register.simple_tag(takes_context=True)
def menuitem(context, url, title):
if context.request.path == url:
return '<li class="active"><a href="' + url + '">' + title + '</a></li>'
else:
return '<li><a href="' + url + '">' + title + '</a></li>
```
## Définition du template
```django
<!-- templates/file.html -->
{% load menuitems %}
<ul>
{% menuitem '/' 'Overview' %}
{% menuitem '/gymnast/' 'Gymnasts' %}
{% menuitem '/routines/' 'Routines' %}
</ul>
```

View File

@ -0,0 +1,86 @@
---
Title: Un site multilingue avec Pelican
Date: 2016-04-22
Slug: multilingual-site-with-pelican
Tags: pelican, dev, static, site, web
---
Pour un petit projet perso, on m'a demandé de développer un petit site de location de chambres. En soi, j'aurais pu simplement créer quelques pages HTML statiques, copier le tout sur un serveur et le tour aurait été joué. Il y avait cependant quelques contraintes à prendre en compte:
* Besoin d'un site multilangues (en, fr, nl)
* Besoin d'une gallerie d'images, une gallerie devant être associée à une page.
* Besoin d'une carte, pour présenter les choses intéressantes à voir dans le quartier.
* Et beaucoup de discussions, pour arriver à un résultat convaincant.
Il me fallait quelque chose de flexible, permettant de gérer facilement les images, de mettre le texte en forme, de gérer l'affichage des informations, ... Le tout, sans que cela ne prenne trop de temps.
En parcourant les différents CMS existants, je me suis rendu compte qu'il existait assez peu de solutions autorisant ces trois contraintes nativement. [Wordpress](https://wordpress.org/) le permet, mais seulement grâce à l'installation de plusieurs plugins; [Drupal](https://www.drupal.org/) me semblait trop compliqué à appréhender; [PluXML](http://www.pluxml.org/) nécessiterait sans doute trop de paramétrage, ... Celle qui m'aurait le plus botté était [Wagtail](https://wagtail.io/), mais le temps fait (un peu) défaut pour le moment... En gros, j'avais le choix entre les deux solutions suivantes:
1. Un vrai CMS dynamique, où mon utilisateur adoré pourrait se débrouiller tout seul. Il aurait alors fallu renforcer la sécurité, configurer la base de données, configurer les backups, vérifier la cohérence des données au restore, le former à la manipulation des données et aux traductions
2. Ou alors, partir sur un site statique présentant les informations de manière fixe, nécessitant relativement peu de paramétrage, mais l'obligeant à me contacter pour toute modification.
Puisqu'on parle d'une personne (très) proche, je suis parti sur la seconde option. Et puisqu'on parle d'un site statique, je suis parti sur [Pelican](docs.getpelican.com/).
## Multilinguisme
Pour gérer un site multilangue, il y a deux solutions:
1. Soit utiliser les métadonnées `lang` et `slug` dans chaque article/page. Cette solution ne convient qu'à moitié, car la langue principale du site reste inchangée, quelle que soit l'entrée sur laquelle l'utilisateur se trouve: si le visiteur clique sur un lien, il arrivera donc sur la page traduite dans la langue par défaut, mais s'il se trouvait sur une page traduite dans une autre langue. Ce qu'on souhaite, c'est que le site soit en fait un sous-site du site principal, avec sa langue spécifique: en traduisant la page dans une langue, on *resterait* dans cette langue-ci.
2. Soit utiliser le plugin `i18n_subsites`, qui permet de définir un site principale (basé sur une langue), puis un ensemble de sous-sites, chacun d'entre eux correspondant à une langue.
En gros, la structure devient la suivante:
```
pages/
en/
page1.md
page2.md
fr/
page1.md
page2.md
nl/
page1.md
page2.md
```
Ceci colle déjà plus à ce que l'on souhaite. Pour activer le plugin, il suffit de cloner le dépôt [pelican-plugins](https://github.com/getpelican/pelican-plugins.git) (ou de copier le plugin dans un sous-répertoire de l'installation, à la hussarde). Attention qu'on trouve quelques problèmes de compatibilité entre greffons: `i18n_subsites` pose plus ou moins problème avec le plugin `gallery` si des images d'albums différents portent le même nom.
## Gallerie d'images
Pour la gallerie d'images, je suis parti des plugins `gallery` et `thumbnailer`. Le premier permet de créer une gallerie et y copiant un ensemble de photos; le second permet de créer des thumbnails pour chacune des images (au format *square*, *wide* ou *cropped*). Dans chaque page, j'ajoute une métadonnée `gallery: `, qui indique le nom du répertoire dans lequel les images doivent être récupérées. Dans le template (également fixé au niveau des propriétés de la page), on peut alors parcourir les différentes images et les afficher. En associant ce mécanisme avec un plugin [Lightbox pour jQuery](http://lokeshdhakar.com/projects/lightbox2/), on peut ainsi charger les miniatures à l'affichage, et présenter l'image originelle lorsque l'utilisateur cliquera sur le thumbnail.
```
{% for image in page.galleryimages %}
<a class="thumbnail" data-lightbox="pictures-set" href="/pictures/gallery/{{page.album}}/{{ image }}">
<img class="img-thumbnail img-responsive" src="/thumbnails/thumbnail_wide/{{ image }}" alt="">
</a>
{% endfor %}
```
## Index
La page d'accueil est fixée et ne se base pas sur du contenu. Je l'ai configurée directement au niveau du thème.
La navigation pour l'utilisation est la suivante: lorsqu'il se connecte au site, il arrive sur la page d'accueil - quelques images, menu en anglais par défaut. Il peut alors sélectionner une langue, ce qui l'enverra vers la page `{lang}/index.html`. Cette page-ci correspond à du contenu rédigé en Markdown, dans lequel on trouve les métadonnées suivantes:
```
Title: Around us
Slug: index
Lang: en
Translation: true
Template: pagewithmap
Gallery: brussels
```
A priori, toutes les propriétés spécifiées ont une utilisé:
* Le titre est utilisé dans le template et dans les menus de navigation
* Le slug permet d'overrider le nom du fichier (mais on aurait aussi pu passer par la propriété `save_as`). Il s'agit aussi de la clé qui associera tous les contenus ayant le même `slug`.
* La propriété `Lang` définit la langue
* Le `template` permet d'associer la présentation à une page HTML spécifique. Celle-ci est placée dans le répertoire `themes/{theme_name}/templates/pagewithmap.html`.
* La propriété `gallery` est utilisée par le plugin `gallery` que l'on a chargé plus haut. Celui-ci va charger toutes les images qui se trouvent dans le répertoire `pictures/gallery/{gallery_name}/`.
## A améliorer
Plutôt que d'avoir des pages statiques, toutes liées à un template, j'aurais apprécié pouvoir structurer mes données dans des blocs. Par exemple, dans une page, j'aurais pu avoir un bloc `description`, un bloc `comment nous joindre`, un bloc `prix`, ... A répéter dans les différentes langues. [Wagtails](https://wagtail.io/) le permet (et c'est même son mode de structuration par défaut - à coupler avec [Django Medusa](https://github.com/mtigas/django-medusa)). Je crois [Hyde](https://hyde.github.io/) fonctionne un peu selon le même principe, avec l'avantage qu'il génère par défaut du contenu statique. En fait, il y a des propriétés globales au site que j'aurais aimé pouvoir définir (dans les différentes langues :-)) et réutiliser à des endroits précis.
En même temps, j'aurais aimé pouvoir associer plusieurs galleries d'images à une même page. Le plugin `gallery` ne permet d'associer qu'un répertoire; il ne gère pas les sous-répertoires (la méthode utilisée est un `listdir` et pas un `walk` - et cela impliquerait de modifier la structure de retour pour associer le nom du sous-répertoire aux images, en plus du répertoire initial).

View File

@ -0,0 +1,25 @@
---
Title: An introduction to programming in Go
Date: 2016-05-10
Tags: ebook, go
Image: book/an-introduction-to-programming-in-go.jpg
Slug: an-introduction-to-programming-in-go
---
[Go](https://golang.org/) est un langage relativement à la mode. Il est parfois présenté comme une évolution du langage [Python](https://www.python.org/), parfois comme un subset aux langages C/C++. Avant de vous faire un avis tranché, essayez-le. Il présente de très bonnes idées, tout en restant relativement *low-level*.
Le livre que je viens de terminer s'intitule [An introduction to programming in Go](http://www.golang-book.com/books/intro) (disponible gratuitement à l'adresse ci-contre) et reprend les bases du langages. Les premiers chapitres sont d'ailleurs vraiment accessibles.
> A short, concise introduction to computer programming using the language Go. Designed by Google, Go is a general purpose programming language with modern features, clean syntax and a robust well-documented common library, making it an ideal language to learn as your first programming language.
Les premiers chapitres donnent un aperçu extrêmement basique du langage (installation, utilisation du terminal, déclaration de variables, les conditions, boucles, ...). Le chapitre 5 devient réellement intéressant, avec les fonctions, *go-routine*, gestion des threads et de la concurrence.
Une des applications qui m'a un peu tapé dans l'oeil, c'est [Gogs](https://gogs.io/): il s'agit d'une interface Web de gestion de dépôts [Git](https://git-scm.com/). En cherchant un peu, on trouve pas mal d'autres exemples qui ont l'air intéressants:
* [Macaron](https://go-macaron.com/), *a high productive and modular web framework in Go*.
* [Peach](https://peachdocs.org/), *a web server for multi-language, real-time synchronization and searchable documentation*.
* [XORM](https://github.com/go-xorm/xorm) comme ORM.
Bref, à l'occasion. Le PDF vaut clairement le coup et s'avale assez rapidement (165 pages au garot).
Prochains livres à lire (ou déjà en cours): [Why Rust?](http://www.oreilly.com/programming/free/why-rust.csp) et [Enterprise Pharo](http://files.pharo.org/books/enterprise-pharo/) :) .

View File

@ -0,0 +1,47 @@
---
Title: Extraction de données en CSV avec PSQL
Date: 2015-06-23
Slug: extract-data-as-csv-with-psql
Tags: db, sql, psql, csv, extract
---
Pour copier des résultats chopés depuis une commande `psql`, la syntaxe est relativement simple quand elle est décomposée. En gros, la commande ressemble à ceci:
```
$ psql -c "\copy (<ma_requête>) to <output_file> DELIMITER E'\t' CSV HEADER;" <db_user> <db_password>
```
Ce qui signifie plus prosaïquement:
> Cher PSQL, copie-moi les résultats de `<ma_requête>` vers le fichier `<output_file>`, en utilisant une tabulation pour séparer les champs (tu peux inclure le `HEADER`, merci). Pour la connexion, tu peux utiliser `<db_user>` et `<db_password>`. Bisous.
Le script complet (en `.bat`), avec un fichier de sortie timestampé.
```
@echo off
set output_dir="C:\Extracts"
set db_host=localhost
set db_user=postgres
set db_name=db_name
set db_passwd=db_password
set psql_dir="C:\PSQL\9.2\bin"
for /f "tokens=1-3 delims=/ " %%i in ("%date%") do (
set day=%%i
set month=%%j
set year=%%k
)
for /f "tokens=1-3 delims=: " %%l in ("%time%") do (
set hour=%%l
set min=%%m
)
set datestr=%year%%month%%day%_%hour%_%min%
set output_file=%output_dir%_%datestr%.list.txt
SET PGPASSWORD=%db_passwd%
%psql_dir%\psql -c "\copy (Select SubscriberId, LastName, FirstName From subscriber) to '%output_file%' DELIMITER E'\t' CSV HEADER;" %db_user% %db_passwd%
```

View File

@ -0,0 +1,132 @@
---
Title: Intégration continue sur Gitlab
Date: 2016-08-05
Slug: integration-continue-gitlab
Tags: ci, intégration, gitlab, flake8, coverage
---
J'ai récemment créé un nouveau projet sur [Framagit](https://framagit.org/Grimbox/heima), qui propose une des dernières versions de [Gitlab](https://about.gitlab.com/), avec son [module d'intégration continue](https://about.gitlab.com/gitlab-ci/). Cela devient juste super facile de mettre un process d'intégration continue sur son projet :-)
Pour mon projet Django, j'ai ajouté deux dépendances (bon, trois, avec Django): `coverage` et `django_coverage_plugin` dans un fichier `requirements/dev.txt`:
```shell
# requirements/base.txt
django
```
```shell
# requirements/dev.txt
-r base.txt
coverage
django_coverage_plugin
```
On crée ensuite un fichier `.gitlab-ci.yml` à la racine du projet, avec le contenu suivant:
```shell
before_script:
- pip install -r requirements/dev.txt
test:python-3.4:
stage: test
image: python:3.4-slim
script:
- coverage run src/manage.py test sherlock
- coverage report -m
```
Les paramètres importants sont:
* L'`image` qui spécifie l'image [Docker](https://hub.docker.com/_/python/) à utiliser (sans quoi les binaires `python` ne seront par exemple pas disponible). En fonction du langage utilisé, choisissez [une autre image](https://hub.docker.com/).
* Et la partie `script`. Ici, `sherlock` est le nom de mon application Django; elle se situe dans le sous-répertoire `src/`.
## En bonus
Pour avoir le pourcentage de couverture de code, ajoutez, dans les paramètres du projet, la valeur `\d+\%\s*$` à l'option `test coverage parsing`.
Au niveau du fichier `README.md`, ajoutez le contenu suivant: `[![build status](https://framagit.org/Grimbox/heima/badges/master/build.svg)](https://framagit.org/Grimbox/heima/commits/master)`
## Résultat
Résultat, les dépendances sont installées, les tests sont lancés et la couverture de code indique un pourcentage qui est chopée au passage par la regex ci-dessus. Magique. Petit badge de réussite sur la page d'accueil, couverture de code indiquée sur les tests, tout va bien.
```shell
Running with gitlab-ci-multi-runner 1.4.1 (fae8f18)
Using Docker executor with image python:3.4-slim ...
Pulling docker image python:3.4-slim ...
Running on runner-ed7dbd37-project-7675-concurrent-0 via ruth...
Fetching changes...
HEAD is now at ff9e6aa add coverage
From https://framagit.org/Grimbox/heima
+ ff9e6aa...aaf716d master -> origin/master (forced update)
Checking out aaf716d6 as master...
$ pip install -r requirements/dev.txt
Collecting django (from -r requirements/base.txt (line 1))
Downloading Django-1.10-py2.py3-none-any.whl (6.8MB)
Collecting coverage (from -r requirements/dev.txt (line 2))
Downloading coverage-4.2.tar.gz (359kB)
Collecting django_coverage_plugin (from -r requirements/dev.txt (line 3))
Downloading django_coverage_plugin-1.3.1.tar.gz
Collecting six>=1.4.0 (from django_coverage_plugin->-r requirements/dev.txt (line 3))
Downloading six-1.10.0-py2.py3-none-any.whl
Installing collected packages: django, coverage, six, django-coverage-plugin
Running setup.py install for coverage: started
Running setup.py install for coverage: finished with status done
Running setup.py install for django-coverage-plugin: started
Running setup.py install for django-coverage-plugin: finished with status done
Successfully installed coverage-4.2 django-1.10 django-coverage-plugin-1.3.1 six-1.10.0
$ coverage run src/manage.py test sherlock
....
----------------------------------------------------------------------
Ran 4 tests in 0.024s
OK
Creating test database for alias default...
Destroying test database for alias default...
$ coverage report -m
Name Stmts Miss Cover Missing
-----------------------------------------------------------------------
src/heima/__init__.py 0 0 100%
src/heima/settings.py 18 0 100%
src/manage.py 13 6 54% 9-21
src/sherlock/__init__.py 0 0 100%
src/sherlock/admin.py 8 0 100%
src/sherlock/migrations/0001_initial.py 7 0 100%
src/sherlock/migrations/__init__.py 0 0 100%
src/sherlock/models.py 18 0 100%
src/sherlock/tests.py 30 0 100%
-----------------------------------------------------------------------
TOTAL 94 6 94%
Build succeeded
```
## Intégration avec Flake8
[Update du 08/08/2016]
Toujours dans la même veine, on peut pousser l'intégration avec [Flake8]({{< relref "2015-08-19-pep8.md" >}}). Il suffit d'ajouter `flake8` dans les prérequis, ainsi qu'un fichier `.tox.ini` dans lequel on trouvera le contenu suivant:
```shell
[flake8]
max-line-length = 100
exclude = migrations, manage.py
```
On modifie ensuite le fichier `.gitlab-ci.yml` pour y ajouter l'intégration `flake8`:
```shell
before_script:
- pip install -r requirements/dev.txt
test:python-3.4:
stage: test
image: python:3.4-slim
script:
- flake8 src/
- coverage run src/manage.py test sherlock
- coverage report -m
```
Attention que si `flake8` renvoie le moindre avertissement, l'intégration continue du projet passera en **failed**.

View File

@ -0,0 +1,57 @@
---
Title: Un client SOAP en Python avec Suds
Date: 2016-08-23
Tags: python, soap, suds, wcf
---
La manière la plus simple que j'ai trouvée pour contacter un serveur SOAP (WCF) avec un client Python, est de passer par [Suds](https://fedorahosted.org/suds/wiki/Documentation). Après avoir contacté le serveur, cette librairie construit dynamiquement les différentes méthodes directement sur le service client. Si par exemple le service sur le serveur expose les méthodes `RunQuery` et `Execute`, vous pourrez directement les invoquer *via* `client.service.RunQuery` et `client.service.Execute`.
Attention que la version officielle n'est compatible qu'avec Python2. Pour une compatibilité avec Python3, il existe [suds-py3](https://github.com/cackharot/suds-py3). Les deux sont disponibles sur [Pypi](https://pypi.python.org/pypi), et installable avec `pip`.
La seule difficulté a été de construire les paramètres. Comme les paramètres XML peuvent être beaucoup plus complexes que les paramètres en [JSON](http://json.org/) (ie. *string, number, object, array, bool ou nul*), il vous faudra [construire ces paramètres](https://fedorahosted.org/suds/wiki/Documentation#ComplexArguments) en passant par une `factory`:
```python
from suds.client import Client
wsdl = 'http://localhost:88/WebService.svc?wsdl'
client = Client(wsdl)
print(client)
Suds ( https://fedorahosted.org/suds/ ) version: 1.3.2.0 IN build: 20160428
Service ( HAWWebService )
tns="http://tempuri.org/"
Prefixes (4)
ns0 = "http://schemas.datacontract.org/2004/07/ImagoInboxWebService.Requests"
ns1 = "http://schemas.datacontract.org/2004/07/ImagoInboxWebService.ResponseBase
ns2 = "http://schemas.microsoft.com/2003/10/Serialization/"
ns3 = "http://tempuri.org/"
Ports (1):
(BasicHttpBinding_IHAWWebService)
Methods (1):
RunQuery(ns0:RequestRunQuery request,)
Types (7):
ns0:RequestBase
ns0:RequestRunQuery
ns1:ResponseBase
ns1:ResponseRunQuery
ns2:char
ns2:duration
ns2:guid
```
Dans l'exemple ci-dessus, on voit qu'il y a une méthode `RunQuery`, qui prend un paramètre de type `ns0:RequestRunQuery` (ou, plus précisément, un paramètre de type `http://schemas.datacontract.org/2004/07/ImagoInboxWebService.Requests.RequestRunQuery`. Pour instancier cet objet, il y a deux manières de faire, la seconde étant sans doute préférable:
```python
param1 = client.factory.create('ns0:RequestRunQuery')
param2 = client.factory.create('{http://schemas.datacontract.org/2004/07/ImagoInboxWebService.Requests}RequestRunQuery')
```
Après, cela on envoie les paramètres correctement construits dans la méthode d'appel, et on récupère le résultat:
```python
response = client.service.RunQuery(param2)
print(response)
```

View File

@ -0,0 +1,44 @@
---
Title: Building Maintainable Software
Date: 2016-09-12
Tags: ebook, software, maintenance
Image: book/building-maintainable-software.jpg
Slug: building-maintainable-software
---
En profitant d'une petite promo de -50% chez [O'Reilly](http://www.oreilly.com/>), je me suis offert l'ebook [Building Maintainable Software, édition C-Tartine](http://shop.oreilly.com/product/0636920049555.do). Le livre propose de suivre une série de préceptes permettant d'améliorer la qualité du code produit. Bien que le contenu ressemble parfois plus à une grosse page marketing pour le [Software Improvment Group](https://www.sig.eu/nl/), il reste intéressant et vaut le coup d'être lu. Deux petits problèmes par contre:
1. On part parfois d'un cas concret (qui ressemble parfois à ce que j'ai pu commettre de code foireux), en prenant une méthode dans laquelle on trouve plusieurs concepts différents (appel à la db, cast vers une classe type *DTO*, validation d'input). Juste après, on passe directement sur un autre exemple, beaucoup plus simple à résoudre.
2. J'ai l'impression que cela parle plus de théorie, sans proposer de solution automatique, ce qui rejoint un peu ce que je disais au niveau marketing: "si vous voulez vous améliorer, signez chez nous!".
Les principaux conseils sont les suivants (pour les détails, il vous faudra acheter le bouquin :) ):
## Au niveau des méthodes
* Gardez vos méthodes/fonctions courtes. Pas plus de 15 lignes, en comptant les commentaires. Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9% de plus de 60 lignes; pas plus de 22.3% de plus de 30 lignes, au plus 43.7% de plus de 15 lignes et au moins 56.3% en dessous de 15 lignes). Oui, c'est dur à tenir, mais faisable.
* Conserver une complexité de McCabe en dessous de 5, c'est-à-dire avec quatre branches au maximum. A nouveau, si on a une méthode avec une complexité cyclomatique de 15, la séparer en 3 fonctions avec une complexité de 5 conservera globalement le nombre 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.
* N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc., c'est juste mal: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées. C'est aussi une forme de régression.
* Conservez de petites interfaces. Quatre paramètres, pas plus. Au besoin, refactorisez certains paramètres dans une classe, plus facile à tester.
## Au niveau des classes
* Privilégiez un couplage faible entre vos classes. Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences. L'implémentation du service ``UserNotificationsService`` ne doit pas forcément se trouver embarqué dans une classe ``UserService``. De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction. La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée.
## Au niveau des composants
* Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants également. Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'on ne puisse contacter trop facilement des couches séparées de l'architecture. Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "juste parce qu'elle en a la possibilité". Vous pourrez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que `IFileTransfer` avec ses méthodes `put` et `get`, et non pas les détails d'implémentation complet d'une classe `FtpFileTransfer` ou `SshFileTransfer`).
* Conserver un bon balancement au niveau des composants: évitez qu'un composant **A** ne soit un énorme mastodonte, alors que le composant juste à côté n'est capable que d'une action. De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités plus faciles à gérer. Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que ces composants soit approximativement de même taille.
## Et de manière plus générale
* Conserver une densité de code faible: il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois. Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisant du refactoring (intensif?), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux. Mais surtout en évitant de copier/coller bêtement du code existant.
* Automatiser les tests, ajouter un environnement d'intégration continue dès le début du projet et vérifier par des outils les points ci-dessus.
Ceci est sans doute un des points les plus ennuyants de ce livre: il n'y a finalement que très peu d'exemples concrets, notamment pour la mise en place d'un tel environnement. Ok, ça parle de Jenkins du début à la fin, mais plus comme un exemple à suivre (ou parfois à ne pas suivre) que comme un outil à utiliser. De manière plus générale, j'ai l'impression que le code .Net reste extrêmement fermé à des outils open source permettant d'augmenter la qualité du code. Il existe des *linters* pratiquement pour tous les langages, mais si vous voulez quelque chose de fonctionnel pour C#, il va falloir passer par la caisse. Une stratégie MS classique en sommme: "on vous offre les outils pour pas grand chose, et pour aller plus loin, vous douillez".
En regardant un peu à droite-à gauche, pour du code .Net, les outils suivants ont l'air sympa, comme:
* [SonarLint](http://www.sonarlint.org) (en [cli](http://www.sonarlint.org/commandline/index.html), intégré dans [un IDE](http://www.sonarlint.org/visualstudio/index.html) ou en mode [CI](http://www.sonarqube.org/)) (dont je ne peux plus me passer :-) )
* [StyleCop](https://github.com/DotNetAnalyzers/StyleCopAnalyzers)
* [Refactoring Essentials](http://vsrefactoringessentials.com/)
* [CodeMaid](https://visualstudiogallery.msdn.microsoft.com/76293c4d-8c16-4f4a-aee6-21f83a571496?SRC=VSIDE), pour un code propre et soyeux. En zieutant la description, on trouve que *CodeMaid is an open source Visual Studio extension to cleanup, dig through and simplify our C#, C++, F#, VB, PHP, JSON, XAML, XML, ASP, HTML, CSS, LESS, SCSS, JavaScript and TypeScript coding*.

View File

@ -0,0 +1,31 @@
---
Title: Recherche full-text dans toute la db... avec grep
Date: 2016-09-30
Tags: db, python, mssql, extract
Slug: full-text-search-db-with-pyodbc-and-grep
---
Je n'en suis franchement pas fier parce que c'est tout sauf propre et réutilisable, mais cela pourrait peut-être à nouveau servir à l'occasion... Le script ci-dessous lit l'ensemble d'une base de données en utilisant [pyodbc](https://github.com/mkleehammer/pyodbc) et écrit leur contenu dans un fichier texte, nommé d'après la table dont elles sont issues. En mode bourrin, en somme, puisqu'on liste toutes les tables, et qu'on fait ensuite un `select` sur chacune d'entre elles, avant d'en dumper le contenu dans un fichier texte. Intérêt? Exécuter un `grep -rin` par la suite sur les fichiers écrits.
```python
import pyodbc
CXSTR = 'DRIVER={SQL Server};SERVER=...;DATABASE=...;UID=...;PWD=...;'
if __name__ == '__main__':
cnxn = pyodbc.connect(CXSTR)
cursor = cnxn.cursor()
cursor.execute("SELECT * FROM information_schema.tables WHERE TABLE_TYPE='BASE TABLE'")
for row in cursor.fetchall():
cursor2 = cnxn.cursor()
cursor2.execute("Select * From " + row[2])
print('using {}'.format(row[2] + '.txt'))
with open(row[2] + '.txt', 'wt', encoding='utf-8') as f:
for row2 in cursor2.fetchall():
f.write(str(row2))
```
Quand je disais qu'il y avait moyen de faire plus propre... :-) Allez, pour le fun: le script s'en sort avec un score PyLint de 2.67/10 (*missing docstring*, *invalid name*, *lines too long*, *no member* et *anomalous backslash in string*).

View File

@ -0,0 +1,58 @@
---
Title: Django et ses contexts processors
Date: 2016-10-25
Slug: django-context-processors
Tags: django,"python, config, dev
---
De manière très grossière, les *`contexts processors`* permettent de définir du contenu accessible globalement dans l'application, par exemple pour définir un menu de navigation ou le titre du site. On parle donc d'éléments qui ne sont pas spécifiques au contexte (vue, fonction, ...) actuel, mais plutôt définis globalement par rapport à l'application.
Au niveau de la vue, on a pour habitude de passer un dictionnaire au template; celui-ci sera complété par un ensemble d'autres informations, définies dans les *contexts processors*.
Techniquement, il s'agit simplement de fonctions référencées au niveau de la configuration de l'application. Si on souhaite générer un menu global, il suffit dès lors de définir une fonction dans un module particulier:
```python
# my_module/navigation.py
def main_menu(request):
return { 'links': ['Accueil', 'Se restaurer', 'Informations pratiques' ] }
```
On peut dès lors y accéder dans le template de base:
```django
<!-- templates/base.html -->
<!DOCTYPE HTML>
<html>
{% for link in links %}
<a href="#">{{ link }}</a>
{% endfor %}
</html>
```
Pour éviter que ces variables ne soient écrasées par la construction de la vue, la transmission du dictionnaire vers la vue doit être construite en utilisant la fonction `render(request, template, context)`, de la manière suivante. Evitez la fonction `render_to_response`:
```python
def events(request):
context = { 'events': events = Event.objects.all() }
return render(request, 'events.html', context)
```
Depuis Django 1.10, la variable `TEMPLATE_CONTEXT_PROCESSORS` est dépréciée et doit être remplacée par l'option `context_processors`:
```python
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates', ],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'...',
],
},
},
]
```

View File

@ -0,0 +1,157 @@
---
Title: Modéliser une arborescence dans une DB relationnelle
Summary: Ou comment remonter jusqu'à Charlemagne en une seule requête.
Tags: db, model, sql
Category: Code
---
Il existe plusieurs manières de représenter une hiérarchie ou une arborescence dans une base de données relationnelles. **TL; DR**: il existe en fait cinq modélisations principales connues; chaque présentation présente des avantages et désavantages. L'une d'entre elles sort du lot (les *Closures*) avec un tout-petit-micro-inconvénient par rapport à tous ses avantages.
La représentation d'une structure hiérarchique peut être faite de plusieurs manières:
* Autant de tables qu'il y a de niveaux
* Adjency lists
* Path Enumeration
* Nested sets
* Closure trees
## 1 table = 1 niveau
Cette représentation est la plus naïve du lot: on aurait autant de tables qu'il y a de niveaux à représenter. De cette manière, il est facile de faire des jointures entre les différentes tables. Le problème est que chacune de ces tables aura les mêmes champs que les autres; une modification dans l'une d'entre elle devra sans doute être répercutée dans toutes les autres tables. Si un nouveau niveau peut être ajouté, cela équivaudra à ajouter une nouvelle table (avec autant de nouvelles contraintes que celles déjà présentes pour les autres tables).
```python
# simple.models.py
from django.db import models
class FirstLevel(models.Model):
name = models.CharField(max_length=50)
def breadcrumb(self):
return self.name
class SecondLevel(models.Model):
name = models.CharField(max_length=50)
parent = models.ForeignKey(FirstLevel)
def breadcrumb(self):
return '{0} / {1}'.format(
self.parent.name,
self.name
)
class ThirdLevel(models.Model):
name = models.CharField(max_length=50)
parent = models.ForeignKey(SecondLevel)
def breadcrumb(self):
return '{0} / {1} / {2}'.format(
self.parent.parent.name,
self.parent.name,
self.name
)
```
```python
>>> from simple.models import *
>>> l1 = FirstLevel(name='Niveau 1')
>>> l2 = SecondLevel(name='Niveau 2', parent=l1)
>>> l3 = ThirdLevel(name='Niveau 3', parent=l2)
>>> l3.breadcrumb()
'Niveau 1 / Niveau 2 / Niveau 3'
```
Avant de passer à l'étape suivante, on voit clairement que les champs `name` et la fonction `breadcrumb()` sont copiés/collés entre les différentes classes. En allant plus loin,
## Adjency Lists
Les *Adjency lists* représentent chaque enregistrement avec une clé vers son parent. C'est une des représentations les plus naïves: elle permet une insertion extrêmement rapide, mais pose de gros problèmes de récupération lorsque la profondeur de la structure est inconnue (puisqu'on doit faire autant de jointures qu'il y a de niveaux... et que ceux-ci sont potentiellement infini...). Pour faciliter ceci, il est possible de passer par une fonction SQL, mais cela ne changera intrinsèquement rien au nombre de requêtes à effectuer.
C'est une bonne solution s'il y a beaucoup plus d'écritures que de lectures. Dans le cas contraire, oubliez la: les performances vont rapidement se dégrader et les interrogations sur la base seront de plus en plus compliquées, notamment si on cherche à récupérer des noeuds spécifiques.
```python
# file adjency_list.models.py
from django.db import models
class Node(models.Model):
name = models.CharField(max_length=50)
parent = models.ForeignKey('self', null=True)
def breadcrumb_list(self):
if self.parent:
return self.parent.breadcrumb_list() + [self.name]
return [self.name]
def breadcrumb(self):
return ' / '.join(self.breadcrumb_list())
def __str__(self):
return self.name
```
```python
>>> from adjency_list.models import *
>>> n1 = Node(name='A')
>>> n2 = Node(name='B', parent=n1)
>>> n3 = Node(name='C', parent=n2)
>>> n3.breadcrumb()
'A / B / C'
```
```json
// from django.db import connection
// print(connection.queries)
[
{
'sql': 'SELECT * FROM "adjency_list_node" WHERE "adjency_list_node"."name" = \'C\'',
'time': '0.000'
},
{
'sql': 'SELECT * FROM "adjency_list_node" WHERE "adjency_list_node"."id" = 2',
'time': '0.000'
},
{
'sql': 'SELECT * FROM "adjency_list_node" WHERE "adjency_list_node"."id" = 1',
'time': '0.000'
}
]
```
Pour obtenir l'arborescence de cette structure, on voit bien que l'ORM de Django exécute trois requêtes: la première pour récupérer l'objet dont le nom est `C` (celui que l'on cherche), puis son parent (sur base de la propriété `parent = 2`), puis le "parent du parent" (sur base de la propriété `parent = 1`).
Imaginez maintenant que vous récupériez une arborescence complète sur six niveaux max avec plusieurs centaines de noeuds... :)
L'avantage de cette présentation est que l'écriture (ajout ou modification) est extrêmement rapide: il suffit de modifier la valeur de la propriété `parent` d'une instance pour que l'arborescence soit modifiée. L'intégrité de la base de données est constamment conservée.
## Path enumeration
L'énumération du *path* consiste à ajouter une colonne dans laquelle on stockerait le chemin complet (par exemple `ancêtre1 / ancêtre2 / enfant`). Dans une certaine mesure, cela revient à stocker toutes les relations avec un noeud dans un champ textuel (type *jaywalking* - d'une certaine manière, on utilise un champ pour y conserver toutes les relations vers les ancêtres)... Et c'est hyper galère à maintenir car:
* Si un noeud est modifié, il faut modifier tous les champs qui y font référence.
* Il n'existe aucune aide relative aux méthodes d'insertion/mise à jour/suppression.
* Le caractère d'échappement doit être unique.
Et à nouveau, rien ne garantit l'intégrité relationnelle de la base de données: si un petit comique modifie la base sans passer par l'API, on perdra toute cohérence.
## Nested sets
Les *nested sets* ont pour but de simplifier la gestion présentée ci-dessous, en ajoutant un niveau de complexité sur la modélisation: la lecture de toute la hiérarchie peut se faire en une seule requête, mais l'écriture (ajout, modification et suppression) reste compliquée à implémenter. Un autre problème est qu'on perd une partie de la cohérence de la base de données: tant que le processus de mise à jour n'est pas terminée, la base peut se trouver dans un état de *Schrödinger*.
L'implémentation consiste à ajouter des valeurs `gauche` et `droite` sur chaque noeud. L'attribution des valeurs se fait selon un parcours préfixe: pour chaque enregistrement, la valeur `gauche` est plus petite que toutes les valeurs utilisées dans ses descendants; la valeur `droite` est plus grande que celle utilisée par tous ses descendants... Et ces valeurs sont **entre** toutes les valeurs gauches/droites de ses ancêtres.
Le problème intervient lorsque vous souhaitez ajouter, modifier ou supprimer un noeud: il vous faut alors recalculer l'ensemble des arborescences, ce qui est loin d'être performant. En plus de cela, on perd l'intégrité relationnelle, puisqu'un enfant pourrait avoir des valeurs incohérentes: avoir par exemple un parent qui pointe vers un noeud alors que le recalcul des valeurs est en cours.
Il existe des librairies toutes faites pour cela... Regardez du côté du pattern [MPTT](https://github.com/django-mptt/django-mptt) si vous ne trouvez rien sur les nested sets.
## Closure trees
Le dernier, la *closure* consiste à créer une table supplémentaire reprenant **toutes** les relations d'un noeud vers ses enfants:
* On doit donc passer par une table connexe pour connaître la structure
* Cette table peut grandir très rapidement: pour cinq niveaux donnés, le niveau 1 sera lié à tous ses descendants (ainsi qu'à lui-même); le niveau 2 à tous ses descendants; etc. Pour cinq niveaux, on aura donc déjà 5 + 4 + 3 + 2 + 1 entrées.
L'avantage est qu'on conserve l'intégrité relationnelle et qu'elle est extrêmement simple à maintenir.

View File

@ -0,0 +1,3 @@
---
Title: Django Waffles
---

View File

@ -0,0 +1,11 @@
---
Title: Rafactoring - Improving the Design of Existing Code
Writers: Martin Fowler, Kent Beck, John Brant, Willam Opdyke, Don Roberts
Tags: refactoring, design patterns, code
---
> Code will be read more often than it will be written.
Modifier du code existant implique des risques: introduction subtile de nouveaux bugs et modifications du comportement de certaines fonctionnalités. En fonction de la base de code existante, une phase de *refactoring* peut prendre entre plusieurs jours et plusieurs semaines: au plus on creuse, au plus on trouve de choses nécessitant une modification, qui implique à nuoveau de creuser encore, ...
> The more you dig, the more stuff you turn up... and the more changes you make. Eventually, you dig yourself into a hole you can't get off. To avoid digging your own grave, refactoring must be done systematically.

View File

@ -0,0 +1,138 @@
---
Title: Jouons un peu avec Jinja2 et YAML
Summary: Toi aussi, crée ton site statique en buvant un cocktail avec une main dans le dos et les pieds sur le bureau.
Tags: yaml, jinja, ssg
---
Bon, et si je voulais développer mon propre générateur de sites statiques? Pas que ce soit nécessaire, car il y en a des dizaines de centaines de milliers (+une petite dizaine chaque jour), juste que j'en ai envie.
Les quelques idées sont les suivantes:
* Utiliser `markdown` et ses [extensions](https://pythonhosted.org/Markdown/extensions/index.html#officially-supported-extensions) officielles pour la génération de code HTML.
* Passer le tout dans [Pygments](http://pygments.org/docs/quickstart/) pour la coloration de code
* Ajouter une couche de [Typogrify](https://pypi.python.org/pypi/typogrify) pour gérer quelques cas particuliers de mises en forme.
```python
# coding: utf-8
from models import Site
from jinja2 import Environment, PackageLoader, select_autoescape
env = Environment(
loader=PackageLoader('grnx', 'templates'),
autoescape=select_autoescape(['html'])
)
if __name__ == "__main__":
root = Site('content')
root.serialize()
template = env.get_template('single.html')
for article in root.articles:
print(template.render(article.__dict__))
```
```python
# coding: utf-8
import datetime
import json
import re
import os
import yaml
RE_HEADER = re.compile('---((.)?(\n)?)*---')
date_handler = lambda obj: (
obj.isoformat()
if isinstance(obj, datetime.datetime)
or isinstance(obj, datetime.date)
else None
)
def json_handler(obj):
"""Handles the JSON object serializer.
Returns:
The iso format if the object is a date.
The __dict__ attribute for any other JSON serializable object.
Excepts:
TypeError when object is not JSON serialisable.
"""
if hasattr(obj, 'isoformat'):
return obj.isoformat()
elif obj.__dict__:
return obj.__dict__
else:
raise TypeError('Object of type %s with value of %s is not JSON serializable' % (type(obj), repr(obj)))
class Content(object):
def __init__(self, filepath):
with open(filepath, 'r') as f:
try:
file_content = f.read()
print(file_content)
regex_result = RE_HEADER.search(file_content)
if regex_result:
header = file_content[regex_result.start():regex_result.end()-3]
self.properties = yaml.load(header)
headers = self.properties
print('headers: ' + str(headers))
self.published_date = headers.pop('Date', None)
self.last_modified_date = headers.pop('LastModified', self.published_date)
self.title = headers.pop('Title', None)
self.slug = headers.pop('Slug', None)
self.path = filepath
except yaml.YAMLError as error:
print(error)
class Site(object):
"""Represents a Site object.
Args:
articles: containt all articles.
metadata: hum. I don't remember what I would store in this property.
taxonomies: the taxonomies we find for this site. Tags, categories, ...
"""
def __init__(self, current_path):
self.articles = []
self.metadata = {}
self.taxonomies = {}
for directory, dirnames, files in os.walk(current_path):
for file in files:
if file.endswith(".md"):
self.manage_article(directory, file)
def manage_article(self, directory, file):
article = Content(os.path.join(directory, file))
self.articles.append(article)
for taxonomy in article.properties:
if taxonomy not in self.taxonomies:
self.taxonomies[taxonomy] = []
# TODO: we have to get the VALUE _and_ the article slug instead of just the slug.
self.taxonomies[taxonomy].append(article.slug)
def to_json(self):
"""Serialize the content of the current structure to JSON format."""
return json.dumps(self, default=json_handler, sort_keys=True, indent=4)
def serialize(self):
"""Serialize the current files structure to index.json"""
with open('index.json', 'w') as json_serialized_file:
json_serialized_file.write(self.to_json())
```

View File

@ -0,0 +1,3 @@
Tous les six mois, relisez le cahier de charges. Et la réponse du fournisseur.
Faire la balance des plus et des moins.

View File

@ -0,0 +1,24 @@
Python ical
Pour un exemple (repris d'un [plugin pour pelican][https://github.com/getpelican/pelican-plugins/blob/master/events/events.py#L127))
``` python
for e in curr_events:
ie = icalendar.Event(
summary=e.metadata['summary'],
dtstart=e.dtstart,
dtend=e.dtend,
dtstamp=e.metadata['date'],
priority=5,
uid=e.metadata['title'] + e.metadata['summary'],
)
if 'event-location' in e.metadata:
ie.add('location', e.metadata['event-location'])
ical.add_component(ie)
with open(ics_fname, 'wb') as f:
f.write(ical.to_ical())
```
https://www.getlektor.com/docs/plugins/dev/ pour le développement des plugins lektor.

View File

@ -0,0 +1,10 @@
---
Title: Générer de la documentation Python avec Pydoc
Tags: documentation, pydoc, python
---
Je cherchais un moyen simple d'afficher de la documentation dans un module d'administration Django, simplement pour égayer un peu l'interface d'admin (et aussi pour guider un peu l'utilisateur).
Par défaut, il existe le module `pydoc` qui permet d'utiliser la fonction `help()`, qui affiche toutes la documentation pour un objet (classe, méthode, propriété, etc.). On a aussi un objet `html` qui permet de parser correctement la documentation et d'y mettre plein de couleurs guillerettes.
Les outils de documentation inclus par défaut ou en modules dans Python: https://wiki.python.org/moin/DocumentationTools

View File

@ -0,0 +1,5 @@
Queryset, managers, values et values_list
* values permet de récupérer uniquement certaines valeurs d'un queryset. Cela renvoie un dictionnaire avec des clés/valeurs.
* values_list ne retourne _que_ la valeur, mais toujours dans un queryset.
* values_list prend également un paramètre *flat=True*, qui permet *'d'applatir* le résultat (et en profiter pour se débarasser du queryset).

View File

@ -0,0 +1,15 @@
---
Title: Quel outil pour gérer la pyramide documentaire?
---
La pyramide documentaire s'étale sur cinq niveaux:
1. Le Guide (ou Manuel Qualité): Qui êtes-vous, quel est votre SMQ?
2. Documents organisationnels (Flux et procédures): Vue macro des processus transversaux. Qui fait quoi, où, quand et pourquoi ?
3. Documents opérationnels (Modes opératoires): Etape unitaire, déroulement pratique. Décrit la méthode.
4. Documents de supports (Formulaires, Informations, Documents de formations et documents externes): Template pour recueillir des informations, donne des informations complémentaires.
5. Enregistrements: Preuves et traces; chaque document est unique et non révisable.
Pour donner un ordre de grandeur, on aura **un** MQ, **10** documents organisationnels, **100** documents opérationnels, **200** documents de support et une infinité d'enregistrements.
Les quatre premiers niveaux constituent les documents dynamiques, nécessaires au maintien du système qualité, contrairement aux enregistrements qui sont des documents statiques, générés par le système qualité.
L'ensemble de ces documents constituent le système documentaire, l'ensemble de tous les documents existant dans le cadre du système qualité.

View File

@ -0,0 +1,4 @@
Title: Utiliser SharePoint comme outil de suivi de projet
Summary: Microsoft Project sans MS Project.

View File

@ -0,0 +1,2 @@
https://www.habaneroconsulting.com/stories/insights/2013/term-based-navigation-in-sharepoint-2013

View File

@ -0,0 +1,26 @@
---
Title: SQL Antipatterns
Subtitle: Avoiding the pitfalls of Database Programming
Status: draft
Tags: ebook, sql, antipatterns
Writer: Bill Karwin
---
[SQL Antipatterns](https://pragprog.com/book/bksqla/sql-antipatterns) est un livre qui reprend les principales choses à ne pas faire en SQL. Comme le souligne l'auteur dans sa préface, la plupart des programmeurs SQL ont appris sur le tas; de nombreuses erreurs peuvent ainsi être évitées grâce à ces quelques (deux-trois... voire vingt-quatre) trucs de base.
## Logical Database Design Antipatterns
### Jaywalking
Eviter le *jaywalking*; le principe de stocker plusieurs informations dans un même champ. En stockant par exemple toutes les relations d'un enregistrement dans un même champ (séparé par un caractère déterminé à l'avance), on va droit dans le mur: autant en termes de performances que de recherche, d'insertion et de suppression. Bref, évitez.
### Structures hiérarchiques
### Identifiant requis!
La présence d'une clé primaire est obligatoire, mais ne doit pas spécialement s'appeler `id`: il y a même un avantage à appeler cette clé de manière plus sémantique (`bug_id`, `entity_id`, ...), c'est la possibilité d'utiliser le mot-clé `USING(<key>)` pour que la requête fasse automatiquement le lien entre deux tables sur base de cette colonne. Plutôt que `Select * From A Inner Join B on A.Id = B.bidule_id`, on pourrait directement faire `Select * From A Inner Join B Using (bidule_id)`.
Ensuite, cette clé n'est pas nécessairement un entier: on peut tout à fait utiliser un champ texte, un bigint, ... voire une clé composée. Tout ce qu'on veut, pour peu qu'un index puisse être appliqué. De même, inutile de définir un identifiant entier en clé primaire et un index d'unicité sur un autre champ: on peut directement définir la clé primaire sur ce champ.

View File

@ -0,0 +1,3 @@
https://blogs.msdn.microsoft.com/ecm/2010/06/22/introducing-enterprise-metadata-management/
https://technet.microsoft.com/en-us/library/jj679902(v=office.15).aspx

View File

@ -0,0 +1,43 @@
---
Title: Valider des données sur un modèle Django
Tags: django, python, model
---
Quand on utilise l'ORM de Django, il existe trois niveaux de validation:
1. Les validateurs associés directement au niveau du champ du modèle
2. Le *nettoyage* des champs
3. Le *nettoyage* global du modèle.
## Validateurs
La première manière de valider le contenu d'un champ est aussi la plus simple. En prenant un modèle type:
```python
from django.db import models
def validate_title(value):
if 't' not in value:
raise ValidationError('Title does not start with T')
class Bidule(models.Model):
title = models.CharField(max_length=255, validators=[validate_title])
```
## Clean_<field_name>
Ca, c'est le deuxième niveau. Le contexte donne accès à déjà plus d'informations et permet de valider les informations en interogeant (par exemple) la base de données.
```python
class Bidule(models.Model):
def clean_title(self):
raise ValidationError('Title does not start with T')
```
## Clean
```python
class Bidule(models.Model):
def clean(self):
raise ValidationError('Title does not start with T')
```

View File

@ -0,0 +1,7 @@
---
Title: Two Scoops of Django 1.11
Tags: django, python, dev
---
[Two Scoops of Django][https://www.pydanny.com/two-scoops-of-django-1-11.html) est l'un des livres les plus recommandables pour pratiquer le framework [Django][https://docs.djangoproject.com/fr/]. Je dis bien "pratiquer", parce que le livre n'est pas à mettre entre toutes les mains (débutants, s'abstenir); il est plus que conseillé d'avoir au moins lu le tutorial de base et avoir mis en place un projet, de l'initialisation au déploiement.

View File

@ -0,0 +1,10 @@
---
Title: Crème au chocolat au Tofu
Date: 2017-08-28
Tags: chocolat, miam
---
Vous aurez besoin de:
* 400g de tofu soyeux
* 200g de chocolat.

42
articles/featured.md Normal file
View File

@ -0,0 +1,42 @@
---
Title: Articles favoris
---
Sécurité
--------
* `Configuration de Fail2Ban <{filename}articles/server/2016-06-13-fail2ban.rst}>`_
Services
--------
* `Configuration d'Nginx et installation de MySQL <{filename}articles/conf/2015-04-03 nginx.md>`_ ou par `ici <{filename}articles/conf/2015-01-07-install-nginx.md>`_
* `Configuration du proxy local <{filename}articles/conf/2015-06-17-debian-enterprise.md>`_
* `Lynis <{filename}articles/conf/system/2016-06-01-lynis.rst>`_
* `Envoi de mails <http://sorrodje.alter-it.org/index.php?article4/envoi-de-mail-via-php-et-esmtp-ssmtp>`_ & https://wiki.gandi.net/fr/mail/standard-settings
Certificats
-----------
* `Certbot <{filename}articles/server/2016-06-13-certbot.rst>`_
Owncloud
--------
* `Installation <https://www.linuxbabe.com/linux-server/setup-owncloud-9-server-nginx-mariadb-php7-debian>`_
Todo
----
* `rkhunter <{filename}../articles/server/rkhunter.rst>`_
Réseau
------
* `CNTLM <{filename}articles/conf/network/2013-04-05 cntlm.md>`_: configurer un proxy local authentifié par rapport au proxy du domaine, et comment configurer quelques applications (Git, npm, shell) pour l'utiliser.
Virtualisation
--------------
* Docker : à compléter, un jour.
* `VMWare Converter <{filename}articles/conf/virt/converter.rst>`_, pour convertir facilement (?) une machine physique en virtuel.

View File

@ -0,0 +1,17 @@
---
Title: Sansa Clip Zip
Date: 2012-12-26
Slug: sansa-clip-zip
---
J'ai longuement hésité entre ce modèle, recommandé par certains sites, et d'autres lecteurs plus chers, plus grands, plus tactiles, mieux finis... pour finalement craquer sur celui-ci: il ne fait qu'une et une seule chose, mais il le fait bien.
Aucun problème à l'utilisation, les touches physiques sont accessibles malgré la petitesse de l'appareil et ont du répondant. L'interface est simpliste, mais suffisamment ergonomique. Les transferts de fichiers sont idéaux (pas besoin de passer par un logiciel particulier (iTunes, ...)): on branche et cela fonctionne. La lecture des formats autres que le MP3 est également un gros plus: Flac et Ogg Vorbis nativement. Un bonheur :).
L'écran est petit, présente une très faible résolution, affiche peu d'informations, mais affiche l'essentiel (et même plus: cover si trouvée par exemple :)). Les principales fonctions sont là également: play/pause, mise en veille, suivante, précédente, ... et tombent plus ou moins sous le sens. Pas besoin de chercher de midi à quatorze heure pour faire une action. Efficace.
La qualité de fabrication est un cran en dessous de ce à quoi je m'attendais, mais n'est pas catastrophique non plus: c'est du tout plastique, mais condensé :) Et vu le peu de fonctionnalités présentes (écouter de la musique.- ), l'appareil ne risque pas d'être trituré toutes les deux minutes.
Un dernier point extrêmement intéressant est la possibilité d'ajouter une carte micro SD dans l'appareil afin d'améliorer sa capacité de stockage, ce qui est loin d'être négligeable.
Je voulais un lecteur audio pratique à emporter avec moi, et c'est chose faite!

View File

@ -0,0 +1,17 @@
---
Title: Getting Things Done!
Date: 2014-02-09
Tags: gtd, life
Slug: getting-things-done
Illustration: kelly-sikkema-post-it.jpg
---
Si vous en avez marre que votre boîte mail déborde de tâches à faire et que la méthode du drapeau rouge d'Outlook ne suffit plus, peut-être que vous avez simplement besoin de continuer à faire les choses, mais différemnt :-) Une manière de faire:
* Essayez de vider votre boîte mail. Un message non trié est un message à faire. Si votre boîte est vide, félicitations! Votre journée peut continuer.
* Ne lancez pas systématiquement Outlook en arrivant au boulot le matin. Les actus sont parfois plus intéressantes qu'un message d'insultes de votre chef adoré, vous demandant où en est votre travail.
* Essayez de faire fréquemment un point sur votre état d'avancement. Regardez derrière vous, faites le point sur les choses déjà faites, plutôt que de vous lamenter sur ce qui n'a pas encore été fait.
Voir aussi sur [Lifehacker](http://lifehacker.com/develop-discipline-by-seeing-the-good-in-any-task-1629748447). Les commentaires valent un coup d'oeil.
Photo by [Kelly Sikkema](https://unsplash.com/photos/-1_RZL8BGBM?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on Unsplash

View File

@ -0,0 +1,19 @@
---
Title: 28 aout 2015
Date: 2015-08-28
Status: draft
Tags: life
---
Coucou mon Petilou,
Hier, tu as eu 19 mois. Tu grandis de jour en jour, et je vois tous les efforts que tu fais pour progresser. Les trois dernieres nuits ont ete particulirement difficiles: tu te réveillais sans cesse, demandait a recevoir des calins, pour te rendormir aussi vite.
Ce soir, en venant te chercher à la crêche, tu t'es directement blotti contre mon epaule avant de dire au revoir a tout le monde. Tu as recu des bisous de tous les enfants encore présents, + un gros de Dominique. Nicole était encore occupée a discuter avec deux futurs parents, mais tu lui as quand meme fait un signe de la main.
Dans la poussette, tu as garde une main tendue vers moi. Tu étais fatigué, mais tu ne voulais pas de tute. Les grands ne prennent pas de tute quand ils sont fatigués. Arrivés a la maison, tu as filé vers la cuisine. Un petit rituel entre toi et moi: tu recois toujours un petit quelque chose quand tu le demandes. Il n'y avait plus de pain, tu ne voulais pas de galettes de riz, ... On a fini le raisin, toi et moi. Toi en le grignotant du bout des dents, moi en le lancant en l'air pour le rattraper directement dans ma bouche. Cela te faisait rire :)
Le bain a duré 10 minutes. L'eau n'était pas très chaude, tu n'as pas voulu rester plus longtemps. Tu as reçu ton biberon directement après, mais tu n'en as bu que la moitié. Tu etais fatigué, j'ai proposé de te mettre au lit. On a lu une histoire juste avant, puis on s'est dirigé vers ta chambre. Une fois au lit, tu as recommencé à geindre. J'ai élevé la voix, tu t'es alors simplement allongé sur le ventre et tu t'es endormi.
J'ai du hausser la voix, et je le regrette. Je m'en veux de ne pas réussir à être toujours calme. Ca fait partie du rôle de père, de ton père, mais je ne veux pas de ce role: je veux encore manger du raisin en le lançant en l'air, je veux qu'on rigole tous les deux en regardant les chiens en rue ou en lisant des histoires. Je ne veux pas de confrontations comme celles-ci. J'y perds un peu a chaque fois que je gagne.
Papa.

View File

@ -0,0 +1,11 @@
---
Title: Résistance thermique
Date: 2015-09-20
Slug: thermal-resistance
---
* R = Résistance thermique (en mètre² Kelvin par Watt m²K/W)
* Lambda = conductivité thermique (en Watts par mètre Kelvin)
* e = épaisseur (en mètre)
Et on a la formule `R = e / Lambda`. Plus qu'à compléter le papier pour les primes :)

View File

@ -0,0 +1,52 @@
---
Title: Passage chez EDPNet
Tags: internet, fai
Illustration: nicolas-picard-web.jpg
---
On est le 14 août, j'ai explosé le quota de 150GB qui m'a été grâcement alloué par Proximus pour ma ligne *Internet Comfort*. 150GB de choses légitimes: téléchargements *via* [Good Old Games](https://www.gog.com), réception de données sur le NAS et transferts vers OneDrive. Oui, cela fait beaucoup, mais à côté de cela, Proximus propose des quota allant jusqu'à 750GB pour une ligne plus ou moins identique, mais beaucoup plus chère (à ce jour, je paye 36€/mois avec un EPP - l'offre au-dessus est à 48€, sur laquelle je pourrais obtenir une réduction de 10%, mais cela me reviendrait malgré tout à 10€ de plus/mois - donc 120€ par an -).
J'ai toujours été très satisfait de la qualité de la ligne Proximus; moins des contraintes techniques. La Bbox3 (que j'ai fini par acheter moi-même, puisqu'impossible d'en obtenir une sur simple échange de la Bbox2) ne supporte par exemple pas le [Hairpinning](#), ce qui empêche l'auto-hébergement. En leur demandant conseil sur l'ouverture des ports, personne n'a été en mesure de m'informer par rapport aux limitations de ma ligne. Ensuite ce principe de quota ne me convient pas: une fois dépassé, j'ai la possibilité de commander un *pack* de 20GB pour 5€. Bref, si je veux me débarasser de cette contrainte, je dois passer sur le pack au-dessus.
## 18/08/2017 - la commande.
Début juillet, une nouvelle procédure *Easy Switch* a pointé le bout de son nez. Elle permet, sur base des identifiants de l'opérateur actuel (numéro de ligne, Easy Switch Id et numéro de client) d'autoriser le nouvel opérateur à s'occuper de toutes les démarches administratives de clôture et d'avoir une continuité dans la distribution et l'accès aux services. Les données de facturation sont directement déduites des informations communiquées par la ligne.
> (Attention: le mot de passe initial est transmis par email - à modifier directement)
Total à payer: 50€ de frais d'activation et 35€/mois. Je conserve ma BBOX3 (puisque Proximus n'a jamais voulu changer la BBOX2 contre le "nouveau" modèle - je devrai sans doute aller la rapporter dans une point). Il est aussi possible de commander du matériel AVM (Fr!tzBox 7390 ou 7490) à un prix avantageux (respectivement 99€ et 179€ - pour info, la 7490 est dispo chez [Coolblue](https://www.coolblue.be) pour 209€.
## 18/08/2017 - la commande (suite)
Deux mails reçus dans la journée:
1. Un mandat de migration simple (mais sans directive de savoir quoi en faire, s'il faut le signer ou quoi :))
2. Mes identifiants pour EDPNet Telephony (à nouveau, avec les mots de passe en clair!)
## 21/08/2017 - la confirmation
A nouveau deux petits mails reçus ce jour:
1. Un message de confirmation m'indiquant que ma commande sera activée le 24/08
2. Un deuxième message me communiquant mes identifiants/mots de passe à indiquer dans l'interface du routeur.
## 22/08/2017 - No transferable phone number
- **14h32** : Un ticket est ouvert pour me signaler que mon ancien numéro Proximus ne pourra pas être transféré. Dans le ticket, l'opérateur/trice me demande si je suis d'accord pour recevoir un nouveau numéro de ligne.
- **15h46**: "Oui, pas de soucis, allez-y"
- **16h42**: "Voilà, vous aurez un nouveau numéro".
## 24/08/2017 - Comme promis
En rentrant chez moi, un courrier de Proximis m'attendant pour m'informer que ma ligne avait été coupée. Je conserve un accès de 18 mois à mes emails (que je n'utilise quand même pas) ainsi qu'à quelques autres fonctionnalités. Une facture a été émise par EDPNet, et il m'a suffit de modifier les informations d'identification du routeur pour que la connexion soit établie chez EDPNet.
## Autres notes
* Pour les informations concernant le retour de matériel (BBOX), c'est [par ici](https://www.proximus.be/support/fr/id_sfaqr_device_return_end/particuliers/support/espace-client/demenager-ou-resilier/resilier-votre-contrat/remplacer-ou-renvoyer-un-appareil.html#/modem2).
* En regardant ma dernière facture, je remarque que dorénavant, les promotions d'affiliations ne concerneront que les packs trio (Internet+TV+Téléphone) à partir d'octobre. Encore 4€ que Proximus nous sucre sans réelle raison?
## Epilogue (?) - 05/12/2017
Il me reste ma BBox2, reçue au moment où mon abonnement Proximus avait été commandé. Leur page stipule que *si vous ne recevez pas de courriel ou de courrier vous indiquant la procédure à suivre pour renvoyer le matériel, contactez-nous"*. Ce que j'ai fait: j'ai passé une petite dizaine de minutes avec une personne du support, pour expliquer qu'il me restait du matériel Proximus, que mon contrat avait été résilié. La réponse a été de *contacter mon nouveau fournisseur, pour vérifier que toute la procédure avait bien été suivie"*. Bref, j'ai autre chose à faire que leur courir après pour leur restituer du matériel qu'ils ne semblent pas pressés de récupérer.
Photo by [Nicolas Picard](https://unsplash.com/photos/-lp8sTmF9HA?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on Unsplash

View File

@ -0,0 +1,15 @@
---
Title: Fractal Design Node 804
Summary: Un boitier µ-ATX sympa, mais pas pour tout le monde
Date: 2017-11-21
Illustration: thoughts/fractal-node-804.jpg
---
Mon cahier de charge était relativement simple: on a hérité d'un vieux bureau massif de la famille, tout en moulures, courbes et enluminures. Le problème de cette salo$%ù@ de meuble est que la "hauteur sous tiroir" ne dépasse pas les 54cm et ne donne clairement pas un accès facile aux boutons de la tour, pour peu qu'ils se trouvent sur le dessus. Bref, c'était mal barré pour ma fidèle [Lian-Li PC7B](#) - pas de boutons en façade, dimensions juste trop grandes que pour s'oublier sous le bureau, ...
Je pars en quête d'un nouveau boitier! Le cahier de charge est simple (ou pas: on va le voir juste après):
* Pas plus de 50cm
* Possibilité d'accueillir plusieurs disques (deux disques durs + deux SSD)
* Facilité d'évolution
* Que mon matériel actuel rentre dedans sans forcer et sans sortir le pied-de-biche (Noctua 120 et GTX 960)

6
articles/home/cubetto.md Normal file
View File

@ -0,0 +1,6 @@
---
Title: Cubetto, le jouet pour apprendre à programmer (3 ans et +)
Subtitle: La programmation sans bugs (sauf si présence d'escaliers dans les environs)
---
Le [Cubetto]() est un petit robot cubique tout mignon de quelques centimètres de haut, muni de deux grandes roues et commandé par une tablette en bois sur laquelle l'enfant (ou l'adulte) place un ensemble **d'instructions** représentées par des blocs de couleurs.

View File

@ -0,0 +1,26 @@
---
Title: Play Store
Date: 2015-09-02
Slug: play-Store
---
En trainant un peu sur le Play Store, j'ai un peu zieuté le top des applications que Google propose aux utilisateurs. Parmi celles-ci, on trouve :
* Facebook Messenger
* Facebook
* Whatsapp
* Skype
* Snapchat
Ok. Avec un petit milliard d'utilisateurs pour Facebook (et autant de périphériques Android en circulation?), je suppose qu'il est normal de trouver des applications de discussions (et de réseaux sociaux) dans les top charts. La suite est plus intéressante, avec notamment :
* Candy Crush Saga
* Candy Crush Soda
* Instagram
* Fruit Ninja
* Clash of clans
Bref, pas hyper convaincu par ces quelques propositions... Pour pousser un peu plus loin, je passe désormais par [F-Droid](https://f-droid.org/). Son catalogue ne contient que des applications open source, il s'occupe de faire les mises à jour lorsqu'elles sont disponibles, ...
Un peu comme le Play Store en fait, mais en libre.
Suivant les usages, tout ne sera sans doute pas disponible sur F-Droid, mais ce serait déjà un bon début. A l'occasion, pensez-y pour un premier pas dans la [dégooglification](http://degooglisons-internet.org/).

View File

@ -0,0 +1,34 @@
---
Title: F-Droid
Date: 2016-05-25
Slug: f-droid
Tags: applications, android
---
J'ai récemment (= hier) réinitialisé mon téléphone, suite à un problème de place et de configuration. Après le redémarrage, le constructeur (Samsung) a cru bon d'y ajouter un certain nombre d'applications complètement inutiles: Zalando, un brol pour commander des pizze, des applications propriétaires dans tous les sens, ...
Tant que j'y étais, j'en ai profité pour essayer de passer au maximum par [F-Droid](https://f-droid.org/) (dans un but avoué de [dégooglisation](https://degooglisons-internet.org/)). A première vue, je ne m'en sors pas *trop* mal :) Il reste quelques morceaux non libres, mais c'est déjà un bon début.
Depuis le [dépôt F-Droid](https://f-droid.org/repository/browse/), j'ai sélectionné les applications suivantes:
* [Calendar Widget](https://f-droid.org/repository/browse/?fdfilter=calendar+widget&fdid=com.plusonelabs.calendar), pour la liste de mes prochains rendez-vous sur l'écran d'accueil. Il est personnalisable, fonctionnel et plutôt joli. Deux petits boutons permettent d'ajouter rapidement un nouvel élément ou de rafraîchir les calendriers.
* [Diaspora](https://f-droid.org/repository/browse/?fdfilter=diaspora&fdid=com.github.dfa.diaspora_android). Cette version, issue d'un fork, est normalement la dernière en vigueur et la plus complète.
* [DuckDuckGo](https://f-droid.org/repository/browse/?fdfilter=duckduck&fdid=com.duckduckgo.mobile.android), pour avoir accès au widget de recherche et aux *Stories*.
* F-Droid (obviously...)
* [FBReader](https://f-droid.org/repository/browse/?fdfilter=fbreader&fdid=org.geometerplus.zlibrary.ui.android)
* [Firefox](https://f-droid.org/repository/browse/?fdfilter=firefox&fdid=org.mozilla.firefox)
* [K-9 Mail](https://f-droid.org/repository/browse/?fdfilter=k9&fdid=com.fsck.k9): attention à bien dégager la **signature automatique** (je n'ai jamais compris ce principe...)
* [ownCloud](https://f-droid.org/repository/browse/?fdfilter=owncloud&fdid=com.owncloud.android
* [Shaarlier](https://f-droid.org/repository/browse/?fdfilter=shaarlier&fdid=com.dimtion.shaarlier)
* [Tinfoil](https://f-droid.org/repository/browse/?fdfilter=tinfoil&fdid=com.mill_e.twitterwrapper) (pas encore convaincu) - pour accéder à Twitter de manière *presque* anonyme (ou en tout cas, un peu plus masquée).
* [Torch](https://f-droid.org/repository/browse/?fdfilter=torch&fdid=com.doomy.torch), pour y voir plus clair dans le noir
* [VLC](https://f-droid.org/repository/browse/?fdfilter=vlc&fdid=org.videolan.vlc) pour tous les fichiers media.
* [Writeily Pro](https://f-droid.org/repository/browse/?fdfilter=writeily&fdid=me.writeily) pour l'édition de fichiers Markdown.
Il reste alors les applications suivantes qui dépendent du Play Store:
* [Signal Private Messenger](https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms)
* [Bruxelles Transports](https://play.google.com/store/apps/details?id=be.digitalia.stib&hl=fr) pour ne pas me perdre...
* [Yatse](https://play.google.com/store/apps/details?id=org.leetzone.android.yatsewidgetfree&hl=fr) pour contrôler le media center (sur Openelec).
* [Google Maps](https://play.google.com/store/apps/details?id=com.google.android.apps.maps&hl=fr)
* Et les applis liées au Synology ([DS File](https://play.google.com/store/apps/details?id=com.synology.DSfile&hl=fr) et [DS Finder](https://play.google.com/store/apps/details?id=com.synology.DSfinder&hl=fr), principalement)

View File

@ -0,0 +1,70 @@
---
title: La règle de trois
summary: Ou comment se protéger de ces saletés de ransomwares de m*rde
date: 2017-06-28
tags: securité, backup, synology
category: Sécurité
show-toc: true
---
## TL; DR
Chaque fichier indispensable doit se trouve *a minima* sur trois solutions de sauvegarde: deux en local, une à distance.
Pour les solutions en local:
* Choisissez une solution autorisant un RAID 1. Ce n'est pas une solution de sauvegarde, juste une prévention au cas où un disque meurt.
* Investissez dans un disque externe
Pour les solutions à distance:
* Trouvez une personne en qui vous pouvez avoir confiance et qui pourra héberger vos données.
## En détails
Petite histoire du vendredi soir: Papa m'appelle pour taper la discute et m'annonce au milieu de la conversation qu'il y a un truc bizarre à l'écran: *Vos fichiers ont été chiffrés, payez-nous pour récupérer la clé*. Berf. *Bouge pas, j'arrive*.
Le gros stress. Le truc que je garde dans un coin de mon esprit, pour essayer de trouver une solution complète qui couvre tous les cas de foirage. Et j'étais sûr d'avoir oublié quelque chose, mais je ne savais pas quoi. Est-ce que j'ai bien configuré les sauvegardes? Est-ce qu'elles sont complètes? Est-ce qu'elles sont toujours accessibles ?
Deux-trois considérations à prendre en compte:
1. Cette saloperie ne fait pas **que** chiffrer les fichiers: elle détruit également les sauvegardes et l'historique des fichiers Windows, pour peu qu'ils soient accessibles par l'utilisateur (et elles l'étaient). Concrètement il suffit qu'un des fichiers de conf' soit chiffré pour que Windows n'arrive plus à restaurer quoi que ce soit.
2. Les dernières versions des *ransomwares* sont encore plus fourbes: elles chiffrent les fichiers **et** la MBR (et forcent un *reboot* de la machine; seule solution: avoir des sauvegardes à l'extérieur).
Pour ma part, j'ai installé un #Synology. L'avantage de ce genre de brol, est que cela fonctionne presque *out-of-the-box*, avec un installeur en mode clicodrôme (*next-next-finish*). Le désavavantage, c'est qu'il faut aller un cran plus loin pour avoir une protection réelle. Explications.
### Cloud Station Backup / Cloud Station Sync
Un des premiers trucs à activer sur les postes clients: [Cloud Station Backup](https://www.synology.com/fr-fr/knowledgebase/DSM/help/CloudStationBackup/cloudstationbackup). Après création des répertoires utilisateurs sur le NAS, cette application permet de sélectionner certaines répertoires et fichiers, pour qu'ils soient automatiquement sauvegardés vers le répertoire `/volumeX/homes/<user>/Backup`.
Le problème, c'est que la synchronisation intervient en *presque-temps-réel*: il suffit de réaliser un chouia trop tard que la machine a été infectée pour que la synchronisation ait déjà eut lieu. Ce cas se présente également si on utilise [Cloud Station Drive][https://www.synology.com/fr-fr/dsm/6.1/cloud_file_syncing], qui est *grosso-modo* un ersatz de Dropbox, OneDrive et consorts. Par rapport à ces solutions privées, il n'y a en effet aucun historique de versions, à moins que...
### Synology Hyper Backup
... à moins d'avoir activer une règle de sauvegarde *via* [Hyper Backup](https://www.synology.com/fr-fr/knowledgebase/DSM/help/HyperBackup/data_backup). L'idée ici est d'utiliser une solution propriétaire (beurk) qui s'occupera de créer un *snapshot* à partir d'autres informations présentes sur le disque.
Techniquement, ces snapshots sont supposés se trouver sur un autre volume que les données. En pratique, ce n'est pas toujours faisable.
L'avantage d'Hyper Backup est de proposer une solution de roulement intelligent. On a en fait deux possibilités:
1. Soit un backup est réalisé à intervalle régulier (tous les lundis, quotidiennement, à 19h, etc.), et on ne conserve que les X dernières versions.
2. Soit les sauvegardes sont conservées sur [...]
### Sauvegarde à distance
Pour la sauvegarde à distance, on a à nouveau plusieurs possibilités. La plus simple étant de partir d'office des données présentes sur le NAS, et de le laisser faire ses transferts en dehors des heures de bureau. Il n'est en effet pas nécessaire de plomber votre connexion Internet à un moment où vous pourriez en avoir besoin. Faites ça lui nuit :)
## Outils
En résumé, on a besoin des outils suivants:
1. Une sauvegarde en local, par le système d'exploitation: Windows Restore, File History, whatever.
2. Une sauvegarde à distance vers un serveur accessible, si possible au travers d'un réseau rapide (Ethernet Cat5 minimum - visez le Gigabit)
3. Un système de réplicat / snapshots
4. Un envoi à intervalle régulier de ces snapshots.
Au niveau de la sécurité, pensez à vérifier les points suivants:
1. Les utilisateurs locaux n'ont **pas** besoin d'un accès à toute la machine. Essayez de les identifier en tant qu'utilisateurs normaux.
2. Que les processus de sauvegarde soient exécutés par des utilisateurs isolés.
Petit schéma qui poutre:

View File

@ -0,0 +1,23 @@
---
Title: Authentification en deux étapes
Tags: 2fa, authentication
Illustration: security.jpg
---
Coucou,
Je viens encore vous casser les pieds avec mes rhétoriques de sécurité.
Le mécanisme de 2-factors-authentication (2FA ou Authentification multi-facteurs) permet de s'assurer que la personne qui se connecte est bien celle qu'elle dit être; elle prouve cet état en montrant en entrant un message secret qu'elle aura reçu sur son adresse email ou par SMS.
Si un mot de passe est compromis, le pirate devra malgré tout montrer patte blanche et sera bloqué car il ne connaitra pas le message secret.
Bref, à activer obligatoirement. Les "grandes" applications le permettent (GMail, Facebook, Dropbox, etc.). C'est chiant la première fois, puisqu'on doit sortir son téléphone et entrer le code, mais celui-ci est généralement enregistré pour toute utilisation future.
Pour GMail (et Google en général), c'est par ici: [https://myaccount.google.com/security](https://myaccount.google.com/security)
Pour Facebook, cela doit se trouver dans les paramètres de confidentialité. Idem pour Dropbox.
D'un côté, vous communiquerez une information sensible (votre numéro de téléphone, généralement) à la firme, d'un autre, vous lui donner déjà tellement que cela ne changera pas grand chose :)
Si questions, envoyez ;)
des bisous.

View File

@ -0,0 +1 @@
Pour dégager l'avertissement comme quoi le mot de passe sera envoyé en clair, il faut cocher la clé `security.insecure_field_warning.contextual.enabled` dans la page `about:config`.

View File

@ -0,0 +1,8 @@
---
Title: Chiffrer ses messages électroniques avec GPG et Thunderbird
Tags: thunderbird, gpg, pgp
---
L'intérêt de cet article est d'apprendre à chiffrer ses messages électroniques en utilisant des outils libres.
https://lehollandaisvolant.net/tuto/gpg/

View File

@ -0,0 +1,20 @@
---
Title: Compte hacké!
Tags: securité, 2fa, vps
---
Il y a quelques temps, j'ai reçu un mail de [DigitalOcean](https://www.digitalocean.com/) m'indiquant que ma carte de crédit était arrivée à expiration. On était en mai; ma carte était arrivée à expiration en février; je le savais et je ne m'en inquiétais pas, puisque je n'avais pas besoin de leurs services dans l'immédiat.
En fouillant un peu parmi les informations de mon compte, je remarque malgré tout qu'il y a un truc louche sur ma dernière facture. En fait, je leur devais 176$ pour avoir réserver plusieurs (gros) serveurs. Serveurs que je n'avais jamais commandé, puisque je me suis toujours limité au strict minimum pour faire tourner deux-trois services, sans plus.
En plus de la sécurité de vos serveurs, il y a donc une donnée supplémentaire à prendre en considération: la sécurité de vos comptes utilisateurs.
Le problème ici était simplement que le mot de passe que j'avais utilisé avait déjà été associé à un autre compte, sur une plateforme qui s'était vue piratée (DropBox?).
De plus, la politique de DigitalOcean est assez claire (quand on prend le temps de lire les caractères du contrat): vous pouvez approvisionner votre compte avec l'un des moyens de paiement acceptés, et si ce moyen de paiement est refusé, vous pouvez malgré tout continuer à utiliser leurs services. Le reste devra leur être remboursé à la fin du mois.
J'ai juste eu super chaud: si mon compte n'avait pas été piraté à la fin du mois, les dégâts financiers auraient pu être beaucoup plus importants. Les 176$ s'étalaient sur une période de trois jours maximum. Je me suis arrangé avec eux et ils ont passé l'éponge, en plus de me donner quelques conseils.
Donc:
1. N'utilisez **pas** le même mot de passe sur deux plateformes différentes. Si la première se fait pirater, les vils plaisantains pourront essayer les données qu'ils auront grapiller sur tous les canaux connus. Et il suffit d'avoir la même association (identifiant + mot de passe) pour être franchement dans la mouise.
2. Quand c'est possible, activez le [2-forms authentication]({filename}/Sécurité/2014-12-29-2-steps-verification.md): en plus de connaître vos identifiants, ce mécanisme suppose également que vous avez un accès physique sur un périphérique que vous avez identifié comme étant le vôtre.
3. https://haveibeenpwned.com/

View File

@ -0,0 +1,10 @@
Nothing to Hide est un documentaire sur l'intimité/la vie privée disponible librement sur différentes plateformes de streaming.
Le pitch est relativement simple: en plus des entrevues exposant les ... de plusieurs grands acteurs du monde libre, on a un p'tit gars qui "n'a rien à cacher".
Deux personnes lui proposent alors de récupérer son téléphone portable et son laptop et d'analyser les métadonnées (donc juste "les données qui caractérisent d'autres données" - aucune analyse de contenu n'est faite) pendant une période deux semaines.
Sur ces deux semaines, ils arrivent à sortir les habitudes de vie de la personne, sa localisation, son taux de réponse à des appels, ... De là, ils peuvent en déduire énormément de choses privées, voire des choses que la personne souhaiterait conserver pour elle, dans son intimité.
Il y a plusieurs conclusions à ce documentaire:
1. Il est déjà possible de sortir énormément de conclusions en utilisant uniquement les métadonnées qu'une persone pourrait mettre à disposition, consciemment ou non. Imaginez ce qu'un acteur du Web peut en faire, en disposant d'une plus grande puissance de calcul et en disposant de toutes les informations que vous mettez à sa disposition (comptes et cartes bancaires, contenu des emails, carnets d'adresse, agenda, ...)
2. Que personne n'a "rien à cacher": on commet tous des infractions, des micro-délits, des choses qu'on voudrait garder pour nous.

View File

@ -0,0 +1,36 @@
---
Title: Copie conforme de disques durs
Date: 2013-11-30
Slug: copie-conforme-de-disques-durs-dd
---
Lorsque l'on remplace un disque dur, il est généralement conseillé de réinstaller le système. C'est plus propre, cela permet de refaire les partitions, de nettoyer un peu les dossiers, ... Mais ce n'est pas toujours faisable non plus. Pour passer d'un disque à un autre tout en gardant la même installation de l'OS, le plus efficace est de passer par la commande `dd`.
Pour faire simple:
* Téléchargez un LiveCD / USB Linux
* Identifiez bien chacun des disques (une erreur dans la commande pouvant simplement effacer le contenu)
* Démarrez la machine sur l'environnement Live.
* Identifiez chacun des disques dans le système (/dev/sda, /dev/sdb, ...)
* Lancez un terminal !
A partir d'ici, lancez la commande suivante :
```bash
dd if=/dev/sdX of=/dev/sdY bs=4096 conv=notrunc,noerror
```
En gros:
* `if` identifie la source
* `of` identifie la destination
* `bs` permet de donner la taille des blocs à copier. Il semble que des blocs de 4K soient le meilleur compromis sur les disques actuels.
* `conv` ajoute les informations suivantes:
* `notrunc` pour ne pas tronquer les données
* `noerror` ne s'arrête sur aucune erreur.
Cette manière de faire copiera tout. Les données, les partitions, la MBR, ...
**Sources**: [ici](http://askubuntu.com/questions/139643/update-mbr-of-new-drive-after-cloning), [](http://doc.ubuntu-fr.org/dd) et [](http://en.wikipedia.org/wiki/Dd_%28Unix%29).
Pour info, le temps de copie entre un Seagate 7200.12 500Go et un WD Blue 1To aura pris environ 3h40, à raison de 102MB/s. Après cette étape, le remplacement du disque actuel par le nouveau a été un jeu d'enfant.

View File

@ -0,0 +1,48 @@
---
Title: Création d'une LiveUSB
Date: 2014-05-05
Slug: creation-d-une-cle-live-usb
---
# Vérification du checksum
Lorsque vous téléchargez un fichier depuis un emplacement, il se peut que certains éléments des fichiers diffèrent entre la source et la destination. Pour vérifier que les deux fichiers sont identiques, on peut utiliser des [sommes de contrôle](https://fr.wikipedia.org/wiki/Somme_de_contr%C3%B4le), afin de calculer une empreinte. Cette empreinte est représentée par des caractères alphanumériques, et est unique (ou en tout cas essaie de l'être: certains algorithmes présentent une possibilité de [collisions](http://fr.wikipedia.org/wiki/Collision_%28informatique%29) - notament [MD5](http://fr.wikipedia.org/wiki/MD5)).
Exemple:
```
$ sha256sum wheezy-raspbian.img
7a6cf5d1d96fcd6df0ff91e4f11c40a72a5ef3734ae8ea528e012e1c774273df wheezy-raspbian.img
```
Le fichier analysé est `wheezy-raspbian.img` et son empreinte en utilisant `sha256` est la valeur `7a6cf5d1d96fcd6df0ff91e4f11c40a72a5ef3734ae8ea528e012e1c774273df`.
Il existe plusieurs algos permettant cette vérification. Vérifiez lequel lancer avant d'effectuer la comparaison.
# Copie des informations sur un support amovible
```
# lsblk
# dd bs=4M if=/path/to/archlinux.iso of=/dev/sdx && sync
```
`lsblk` liste les périphériques disponibles, ainsi que les partitions déjà créées. Avec un disque et une clé USB branchée, vous devriez avoir quelque chose comme ceci :
```
[fred@aerys ~]$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 119.2G 0 disk
├─sda1 8:1 0 512M 0 part /boot
├─sda2 8:2 0 2G 0 part
├─sda3 8:3 0 30G 0 part /
└─sda4 8:4 0 86.8G 0 part /home
sdb 8:16 0 465.8G 0 disk
└─sdb1 8:17 0 465.8G 0 part /run/media/fred/Chromatic
```
`/dev/sda` représente mon disque principale, tandis que `/dev/sdb` représente le second, branché en USB. Avec une carte SD, regardez du côté de `/dev/mmcblkX`.
Attention à bien spécifier le périphérique et non la partition. Après cela, faites en sorte de démarrer sur le support amovible. Si cela ne fonctionne pas, et que le BIOS/UEFI vous sort une injure genre "pas de système d'exploitation trouvé", essayez un autre périphérique.
# Sources
* [arch wiki live usb guide](https://wiki.archlinux.org/index.php/USB_flash_installation_media#In_GNU.2FLinux)

View File

@ -0,0 +1,21 @@
---
Title: Retrospective
Date: 2014-08-18
Tags: linux, gnu, arch, fedora, debian
Slug: linux-retrospective
---
J'ai découvert Linux un peu par hasard, chez le marchand de jounaux, en achetant un Linux Magazine qui contenait une Mandrake 7.1 ou 7.2. A l'époque, pas de Live Cd, de Wubi ou de machines virtuelles: la découverte et l'installation ne faisaient qu'un: obligés d'installer l'OS pour voir de quoi il retournait. Heureusement que le dual boot était déjà d'actualité (et facile à mettre en place), sans quoi la machine familiale aurait sans doute eu droit à sa réinstallation mensuelle prématurée de Windows 98...
Après avoir scandé partout que "Linux c'est trop bien! Au fait, comment on installe un .exe?", la chute a été relativement dure. Je me suis un peu accroché, et j'avoue que la première compilation de Gimp avec un tas de commandes incompréhensibles dans un terminal (Konsole, puisqu'il s'agissait d'un environnement KDE) était sacrément agréable pour le petit bébé g33k que j'étais à l'époque. Ceci dit, la Mandrake n'aura pas tenu longtemps. J'aurai réitéré l'expérience avec les successeuses (Mandrake 8 et 9, sorties respectivement en 2001 et 2002), mais elles n'auront pas réussi à me garder. Il faut dire aussi qu'à l'époque, l'ordinateur était principalement utilisé pour du rendu 3D avec Bryce et du chat avec MSN. Pour les jeux, la PlayStation trônait fièrement au milieu du salon.
Entretemps, la distribution Red Hat passera sur le grill (en version 7.x, principalement). J'utiliserai la 8 pour remettre un Celeron 400 à jour. Si je me rappelle bien, cela avait été la croix et la bannière pour faire fonctionner le maudit modem ("modimodem"?) de cet OS. Pas de bol: fournir une station d'accueil pour une connexion Internet, perdu au fin fond du Sud Ouest français aurait été son principal intérêt. Après cela, j'aurai suivi les principales évolutions, mais en restant bien à distance. C'est plus ou moins à ce moment-là que je me suis approché du projet Debian; en suivant pas mal de tutoriaux (et en participant assidument au forum Linux de PCInpact), j'ai réussi à la faire tourner et à la conserver pendant un petit moment. Le bonheur d'apt-get, aptitude et autres outils de gestion. Le moins-bonheur de passer en "testing puis sur Sid, avant de tout réinstaller".
Finalement, celle qui me laissera sans doute le plus de souvenirs (et d'apprentissage) sera Gentoo. Un manuel super bien fait, une communauté présente, des chouettes fonds d'écran (bah quoi? On peut être geek et esthète :)) et beaucoup de temps de compilation devant soi m'ont permis de maintenir le système pendant pratiquement toutes mes années d'université. Ubunutu commençait tout doucement à décoller à ce moment-là, je l'aurai essayé et utilisée (puisqu'installée par défaut dans les salles informatiques, à défaut d'une Red Hat non maintenue à jour). Cette dernière aura beaucoup plus touché les foules, mais pas au même titre que la Gentoo, qui donnait réellement l'impression de maîtriser le système. Je ne pense pas que les flags et les optimisations possibles de GCC aient réellement agit sur la rapidité du système, mais il est clair que la documentation qu'il fallait potasser avant d'arriver à quelque chose m'a appris **BEAUCOUP**.
Mais au final, c'était juste génial d'avoir des dépôts complets, faciles à utiliser (parce que oui, à part le Stage 1 de l'installation, la maintenance demandait principalement du temps de compilation, pas du temps de compréhension par l'utilisateur). Et quand il a fallu installer un compilateur/interpréteur Cobol (tinyCobol) sur la machine hôte, cela a été fait en deux coups de cuillère à Portage. Sans parler de la configuration de l'accès SSH pour tous mes potes étudiants qui n'avaient pas ce qu'il fallait chez eux, qui s'embourbaient dans des compilations de dépendances incompatibles avec leur système ou qui n'y comprenaient simplement rien. Bref, joie, bonheur et cotillons (et un peu de frime aussi, j'avoue).
L'installation d'un adaptateur Wifi non supporté a fini d'achever ma distribution et me forcera à revenir à Windows XP. Puis Vista (pour essayer). Puis 7 (par commodité). Entretemps, j'aurai eu l'occasion d'essayer également une Arch Linux, cousine de Gentoo, mais présentant plus de facilité (et de rapidité) pour l'installation: les paquets étant tous déjà compilés pour une architecture commune (x86 ou x86_64), pas besoin de compiler chaque paquet pour les besoins spécifiques du processeur.
En 2010, je passerai finalement sur une Fedora, dont j'avais plus ou moins suivi les débuts, mais que j'avais abandonné suite aux déboirs de la Red Hat 8. Si je me rappelle bien, Yum n'en était encore qu'à ses débuts... Le passage à une Fedora 15 présentait forcément quelques (gros) changements par rapport à une simple Red Hat 8 :) Les versions se succederont finalement jusqu'à la Fedora 20, où, pris d'une soudaine envie d'essayer la dernière version de Windows (et ayant envie d'avancer sur un gros projet .Net, avec Visual Studio), je scraperai complètement mes partitions pour revenir sur le monde Microsoftien. Qui ne durera finalement que quelques semaines, le temps de constater que oui, Windows 8.1 n'est pas *si* mal, mais que non, définitivement, Linux me convient mieux!
Du coup, ayant envie de revenir un peu sur Arch Linux que j'apprécie tout particulièrement, mais n'ayant pas envie de me retaper toute l'installation, configuration, chroot et tout le bordel, je passerai sur une Antergos, avec (quitte à faire du changement), KDE, environnement sur lequel je n'étais plus revenu depuis ... la Mandrake 7.1.

View File

@ -0,0 +1,48 @@
---
Title: Installation de Debian sur un EeePc
Date: 2014-12-27
Slug: installation-debian-eeepc
---
Un collègue de Lili m'a demandé récemment d'installer Linux sur son portable. La machine ressemblait à un EeePc, avec un Atom Dual core, 1Go de RAM et un disque anémique de 320Go qui tourne à 90 tours par seconde.
Préparation
-----------
La première étape a été de sauver les données actuelles. A part quelques images, PDF et présentations PowerPoint, pas grand chose à mettre sous le disque. Pendant la copie des 20Go de données, j'ai entamé le processus de recherche de la distribution adéquate. Parmi les candidates, je me suis dirigé vers une distribution intégrant d'office LXDE, XFCE ou MATE, le premier me semblant être l'idéal:
1. [Fedora LXDE](https://spins.fedoraproject.org/lxde/) : la version 21 vient en plus de sortir, les logiciels auraient été suffisament à jour et il n'y aurait pas eu grand chose à faire pour que la machine soit fonctionnelle. Cependant, l'OS n'a jamais démarré: un écran noir intervenait à chaque fois après la sélection du système sur lequel démarrer. Je suppose qu'un paramètre au niveau de Grub aurait résolu le problème, mais cela me rassure d'avoir une installation parfaitement fonctionnelle sans bidouillages.
2. Arch Linux : niveau rapidité, ç'aurait été parfait, mais cette distribution ne me semble pas adaptée pour débuter sous Linux. Keep It Simple ! (but accessible en même temps).
3. [Debian avec LXDE](http://lxde.org/) : le site d'LXDE référence [une image de Debian 7.6](http://cdimage.debian.org/debian-cd/current-live/i386/iso-hybrid/debian-live-7.6.0-i386-lxde-desktop.iso). Les liens de téléchargement ne fonctionnant pas, je n'ai pas cherché à approfondir le sujet. Puis tant qu'à installer Debian avec LXDE, autant installer directement Debian, puis LXDE par après.
4. [Lubuntu](http://lubuntu.net/) (Ubuntu 14.10 avec LXDE, vous vous en doutiez). Cela me semblait être une bonne solution, et ç'aurait été la suivante si Debian n'avait pas fonctionné comme je le souhaitais.
Création du disque et installation
----------------------------------
Pour copier l'image sur un périphérique externe (ma clé USB ne fonctionne pas, j'ai choisi une carte SD de 4Go), il suffit d'utiliser la commande `dd`, de spécifier l'image d'origine, le périphérique de destination et la taille des blocks: `dd bs=4M if=/path/to/archlinux.iso of=/dev/sdx && sync` (Pour lister les périphériques disponibles, utilisez la commande `lsblk`).
En fait, l'installation de Debian s'est super bien déroulé. Tout le matériel (à l'exception du Bluetooth, voir plus bas) a été parfaite, à tel point que les deux interfaces réseaux ont été détectées **pendant** l'installation! J'ai l'habitude d'avoir des cartes WiFi propriétaires: que l'installeur me propose automatiquement de me connecter à un réseau disponible m'a étonné. Surtout que je partais avec beaucoup d'*a priori* concernant les nouveautés disponibles dans la distribution.
Durant l'installation, on a le choix des paquets utilisés. J'en ai profité pour supprimé Gnome (par défaut) et cocher LXDE et Mate, OpenBox ne me semblant pas être une bonne solution pour un néophyte.
Les partitions proposées automatiquement étaient un *chouia* bizarres également. 10Go pour le système me semblait un peu limite... J'ai préféré un partitionnement plus classique:
* /boot de 512M
* swap de 2G
* / de 30 ou 40G
* le reste (environ 280G) pour /home
Lors du premier démarrage, plein de bonnes nouvelles:
* J'ai installé Jessie et non Wheezy. Même Mate en était à sa dernière version.
* Toutes les *hotkeys* étaient fonctionnelles. Pas que ce soit étonnant, mais c'est toujours agréable ne pas avoir à tripatouiller la config' d'Xorg pour que cela fonctionne.
* Les interfaces réseaux qui fonctionnent *out-of-the-box*
* Le lecteur de carte SD aussi (heureusement, puisque c'était mon support d'installation...)
Pour le Bluetooth, la seule contrainte a été d'activer les dépôt `non-free` parmi les sources pour les paquets. Après cela, il a fallu installer le paquet `firmware-atheros` (ainsi que l'applet pour la gestion des associations). Pour info, Mate utilise désormais Blueman [depuis sa version 1.8](https://wiki.archlinux.org/index.php/MATE#Bluetooth) et non pas `gnome-bluetooth-applet`.
Niveau performances, cela se traîne un peu au démarrage, mais une fois la session Mate chargée, tout semble bien fonctionner.
La résolution de l'écran est un peu limite, mais à nouveau, elle suffit pour consulter des pages Web et quelques documents.
# Notes
* J'ai activé le `popularity-contest`, afin d'envoyer des statistiques sur l'utilisation des paquets. Pour en modifier le comportement, il suffit de lancer la commande `dpkg-reconfigure popularity-contest`.

View File

@ -0,0 +1,100 @@
Title: Automatisation des sauvegardes Wordpress
http://codex.wordpress.org/WordPress_Backups
Wordpress est un CMS codé en PHP et dont les données sont stockées dans une base MySQL. Les images et paramètres particuliers sont placés dans un répertoire `wp-content`. Toutes ces informations (configuration + assets + base de données) doivent être sauvegardés régulièrement, pour pouvoir restaurer la situation telle qu'elle était au moment souhaité.
Le site [codex.wordpress.org] a bien une section concernant les sauvegardes automatiques, mais son contenu est un peu insuffisant :
> Various plugins exist to take automatic scheduled backups of your WordPress database. This helps to manage your backup collection easily. You can find automatic backup plugins in the Plugin Browser on the WordPress Administration Screens or through the WordPress Plugin Directory.
Quelques liens sont donnés en plus, afin d'aller un peu plus loin dans la sauvegarde des données, notamment :
* [A shell script for a complete wordpress backup](http://theme.fm/2011/06/a-shell-script-for-a-complete-wordpress-backup-4/)
* [Backing up your database](http://codex.wordpress.org/Backing_Up_Your_Database)
* [Restoring your database from backup](http://codex.wordpress.org/Restoring_Your_Database_From_Backup)
Pour la sauvegarde, on va définir les étapes suivantes:
1. Sauvegarder la base de données
2. Sauvegarder les données
3. Automatisation tout ce brol en se basant sur le premier lien, ci-dessus.
Sauvegarde des données
----------------------
Pour la base de données, le plus simple est d'utiliser la commande `mysqldump` pour copier la structure des tables et leurs données dans un fichier. Il existe un plugin ([WP-DBManager](https://wordpress.org/plugins/wp-dbmanager/)), qui fait exactement la même chose, si vous préférez.
```
mysqldump --add-drop-table -h <server_name> -u <db_user> -p <db_name> | gzip > backup-2015-01-04.bak.sql.gz
```
Il faut ensuite sauvegarder le contenu de l'installation, afin de pouvoir restaurer les média, la configuration, les thèmes, etc.
Un script [existe](http://theme.fm/wp-content/uploads/2011/06/wordpress-backup-shell-script.txt) et s'occupe très bien de toutes ces étapes. Ce script doit être adapté un chouia pour correspondre à vos besoins (notamment sur le nom des fichiers générés). En cas de modifications/améliorations, n'hésitez pas à [poster vos changements](http://theme.fm/2011/06/a-shell-script-for-a-complete-wordpress-backup-4/).
Pour ma part, les changements effectués sont dispos ci-dessous et sur [Gist](https://gist.github.com/Grimbox/5609ee6eb8fd1947cc55). Ils prennent les paramètres passés au script pour les attribuer à l'URL du site, au nom d'utilisateur, mot de passe et nom de la base de données. Pas encore parfait, loin de là:
```
#!/bin/bash
# This script creates a compressed backup archive of the given directory and the given MySQL table. More details on implementation here: http://theme.fm
# Feel free to use this script wherever you want, however you want. We produce open source, GPLv2 licensed stuff.
# Author: Konstantin Kovshenin exclusively for Theme.fm in June, 2011
# Modified by me in June 2015.
# Set the date format, filename and the directories where your backup files will be placed and which directory will be archived.
NOW=$(date +"%Y-%m-%d-%H%M")
SITEURL="$1"
USERNAME=$(whoami)
FILE="$SITEURL.$NOW.tar"
BACKUP_DIR="/home/$USERNAME/backups"
WWW_DIR="/var/www/$SITEURL/"
# MySQL database credentials
DB_USER="$2"
DB_PASS="$3"
DB_NAME="$4"
DB_FILE="$SITEURL.$NOW.sql"
# Tar transforms for better archive structure.
WWW_TRANSFORM="s,^var/www/$SITEURL,www,"
DB_TRANSFORM="s,^home/$USERNAME/backups,database,"
# Create the archive and the MySQL dump
tar -cvf $BACKUP_DIR/$FILE --transform $WWW_TRANSFORM $WWW_DIR
mysqldump -u$DB_USER -p$DB_PASS $DB_NAME > $BACKUP_DIR/$DB_FILE
# Append the dump to the archive, remove the dump and compress the whole archive.
tar --append --file=$BACKUP_DIR/$FILE --transform $DB_TRANSFORM $BACKUP_DIR/$DB_FILE
rm $BACKUP_DIR/$DB_FILE
gzip -9 $BACKUP_DIR/$FILE
```
Restaurer les données dans la base
----------------------------------
En partant du fichier `backup-2015-01-04.bak.sql.gz` généré ci-dessus, on doit d'abord le décompresser, puis importer le contenu dans la base SQL. Comme on a ajouté l'option `--add-drop-table`, le drop s'effectue avant que l'import ne s'effectue, si la table existe déjà. Par exemple :
```
DROP TABLE IF EXISTS `wp_commentmeta`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `wp_commentmeta` (
`meta_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`comment_id` bigint(20) unsigned NOT NULL DEFAULT '0',
`meta_key` varchar(255) DEFAULT NULL,
`meta_value` longtext,
PRIMARY KEY (`meta_id`),
KEY `comment_id` (`comment_id`),
KEY `meta_key` (`meta_key`)
) ENGINE=InnoDB AUTO_INCREMENT=95 DEFAULT CHARSET=utf8;
```
Bref, on décompresse le fichier, et on l'importe dans la db:
```
mysql -h localhost -u <db_user> -p <db_name> < backup-2015-01-04.bak.sql
# ex : mysql -h localhost -u wp_user -p wp_db < backup-2015-01-04.bak.sql
```

View File

@ -0,0 +1,159 @@
---
Title: /etc/skel/.bashrc
Date: 2015-01-09
Slug: etc-skel-bashrc
Tags: linux, bash
---
## What the skel?
Le répertoire `/etc/skel/` contient les fichiers par défaut qui seront copiés lorsqu'un nouvel utilisateur sera créé. Sur ma ch'tite [Fedora](http://fedoraproject.org/) adorée, ce dossier contient déjà les fichiers suivants :
* **.bash_profile**, utilisé lorsque l'utilisateur se connecte via un tty
* **.bashrc**, utilisé lorsque l'utilisateur lance connexion à un terminal depuis une session déjà authentifiée.
* **.bash_logout**, vide (ouaip, par défaut, il ne se passe rien; mais la modification de ce fichier permettrait d'exécuter un certain nombre d'actions lorsque l'utilisateur se déconnecte)
Sur [Antergos](http://antergos.com/) par contre, le dossier est complètement vide. On va remplir tout ça.
## Message d'accueil
Pour que `bash` vous souhaite la bienvenue à l'ouverture, passez par `fortune-mod`. C'est une application présente sur la plupart des distributions, et qui sort un ensemble de citations de manière aléatoire. Par contre, l'installation de plugins supplémentaires (genre Calvin & Hobbes ou Chuck Norris) nécessitera peut-être de passer par une installation manuelle. Sous Arch, il y a [les Arch User Repositories](https://aur.archlinux.org/packages.php?O=0&L=0&C=0&K=fortune-mod&SeB=nd&PP=25&do_Search=Go) :)
```
sudo yaourt -S fortune-mod fortune-mod-calvin
fred@gaw ~/Sources $ fortune
The more I know men the more I like my horse.
fred@gaw ~/Sources $ fortune calvin
Calvin: I think we have got enough information now, don't you?
Hobbes: All we have is one "fact" that you made up.
Calvin: That's plenty. By the time we add an introduction, a few
illustrations and a conclusion, it'll look like a graduate
thesis.
```
Après, il suffit d'ajouter `fortune calvin` (ou le mod choisi) au début du fichier `.bashrc`. On peut également ajouter un appel à `cowsay` ou `cowthink` avec un pipe depuis `fortune`, et on aura un beau message de login.
Bref, dans le fichier `.bashrc`, cela revient à ajouter ceci :
```
if [ -x /usr/bin/fortune ]; then
fortune calvin
fi
```
On teste que `fortune` existe, et si oui, on le lance au démarrage de la session du shell.
## Quelques couleurs et informations
Ce fichier, c'est d'abord l'emplacement pour associer un peu de couleurs au shell, en redéfinissant la variable $PS1. Le [wiki d'ArchLinux](https://wiki.archlinux.org/index.php/Color_Bash_Prompt) est assez complet à ce sujet. Ma config' préférée reste cependant celle héritée de [Gentoo](https://www.gentoo.org/):
```
if [[ ${EUID} == 0 ]] ; then
PS1='\[\033[01;31m\]\h\[\033[01;34m\] \W \$\[\033[00m\] '
else
PS1='\[\033[01;32m\]\u@\h\[\033[01;34m\] \w \$\[\033[00m\] '
fi
```
Comme cette condition vérifie si l'utilisateur courant est *root* ou non, il convient de la placer dans le bashrc global, et non au niveau de l'utilisateur courant. L'emplacement de ce fichier-ci dépend vraiment de la distribution. Sous Arch par exemple, il se trouve dans `/etc/bash.bashrc`.
Un autre truc intéressant, c'est de pouvoir afficher la branche git sur laquelle on travaille, ainsi que l'environnement virtuel dans lequel on se trouve. Il existe normalement un fichier de complétion pour le shell, mais je n'ai pas réussi à le faire fonctionner. Du coup, j'ai trouvé [une autre solution](https://coderwall.com/p/fasnya). En combinant avec la variable $PS1 précédente, cela revient à ceci :
``` bash
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/'
}
export PS1="\[\033[01;32m\]\u@\h\[\033[01;34m\] \w\[\033[33m\]\$(parse_git_branch)\[\033[00m\] $ "
```
Pour les environnements virtuels en Python, il suffit de passer par `virtualenvwrapper`.
``` bash
export WORKON_HOME=$HOME/.virtualenvs
export PROJECT_HOME=$HOME/Sources
source /usr/bin/virtualenvwrapper.sh
```
Le nom de l'environnement s'ajoutera automatiquement au début de la ligne.
## Quelques fonctions utiles
Bref, si on veut avoir un ensemble de fonctions prête-à-utiliser, c'est le bon emplacement. Par exemple sur [SamEtMax](http://sametmax.com), on trouve un article intitulé [`à l'intérieur de mon bashrc`](http://sametmax.com/a-linterieur-de-mon-bashrc/). Parmi les quelques fonctions exprimées, on trouve par exemple:
``` bash
extract () {
if [ -f $1 ]
then
case $1 in
(*.7z) 7z x $1 ;;
(*.lzma) unlzma $1 ;;
(*.rar) unrar x $1 ;;
(*.tar) tar xvf $1 ;;
(*.tar.bz2) tar xvjf $1 ;;
(*.bz2) bunzip2 $1 ;;
(*.tar.gz) tar xvzf $1 ;;
(*.gz) gunzip $1 ;;
(*.tar.xz) tar Jxvf $1 ;;
(*.xz) xz -d $1 ;;
(*.tbz2) tar xvjf $1 ;;
(*.tgz) tar xvzf $1 ;;
(*.zip) unzip $1 ;;
(*.Z) uncompress ;;
(*) echo "don't know how to extract '$1'..." ;;
esac
else
echo "Error: '$1' is not a valid file!"
exit 0
fi
}
```
## Conclusion
Au final, mon fichier .bashrc ressemble à ceci:
```bash
if [ -x /usr/bin/fortune ]; then
fortune calvin
fi
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/'
}
export PS1="\[\033[01;32m\]\u@\h\[\033[01;34m\] \w\[\e[0;31m\]\$(parse_git_branch)\[\033[00m\] $ "
export WORKON_HOME=$HOME/.virtualenvs
export PROJECT_HOME=$HOME/Sources
source /usr/bin/virtualenvwrapper.sh
```
Et des fois que je me connecte en root, j'ai ajouté ceci dans le fichier /etc/bash.bashrc:
``` bash
if [[ ${EUID} == 0 ]] ; then
PS1='\[\033[01;31m\]\h\[\033[01;34m\] \W \$\[\033[00m\] '
fi
```
Le contenu du fichier .bash_profile est assez simple. En résumé, il va vérifier si un fichier .bashrc existe (ou non) et le charger.
``` bash
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/.local/bin:$HOME/bin
export PATH
```
## Sources
* http://sphericalcow.wordpress.com/2009/04/05/bashrc-by-example/
* http://www.linfo.org/etc_skel.html
* http://www.joshstaiger.org/archives/2005/07/bash_profile_vs.html

View File

@ -0,0 +1,67 @@
---
Title: rsync
Date: 2015-01-17
---
Pour transférer des fichiers de manière sécurisé, rien de mieux qu'SSH (ou qu'un disque externe coulé dans du béton. Au choix). [SCP](|filename|2013-03-25 scp-basics.md) permet par exemple de transférer des fichiers au travers du protocole ssh, mais ne gère pas la reprise d'un transfert échoué. Pour plus de fonctionnalités, il existe [rsync](https://rsync.samba.org/), qui offre à nos petits yeux ébahis plein de nouvelles fonctionnalités super utiles: la reprise de transfert en cas de foirage complet, la synchronisation de dossiers, ... C'est un outil de sauvegarde pratique, sur lequel se basent notamment [Duplicity](http://duplicity.nongnu.org/) et [Deja-Dup](https://launchpad.net/deja-dup).
Utilisation
-----------
```
$ rsync -r -P -e ssh login@server:path/ folder/
```
Dans l'exemple ci-dessus, les paramètres sont:
* `-r` pour un transfert récursif. Sans ce paramètre, seul les fichiers présents dans le dossier racine seront transférés.
* `-P` pour avoir le même effet que `--partial --progress`: une barre de progression s'affiche pour chaque fichier transféré, et le téléchargement de fichiers incomplets est repris à l'endroit où il avait été abandonné.
* `-e` pour spécifier le shell à utiliser
* Les deux derniers paramètres sont : `SOURCE` et `DESTINATION`. Ici, on va se connecter avec l'utilisateur `login` sur la machine `server` et rapatrier tous les dossiers qui se trouvent dans `path` vers le répertoire local qui s'appelle `folder`.
Parmi les autres paramètres intéressants:
* `-t` pour garder les informations de dates de modifications sur les fichiers
* `-v` pour entrer dans un mode verbeux
* `-u` pour synchroniser les dossiers, en prenant en compte les dates de modifications des fichiers pour savoir s'il doit envoyer une nouvelle version ou non. Attention que ce paramètre n'a pas d'intérêt si le transfert précédent a échoué: comme le fichier (a moitié) transféré a la même heure et date de modification que le fichier qu'rsync tente d'envoyer, il sera simplement *skippé*, alors qu'il aurait dû continuer le transfert grâce au paramètre `-P`. Pour une sauvegarde/copie, évitez ce paramètre, sauf si vous êtes sûr que la copie précédente a réussi.
* `--delete` pour supprimer les fichiers qui se trouvent dans la destination, mais pas dans la source
* `-z` pour compresser les fichiers et gagner un peu au niveau du transfert.
Attention également aux `/` après les chemins vers les répertoires. Oubliez le, et cela créera un nouveau répertoire dans le répertoire cible. Ajoutez le pour ne synchroniser que le contenu.
Synology
--------
Si vous comptez utiliser votre Synology comme répertoire de destination, il se peut que vous tombiez sur le message d'erreur suivant:
```
Permission denied, please try again.
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(226) [sender=3.1.1]
```
Si c'est le cas, vous devrez activer un service sur le NAS:
* Dans `Main Menu`, cliquez sur `Backup & Replication` et sélectionnez `Backup Services`.
* Dans cet onglet, cochez l'option `Enable network backup service`.
A présent, vous pourrez utiliser `rsync` pour transférer vos documents vers le NAS.
Mon but ici est de synchroniser mon profil utilisateur (`~/` en gros). De nouveau, avec la commande ci-dessus, cela va synchroniser **tous** mes documents et fichiers. Problème: cela embarque également les `dot files`. Une solution pour ceci est d'ajouter le paramètre `--exclude ".*"`.
Finalement
----------
En combinant tous les paramètres, ma commande rsync ressemble est la suivante:
```
rsync -rtv -P -e "ssh -p 11111" my_folder/ login@server:path/ --exclude ".*" --delete
```
Sources
-------
* [StackOverflow](http://stackoverflow.com/questions/20860896/is-there-a-way-to-continue-broken-scp-secure-copy-command-process-in-linux)
* [Rsync: Permission Denied @ Synology](http://forum.synology.com/enu/viewtopic.php?t=92627)
* [Synchronizing folders with rsync](http://www.jveweb.net/en/archives/2010/11/synchronizing-folders-with-rsync.html)
* [rsync non standard ssh port](http://mike-hostetler.com/blog/2007/12/08/rsync-non-standard-ssh-port/)

View File

@ -0,0 +1,41 @@
---
Title: GitWeb
Date: 2015-01-22
Slug: gitweb
Tags: git, web, serveur
---
[GitWeb](http://git-scm.com/docs/gitweb.html) est un frontend Web permettant de visualiser des dépôts Git. En gros (et après traduction), ses principales fonctions sont les suivantes:
* Visualiser plusieurs dépôts ayant la même racine
* Parcourir toutes les révisions d'un dépôt donné
* Visualiser le contenu des fichiers du dépôt (à partir de n'importe quelle révision)
* Visualiser les logs
* Générer un flux RSS/Atom pour tous les commits d'une branche
* ...
Bref, sans atteindre les fonctionnalités d'un [GitHub](http://github.com) ou d'un [GitLab](https://about.gitlab.com/), GitWeb fera très bien son boulot.
Commencez par créer l'utilisateur `git`, grâce à la commande `sudo adduser git`. Ceci va (normalement) créer le répertoire `/home/git` et tous les droits qui lui sont associés. Les dépôts seront alors stockés dans le répertoire `/home/git/repositories`.
Pour Nginx, voici un exemple de configuration:
```
location /git/ {
alias /usr/share/gitweb/;
fastcgi_param SCRIPT_FILENAME /usr/share/gitweb/index.cgi;
include fastcgi_params;
gzip off;
fastcgi_param GITWEB_CONFIG /etc/gitweb.conf;
fastcgi_pass unix:/var/run/fcgiwrap.socket;
}
```
Et finalement, comme le thème par défaut est peut-être un peu austère, il est possible de le modifier facilement en suivant les étapes définies [ici](https://github.com/kogakure/gitweb-theme).
Sources
-------
* [Git-scm.com](http://git-scm.com/book/en/Git-on-the-Server-GitWeb)
* [GitWeb & Nginx](https://misterpinchy.wordpress.com/2012/11/01/gitweb-and-nginx/)
* [Running GitWeb in fast-cgi mode](https://sixohthree.com/1402/running-gitweb-in-fastcgi-mode)

View File

@ -0,0 +1,58 @@
---
Title: Virtual Private Server
Date: 2015-02-07
Slug: virtual-private-server
---
J'ai ouvert un compte chez [WebFaction](http://webfaction.com/) en février 2012, suite à une promotion qu'ils faisaient. Pour ~180€ payés, je recevais en fait un crédit de plus de 225€, ce qui revenait plus ou moins à 6,50$ / mois pour un serveur partagé chez eux; càd
* 512 Mo de RAM
* 100 Go d'espace disque
Cet abonnement arrivant à terme (et ayant envie de changer un peu, et de profiter d'un serveur plus complet qu'un shared server), je me dirige vers l'offre VPS Classic d'OVH. Plusieurs raisons :
* Impossible de trouver un [kimsufi](http://www.kimsufi.com/fr/) KS-1. La version KS-2 est dispo par contre, mais coûte quasiment le double pour quelque chose dont je n'aurai sans doute pas besoin.
* Les offres SoYouStart sont hyper tentantes, mais ne répondent sans doute pas non plus à ce que je souhaiterais/aurais besoin
* Idem pour les offres Online.net ([Dedibox et co.](http://www.online.net/en/dedicated-server/dedibox-classic)).
=> Pour me faire la main sur de l'hébergement de sites php/python, bases de données (MySQL/MongoDB/Redis) et un peu de stockage de fichiers ([Seafile](https://www.seafile.com/en/home/)), je me suis pris l'offre la plus basique au niveau des VPS Classic: 1 vCore, 1Go de RAM et 10Go d'espace disque. Quand on disait basique... :)
Au niveau de l'installation, on a le choix parmi plusieurs distributions. Après un petit essai avec une Ubuntu 14.04, je me tente une Debian 7. Quelques points à voir après l'installation de l'OS:
* La Sécurité et la configuration de base du serveur : création d'un utilisateur (ou plusieurs) utilisateurs pour la connexion SSH, désactivation du login root, installation de fail2ban.
* La configuration : installation d'nginx, varnish et memcache. Puis installation de Mysql (mariadb?) pour la DB à utiliser avec Wordpress.
* le ReverseProxy : utilisation d'un nom de domaine acheté chez Gandi :)
# Sécurité et configuration de base
Un article sur le [forum de Clubic](http://www.clubic.com/forum/hardware-general/raspberry-pi-ze-topic-id877239-page1.html#1802451955) donne plein de conseils en prenant le raspberry pi comme base.
# Configuration
Pour nginx, il existe un script d'installation chez [Nicolargo](https://raw.githubusercontent.com/nicolargo/debianpostinstall/master/nginxautoinstall.sh). Pour le téléchoper, utilisez `wget https://raw.githubusercontent.com/nicolargo/debianpostinstall/master/nginxautoinstall.sh --no-check-certificate`.
L'installation consiste entre autre à télécharger les dépendances pour la compilation d'nginx, à ajouter le répertoire [dotdeb](http://www.dotdeb.org/), installer PHP5-FPM, les outils de développement et MemCache.
Après exécution du script, on a un petit récapitulatif d'installation :
```
NGinx configuration folder: /etc/nginx
NGinx default site configuration: /etc/nginx/sites-enabled/default-site
NGinx default HTML root: /var/www
Read this to configure Naxsi: https://github.com/nbs-system/naxsi/wiki/basicsetup
PageSpeed cache directory: /var/ngx_pagespeed_cache
Read this to configure PageSpeed: https://developers.google.com/speed/pagespeed/module/configuration
Installation script log file: /tmp/nginxautoinstall-20141019201236.log
```
Placez un fichier `index.html` dans le répertoire `/var/www/` et accéder à votre instance *via* son URL. Vous devriez vous le contenu.
Voir aussi [ici](http://stackoverflow.com/questions/21524373/nginx-connect-failed-111-connection-refused-while-connecting-to-upstream) après l'installation.
Les logs se trouvent dans `/var/log/nginx/[access|error].log`. La configuration (sur Debian & Ubuntu) se fait grâce à `service [start|stop|restart|status] nginx`.
Pour que Wordpress fonctionne, il faut installer `php5-fpm`. Ce service doit ensuite être démarré s'il ne l'est pas déjà (`service php5-fpm start`).
# Reverse Proxy
Pour le reverse proxy, le plus simple est d'éditer le fichier de zone chez Gandi pour y associer l'adresse ip du VPS à un nom de domaine ou à un sous-domaine. La propagation des informations peut prendre quelques heures. Ensuite, il suffit de spécifier ce domaine ou sous-domaine directement dans le fichier de configuration Nginx.

View File

@ -0,0 +1,68 @@
---
Title: cgit
Date: 2015-03-07
Tags: git, cgit, server
---
[Cgit](https://git.zx2c4.com/cgit/) se présente comme *A hyperfast web frontend for git repositories written in C. Jason A. Donenfeld: about summary refs log tree commit diff stats*. Il se trouve normalement dans les dépôts de votre distribution. Sous Debian, un petit `aptitude install cgit` fera l'affaire. Le paquet viendra avec `liblua` comme dépendance, mais ce sera tout. Pour l'exécuter au travers d'Nginx, vous devrez également installer `fcgiwrap`, puis le démarrer (`systemctl start fcgiwrap.socket`).
Ma configuration Nginx est la suivante. J'ai placé `cgit` à l'url `/cgit`. Il est nécessaire d'ajouter un deuxième emplacement pour les fichiers statiques, qui seront accessibles depuis `/cgit-web`:
``` shell
location /cgit {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi;
fastcgi_pass unix:/run/fcgiwrap.socket;
fastcgi_split_path_info ^(/cgit/?)(.+)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param QUERY_STRING $args;
}
location /cgit-web {
rewrite ^/cgit-web(/.*)$ $1 break;
root /usr/share/cgit;
}
```
En ayant fait l'installation par le *package maneger* de Debian, les fichiers seront placés dans le répertoire `/usr/share/cgit/`, à l'exception du fichier de configuration, accessible depuis le chemin `/etc/cgit/cgitrc`. C'est à cet endroit-là que vous renseignerez notamment les dépôts à afficher, ainsi que l'url à utiliser pour accéder aux fichiers de style (dans la conf' nginx, on a spécifié `cgit-web` alors que le fichier `cgitrc` paramètre par défaut le chemin vers `cgit-css`. A vous de voir, mais faites correspondre les deux).
Pour ajouter des dépôts, le plus simple est d'ajouter la ligne `scan-path=<path>`. L'autre solution est de les ajouter à la main, en utilisant les propriétés `url`, `path` et `desc`, comme proposé sur le [Wiki d'ArchLinux](https://wiki.archlinux.org/index.php/Cgit):
``` shell
#
# List of repositories.
# This list could be kept in a different file (e.g. '/etc/cgitrepos')
# and included like this:
# include=/etc/cgitrepos
#
repo.url=MyRepo
repo.path=/srv/git/MyRepo.git
repo.desc=This is my git repository
repo.url=MyOtherRepo
repo.path=/srv/git/MyOtherRepo.git
repo.desc=That's my other git repository
```
L'utilisateur sous lequel tourne nginx doit cependant avoir accès en lecture aux dépôts. Pour faciliter les choses, j'ai modifié les permissions pour donner accès en lecture au groupe `www-data`, tout en gardant la propriété avec mon compte utilisateur: `sudo chown -R fred:www-data <path>`.
Ajoutez-y une petite description avec l'attribut `root-desc`, ainsi qu'un titre éloquent avec `root-title`, et vous aurez une petite interface pratique, rapide et lisible. Elle n'offrira sans doute pas autant de fonctionnalités qu'un [GitWeb](https://git.wiki.kernel.org/index.php/Gitweb) avec http-backend.
``` shell
#
# cgit config
# see cgitrc(5) for details
css=/cgit-web/cgit.css
logo=/cgit-web/cgit.png
root-title=Git repositories
root-desc=
scan-path=/home/fred/git/
```
Sources:
* [http://wiki.archlinux.org](https://wiki.archlinux.org/index.php/Cgit)

View File

@ -0,0 +1,48 @@
---
Title: SSMTP
Date: 2015-03-07
Slug: ssmtp
Tags: mails, sendmail, smtp
---
Pour envoyer des mails depuis une application, la solution la plus simple consiste à passer par [ssmtp](https://doc.ubuntu-fr.org/ssmtp). Cela permet de configurer facilement l'envoi de mails sans se farcir la configuration d'un serveur SMTP complet (postfix, exim, ...).
Pour l'installation, on passe par apt, comme d'hab: `apt install ssmtp`. On modifie ensuite le fichier `/etc/ssmtp/ssmtp.conf`. La configuration ci-dessous fonctionne pour une adresse hébergée chez Gandi.
```shell
#
# Config file for sSMTP sendmail
#
# The person who gets all mail for userids < 1000
# Make this empty to disable rewriting.
root=admin@grimbox.be
# The place where the mail goes. The actual machine name is required no
# MX records are consulted. Commonly mailhosts are named mail.domain.com
mailhub=mail.gandi.net:587
# Where will the mail seem to come from?
rewriteDomain=grimbox.be
# The full hostname
hostname=grimbox.be
# Are users allowed to set their own From: address?
# YES - Allow the user to specify their own From: address
# NO - Use the system generated From: address
FromLineOverride=YES
UseSTARTTLS=YES
AuthUser=<smtp_username>
AuthPass=<smtp_password>
```
Pour tester:
```shell
$ mail -s "Test" destinataire@example.org < /dev/null
Null message body; hope thats ok
```
Vous devriez recevoir le message (relativement) rapidement.

View File

@ -0,0 +1,26 @@
---
Title: Installation de Python sous CentOS
Date: 2015-03-09
Slug: install-python-on-centos
---
On installe les dépendances: `sudo yum install zlib-devel bzip2-devel openssl-devel ncurses-devel`.
Puis on procède à l'installation:
```
cd /opt
wget http://python.org/ftp/python/2.7.9/Python-2.7.9.tgz
tar xvfz Python-2.7.9.tgz
cd Python-2.7.9
./configure --prefix=/usr/local --enable-unicode=ucs4 --enable-shared LDFLAGS="-Wl,-rpath /usr/local/lib"
make && make altinstall
```
La partie `LDFLAGS` permet d'identifier le chemin vers lequel les librairies partagées seront installées. Sans cette option, on risque de tomber sur une erreur de type `error while loading shared libraries: libpython2.7.so.1.0`. L'autre partie hyper importante est d'utiliser `make altinstall` (et non pas `make install`): cela installera la nouvelle version de Python à côté de l'ancienne, sans tout casser (*a priori*, c'est toujours mieux de laisser l'OS gérer lui-même les dépendances dont il a besoin...).
Sources
-------
* http://toomuchdata.com/2014/02/16/how-to-install-python-on-centos/
* http://sametmax.com/installer-python-2-7-x-sur-centos-6-x-les-doigts-dans-le-nez/

View File

@ -0,0 +1,24 @@
---
Title: OVH Public Cloud
Date: 2015-06-24
Slug: ovh-public-cloud
Tags: ovh, cloud, server
---
Après quelques déboires avec un VPS Classic 1, puis un VPS basique sur Digital Ocean, je me laisse tenter par la nouvelle offre OVH. Les commentaires semblent aller dans le bon sens et le prix est abordable pour un payement mensuel (6€ HTVA).
Dans l'ordre, j'ai créé un nouveau serveur en choisissant le modèle (KS-2), puis une clé SSH pour s'y connecter. Contrairement aux autres serveurs, l'OS est déjà un brin configuré: sur une Debian 8, `sudo` est déjà présent et l'utilisateur principal (admin) doit obligatoirement se connecter en fournissant la clé privée pour s'authentifier. Je n'ai pas encore trouvé comment associer une nouvelle clé à une instance existante... On va donc éviter de la perdre :-).
Une fois connecté, j'ai suivi les étapes habituelles:
1. Mise à jour du système (`apt-get update` et `apt-get upgrade`)
2. Création d'un nouvel utilisateur avec `sudo adduser fred`
En vrac
-------
Pour ce test, j'ai utilisé le script [Curl Speedtest](http://dl.getipaddr.net/), dispo sur [Github](https://raw.github.com/blackdotsh/curl-speedtest).
Le processeur est un Intel Xeon E12xx (un seul coeur), 4Go de RAM, 20Go d'espace disque. La copie d'un fichier d'1Go a été faite a une vitesse de 395Mo/s. Je suppose qu'il s'agit juste d'un bourrage d'octets en vrac. Cela ne concerne donc clairement pas la lecture ou l'écriture de petits fichiers de moins de 4Ko. Le calcul des 5000 premières décimales de Pi a pris 0m19.507.
En substance, cela donne ceci une vitesse de download en fonction du serveur distant (mais en arrivant facilement à plus de 10Mo/s), et une vitesse d'upload équivalente.

View File

@ -0,0 +1,33 @@
---
Title: screen
Date: 2015-07-01
Slug: screen
---
[Screen](https://www.gnu.org/software/screen/) permet *grosso modo* de gérer plusieurs terminaux au sein d'une seule et même instance. Cela permet par exemple d'ouvrir une connexion SSH et d'ouvrir plusieurs onglets depuis cette seule connexion.
Les commandes principales pour bien démarrer sont les suivantes:
* `screen` pour *ouvrir* le gestionnaire
* `ctrl-a, c` pour ouvrir un nouveau shell dans cette instance de Screen
* `ctrl-a, w` pour lister les shells ouverts par Screen
* `ctrl-a, n` pour aller au prochain shell
* `ctrl-a, N` pour aller au précédent shell
Histoire de s'y retrouver un peu, copiez/collez le contenu suivant dans le fichier `~/.screenrc` (Source:??):
``` shell
startup_message off
caption always
# Caption JCK (met en évidence %{=b kg} le screen actif [%n %t]
# puis la liste des autres screen %{=s ky}%W
# affiché à droite %=
# le nom de la machine %{=b}%H
# et l'heure %{=s}%c sont affich▒s
caption string "%{=b kg} [%n %t] %{=s ky}%W %= %{=b}%H %{=s}%c "
# Better vim intergration
term "screen-256color"
```

View File

@ -0,0 +1,25 @@
---
Title: Xpra
Date: 2015-08-11
Slug: xpra
---
[Xpra](http://xpra.org/) permet de faire tourner des applications X sur une machine distante, un peu à la manière d'un X-Forwarding, avec un avantage sur les algorithmes de compression d'informations (h264, vp9, png, webp, ...) et la possibilité de transporter le son et la vidéo. Son fonctionnement consiste à lancer le serveur sur un poste, puis à y accéder depuis une machine cliente (distante) au travers d'une connexion ouverte (SSH, TCP, TCP+AES).
Depuis le serveur, installez `xpra` grâce aux dépôts [Winswitch](https://winswitch.org/downloads/). Après l'ajout du dépôt et des clés, évitez juste la dernière étape qui consiste à installer `winswitch`: installez juste `xpra`, à moins que vous n'ayez besoin de plus.
Une fois que c'est fait, lancez un terminal, et lancez les commandes suivantes:
* `xpra start :100`
* `export DISPLAY=:100`
En gros, on crée un affichage pour `xpra`, qui porte l'identifiant 100.
Depuis la machine distance, installez le client [xpra](http://xpra.org/); vous aurez ensuite deux choix: soit passer par le client graphique, soit lancer xpra depuis une console. Sous Windows, cela ressemble à ceci:
```
cd C:\Program Files (x86)\xpra
xpra_cmd.exe attach ssh:<ip>:<display>
```
Une fenêtre vous demandera votre login et votre mot de passe; ensuite, toute application graphique lancée depuis le serveur dans le terminal sera exportée vers le poste client.

View File

@ -0,0 +1,27 @@
---
Title: Informations sur un fichier en CLI
Date: 2015-08-13
Slug: informations-sur-un-fichier-en-cli
---
Pour connaître des informations sur un fichier, dans un terminal:
```shell
[fred@aerys ~]$ file -i entries/conf/rsync.md
entries/conf/rsync.md: text/plain; charset=iso-8859-1
```
Pour encoder un fichier dans un autre format:
```shell
fred@aerys ~]$ iconv -f iso-8859-1 -t utf-8 entries/conf/rsync.md > entries/conf/rsync2.md
```
Attention à bien rediriger la sortie vers un fichier qui n'est pas le même que la source... Sans quoi, vous vous retrouverez avec un beau fichier vide.
En recommençant la première opération, le `charset` aura normalement été modifié:
```shell
[fred@aerys ~]$ file -i entries/conf/rsync.md
entries/conf/rsync.md: text/plain; charset=utf-8
```

View File

@ -0,0 +1,129 @@
---
Title: Quelques commandes MySQL de base
Date: 2015-09-10
Slug: quelques-commandes-mysql-de-base
Tags: mysql, database
---
## Connexion au shell
Pour se connecter sur le shell MySQL : `$ mysql -u root -p`.
Des fois que vous ayez oublié les utilisateurs définis dans la DB:
```
mysql> select host, user, password from mysql.user;
+-----------+------------------+-------------------------------------------+
| host | user | password |
+-----------+------------------+-------------------------------------------+
| localhost | root | *AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA |
| localhost | wordpressuser | *BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB |
| 127.0.0.1 | root | *CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC |
| ::1 | root | *CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC |
| localhost | debian-sys-maint | *DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD |
+-----------+------------------+-------------------------------------------+
5 rows in set (0.17 sec)
```
## Actions sur les tables
Pour lister les bases de données enregistrées:
```
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| xxx |
| mysql |
| performance_schema |
+--------------------+
4 rows in set (0.05 sec)
```
Et pour supprimer une des bases, c'est grâce à:
```
mysql> drop database if exists wordpress;
Query OK, 0 rows affected (0.04 sec)
```
(on peut laisser tomber le `if exists`, mais cela jettera une erreur si la db n'existe pas.
Il est aussi possible de [lister les tables appartenant à une base en particulier](http://dev.mysql.com/doc/refman/5.0/en/show-tables.html):
```
mysql> show tables from reader;
+--------------------------+
| Tables_in_reader |
+--------------------------+
| access_keys |
| archived_feeds |
| cat_counters_cache |
| counters_cache |
| enclosures |
| entries |
| entry_comments |
| error_log |
| feed_categories |
| feedbrowser_cache |
| feeds |
| ... |
+--------------------------+
31 rows in set (0.00 sec)
```
## Dump et restore
Pour *dumper* les données d'une base en particulier, utilisez le petit script ci-dessous:
```shell
NOW=$(date +"%Y-%m-%d-%H%M")
DB_USER=""
DB_PASS=""
DB_NAME=""
DB_FILE="$DB_NAME.db.$NOW.sql"
mysqldump -u$DB_USER -p$DB_PASS $DB_NAME > $DB_FILE
```
Et pour restaurer les données vers une base, le principe est de rediriger le contenu d'un fichier vers la connexion de la base de données. Ce qu'on a fait juste au dessus, pour le dump, c'est d'utiliser une redirection **vers** un fichier `>`. Ici, c'est l'inverse: `<`.
```
mysql -u $DB_USER -p$DB_PASS $DB_NAME < fichier.sql
```
## Création d'un nouvel utilisateur
Commencez par lancer `mysql -u root -p` pour entrer dans le shell MySQL.
La commande ci-dessous va créer un nouvel utilisateur, `newuser`, dont le mot de passe sera `password`. Le champ `localhost` peut rester tel quel.
```sql
CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password';
```
Une fois que le nouvel utilisateur est créé, il faut encore lui accorder des privilèges. Pour cela, choisissez la base sur laquelle vous comptez lui donner un accès. Si elle n'existe pas encore, créez la:
```sql
CREATE DATABASE IF NOT EXISTS database_name;
```
Et finalement, on accorde les privilèges à l'utilisateur `newuser` sur la base `database_name`:
```sql
GRANT ALL PRIVILEGES ON database_name.* TO 'newuser'@'localhost';
FLUSH PRIVILEGES;
```
## Permissions
Plutôt que d'accorder un petit `Grant All Privileges`, il y a [plusieurs niveaux de privilèges](https://www.digitalocean.com/community/tutorials/how-to-create-a-new-user-and-grant-permissions-in-mysql):
* **ALL PRIVILEGES** - attribue l'ensemble des privilèges ci-dessous à l'utilisateur, pour une base de données spécifiée. Si aucune base de données n'est spécifiée, les privilèges seront attribués au niveau du système.
* **CREATE** - permet à l'utilisateur de créer de nouvelles tables ou bases de données
* **DROP** - autorise la suppression de tables ou de bases de données
* **DELETE** - autorise la suppression d'enregistrements dans une table
* **INSERT** - autorise l'insertion d'enregistrements dans une table
* **SELECT** - autorise la sélection de données présentes dans la base
* **UPDATE** - autorise la mise à jour d'enregistrements présents dans une table
* **GRANT OPTION** - autorise la gestion des privilèges pour d'autres utilisateurs.

View File

@ -0,0 +1,46 @@
---
Title: Installation et configuration basique de Postgresql
Date: 2016-02-12
Slug: installation-configuration-basique-postgresql
Tags: db, pgsql, sql
---
Dans ma petite tête de développeur, [Postgresql](http://www.postgresql.org/) se trouve dans le haut du panier. Je n'ai clairement pas autant l'occasion de jouer avec que je le souhaiterais, à cause d'un employeur un tantinet orienté Oracle/MSSQL, mais les quelques fois où j'ai pu la tripoter, c'était un petit plaisir.
Bref, pour ceux qui désireraient se lancer dans l'aventure (ou essayer autre chose qu'un [MariaDB](https://mariadb.org/) packagé par défaut pour du PHP), un petit tuto est disponible ici: [doc.fedora-fr.org](http://doc.fedora-fr.org/wiki/Installation_et_configuration_de_PostgreSQL). Il couvre l'installation du service, la création d'un utilisateur, la sécurisation et l'utilisation d'un client ([pgadmin](https://launchpad.net/pgadmin).
## Installation et initialisation
Commencez par chercher un paquet qui ressemble à `postgresql-server` *via* votre gestionnaire préféré. Après l'installation, vous pourrez initialiser cette nouvelle instance avec la commande suivante: `sudo postgresql-setup --initdb`:
```shell
* Initializing database in '/var/lib/pgsql/data'
* Initialized, logs are in /var/lib/pgsql/initdb_postgresql.log
```
On démarre ensuite le service si ce n'est pas déjà fait:
```shell
$ sudo systemctl start postgresql && sudo systemctl status postgresql
[...]
Feb 12 21:13:09 aerys systemd[1]: Started PostgreSQL database server.
```
Après tout ceci, un utilisateur `postgres` a été créé au niveau système -- donc au même titre que votre utilisateur à vous - il ne s'agit pas d'un compte interne à la base de données, mais bien d'un compte avec lequel vous pouvez **vous connecter**. Comme expliqué dans la [doc](http://doc.fedora-fr.org/wiki/Installation_et_configuration_de_PostgreSQL) et au niveau de la base de données, il s'agit de l'équivalent au super-utilisateur `root`. En gros, super-pouvoir, super-responsabilités et super-gaffes si vous ne faites pas un minimum attention. Allez zou, maintenant qu'on sait qu'il ne faut rien supprimer, on peut s'y connecter. On passe en `root` pour l'impersonnification, puis on se connecte avec le compte `postgres` avant de se connecter à la db avec la commande `psql`:
```
sudo su - postgres
[sudo] password for fred:
-bash-4.3$ psql
psql (9.4.5)
Type "help" for help.
postgres=#
postgres=# ALTER USER postgres WITH PASSWORD 'mot_de_passe';
```
Dans la mesure du possible, tapez les commandes ci-dessus plutôt que de passer par un copié/collé: `psql` semble avoir une tendance un peu prononcée pour l'interprétation foireuse des données copiées. Pour la sécurisation du mot de passe, il est conseillé de modifier la méthode d'identification `ident` en `md5` dans le fichier `/var/lib/pgsql/pg_hba.conf`.
## Documentation en rab'
* `Pgsql en français <http://docs.postgresqlfr.org/>`_

View File

@ -0,0 +1,35 @@
---
Title: Lynis
Date: 2016-06-01
Tags: audit, securité, server
Slug: lynis
---
[Lynis](https://cisofy.com/lynis/) permet de faire un audit du système. Il s'installe et se lance très facilement (mais aura besoin d'un accès `root`) et vous proposera un ensemble de suggestions, de paramètres à modifier et la liste des choses en ordre.
```shell
sudo aptitude install lynis
sudo lynis --check-all
```
```shell
[+] Initializing program
------------------------------------
- Detecting OS... [ DONE ]
- Clearing log file (/var/log/lynis.log)... [ DONE ]
---------------------------------------------------
Program version: 1.6.3
Operating system: Linux
Operating system name: Debian
Operating system version: 8.4
Kernel version: 4.5.1
Hardware platform: x86_64
Profile: /etc/lynis/default.prf
Log file: /var/log/lynis.log
Report file: /var/log/lynis-report.dat
Report version: 1.0
Plugin directory: /etc/lynis/plugins
```
Au final, vous recevrez la liste des avertissements, erreurs et suggestions, accompagnés de liens permettent leur correction ou prise en charge :)

View File

@ -0,0 +1,48 @@
---
Title: Auto-update d'un site statique avec Pelican
Date: 2016-06-05
Slug: auto-update-site-pelican-git
Tags: http, nginx, git, pelican, amazon, s3
---
Petite mise à jour du script [du 26 juillet 2013]({filename}2013-07-26-pelican-webfaction.md), puisque j'ai déménagé le serveur [depuis un petit temps]({filename}../server/2015-12-29-scaleway.md) et que je passe dorénavant par [Nginx]({filename}../server/2015-04-03-nginx.md). Du workflow, je conserve le lien avec Git et la centralisation dans un Gitlab. Pour la publication, je garde la génération automatique, mais uniquement si le dépôt a été mis à jour.
Bon, à un moment, j'ai pensé déplacer le contenu statique vers `Amazon S3 <https://aws.amazon.com/fr/s3/>`_, avec un petit pointeur DNS vers le bon noeud. Il y a pas mal de ressources utiles sur ce point, je vous laisse donc faire votre shopping, mais ayant un serveur à disposition, j'ai +/- laissé tomber. Vous trouverez déjà tous les liens rassemblés ci-dessous:
* `Matt McManus - How I set up, designed, and published this website <http://www.mamcmanus.com/posts/how-i-built-this-site>`_
* `Matt McManus - Setting up a Pelican site under your own domain using Amazon S3 <http://www.mamcmanus.com/posts/amazon-s3-pelican-site>`_
* `Better Pelican hosting with Amazon S3 and CloudFront <https://pmac.io/2014/06/pelican-s3-cloudfront/>`_
* `Setting Up a blog with Pelican and Amazon S3 <http://lexual.com/blog/setup-pelican-blog-on-s3/>`_
* `Moving to Amazon S3 <http://cbudjan.com/2013/06/amazon-s3/>`_
* `Amazon S3 Versioning - What, how and why? <https://linuxacademy.com/blog/amazon-web-services-2/amazon-s3-versioning-what-how-why/>`_
Le script ressemble à ceci:
``` shell
#! /usr/bin/env bash
git fetch origin
reslog=$(git log HEAD..origin/master --oneline)
if [[ "${reslog}" != "" ]] ; then
echo "Updating pelican-site"
git merge origin/master
virtualenv -p /usr/bin/python3 .
source bin/activate
pip install -r requirements/base.txt
pelican
fi
```
Pour son exécution, j'ai ajouté une ligne dans Cron:
``` shell
30 * * * * /var/www/{site_name}/update_site.sh
```
Nginx pointe alors vers le répertoire `output`, qui sera généré par [Pelican](https://getpelican.com).
Sources
-------
* [Check if pull is needed](http://stackoverflow.com/questions/3258243/check-if-pull-needed-in-git/12791408#12791408)

View File

@ -0,0 +1,47 @@
---
Title: Certbot
Date: 2016-06-13
Slug: certbot
Tags: eff, nginx, letsencrypt, certificats
---
> Certbot, previously the Let's Encrypt Client, is EFF's tool to obtain certs from Let's Encrypt, and (optionally) auto-enable HTTPS on your server. It can also act as a client for any other CA that uses the ACME protocol.
Ma commande [Certbot-auto](https://github.com/certbot/certbot) pour la création et la prolongation des certificats sur mes serveurs:
``` shell
./certbot-auto certonly --standalone --standalone-supported-challenges http-01 --email me@grimbox.be -d grimbox.be
```
Si nécessaire, n'hésitez pas à ajouter les sous-domaines à la suite: `-d grimbox.be -d boulet.grimbox.be -d blabla.grimbox.be`...
Les certificats seront placés dans le répertoire ``/etc/letsencrypt/live/<nom_du_domaine>/``, et devront être référencés de la manière suivante dans Nginx:
``` shell
server {
listen 80;
listen [::]80;
server_name <nom_du_domaine>;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name <nom_du_domaine>;
ssl_certificate /etc/letsencrypt/live/<nom_du_domaine>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<nom_du_domaine>/privkey.pem;
[...]
}
```
Todos
=====
* Visiblement, il existe [un paquet dans les backports pour Jessie](https://certbot.eff.org/#debianjessie-nginx).
* Puis ajouter une élévation de privilèges pour arrêter/démarrer Nginx.

View File

@ -0,0 +1,51 @@
---
Title: Fail2ban
Date: 2016-06-13
Slug: fail2ban
---
Dans la catégorie des petits outils top biches, on a [fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page), qui est un *framework de prévention contre les intrusions*. Il permet par exemple de détecter un trop grand nombre de tentative de connexion. Fail2Ban est ensuite utilisé pour modifier les règles du pare-feu, pour rejeter par défaut l'adresse IP incriminée.
Par exemple, juste après son installation, on peut trouver ceci parmi les logs:
``` shell
2016-06-13 05:23:40,337 fail2ban.actions[4160]: WARNING [ssh] Ban 5.39.218.148
2016-06-13 05:33:41,022 fail2ban.actions[4160]: WARNING [ssh] Unban 5.39.218.148
2016-06-13 05:33:47,040 fail2ban.actions[4160]: WARNING [ssh] Ban 5.39.218.148
2016-06-13 05:43:47,717 fail2ban.actions[4160]: WARNING [ssh] Unban 5.39.218.148
2016-06-13 05:43:50,732 fail2ban.actions[4160]: WARNING [ssh] Ban 5.39.218.148
2016-06-13 05:53:51,398 fail2ban.actions[4160]: WARNING [ssh] Unban 5.39.218.148
2016-06-13 05:53:56,415 fail2ban.actions[4160]: WARNING [ssh] Ban 5.39.218.148
2016-06-13 06:03:57,097 fail2ban.actions[4160]: WARNING [ssh] Unban 5.39.218.148
2016-06-13 06:04:01,113 fail2ban.actions[4160]: WARNING [ssh] Ban 5.39.218.148
2016-06-13 06:14:01,807 fail2ban.actions[4160]: WARNING [ssh] Unban 5.39.218.148
2016-06-13 06:14:07,825 fail2ban.actions[4160]: WARNING [ssh] Ban 5.39.218.148
2016-06-13 06:24:08,516 fail2ban.actions[4160]: WARNING [ssh] Unban 5.39.218.148
```
On voit clairement qu'il s'agit d'une tentative de connexion malicieuse: toujours la même adresse IP, à des heures pas franchement folichonnes. Ici, Fail2Ban est configuré pour *relâcher* une IP après une dizaine de minutes.
Pour modifier ce comportement, on va ajouter un mécanisme de *jails*: ouvrez le fichier `/etc/fail2ban/jail.conf`. La section `[DEFAULT]` reprend des paramètres classiques (*bantime* de 10 minutes, nombre maximal de tentatives à 6, ...). Il est possible de spécifier ces *jails*, en prenant par exemple la configuration pour `SSH`:
``` shell
[ssh]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
```
Et voila! Ban d'1h, nombre max de tentatives: 3. On redémarre ensuite Fail2Ban *via* `/etc/init.d/fail2ban restart`.
Deux-trois liens supplémentaires:
* [How to protect SSH with Fail2ban on Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-protect-ssh-with-fail2ban-on-ubuntu-14-04)
* [How to protect Nginx with Fail2ban on Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-protect-an-nginx-server-with-fail2ban-on-ubuntu-14-04) (parce que oui, on peut aussi monitorer les tentatives de connexion foireuses directement *via* Nginx).

View File

@ -0,0 +1,36 @@
---
Title: Owncloud -> Nextcloud
Date: 2016-07-08
Slug: from-owncloud-to-nextcloud
Tags: http, nginx, conf, server, owncloud, nextcloud
---
Suite aux dernières annonces concernant Owncloud, je suis fraîchement passé sur son fork, Nextcloud. Après l'installation, qui a consisté en gros à
1. Copier les données et le fichier de conf',
2. Ecraser sauvagement tous les fichiers présents,
3. remettre les données et le fichier `config.php` (et oui, cela fonctionne),
il restait deux-trois problèmes liés à la configuration du serveur-même. Tous ces avertissements sont dispos en grands, gras, gros et large sur n'importe quelle page d'administration. Il suffit juste de creuser un peu la documentation pour trouver la solution.
Pour ma part:
> php ne semble pas être configuré de manière à récupérer les valeurs des variables denvironnement. Le test de la commande getenv("PATH") retourne seulement une réponse vide.
Il suffit de modifier le fichier `/etc/php5/fpm/pool.d/www.conf` et de suivre [les recommandations](https://docs.nextcloud.org/server/9.0/admin_manual/installation/source_installation.html#php-fpm-configuration-notes).
> L'en-tête HTTP "X-Download-Options" n'est pas configurée pour être égale à "noopen" créant potentiellement un risque relié à la sécurité et à la vie privée. Il est donc recommandé d'ajuster ce paramètre.
On ajoute simplement `add_header X-Download-Options noopen;` dans la conf' Nginx.
> L'en-tête HTTP "X-Permitted-Cross-Domain-Policies" n'est pas configurée pour être égale à "none" créant potentiellement un risque relié à la sécurité et à la vie privée. Il est donc recommandé d'ajuster ce paramètre.
Idem que ci-dessus: on ajoute `add_header X-Permitted-Cross-Domain-Policies none;`.
> Des fichiers n'ont pas passé la vérification dintégrité. Consultez la documentation pour avoir plus d'informations sur comment résoudre ce problème.
Ici, c'était un peu plus con: deux fichiers n'étaient pas conformes. En fait, le problème était identique pour les deux: la commande `cp -r <origine> <destination>` ne copie pas les *dotfiles*. J'avais donc un fichier avec le mauvais hash (`.htaccess`) et un fichier manquant (`.user.ini`).
> Aucun cache mémoire n'est configuré. Si possible, configurez un cache pour augmenter les performances. Consultez la documentation pour avoir plus d'informations à ce sujet.
Là, par contre, [l'installation de memcache](https://docs.nextcloud.org/server/9.0/admin_manual/configuration_server/oc_server_tuning.html) sera pour plus tard ;)

Some files were not shown because too many files have changed in this diff Show More