Le Blog de Thomas

Logiciels libres, Linux embarqué, et autres ...

Jour 2, Jeudi 21 Juillet: Encore des conférences


Pour le petit-déjeuner, on commence par un petit détour au Starbucks de la veille. Impressionant, le gars se souvient de nous, et nous demande si on veut la même chose qu'hier. Je trouve ça impressionant que dans une si grosse ville, avec autant de monde, il ait pu se souvenir de nous, comme si on était au café du village perdu au fin fond de la France. D'ailleurs, c'est peut-être à cause de notre look de français et de notre accent français qu'il s'est souvenu de nous ;-)

Nfsim: untested code is buggy code


Cette http://www.linuxsymposium.org/2005/view_abstract.php?content_key=70 était donnée par le fameux Rusty Russell (qui a donné il y a deux ans une interview sur KernelTrap), avec Jeremy Kerr, un de ses collègues de travail au Linux Technology Center d'IBM. Les slides de la présentation sont disponibles au format OpenOffice.

Les écluses menant du fleuve des Outaouais au canal
Les écluses menant du fleuve des Outaouais au canal

Russell commence par exposer l'infrastructure de test qui était utilisée pour Netfilter, le firewall du noyau Linux, avant son travail sur Nfsim. Cette infrastructure était principalement composée de scripts shell qui appelent de basiques programmes en C. Néanmoins, ce n'était pas suffisant, et Rusty dresse une liste de ce que devrait permettre le nouvel outil : être simple à exécuter, que ce soit facile d'écrire de nouveaux tests, que ce soit écrit dans un langage connu des développeurs, que ça permette l'utilisation d'outils de débogages modernes tels que Valgrind, que les tests aient une bonne couverture du code et que celle-ci soit mesurable, et surtout que ça donne envie aux développeurs de l'utiliser. Ce dernier point est particulièrement important: les développeurs rechignent souvent à faire des tests unitaires automatisés, car ce n'est pas perçu comme une activité aussi
créatrice que le développement du code principal.

Nfsim tente de répondre à ces objectifs. Nfsim est une implémentation en mode utilisateur des API noyau utilisées par Netfilter, ainsi qu'une interface en ligne de commande. Il permet donc d'exécuter le code de Netfilter comme une application utilisateur, tout simplement, ce qui facilite grandement les tests. Lors de la compilation de Nfsim, ce sont directement les sources du noyau dans net/ipv4/netfilter/ qui sont récupérées et compilées. Le code noyau représente 25.000 lignes de code. Le code de Nfsim lui-même est composé de 2000 lignes de code pour simuler une pile Ipv4, de 1000 lignes de code pour implémenter la glue remplaçant l'API noyau et de 9000 lignes de code concernant l'interface en ligne de commande. L'outil réutilise même le code d'iptables, la commande utilisée pour contrôler le Netfilter du noyau : en utilisant un LD_PRELOAD, Nfsim intercepte les lectures/écritures de /proc et les appels sockopt() réalisés par iptables. Ainsi, le code d'iptables n'est pas dupliqué !

Les tests sont écrits dans un langage simple, donc faciles à écrire, Nfsim est écrit en langage C, familier des développeurs, on peut lancer la suite de test avec Valgrind pour trouver des bugs ou avec Gcov pour mesurer la couverture de code. Rusty et son collègue font une petite démonstration de l'outil. Effectivement, en quelques secondes, le code de Netfilter et Nfsim est compilé, et les suites de test sont exécutés. Génial !

La présentation se poursuit pour parler plus précisement de la couverture de code. Souvent, les tests couvrent le chemin d'exécution “normal” du code, et ne testent pas les cas d'erreur (une allocation mémmoire qui échoue, par exemple). Ceci implique qu'une grande partie du code est mort, qu'il n'est pas testé. Et comme l'indique le titre de la présentation : du code non testé est du code buggé. Les tests doivent donc couvrir à la fois le succès des différentes opérations, mais également l'échec de ces opérations.

Le problème, c'est que les opérations qui peuvent échouer sont nombreuses : allocations mémoire, copies de et vers l'espace utilisateur (les fameux copy_from_user et copy_to_user), l'enregistrement d'entrées dans le /proc ou les opérations sur les sémaphores. Le nombre de cas à tester est gigantesque, exponentiel !

Pour simuler des échecs, des appels à une fonction magique should_i_fail() sont faits dans les fonctions qui peuvent échouer, et ça leur indique si il faut échouer ou non. Par exemple, lorsqu'une fonction d'allocation mémoire appelle should_i_fail() et que cette dernière renvoie vrai, alors l'allocation doit échouer. Simple non ?

Pour gérer l'arbre des cas de succès et d'erreur, Rusty et son collègue ont été malins : au lieu de gérer ça de manière complexe dans leur logiciel de test, ils reposent entièrement sur le mécanisme de fork(). À chaque fois que quelque chose peut échouer, ils forkent le processus de test : le parent testera le cas de succès, et le fils testera le cas d'échec. En général, le fils ne s'exécutera pas longtemps, car le cas d'erreur ne permet pas d'aller très loin. Ainsi, l'arbre des cas est testé grâce à l'arbre des processus.

Les tests de regression prennent 5 secondes à s'éxécuter. Avec le test des cas d'erreurs, on passe à 7 minutes et demi, ce qui reste tout à fait raisonnable. Les tests de regression dans Valgrind prennent seulement 3 minutes, par le test des cas d'erreurs dans Valgrind prend ... plus de 18 heures. D'après Rusty, Nfsim a permis de trouver un certain nombre de bugs dans Netfilter, il en donne quelques exemples dans les slides.
Dans les idées pour le futur, les présentateurs ont mentionné la possibilité d'injecter dans Nfsim une configuration complète de firewall et d'interfaces réseau pour diagnostiquer les problèmes d'utilisateur ou la possibilité de simuler des réseaux virtuels avec plusieurs machines. Ils aimeraient également étendre Nfsim, ou au moins l'idée de Nfsim, à d'autres parties du noyau.

J'ai vraiment trouvé cette conférence très intéressante, et le travail réalisé sur Nfsim me semble remarquable. Du code noyau est en général assez difficile à tester, il faut mettre en place tout un environnement (des machines en réseau et envoyer les paquets qu'il faut dans le cas Netfilter) pour tester un bout de code. C'est tellement complexe que personne ne le fait. Là, Nfsim permet d'automatiser ce processus de test, ce qui donne envie de l'utiliser plus fréquemment, et donc de trouver plus rapidement des régressions ou des bugs. Ce genre de tests unitaires est vraiment quelque chose à faire pour d'autres parties du noyau, ou même d'autres logiciels. Le problème, c'est que parfois, l'environnement de simulation est difficile à reproduire. Par exemple, dans le cas d'une application graphique par exemple, il est difficile d'automatiser les clics de souris, et d'automatiser la vérification du résultat affiché de manière graphique. De même, pour certaines parties du noyau, la quantité de choses à réimplémenter en espace utilisateur risque d'être très importante.

Pour finir, je ne peux que vous recommander d'aller consulter le site de Nfsim, et d'envisager le même type de mécanisme pour votre projet logiciel ! ;-)

Linux Multipathing


Cette http://www.linuxsymposium.org/2005/view_abstract.php?content_key=91, présentée par Ed Goggin et Christophe Varoqui, présentaient l'implémentation du multipathing dans le noyau Linux. Comme moi au moment de la conférence, vous ne savez certainement pas ce qu'est le multipathing, et je vais donc tâcher d'expliquer ce que j'en ai compris (les informations sont à vérifier scrupuleusement...).

Le multipathing est utile dans le cadre des SAN, ou Storage Area Network. En gros, ce sont des baies de disques matérielles accessibles par le réseau en mode bloc (contrairement aux NAS qui sont accessibles au travers d'un système de fichiers). Tout ce bazar est souvent interconnecté grâce à du Fibre Channel. La façon dont les serveurs et les SAN sont connectés font qu'il peut exister plusieurs chemins depuis un serveur pour accéder aux unités logiques du SAN.

Utiliser ces multiples chemins peut être utile, soit pour améliorer la disponibilité (lorsqu'un chemin devient inutilisable, on en utilise un autre) ou pour améliorer les performances (en utilisant plusieurs liens simultanément). La conférence visait donc à présenter la solution permettant de réaliser cela sous Linux.

L'implémentation du multipathing repose dans le noyau du device mapper, un mécanisme générique du noyau qui permet de modifier les requêtes blocs en “empilant” des pilotes. Par exemple, device mapper est utilisé pour implémenter du RAID logiciel. Dans le cas du multipathing, device mapper permet à un pilote spécifiquer de créer un ou plusieurs périphériques bloc “virtuels” à partir d'un ou plusieurs périphériques bloc “physiques”. Dans le cas du multipathing, le module se charge de répartir les requêtes bloc sur les différents chemins menant au périphérique bloc “physique” (en réalité accessible via le réseau Fibre Channel), et de marquer les chemins non-fonctionnels comme tels. Ainsi, il ne retournera une erreur que si tous les chemins menant au périphérique bloc physique concerné sont inutilisables.

Au niveau de l'espace utilisateur, un démon appelé multipathd est averti (grâce à hotplug et udev) des problèmes rencontrés par le noyau lors de l'utilisation de certains chemins. Il va alors tester ces chemins, et lorsque certains redeviendront actifs, il informera le noyau de leur disponibilité, afin que ce dernier puisse à nouveau les utiliser.

Grâce à device mapper, la partie noyau pour utiliser les différents chemins ne fait que 2000 lignes, le gros du code étant donc en espace utilisateur (environ 17.000 lignes d'après les développeurs).

En réalité, l'architecture est un peu plus complexe que ça, elle est expliquée en détail dans le papier et dans le Book Reference disponible sur le Wiki de Christophe Varoqui.
Ce qui m'a particulièrement étonné, c'est que l'on peut créer des groupes de chemins et les gérer différemment, ce qui sous-entend qu'il peut avoir de très nombreux chemins pour accéder à un périphérique bloc. Je ne connaissais pas du tout ces systèmes de stockage (et d'ailleurs, je ne les connais pas encore vraiment), et j'ai été surpris par les possibilités de ces choses.

State of the Art: Where we are with the Ext3 Filesystem ?


Cette http://www.linuxsymposium.org/2005/view_abstract.php?content_key=90 de Mingming Cao, d'IBM, visait à faire le tour sur les travaux réalisés ou en cours au niveau du système de fichiers Ext3. Ext3 est certainement le système de fichiers le plus utilisé sous Linux. Il s'agit d'une extension d'Ext2 qui supporte la journalisation. Sur disque, il a exactement la même structure qu'Ext2, et cet objectif de conservation de la compatibilité semble être important pour les développeurs.

La présentatrice a détaillé chaque amélioration, déjà apportée ou en cours de réalisation, de manière assez précise, prenant le temps d'expliquer en quoi cela consistait et quels avantages cela apportait. La présentation, agrémentée de nombreux schémas, était très compréhensible, et donc vraiment intéressante même pour quelqu'un qui n'y connaît rien en structure des systèmes de fichiers. C'est d'ailleurs à ça qu'on reconnaît un bon présentateur: la capacité à expliquer de manière simple et abordable des choses assez complexe. Je déteste les conférences où le présentateur se positionne à un niveau que seuls les 2 autres développeurs du domaine peuvent comprendre de quoi il parle. C'est strictement inutile, ça n'apporte rien à l'auditoire. Bref, revenons à cette conférence sur Ext3, qui elle, était très bien.

Au niveau des améliorations déjà apportées, il a été question de :

Pour les points suivants, des patches sont déjà prêts, mais ils nécessitent encore un peu de travail avant d'être soumis à l'intégration dans la branche principale du noyau. Il s'agit de :

Dans la suite de la présentation, elle a précisé que pour eux, il était primordial que ext3 ne change pas la représentation des données sur le disque et reste compatible avec ext2. Pour cette raison, l'implémentation des extents n'est pas vraiment possible, et ils travaillent donc sur des solutions de préallocation n'utilisant pas d'extents.

Il a également été question de l'amélioration des performances de l'opération de troncature d'un fichier, très longue avec l'implémentation actuelle, d'améliorer la scalabilité en autorisant des opérations en parallèle sur les répertoires, d'accroître le nombre d'entrées possibles dans les répertoires, et également de disposer de timestamps plus précis qu'actuellement.

Pour étudier les améliorations de performances obtenues par ces optimisations, les développeurs ont utilisés différents outils de benchmarks: dbench, tiobench, FFSB, sqlbench et iozone.

Il serait intéressant de trouver les slides de cette présentation, qui comportaient de nombreux schémas explicatifs sur les différents points.

Block Devices and Transport Classes: Where are we going ?


Cette http://www.linuxsymposium.org/2005/view_abstract.php?content_key=175 était donnée par James Bottomley, un bonhomme à l'attitude très anglaise : chemise à carreau, noeud papillon. L'accent anglais allait avec, évidemment. Amusant ;-)

Au sujet de la conférence proprement dite, je vais être assez bref. Il était en effet question d'amélioration de la structure de la couche SCSI, dont je ne connais absolument rien.

Pour résumer, l'idée était de bien différencier les commandes envoyées aux périphériques blocs de leur mode de transport (SCSI, Fibre Channel, Firewire), afin de pouvoir réutiliser les différents pilotes de périphériques indépendamment de la couche de transport sous-jacente.
Son papier, assez court, résume la situation actuelle et les visions pour le futur à ce sujet.

Enhancements to Linux I/O scheduling


Cette http://www.linuxsymposium.org/2005/view_abstract.php?content_key=148 présentait des travaux et des réflexions sur l'amélioration de l'ordonnancement des entrées sorties dans Linux.

Une jolie maison du centre d'Ottawa
Une jolie maison du centre d'Ottawa

Lorsqu'une application effectue des entrées-sorties sur des fichiers, celles-ci ne sont pas effectuées directement sur le disque, mais via le page cache, dont il a été question dans une autre conférence résumée précédemment. Le page cache a, quant à lui, besoin de pouvoir lire et écrire sur les disques. Plutôt que de servir les requêtes sur les disques une par une dans l'ordre où elles arrivent, le noyau Linux comme tous les noyaux de système d'exploitation modernes, réalise un ordonnancement des requêtes. Au lieu de lancer les requêtes immédiatement, il attend un peu, et tente de rassembler les requêtes ayant lieu sur des blocs contigus. D'autre part, il réordonne les requêtes de manière à minimiser le nombre de déplacements de la tête du disque (il faut savoir que le temps de déplacement et de positionnement de la tête d'un disque est énorme comparé au temps nécessaire pour lire un bloc de données). Ainsi, si des requêtes sur les blocs 2, 235, 4, 237, 3, 236 arrivent dans cet ordre, l'ordonnanceur va essayer de les réordonner pour les mettre dans l'ordre 2, 3, 4, 235, 236, 237 (un déplacement de tête au lieu de cinq). Parallèlement, un ordonnanceur d'entrées-sorties ne doit pas attendre trop longtemps non plus avant d'exécuter réellement les requêtes, afin d'assurer que les requêtes seront satisfaites relativement rapidement.
En pratique, les politiques d'ordonnancement peuvent être bien plus complexes, et Linux propose un https://lwn.net/Articles/102976/ pour connecter différents ordonnanceurs d'entrées-sorties. Il en existe à l'heure actuelle quatre : le noop, le deadline scheduler, le anticipatory scheduler et le CFQ scheduler.

Basiquement, le noop ne fait pas de réordonnancement, il est donc utile pour les périphériques blocs qui n'ont pas de mécanique type tête de disque, comme par exemple les mémoires flashs et les disques en RAM. Le deadline comme son nom l'indique fonctionne en affectant aux différentes requêtes des deadlines avant lesquelles celles-ci doivent être services. Le anticipatory scheduler, celui par défaut, anticipe les futures requêtes. Le CFQ scheduler est une amélioration de l'anticipatory scheduler, réalisée par Jens Axboe, qui permet de garantir des latences moins grande, tout en conservant les propriétés intéressantes de l'anticipatory scheduler. Des discussions ont d'ailleurs eu lieu pour savoir de CFQ ou d'AS quel était le scheduler à utiliser par défaut dans Linux, et bien qu'AS ait été choisi, il semblerait qu'un certain nombre de distributions préfèrent utiliser CFQ par défaut. Le papier détaille le fonctionnement du deadline scheduler et de l'anticipatory scheduler.

Le développeur est parti d'un constat qu'il a fait sur l'anticipatory scheduler : dans certains pathologiques, ce dernier réagit très mal, conduisant à une famine pour certains processus pour lesquelles les requêtes ne sont traitées que très très longtemps après leur demande. Ces cas pathologiques interviennent quand un groupe de processus réalise des requêtes synchrones sur des blocs situés à des endroits éparpillés sur le disque, et que ces processus naissent et meurrent rapidement. Dans ce cas, l'anticipation réalisée par l'anticipatory scheduler est très mauvaise, et le nombre de seeks (déplacements de la tête) augmente énormément. Ce genre de cas intervient par exemple lorsque l'on lance plein de petits processus qui font des choses sur des petits fichiers, par exemple sur les sources du noyau quand on fait un “find . -exec cat {} \;”. Le présentateur appelle ces processus des processus coopérants (cooperative processes), et propose un ordonnanceur détectant ces groupes de processus, et les prenant en compte dans l'anticipation réalisée.

Ensuite, le présentateur a présenté ses réflexions sur le changement dynamique d'ordonnanceur. En fonction de la charge de travail en entrées-sorties, il peut être intéressant d'utiliser un ordonnanceur plutôt qu'un autre, et il semblerait donc pertinent que celui-ci soit changé automatiquement lorsque c'est nécessaire.

Il y a donc testé chaque ordonnanceur en utilisant comme métrique la taille de la requête. Il a ainsi pu déterminer que tel ou tel ordonnanceur est meilleur pour telle ou telle taille de requête. Ensuite, il a utilisé ces mesures pour pouvoir sélectionner dynamiquement le meilleur ordonnanceur en fonction de la charge de travail. Ici, la sélection est donc basée sur des mesures réalisées a priori pour déterminer le meilleur ordonnanceur pour la charge de travail courante. Néanmoins, les travaux à ce sujet sont encore en cours, et peu de résultats ont été donné.

Ce problème de sélection de l'ordonnanceur cache un problème nettement plus complexe : celui de la caractérisation de la charge de travail en entrées-sorties. Ici, le développeur a utilisé simplement la taille des requêtes pour caractériser la charge de travail, ce qui naturellement est insuffisant. Une charge de travail purement séquentielle (bloc 1, puis 2, puis 3, etc.) est complètement différente d'une charge de travail aléatoire (bloc 2, bloc 7823, bloc 23, bloc 2987323, etc.), même si la taille de requête reste identique. De même, il y a une nette différence si c'est une seule application qui réalise des entrées sorties ou plusieurs applications qui font des entrées sorties simultanément. Bref, la caractérisation de la charge de travail est un problème difficile, s'approchant des problèmes de modélisation mathématique. Il faudrait à partir d'informations sur les requêtes (taille, distance de déplacement des têtes, espacement temporel des requêtes) construire un modèle de la charge de travail, et pouvoir sélectionner l'ordonnanceur adapté.
Un papier de Steven Pratts et Dominique Heger, présenté au Linux Symposium en 2004 discute également de ces différents ordonnanceurs et de leurs performances comparées face à différentes charges de travail. Il est disponible page 139 du http://www.finux.org/proceedings/LinuxSymposium2004_V2.pdf.

NFSv4 BOF


Cet atelier avait pour objectif de discuter de l'état d'avancement de la version 4 de NFS et des choses à venir sur ce projet.

La petite présentation en elle-même n'a pas apportée beaucoup d'informations. On y apprend que NFSv4 supporte maintenant correctement la sécurité, et permet d'obtenir des performances nettement meilleures que NFSv3 grâce à l'utilisation des caches locaux et de mécanisme d'invalidation des caches distants lorsque cela est nécessaire.

Suite à cette courte présentation, le débat a principalement tourné autour de l'utilisation de Kerberos. Certains dans la salle reprochaient sa lourdeur d'installation et de configuration et auraient souahaité pouvoir disposer de mécanismes plus simples à mettre en oeuvre. D'autres disaient que c'était simple. Pour celui qui ne connaît pas Kerberos et les problématiques qui s'y rapportent (moi, par exemple), le débat était plutôt abscon. Un atelier assez décevant donc.

Developing iSCSI Storage Systems on Linux


Cet atelier était présenté par Tomonori Fujita. Il a tout d'abord réalisé une présentation complète de ce qu'était iSCSI et des différentes solutions disponibles sous Linux, de leurs avantages et de leurs inconvénients. J'ai vraiment trouvé sa présentation très intéressante, car elle permettait en 30-45 min d'avoir une vision synthétique de l'iSCSI sous Linux, d'une façon abordable pour celui qui ne connaît pas ce domaine. En plus, les slides de sa présentation sont disponibles, et sous une licence libre !

iSCSI est un principe qui consiste à envoyer des commandes SCSI au travers d'une connexion TCP/IP pour accéder à distance à un périphérique SCSI. Ainsi, le pilote SCSI du noyau de votre machine local, au lieu de commander des disques SCSI locaux, encapsule ces commandes dans des paquets réseau et les envoie au serveur. De son coté, le serveur désencapsule les commandes SCSI, les effectue matériellement sur ses périphériques, et renvoie le résultat au client. En gros, au lieu d'utiliser comme médium de transport des commandes SCSI un simple cable reliant une carte contrôleur à un disque, une utilisation une connexion réseau. Au niveau utilisateur, un disque iSCSI apparait comme un disque SCSI normal.

Le parlement, spectable son et lumière
Le parlement, spectable son et lumière



Dans la terminologie iSCSI, le client est appelé initiator (puisqu'il initie la commande SCSI) et le serveur est appelé target (puisque c'est la cible de la commande). Le protocole iSCSI est décrit dans la RFC 3720.
Au niveau des initiator, Tomonori a présenté :

D'après le présentateur, c'est donc vers open-iscsi qu'il faut désormais se tourner.

Du coté des target, on a :

Le présentateur a ensuite présenté une petite étude des performances des différentes solutions, sous forme de graphiques, visibles dans les slides.
Enfin, il a présenté les problèmes restants à régler avec ces différentes solutions. En réalité, le premier problème, « System locks in out-of-memory » situation, a occupé le reste de l'atelier. Le problème en question est expliqué dans le slide, mais je vais tenter d'en faire un résumé compréhensible.
Lorsque l'on accède à un périphérique bloc, les données lues et écrites sont cachées au sein du buffer cache (voir les présentations précédentes). Lorsqu'il n'y a plus de mémoire pour agrandir le buffer cache, alors on doit récupérer de la place dans celui-ci. Deux choix:

Jusqu'ici, ce mécanisme ne pose aucun problème. Il se complique lorsque le disque en question se trouve sur le réseau, ce qui est le cas dans les solutions iSCSI. Dans ce cas, pour pouvoir répercuter les modifications des pages “sales” sur le disque, il faut effectuer des transferts réseau, ce qui nécessite d'allouer de la mémoire pour les messages (les fameux socket buffers). Or, lorsque le système n'a plus du tout de mémoire libre, et que pour libérer de la mémoire, il faut “flusher” des pages “sales” du cache, on se retrouve dans un cas de deadlock : pour libérer de la mémoire, on doit allouer de la mémoire, alors qu'il n'y a plus de mémoire disponible. Et bing, tout s'arrête. Ce problème n'est pas soluble directement au sein des implémentations iSCSI: il nécessite des modifications dans le coeur du noyau pour être résolu.

Le parlement, spectable son et lumière
Le parlement, spectable son et lumière



Ce problème a été discuté durant le Linux Kernel Summit qui s'est tenu juste avant, et même encore avant sur les listes de diffusion. Andrew Morton, le mainteneur de la branche -mm du noyau, était présent dans la salle, et une discussion s'est donc entamée autour de cet épineux problème. Il a effectivement dit que ce sujet a été longuement discuté lors du Linux Kernel Summit, mais que les développeurs du noyau n'ont jamais pu voir ce deadlock se produire, et ne peuvent donc pas analyser exactement ce qu'il se passe. Il a donc demandé à l'orateur de fournir si possible une configuration précise et un cas de test précis dans lequel ce problème se produisait. Il semblerait que cela soit nécessaire pour convaincre les développeurs du noyau de la réalité du problème. La présentation s'est donc terminée sur cette très intéressante discussion.

Depuis, Daniel Philipps a proposé sur la liste du noyau un début de solution à ce problème, en ce qui concerne la réception des paquets réseau. Ce sujet est toujours d'actualité: il a été traité de nouveau sur LWN, et Daniel Philipps a proposé récemment la http://lwn.net/Articles/147288/ de son patch proposant une solution à ce problème d'interbloquage.

Spectacle son et lumière au Parlement


À l'hôtel, en regardant les prospectus, nous avions découvert qu'un spectacle son et lumière animait le Parlement tous les soirs pendant l'été. Nous nous sommes donc rendu devant le Parlement du Canada et avons assisté à ce spectacle d'environ une demi-heure, en écoutant en texte lu en anglais et en français. Le texte résonnait très patriotique, mais la musique et la lumière donnaient des effets sympathiques sur le bâtiment, qu'on découvrait sous un autre angle, comme vous pouvez le voir sur les photos.
Il n'y a pas de commentaire sur cette page. [Afficher commentaires/formulaire]