Empaquetez (facilement) votre projet avec upt

Magazine
Marque
GNU/Linux Magazine
Numéro
222
Mois de parution
janvier 2019
Spécialité(s)


Résumé
Les logiciels que nous utilisons sur nos distributions GNU/Linux et *BSD proviennent généralement des dépôts associés à ces dernières. Ils y sont présents, car les mainteneurs de notre OS préféré les ont empaquetés : cela nous évite de devoir recompiler les sources nous-mêmes, de résoudre les problèmes de dépendances ou de parfaire l'intégration avec le reste de notre système. Nous verrons dans cet article comment les empaqueteurs ont réussi à automatiser une partie de leur travail. Nous proposerons ensuite une solution logicielle permettant à toutes les distributions d'empaqueter facilement le code disponible sur des plateformes telles que PyPI, RubyGems ou CPAN.

Body


1. Le travail de l'empaqueteur

1.1 Convertir un paquet

Cet article concerne uniquement les logiciels disponibles sur des dépôts spécifiques à un langage particulier (tels que PyPI, RubyGems, CPAN) et généralement installables grâce aux gestionnaires de paquets associés (tels que pip, gem ou cpanm). Dans ce cas, le travail de l'empaqueteur consiste principalement à convertir un paquet upstream en un paquet downstream.

Prenons l'exemple de Python. La plateforme PyPI fournit, pour chaque projet qui s'y trouve, un tarball par version, ainsi que des métadonnées (numéro de version, description du paquet, etc.). Le mainteneur d'une distribution doit réorganiser le code et les métadonnées afin de se conformer au format utilisé par sa distribution. Ainsi, la bibliothèque requests nécessite, sous Fedora, la création des fichiers suivants :

python-requests/

├── dont-import-OrderedDict-from-urllib3.patch

├── Don-t-inject-pyopenssl-into-urllib3.patch

├── patch-requests-certs.py-to-use-the-system-CA-bundle.patch

├── python-requests.spec

├── Remove-tests-that-use-the-tarpit.patch

├── requests-2.12.4-tests_nonet.patch

└── sources

Fedora choisit de patcher le code, d'où la présence de nombreux patches. Les métadonnées, la liste des dépendances, les instructions spécifiques à l'installation, etc. sont présentes dans le fichier python-requests.spec. Enfin, le fichier sources contient le SHA512 du tarball utilisé.

Sous Debian, le format est différent, mais l'idée est similaire :

requests/debian/

├── changelog

├── clean

├── compat

├── control

├── copyright

├── docs

├── python3-requests.pyremove

├── python-requests.pyremove

├── rules

├── source

│   └── format

├── upstream

│   └── signing-key.asc

└── watch

Le code est ici aussi patché (les fichiers .pyremove listent des modules à supprimer), les dépendances sont spécifiées dans control, etc.

Un dernier exemple, avec les ports d'OpenBSD :

py-requests

├── distinfo

├── Makefile

├── pkg

│   ├── DESCR

│   └── PLIST

Ici, un simple Makefile décrit la procédure d'installation. On retrouve également des métadonnées (liste des fichiers dans PLIST, description du paquet dans DESCR, hachage cryptographique dans distinfo).

1.2 Difficultés

Empaqueter des logiciels est un travail plutôt long et quelque peu fastidieux. Voyons pourquoi.

1.2.1 De multiples sources

Il existe aujourd'hui de nombreux langages, généralement fournis avec un gestionnaire de paquets qui permet d'installer des paquets provenant d'une plateforme spécifique. Il est donc souvent nécessaire de connaître plusieurs environnements logiciels, similaires, mais différents.

1.2.2 De multiples cibles

Les développeurs procèdent rarement eux-mêmes à l'empaquetage de leur code : un développeur Python, par exemple, fera en sorte que sa production soit installable depuis PyPI, avec pip, mais déléguera l'empaquetage pour Debian/Fedora/OpenBSD à d'autres développeurs.

Un même logiciel est donc empaqueté de multiples fois, ce qui crée une grande quantité de travail. Il est toutefois à noter que certains paquets peuvent être partagés entre des distributions (un paquet Debian se porte relativement facilement sur Ubuntu, DragonflyBSD réutilise les ports FreeBSD…).

1.2.3 L'enfer des dépendances

La plupart des logiciels ne sont pas écrits « depuis zéro » : ils utilisent des bibliothèques. Empaqueter un logiciel nécessite donc d'empaqueter ses dépendances, et les dépendances de ses dépendances, etc. Un empaqueteur doit donc souvent créer plusieurs paquets d'un coup. Les bibliothèques requises peuvent déjà exister dans la distribution, mais dans une version trop ancienne : il faut alors les mettre à jour. On rencontre également des dépendances circulaires, qui sont un casse-tête.

1.2.4 Déverminage

Une fois que le paquet est écrit et installé, il se peut que le logiciel empaqueté ne se lance pas, ou présente des bugs. Il faut donc débugger le code, communiquer avec l'upstream et inclure des patches dans le paquet : il se peut qu'ils soient spécifiques à la distribution, ce qui veut dire qu'il faudra les maintenir à tout jamais, sans espoir de les voir inclus upstream.

2. Automatisation

De nombreuses étapes de la création d'un paquet semblent automatisables. Ainsi, créer un répertoire, créer des fichiers, noter la version du logiciel… ne sont pas des tâches passionnantes et sont souvent réalisées mécaniquement. Est-il possible pour les empaqueteurs d'automatiser ces étapes faciles et de se concentrer sur la gestion des dépendances et le débuggage ?

2.1 Disponibilité des métadonnées

Des plateformes telles que PyPI ou RubyGems montrent à leurs utilisateurs énormément de métadonnées sur des pages HTML. Mais elles sont également accessibles en JSON !

Pour les paquets Python, les informations sont disponibles à l'adresse https://pypi.org/pypi/<paquet>/json. Par exemple, pour la bibliothèque requests, on obtient cette sortie (tronquée par souci de lisibilité) :

{

   "info" : {

      "name" : "requests",

      "version" : "2.19.1",

      "summary" : "Python HTTP for Humans.",

      "home_page" : "http://python-requests.org",

      "license" : "Apache 2.0",

      "requires_dist" : [

         "PySocks (!=1.5.7,>=1.5.6); extra == 'socks'",

         "idna (>=2.0.0); extra == 'security'",

         "cryptography (>=1.3.4); extra == 'security'",

         "pyOpenSSL (>=0.14); extra == 'security'",

         "certifi (>=2017.4.17)",

         "urllib3 (<1.24,>=1.21.1)",

         "idna (<2.8,>=2.5)",

         "chardet (<3.1.0,>=3.0.2)"

      ],

   }

   ...

}

On voit ici qu'on retrouve des indications basiques sur le paquet (nom, version, description sommaire…) ainsi que des informations avancées : la liste des dépendances est donnée ! Certaines sont obligatoires (certifi, urllib3, idna et chardet), les autres sont optionnelles.

RubyGems fournit également du JSON (https://rubygems.org/api/v1/gems/<paquet>.json), ainsi que CPAN (https://fastapi.metacpan.org/v1/release/<paquet>). Le format est différent pour chaque outil, mais on y retrouve le même type de métadonnées.

On comprend alors qu'il est sans doute possible d'automatiser une partie non négligeable du travail d'un empaqueteur. Il n'est en effet pas très compliqué d'écrire un script qui prend en paramètre un nom de paquet, construit la bonne URL, récupère le JSON, le parcourt et réécrit les informations obtenues dans le format attendu par la distribution.

2.2 Les outils des distributions

De nombreux développeurs ont eu cette idée, et ils ont écrit plusieurs outils. En voici une liste non exhaustive :


Debian

Fedora

Guix

FreeBSD

OpenBSD

CPAN

dh-make-perl

cpan2rpm

guix import

?

PortGen

NPM

npm2deb

npm2rpm

N/A

?

?

PyPI

pypi2deb

pyp2rpm

guix import

pytoport

PortGen

Ruby

gem2deb

gem2rpm

guix import

?

PortGen

On remarque que Debian et Fedora ont chacun un outil par plateforme upstream. GNU Guix et OpenBSD ont chacun un outil unique, respectivement guix import et PortGen. Cette façon de faire a plusieurs avantages :

  • factoriser une partie du code ;
  • proposer à l'utilisateur une interface unifiée, alors que les outils Debian/Fedora ont tous des options différentes ;
  • proposer à l'utilisateur un comportement unifié : certains outils vont se contenter de créer la source du paquet, d'autres vont procéder à la compilation… Un utilisateur voulant utiliser plusieurs de ces logiciels risque de s'emmêler les pinceaux.

2.3 Le vrai besoin

Ne pourrait-on pas écrire un seul outil qui remplacerait tous ceux vus précédemment ? Voyons comment cela pourrait constituer une amélioration de l'existant.

2.3.1 Interface unique

Un développeur d'une distribution (par exemple Debian) souhaitant empaqueter deux logiciels venant de deux plateformes différentes (par exemple PyPI et RubyGems) doit utiliser deux logiciels différents pour l'aider dans son travail (ici, pypi2deb et gem2deb).

De même, un développeur Python qui voudrait empaqueter lui-même son logiciel pour deux distributions différentes devrait utiliser deux outils différents (pypi2deb et pyp2rpm, par exemple).

Utiliser plusieurs outils différents implique de mémoriser plusieurs lignes de commandes différentes, d'écrire éventuellement un fichier de configuration pour chaque outil, de se souvenir des particularités des uns et des autres… Il serait beaucoup plus simple d'avoir une seule interface qui pourrait gérer toutes les plateformes de distribution de code et toutes les distributions.

2.3.2 Comportement unifié

De la même façon, devoir jongler entre plusieurs outils impose de connaître leurs comportements : pour Debian, il est possible qu'un outil crée le dossier debian/, et pas un autre ; un outil va lancer la construction du .deb, et pas un autre ; etc.

Avoir un seul outil, capable de gérer tous les dépôts upstream et toutes les distributions, permettrait d'avoir toujours le même comportement.

2.3.3 Outil modulaire

Tous ces outils réimplémentent souvent des fonctionnalités identiques. Ainsi, tous les logiciels travaillant avec des paquets Python vont lire le JSON présenté plus haut afin de récupérer des métadonnées intéressantes. Ils vont aussi essayer de trouver les dépendances d'un paquet donné. Et c'est souvent beaucoup plus compliqué qu'il n'y paraît. Elles peuvent être données dans le JSON, mais ce n'est pas toujours le cas. Elles peuvent être trouvées dans le fichier .whl distribué sur PyPI (un fichier zip pouvant être installé par pip), mais pas forcément. Il n'est pas toujours évident de savoir quelles dépendances sont uniquement requises pour les tests, lesquelles sont optionnelles, etc. Cela s'explique par le fait que les pratiques concernant l'empaquetage et les métadonnées des paquets Python ont beaucoup évolué au cours des dernières années. Le lecteur désireux d'en savoir plus pourra visionner la conférence donnée par Joachim Jablon et Stéphane Angel lors de la PyCon FR 2017 [1], dont les slides sont disponibles en ligne [2]. Même récupérer la licence du paquet demande plus d'efforts que simplement lire une ligne du fichier JSON contenant les métadonnées.

De même, tous les outils créant des paquets pour une distribution spécifique vont devoir créer les mêmes dossiers, les mêmes fichiers, et produiront les mêmes bugs.

On pourrait donc éviter de dupliquer énormément de code en écrivant un logiciel modulaire : chaque plateforme upstream aurait son propre module, chaque distribution downstream aurait son propre module, et tous ces modules communiqueraient entre eux.

3. Un outil unique : upt (the Universal Packaging Tool)

3.1 Concept

upt

Fig. 1 : Architecture d'upt.

Comme on peut le voir en figure 1, les informations disponibles sur les plateformes upstream (PyPI, CPAN, RubyGems) sont lues par des modules frontend (upt-pypi, upt-cpan, upt-rubygems), qui les transmettent à upt. Ce dernier les fait ensuite parvenir aux modules backend (upt-fedora, upt-freebsd, upt-guix, upt-nix, upt-openbsd), qui génèrent du code downstream (un fichier .spec, un Makefile, du code Guile, ou un fichier default.nix).

Bien évidemment, il n'est pas nécessaire d'utiliser tous les modules : ainsi, pour empaqueter un logiciel écrit en Python pour Fedora, seuls upt-pypi, upt et upt-fedora sont nécessaires. De même, pour empaqueter un logiciel disponible sur CPAN pour OpenBSD, seuls upt-cpan, upt et upt-openbsd devront être installés.

Cette architecture n'est pas particulièrement originale : les compilateurs fonctionnent de la même façon. Leurs frontends savent parcourir le code d'un langage particulier (C ou C++ par exemple), le transformer en une représentation interne, qui est utilisée par leurs backends afin de générer du code assembleur pour une plateforme donnée (x86, x64, MIPS…). De même, pandoc [3] sait convertir des documents d'un langage de balisage à un autre grâce à une architecture similaire.

Terminons la présentation de l'architecture en notant que les modules upt ont chacun leur propre dépôt git. Il est ainsi possible de créer un nouveau module sans devoir le faire valider par l'upstream du projet upt.

3.2 Utilisation

Installer upt se fait facilement depuis PyPI :

$ pip install upt

On peut ensuite installer les modules dont nous avons besoin un par un :

$ pip install upt-pypi

$ pip install upt-fedora

...

On peut aussi installer upt et tous les frontends disponibles :

$ pip install upt[frontends]

Ou installer upt et tous les backends disponibles :

$ pip install upt[backends]

Ou encore upt et tous les modules disponibles :

$ pip install upt[frontends,backends]

Il est ensuite possible de vérifier quels modules sont disponibles :

$ upt list-frontends

cpan

pypi

rubygems

$ upt list-backends

fedora

freebsd

guix

nix

openbsd

Essayons maintenant d'empaqueter upt pour FreeBSD, depuis PyPI :

$ upt package --frontend pypi --backend freebsd -o /tmp/upt upt

[INFO ] [Backend] Creating /tmp/upt/py-upt

[INFO ] [Backend] Creating /tmp/upt/py-upt/Makefile

[INFO ] [Backend] Creating /tmp/upt/py-upt/pkg-descr

[INFO ] [Backend] Creating /tmp/upt/py-upt/distinfo

make: Entering directory '/tmp/upt/py-upt'

Makefile:20: *** missing separator. Stop.

make: Leaving directory '/tmp/upt/py-upt'

[WARNING ] [Backend] make makesum failed. Not generating /tmp/upt/py-upt/distinfo

$ tree /tmp/upt/py-upt/

/tmp/upt/py-upt/

├── Makefile

└── pkg-descr

0 directories, 2 files

On voit que le backend upt-freebsd a réussi à créer les fichiers Makefile et pkg-descr, mais qu'il n'a pas su générer distinfo. C'est normal : la création de ce fichier se fait via la commande make makesum, lancée depuis l'arbre des ports FreeBSD. Celle-ci échoue, car la commande est ici lancée sur Debian, mais fonctionnerait sur une machine FreeBSD.

$ cat /tmp/upt/py-upt/Makefile

# $FreeBSD$

PORTNAME= upt

DISTVERSION= 0.4.1

CATEGORIES= XXX python

MASTER_SITES= CHEESESHOP

PKGNAMEPREFIX= ${PYTHON_PKGNAMEPREFIX}

MAINTAINER= python@FreeBSD.org

COMMENT= Package software from any package manager to any distribution

LICENSE= BSD3CLAUSE

LICENSE_FILE= ${WRKSRC}/XXX

RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}spdx-lookup>0:XXX/py-spdx-lookup@${FLAVOR}

USES= python

USE_PYTHON= autoplist distutils

.include <bsd.port.mk>

Le Makefile généré n'est pas prêt à être utilisé : certaines informations n'ont pas pu être obtenues par upt, qui a inséré la chaîne de caractères XXX afin de signifier au mainteneur ce qu'il doit corriger. Ainsi upt n'a pas su déterminer à quelle catégorie de ports ce paquet appartient, ni le fichier dans lequel est contenu la licence, ni la catégorie de l'unique dépendance de ce paquet.

Il reste donc un peu de travail au mainteneur, mais beaucoup moins que s'il avait dû tout écrire lui-même ! Les outils qu'upt entend remplacer ne sont en général pas non plus capables de générer un paquet complètement valide, qui ne nécessite aucune retouche.

3.3 Ajouter un module

Un des intérêts de l'architecture modulaire d'upt est de permettre d'ajouter (relativement) facilement de nouveaux frontends ou backends. Le projet met à disposition un modèle [4] au format cookiecutter [5]. Si nous voulions créer un backend pour Arch Linux, il serait recommandé de procéder comme suit :

$ cookiecutter https://framagit.org/upt/cookiecutter-upt

project_name []: upt-archlinux

project_slug [upt_archlinux]:

author []: John Doe

email []: john@doe.org

short_description []: Arch Linux backend for upt.

url [https://framagit.org/upt/upt-archlinux]:

Select license:

1 - BSD-3

2 - other

Choose from 1, 2 (1, 2) [1]: 1

Select kind:

1 - backend

2 - frontend

Choose from 1, 2 (1, 2) [1]: 1

entrypoint_name [archlinux]:

main_class_name [ArchlinuxBackend]:

Initialized empty Git repository in /tmp/upt-archlinux/.git/

[master (root-commit) 76b7a4f] Initial commit.

 12 files changed, 192 insertions(+)

 create mode 100644 .gitignore

 create mode 100644 CHANGELOG

 create mode 100644 LICENSE

 create mode 100644 MANIFEST.in

 create mode 100644 README.md

 create mode 100644 setup.cfg

 create mode 100644 setup.py

 create mode 100644 tox.ini

 create mode 100644 upt_archlinux/__init__.py

 create mode 100644 upt_archlinux/templates/base.tmpl

 create mode 100644 upt_archlinux/tests/__init__.py

 create mode 100644 upt_archlinux/upt_archlinux.py

Il suffit de répondre à quelques questions afin de créer automatiquement un dépôt git contenant un squelette de module. Il n'y a ensuite plus qu'à compléter le code du module. Le README du projet upt [6] contient des informations au sujet de l'API d'upt, que les lecteurs intéressés pourront consulter.

4. Une autre approche : fpm

L'auteur d'upt (qui est aussi le rédacteur de cet article) a bien entendu fait un état de l'art, et a découvert fpm [7] dont le concept est similaire à celui d'upt.

Cet exemple tiré de la documentation officielle montre comment utiliser fpm :

% fpm -s python -t rpm pyramid

Trying to download pyramid (using easy_install)

Searching for pyramid

Reading http://pypi.python.org/simple/pyramid/

Reading http://docs.pylonshq.com

Reading http://docs.pylonsproject.org

Best match: pyramid 1.0

...

Created /home/jls/python-pyramid-1.0.noarch.rpm

Notons que fpm, écrit en Ruby, supporte énormément d'options pour chacun de ses modules, afin de permettre aux utilisateurs de créer des paquets aux petits oignons.

Comme upt, fpm a une approche modulaire et sait même gérer plus de sources et de cibles qu'upt ! La principale différence est qu'il génère un paquet binaire plutôt qu'un paquet source. Par exemple, dans le cas de Fedora, il pourra créer un fichier RPM, mais ne créera pas le fichier .spec associé [8]. C'est donc un outil très utile pour créer un paquet installable par le gestionnaire de votre distribution, mais il ne permet pas vraiment de réaliser le travail d'un empaqueteur.

Conclusion

Nous avons présenté dans cet article le travail des empaqueteurs, et les outils qu'ils utilisent actuellement pour automatiser une partie de ce travail. Nous avons ensuite défendu l'idée qu'un outil modulaire pourrait avantageusement remplacer la pléthore d'outils spécifiques actuellement utilisés. Enfin, nous avons décrit l'architecture d'un tel outil, et présenté quelques exemples d'utilisation d'upt. De nombreux modules (backends comme frontends) restent cependant encore à écrire avant que ce logiciel puisse vraiment être un outil d'empaquetage universel. Si l'absence d'un module vous empêche d'utiliser upt, n'hésitez pas à venir en discuter sur #upt-packaging, sur Freenode.

Références

[1] Programme de la PyConFR, « Les aventuriers du packaging perdu » : https://www.pycon.fr/2017/programme.html#les-aventuriers-du-packaging-perdu

[2] Diapositives - « Les aventuriers du packaging perdu » : https://twidi.github.io/python-packaging-talk/fr.html

[3] Site officiel de pandoc : https://pandoc.org/

[4] Dépôt git de cookiecutter-upt : https://framagit.org/upt/cookiecutter-upt

[5] Documentation de cookiecutter : https://cookiecutter.readthedocs.io/en/latest/

[6] Fichier README d'upt : https://framagit.org/upt/upt/blob/master/README.md

[7] Site officiel de fmp : https://fpm.readthedocs.io/en/latest/

[8] Discussion au sujet du comportement de fpm : https://github.com/jordansissel/fpm/issues/1507#issuecomment-411390171




Article rédigé par

Par le(s) même(s) auteur(s)

Étendez Pandoc avec Lua

Magazine
Marque
GNU/Linux Magazine
Numéro
199
Mois de parution
décembre 2016
Spécialité(s)
Résumé
Il est parfois nécessaire de convertir un fichier d'un langage de balisage vers un autre : de Markdown vers du HTML, d'Org-mode vers LaTeX, etc. Pandoc est un outil permettant ce type de conversion, et il est capable de gérer un nombre impressionnant de formats différents. Que faire si l'on souhaite utiliser un format ésotérique inconnu de Pandoc ? Il est possible de l'étendre en Lua !

Les derniers articles Premiums

Les derniers articles Premium

PostgreSQL au centre de votre SI avec PostgREST

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Dans un système d’information, il devient de plus en plus important d’avoir la possibilité d’échanger des données entre applications. Ce passage au stade de l’interopérabilité est généralement confié à des services web autorisant la mise en œuvre d’un couplage faible entre composants. C’est justement ce que permet de faire PostgREST pour les bases de données PostgreSQL.

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 125 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous