Retour au site

Uptime Formation - Formations Uptime

Supports de formation : Elie Gavoty et Hadrien Pélissier Conçus initialement dans le cadre d'un cursus Uptime Formation. Sous licence CC-BY-NC-SA - Formations Uptime


Table des matières :

Environnement de travail

Pour cette formation en distanciel nous utiliserons les outils suivants:

  • L’espace de travail Teams fournis pour la formation dans lequel a lieu les conférences Videos/Audio. Il permet également de faire des remarques et partager des fichier entre nous.
  • Ce site internet qui contient tout le contenu de la formation. Le site internet est maintenu à jour tout au long de la formation (pensez à recharger les pages régulièrement) vous pourrez l’imprimer en PDF à la fin.
  • Pour toute la partie pratique nous utiliserons des serveurs distants accessibles en mode graphique(VNC) via le site guacamole.dopl.uk

Serveurs distants

L’intérêt des serveurs distant est double:

  • L’environnement est fiable et identique pour tout le monde ce qui évitera les bugs spécifiques en fonction des environnements de chacun.
  • Ils facilite le travail à distance car tous les stagiaires peuvent voir l’écran du formateur et inversement le formateur peut intervenir rapidement sur les machines de tous les stagiaires.

Connexion à Guacamole

  • Connectez-vous à guacamole.dopl.uk avec comme login votre prenomnom (sans point ni tirets) et comme mot de passe devops101.

Normalement vous devriez avoir deux connexions disponibles:

  • Ouvrez chacune des connexions dans un nouvel onglet ou une nouvelle fenêtre.

Vous pouvez désormais suivre les actions du formateur et les reproduire de votre côté sur le serveur.

Pour la sauvegarde de votre code nous créerons un dépôt git sur github au cours de la formation.

Introduction

Introduction

DevOps

Le nouveau paradigme de l’informatique

Introduction DevOps

A propos de moi

Élie Gavoty

  • Developpeur backend et DevOps (Sewan Group / Yunohost)
  • Formateur DevOps, Linux, Python
  • Philosophie de la technique

A propos de vous

  • Attentes ?
  • Début du cursus :
    • Est-ce que ça vous plait ?
    • Quels modules avez vous déjà fait ?

Le mouvement DevOps

Le DevOps est avant tout le nom d’un mouvement de transformation professionnelle et technique de l’informatique.

Ce mouvement se structure autour des solutions humaines (organisation de l’entreprise et des équipes) et techniques (nouvelles technologies de rupture) apportées pour répondre aux défis que sont:

  • L’agrandissement rapide face à la demande des services logiciels et infrastructures les supportant.
  • La célérité de déploiement demandée par le développement agile (cycles journaliers de développement).
  • Difficultées à organiser des équipes hétérogènes de grande taille et qui s’agrandissent très vite selon le modèle des startups.et

Il y a de nombreuses versions de ce que qui caractérise le DevOps mais pour résumer:

Du côté humain:

  • Application des process de management agile aux opérations et la gestion des infrastructures (pour les synchroniser avec le développement).
  • Remplacement des procédés d’opérations humaines complexes et spécifiques par des opérations automatiques et mieux standardisées.
  • Réconciliation de deux cultures divergentes (Dev et Ops) rapprochant en pratique les deux métiers du développeur et de l’administrateur système.

Du côté technique:

  • L’intégration et le déploiement continus des logiciels/produits.
  • L’infrastructure as code: gestion sous forme de code de l’état des infrastructures d’une façon le plus possible déclarative.
  • Les conteneurs (Docker surtout mais aussi Rkt et LXC/LXD): plus léger que la virtualisation = permet d’isoler chaque service dans son “OS” virtuel sans dupliquer le noyau.
  • Le cloud (Infra as a service, Plateforme as a Service, Software as a service) permet de fluidifier l’informatique en alignant chaque niveau d’abstraction d’une pile logicielle avec sa structuration économique sous forme de service.

L’agilité en informatique

  • Traditionnellement la qualité logicielle provient :

    • d’une conception détaillée en amont = création d’un spécification détaillée
    • d’un contrôle de qualité humain avant chaque livraison logicielle basé sur une processus = vérification du logiciel par rapport à la spécification
  • Problèmes historiques posé par trop de spécification et validation humaine :

    • Lenteur de livraison du logiciel (une version par an ?) donc aussi difficulté de fixer les bugs et problèmes de sécurité a temps
    • Le Travail du développeur est dominé par des process formels : ennuyeux et abstrait
    • difficulté commerciale : comment répondre à la concurence s’il faut 3 ans pour lancer un produit logiciel.
Solution : développer de façon agile c’est à dire itérative
  • Sortir une version par semaine voir par jour
  • Créer de petites évolution plutôt que de grosses évolution
  • Confronter en permanence le logiciel aux retours clients et utilisateurs

Mais l’agilité traditionnelle ne concerne pas l’administration système.

La motivation au coeur du DevOps : La célérité

  • La célérité est : la rapidité (itérative) non pas seulement dans le développement du logiciel mais plus largement dans la livraison du service au client:

Exemple : Netflix ou Spotify ou Facebook etc. déploient une nouvelle version mineure de leur logiciel par jour.

  • Lorsque la concurrence peut déployer des innovations en continu il devient central de pouvoir le faire.

Le problème que cherche à résoudre le DevOps

La célérité et l’agrandissementest sont incompatibles avec une administration système traditionnelle:

Dans un DSI (département de service informatique) on organise ces activités d’admin sys en opérations:

  • On a un planning d’opération avec les priorités du moment et les trucs moins urgents
  • On prépare chaque opération au minimum quelques jours à l’avance.
  • On suit un protocole pour pas oublier des étapes de l’opération (pas oublier de faire une sauvegarde avant par exemple)

La difficulté principale pour les Ops c’est qu’un système informatique est:

  • Un système très complexe qu’il est quasi impossible de complètement visualiser dans sa tête.
  • Les évènements qui se passe sur la machines sont instantanés et invisibles
  • L'état actuel de la machine n’est pas ou peu explicite (combien d’utilisateur, machine pas connectée au réseau par exemple.)
  • Les interractions entre des problèmes peu graves peuvent entrainer des erreurs critiques en cascades.

On peut donc constater que les opérations traditionnelles implique une culture de la prudence

  • On s’organise à l’avance.
  • On vérifie plusieurs fois chaque chose.
  • On ne fait pas confiance au code que nous donnent les développeurs.
  • On suit des procédures pour limiter les risques.
  • On surveille l’état du système (on parle de monitoring)
  • Et on reçoit même des SMS la nuit si ya un problème :S

Bilan

Les opérations “traditionnelles”:

  • Peuvent pas aller trop vite car il faut marcher sur des oeufs.
  • Les Ops veulent pas déployer de nouvelles versions trop souvent car ça fait plein de boulot et ils prennent des risques (bugs / incompatilibités).
  • Quand c’est mal organisé ou qu’on va trop vite il y a des catastrophes possibles.

L’objectif technique idéal du DevOps : Intégration et déploiement continus (CI/CD)

Du côté des développeurs avec l’agilité on a déjà depuis des années une façon d’automatiser pleins d’opérations sur le code à chaque fois qu’on valide une modification.

  • Chaque modification du code est validée dans le gestionnaire de version Git.
  • Ensuite est envoyée sur le dépot de code commun.
  • Des tests logiciels se lancent automatiquement pour s’assurer qu’il n’y a pas de bugs ou de failles.
  • Le développeurs est averti des problèmes.

C’est ce qu’on appelle l’intégration continue.

Le principe central du DevOps est d’automatiser également les opérations de déploiement et de maintenance en se basant sur le même modèle.

Mais pour que ça fonctionne il faut résoudre des défi techniques nouveau => innovations

Les innovations techniques du DevOps

Le Cloud

Le cloud techniquement c’est l’ensemble des trois :

  • Infrastructure as a Service (IaaS): on commande du linux, du réseau et des loadbalancer etc. à la demande

Exemple: Amazon Web Services, DigitalOcean, Azure etc

  • Plateforme as a Service (PaaS): on commande directement un environnement PHP ou NodeJS pour notre application

Exemple: heroku, netlify,

  • Software as a service (SaaS): des services web à la demande pour des utilisateurs finaux

Exemple: Netflix plutôt que VLC, Spotify vs Itunes, etc.

On peut dire que chaque couche (d’abstraction) de l’informatique est commandable à la demande.

Nous utiliserons surtout l’IaaS avec DigitalOcean dans le module Docker.

Les conteneurs (Docker et Kubernetes)

Faire des boîtes isolées avec nos logiciels:

  • Un façon standard de packager un logiciel
  • Cela permet d’assembler de grosses applications comme des legos
  • Cela réduit la complexité grâce:
    • à l’intégration de toutes les dépendance déjà dans la boîte
    • au principe d’immutabilité qui implique de jeter les boîtes ( automatiser pour lutter contre la culture prudence). Rend l’infra prédictible.

L’infrastructure as code (IaC)

Il s’agit comme son nom l’indique de gérer les infrastructures en tant que code c’est-à-dire des fichiers textes avec une logique algorithmique/de données et suivis grâce à un gestionnaire de version (git).

Le problème identifié que cherche a résoudre l’IaC est un écheveau de difficulées pratiques rencontrée dans l’administration système traditionnelle:

  1. Connaissance limité de l’état courant d’un système lorsqu’on fait de l'administration ad-hoc (manuelle avec des commandes unix/dos).
  • Dérive progressive de l’état des systèmes et difficultés à documenter leur états.
  • Fiabilité limitée et risques peu maîtrisés lors de certaines opérations transversales (si d’autres méchanismes de fiabilisation n’ont pas été mis en place).
  • Problème de communication dans les grandes équipes car l’information est détenue implicitement par quelques personnes.
  1. Faible reproductibilité des systèmes et donc difficultée/lenteur du passage à l’échelle (horizontal scaling).
  • Multiplier les serveurs identiques est difficile si leur état est le résultat d’un processus manuel partiellement documenté.
  • Difficulté à reproduire/simuler l’état précis de l’infrastructure de production dans les contextes de tests logiciels.
  1. Difficultés du travail collaboratif dans de grandes équipes avec plusieurs culture (Dev vs Ops) lorsque les rythmes et les modes de travail diffèrent
  • L’IaC permet de tout gérer avec git et des commits.
  • L’IaC permet aux Ops qui ne le faisait pas de se mettre au code et aux développeur de se confronter plus facilement.
  • L’IaC permet d’accélérer la transformation des infrastructures pour l’aligner sur la livraison logicielle quotidienne (idéalement ;) )

Notre programme

  • Docker : les conteneurs et l’infra as code
  • Ansible : couteau suisse de l’infra as code
  • Kubernetes : infrastructure de conteneurs (iac et cloud)
  • Jenkins : CI/CD pour intégrer ensemble le dev et les opérations

Aller plus loin

Préparation

Un peu de logistique

  • Les supports de présentation et les TD sont disponibles à l’adresse https://cours.hadrienpelissier.fr

  • Pour exporter les TD utilisez la fonction d’impression pdf de google chrome.

⚠️ Pour l’anglais, si un texte ne vous paraît pas clair, quelques liens :

Se connecter au lab via Apache Guacamole

  • Les TP sont réalisables dans une VM disponible depuis votre navigateur, en allant sur https://lab.hadrienpelissier.fr

  • Se connecter avec votreprenom (en minuscules) et le mot de passe donné.

  • Puis cliquez sur la machine vnc-votreprenom (si besoin, le mot de passe dans la VM est le même que celui pour accéder au lab)

  • Ouvrez un autre onglet et cliquez aussi sur la machine appelée vnc-formateur-...

  • Pour faire un copier-coller depuis l’extérieur à votre VM, il faut appuyer sur les touches Ctrl+Alt+Maj, puis coller ce que l’on veut dans le presse-papier, et refermer la sidebar avec Ctrl+Alt+Maj.

Installer quelques logiciels

  • Installez VSCode avec le gestionnaire de paquet snap install code --classic
  • En ligne de commande (apt) installez git, htop, ncdu

Explorer Ubuntu Bionic (18.04) : Démo

Explorer l’éditeur VSCode : Démo



Ansible

Module 1

Ansible

Découvrir le couteau suisse de l’automatisation et de l’infrastructure as code.

Plan

Module 1 : Installer ansible, configurer la connexion et commandes ad hoc ansible

Installation

  • créer un lab avec LXD
  • configurer SSH et python pour utiliser ansible

configurer ansible

  • /etc ou ansible.cfg
  • configuration de la connexion
  • connexion SSH et autres plugins de connection
  • versions de Python et d’Ansible

L’inventaire ansible

  • gérer des groupes de machines
  • L’inventaire est la source d’information principale pour Ansible

Ansible ad-hoc et les modules de base

  • la commande ansible et ses options
  • explorer les nombreux modules d’Ansible
  • idempotence des modules
  • exécuter correctement des commandes shell avec Ansible
  • le check mode pour controller l’état d’une ressource

TP1: Installation, configuration et prise en main avec des commandes ad-hoc

Module 2 : Les playbooks pour déployer une application web

syntaxe yaml des playbooks

  • structure d’un playbook

modules de déploiement et configuration

  • Templates de configuration avec Jinja2
  • gestion des paquets, utilisateurs et fichiers, etc.

Variable et structures de controle

  • explorer les variables
  • syntaxe jinja des variables et lookups
  • facts et variables spéciales
  • boucles et conditions

Idempotence d’un playbook

  • handlers
  • contrôler le statut de retour des tâches
  • gestion de l’idempotence des commandes Unix

debugging de playbook

  • verbosite
  • directive de debug
  • gestion des erreurs à l’exécution

TP2: Écriture d’un playbook simple de déploiement d’une application web flask en python.

Module 3 : Structurer un projet, utiliser les roles

Complexifier notre lab en ajoutant de nouvelles machines dans plusieurs groupes.

  • modules de provisionning de machines pour Ansible
  • organisation des variables de l’inventaire
  • la commande ansible-inventory

Les roles

  • Ansible Galaxy pour installer des roles.
  • Architecture d’un role et bonnes pratiques de gestion des roles.

Écrire un role et organiser le projet

  • Imports et includes réutiliser du code.
  • Bonne pratiques d’organisation d’un projet Ansible
  • Utiliser des modules personnalisés et des plugins pour étendre Ansible
  • gestion de version du code Ansible

TP3: Transformation de notre playbook en role et utilisation de roles ansible galaxy pour déployer une infrastructure multitiers.

Module 4 : Orchester Ansible dans un contexte de production

Intégration d’Ansible

  • Intégrer ansible dans le cloud un inventaire dynamique et Terraform
  • Différents type d’intégration Ansible

Orchestration

  • Stratégies : Parallélisme de l’exécution
  • Délégation de tâche
  • Réalisation d’un rolling upgrade de notre application web grace à Ansible
  • Inverser des tâches Ansible - stratégies de rollback
  • Exécution personnalisée avec des tags

Sécurité

  • Ansible Vault : gestion des secrets pour l’infrastructure as code
  • desctiver les logs des taches sensibles
  • Renforcer le mode de connexion ansible avec un bastion SSH

Exécution d’Ansible en production

  • Intégration et déploiement avec Gitlab
  • Gérer une production Ansible découvrir TOWER/AWX
  • Tester ses roles et gérer de multiples versions

TP4: Refactoring de notre code pour effectuer un rolling upgrade et déploiement dans le cloud + AWX

Cours 1 - Présentation

Plan

Module 1 : Installer ansible, configurer la connexion et commandes ad hoc ansible

Installation

  • créer un lab avec LXD
  • configurer SSH et python pour utiliser ansible

configurer ansible

  • /etc ou ansible.cfg
  • configuration de la connexion
  • connexion SSH et autres plugins de connection
  • versions de Python et d’Ansible

L’inventaire ansible

  • gérer des groupes de machines
  • L’inventaire est la source d’information principale pour Ansible

Ansible ad-hoc et les modules de base

  • la commande ansible et ses options
  • explorer les nombreux modules d’Ansible
  • idempotence des modules
  • exécuter correctement des commandes shell avec Ansible
  • le check mode pour controller l’état d’une ressource

TP1: Installation, configuration et prise en main avec des commandes ad-hoc

Module 2 : Les playbooks pour déployer une application web

syntaxe yaml des playbooks

  • structure d’un playbook

modules de déploiement et configuration

  • Templates de configuration avec Jinja2
  • gestion des paquets, utilisateurs et fichiers, etc.

Variable et structures de controle

  • explorer les variables
  • syntaxe jinja des variables et lookups
  • facts et variables spéciales
  • boucles et conditions

Idempotence d’un playbook

  • handlers
  • contrôler le statut de retour des tâches
  • gestion de l’idempotence des commandes Unix

debugging de playbook

  • verbosite
  • directive de debug
  • gestion des erreurs à l’exécution

TP2: Écriture d’un playbook simple de déploiement d’une application web flask en python.

Module 3 : Structurer un projet, utiliser les roles

Complexifier notre lab en ajoutant de nouvelles machines dans plusieurs groupes.

  • modules de provisionning de machines pour Ansible
  • organisation des variables de l’inventaire
  • la commande ansible-inventory

Les roles

  • Ansible Galaxy pour installer des roles.
  • Architecture d’un role et bonnes pratiques de gestion des roles.

Écrire un role et organiser le projet

  • Imports et includes réutiliser du code.
  • Bonne pratiques d’organisation d’un projet Ansible
  • Utiliser des modules personnalisés et des plugins pour étendre Ansible
  • gestion de version du code Ansible

TP3: Transformation de notre playbook en role et utilisation de roles ansible galaxy pour déployer une infrastructure multitiers.

Module 4 : Orchester Ansible dans un contexte de production

Intégration d’Ansible

  • Intégrer ansible dans le cloud un inventaire dynamique et Terraform
  • Différents type d’intégration Ansible

Orchestration

  • Stratégies : Parallélisme de l’exécution
  • Délégation de tâche
  • Réalisation d’un rolling upgrade de notre application web grace à Ansible
  • Inverser des tâches Ansible - stratégies de rollback
  • Exécution personnalisée avec des tags

Sécurité

  • Ansible Vault : gestion des secrets pour l’infrastructure as code
  • desctiver les logs des taches sensibles
  • Renforcer le mode de connexion ansible avec un bastion SSH

Exécution d’Ansible en production

  • Intégration et déploiement avec Gitlab
  • Gérer une production Ansible découvrir TOWER/AWX
  • Tester ses roles et gérer de multiples versions

TP4: Refactoring de notre code pour effectuer un rolling upgrade et déploiement dans le cloud + AWX

Présentation d’Ansible

Ansible

Ansible est un gestionnaire de configuration et un outil de déploiement et d’orchestration très populaire et central dans le monde de l'infrastructure as code (IaC).

Il fait donc également partie de façon centrale du mouvement DevOps car il s’apparente à un véritable couteau suisse de l’automatisation des infrastructures.

Histoire

Ansible a été créé en 2012 (plus récent que ses concurrents Puppet et Chef) autour d’une recherche de simplicité et du principe de configuration agentless.

Très orienté linux/opensource et versatile il obtient rapidement un franc succès et s’avère être un couteau suisse très adapté à l’automatisation DevOps et Cloud dans des environnements hétérogènes.

Red Hat rachète Ansible en 2015 et développe un certain nombre de produits autour (Ansible Tower, Ansible container avec Openshift).

Architecture : simplicité et portabilité avec ssh et python

Ansible est agentless c’est à dire qu’il ne nécessite aucun service/daemon spécifique sur les machines à configurer.

La simplicité d’Ansible provient également du fait qu’il s’appuie sur des technologies linux omniprésentes et devenues universelles.

  • ssh : connexion et authentification classique avec les comptes présents sur les machines.
  • python : multiplateforme, un classique sous linux, adapté à l’admin sys et à tous les usages.

De fait Ansible fonctionne efficacement sur toutes les distributions linux, debian, centos, ubuntu en particulier (et maintenant également sur Windows).

Ansible pour la configuration

Ansible est semi-déclaratif c’est à dire qu’il s’exécute séquentiellement mais idéalement de façon idempotente.

Il permet d’avoir un état descriptif de la configuration:

  • qui soit auditable
  • qui peut évoluer progressivement
  • qui permet d'éviter que celle-ci ne dérive vers un état inconnu

Ansible pour le déploiement et l’orchestration

Peut être utilisé pour des opérations ponctuelles comme le déploiement:

  • vérifier les dépendances et l’état requis d’un système
  • récupérer la nouvelle version d’un code source
  • effectuer une migration de base de données (si outil de migration)
  • tests opérationnels (vérifier qu’un service répond)

Ansible à différentes échelles

Les cas d’usages d’Ansible vont de …:

  • petit:

    • … un petit playbook (~script) fournit avec le code d’un logiciel pour déployer en mode test.
    • … la configuration d’une machine de travail personnelle.
    • etc.
  • moyen:

    • … faire un lab avec quelques machines.
    • … déployer une application avec du code, une runtime (php/jav etc) et une base de données à migrer.
    • etc.
  • grand:

    • … gestion de plusieurs DC avec des produits multiples.
    • … gestion multi-équipes et logging de toutes les opérations grâce à Ansible Tower.
    • etc.

Ansible et Docker

Ansible est très complémentaire à docker:

  • Il permet de provisionner des machines avec docker ou kubernetes installé pour ensuite déployer des conteneurs.
  • Il permet une orchestration simple des conteneur avec le module docker_container.

Plus récemment avec l’arrivé d'Ansible container il est possible de construire et déployer des conteneurs docker avec du code ansible. Cette solution fait partie de la stack Red Hat Openshift. Concrêtement le langage ansible remplace (avantageusement ?) le langage Dockerfile pour la construction des images Docker.

Partie 1, Installation, configuration et commandes ad hoc.

Pour l’installation plusieurs options sont possibles:

  • Avec le gestionnaire de paquet de la distribution ou homebrew sur OSX:
    • version généralement plus ancienne (2.4 ou 2.6)
    • facile à mettre à jour avec le reste du système
    • Pour installer une version récente on il existe des dépots spécifique à ajouter: exemple sur ubuntu: sudo apt-add-repository --yes --update ppa:ansible/ansible
  • Avec pip le gestionnaire de paquet du langage python: sudo pip3 install
    • installe la dernière version stable (2.8 actuellement)
    • commande d’upgrade spécifique sudo pip3 install ansible --upgrade
    • possibilité d’installer facilement une version de développement pour tester de nouvelles fonctionnalité ou anticiper les migrations.

Pour voir l’ensemble des fichier installé par un paquet pip3 :

pip3 show -f ansible | less

Pour tester la connexion aux serveurs on utilise la commande ad hoc suivante. ansible all -m ping

Les inventaires statiques

Il s’agit d’une liste de machines sur lesquelles vont s’exécuter les modules Ansible. Les machines de cette liste sont:

  • Classées par groupe et sous groupes pour être désignables collectivement (exp executer telle opération sur)

  • La méthode connexion est précisée soit globalement soit pour chaque machine.

  • Des variables peuvent être définies pour chaque machine ou groupe pour contrôler dynamiquement par la suite la configuration ansible.

  • Classées par groupe et sous groupes pour être désignables collectivement (exp executer telle opération sur)

  • La méthode connexion est précisée soit globalement soit pour chaque machine.

  • Des variables peuvent être définies pour chaque machine ou groupe pour contrôler dynamiquement par la suite la configuration ansible.

Exemple :

[all:vars]
ansible_ssh_user=elie
ansible_python_interpreter=/usr/bin/python3

[awx_nodes]
awxnode1 node_state=started ansible_host=10.164.210.101 container_image=centos_ansible_20190901

[dbservers]
pgnode1 node_state=started ansible_host=10.164.210.111 container_image=centos_ansible_20190901
pgnode2 node_state=started ansible_host=10.164.210.112 container_image=centos_ansible_20190901

[appservers]
appnode1 node_state=started ansible_host=10.164.210.121 container_image=centos_ansible_20190901
appnode2 node_state=started ansible_host=10.164.210.122 container_image=centos_ansible_20190901

Les inventaires peuvent également être au format YAML (plus lisible mais pas toujours intuitif) ou JSON (pour les machines).

Configuration

Ansible se configure classiquement au niveau global dans le dossier /etc/ansible/ dans lequel on retrouve en autre l’inventaire par défaut et des paramètre de configuration.

Ansible est très fortement configurable pour s’adapter à des environnement contraints. Liste des paramètre de configuration:

Alternativement on peut configurer ansible par projet avec un fichier ansible.cfg présent à la racine. Toute commande ansible lancée à la racine du projet récupère automatiquement cette configuration.

La commande ansible

  • version minimale : ansible <groupe_machine> -m <module> -a <arguments_module>

  • ansible all -m ping: Permet de tester si les hotes sont joignables et ansible utilisable (SSH et python sont présents et configurés).

  • version plus complète : ansible <groupe_machine> --inventory <fichier_inventaire> --become -m <module> -a <arguments_module>

Les modules Ansible

Ansible fonctionne grâce à des modules python téléversés sur sur l’hôte à configurer puis exécutés. Ces modules sont conçus pour être cohérents et versatiles et rendre les tâches courantes d’administration plus simples.

Il en existe pour un peu toute les tâches raisonnablement courantes : un slogan Ansible “Batteries included” ! Plus de 1300 modules sont intégrés par défaut.

  • ping: un module de test Ansible (pas seulement réseau comme la commande ping)

  • yum/apt: pour gérer les paquets sur les distributions basées respectivement sur Red Hat ou Debian.

... -m yum -a "name=openssh-server state=present"

  • systemd (ou plus générique service): gérer les services/daemons d’un système.

... -m systemd -a "name=openssh-server state=started"

  • user: créer des utilisateurs et gérer leurs options/permission/groupes

  • file: pour créer, supprimer, modifier, changer les permission de fichiers, dossier et liens.

  • shell: pour exécuter des commandes unix grace à un shell

Option et documentation des modules

La documentation des modules Ansible se trouve à l’adresse https://docs.ansible.com/ansible/latest/modules/file_module.html

Chaque module propose de nombreux arguments pour personnaliser son comportement:

exemple: le module file permet de gérer de nombreuses opérations avec un seul module en variant les arguments.

Il est également à noter que la plupart des arguments sont facultatifs.

  • cela permet de garder les appel de modules très succints pour les taches par défaut
  • il est également possible de rendre des paramètres par défaut explicites pour augmenter la clarté du code.

Exemple et bonne pratique: toujours préciser state: present même si cette valeur est presque toujours le défaut implicite.

Commençons le TP1

Cours 2 - Les playbooks Ansible

Les commandes ad-hoc sont des appels directs de modules Ansible qui fonctionnent de façon idempotente mais ne présente pas les avantages du code qui donne tout son intérêt à l’IaC:

  • texte descriptif écrit une fois pour toute
  • logique lisible et auditable
  • versionnable avec git
  • reproductible et incrémental

La dimension incrémentale du code rend en particulier plus aisé de construire une infrastructure progressivement en la complexifiant au fur et à mesure plutôt que de devoir tout plannifier à l’avance.

Le playbook est une sorte de script ansible, c’est à dire du code. Le nom provient du football américain : il s’agit d’un ensemble de stratégies qu’une équipe a travaillé pour répondre aux situations du match. Elle insiste sur la versatilité de l’outil.

Syntaxe yaml

Les playbooks ansible sont écrits au format YAML.

  • YAML est basé sur les identations à base d’espaces (2 espaces par indentation en général). Comme le langage python.
  • C’est un format assez lisible et simple à écrire bien que les indentations soient parfois difficiles à lire.
  • C’est un format assez flexible avec des types liste et dictionnaires qui peuvent s’imbriquer.
  • Le YAML est assez proche du JSON (leur structures arborescentes typées sont isomorphes) mais plus facile à écrire.

A quoi ça ressemble ?

Une liste

- 1
- Poire
- "Message à caractère informatif"

Un dictionnaire

clé1: valeur1
clé2: valeur2
clé3: 3

Un exemple imbriqué plus complexe

marché: # debut du dictionnaire global "marché"
  lieu: Crimée Curial
  jour: dimanche
  horaire:
    unité: "heure"
    min: 9


    max: 14 # entier
  fruits: #liste de dictionnaires décrivant chaque fruit
    - nom: pomme
      couleur: "verte"
      pesticide: avec #les chaines sont avec ou sans " ou '
            # on peut sauter des lignes dans interrompre la liste ou le dictionnaire en court
    - nom: poires
      couleur: jaune
      pesticide: sans
  légumes: #Liste de 3 éléments
    - courgettes
    - salade

    - potiron
#fin du dictionnaire global

Pour mieux visualiser l’imbrication des dictionnaires et des listes en YAML on peut utiliser un convertisseur YAML -> JSON : https://www.json2yaml.com/.

Notre marché devient:

{
  "marché": {
    "lieu": "Crimée Curial",
    "jour": "dimanche",
    "horaire": {
      "unité": "heure",
      "min": 9,
      "max": 14
    },
    "fruits": [
      {
        "nom": "pomme",
        "couleur": "verte",
        "pesticide": "avec"
      },
      {
        "nom": "poires",
        "couleur": "jaune",
        "pesticide": "sans"
      }
    ],
    "légumes": [
      "courgettes",
      "salade",
      "potiron"
    ]
  }
}

Observez en particulier la syntaxe assez condensée de la liste “fruits” en YAML qui est une liste de dictionnaires.

Structure d’un playbook

--- 
- name: premier play # une liste de play (chaque play commence par un tiret)
  hosts: serveur_web # un premier play
  become: yes
  gather_facts: false # récupérer le dictionnaires d'informations (facts) relatives aux machines

  vars:
    logfile_name: "auth.log"

  var_files:
    - mesvariables.yml

  pre_tasks:
    - name: dynamic variable
      set_fact:
        mavariable: "{{ inventory_hostname + 'prod' }}" #guillemets obligatoires

  roles:
    - flaskapp
    
  tasks:
    - name: installer le serveur nginx
      apt: name=nginx state=present # syntaxe concise proche des commandes ad hoc mais moins lisible

    - name: créer un fichier de log
      file: # syntaxe yaml extensive : conseillée
        path: /var/log/{{ logfile_name }} #guillemets facultatifs
        mode: 755

    - import_tasks: mestaches.yml

  handlers:
    - systemd:
        name: nginx
        state: "reloaded"

- name: un autre play
  hosts: dbservers
  tasks:
    ... 
  • Un playbook commence par un tiret car il s’agit d’une liste de plays.

  • Un play est un dictionnaire yaml qui décrit un ensemble de taches ordonnées en plusieurs sections. Un play commence par préciser sur quelles machines il s’applique puis précise quelques paramètres faculatifs d’exécution comme become: yes pour l’élévation de privilège (section hosts).

  • La section hosts est obligatoire. Toutes les autres sections sont facultatives !

  • La section tasks est généralement la section principale car elle décrit les taches de configuration à appliquer.

  • La section tasks peut être remplacée ou complétée par une section roles et des sections pre_tasks post_tasks

  • Les handlers sont des tâches conditionnelles qui s’exécutent à la fin (post traitements conditionnels comme le redémarrage d’un service)

Ordre d’execution

  1. pre_tasks
  2. roles
  3. tasks
  4. post_tasks
  5. handlers

Les roles ne sont pas des tâches à proprement parler mais un ensemble de tâches et ressources regroupées dans un module un peu comme une librairie developpement. Cf. cours 3.

bonnes pratiques de syntaxe

  • Indentation de deux espaces.
  • Toujours mettre un name: qui décrit lors de l’execution la tache en court : un des principes de l’IaC est l’intelligibilité des opérations.
  • Utiliser les arguments au format yaml (sur plusieurs lignes) pour la lisibilité, sauf s’il y a peu d’arguments

Pour valider la syntaxe il est possible d’installer et utiliser ansible-linter sur les fichiers YAML.

Imports et includes

Il est possible d’importer le contenu d’autres fichiers dans un playbook:

  • import_tasks: importe une liste de tâches (atomiques)
  • import_playbook: importe une liste de play contenus dans un playbook.

Les deux instructions précédentes désignent un import statique qui est résolu avant l’exécution.

Au contraire, include_tasks permet d’intégrer une liste de tâche dynamiquement pendant l’exécution

Par exemple:

vars:
  apps:
    - app1
    - app2
    - app3

tasks:
  - include_tasks: install_app.yml
    loop: "{{ apps }}"

Ce code indique à Ansible d’executer une série de tâches pour chaque application de la liste. On pourrait remplacer cette liste par une liste dynamique. Comme le nombre d’import ne peut pas facilement être connu à l’avance on doit utiliser include_tasks.

Élévation de privilège

L’élévation de privilège est nécessaire lorsqu’on a besoin d’être root pour exécuter une commande ou plus généralement qu’on a besoin d’exécuter une commande avec un utilisateur différent de celui utilisé pour la connexion on peut utiliser:

  • Au moment de l’exécution l’argument --become en ligne de commande avec ansible, ansible-console ou ansible-playbook.

  • La section become: yes

    • au début du play (après hosts) : toutes les tâches seront executée avec cette élévation par défaut.
    • après n’importe quelle tâche : l’élévation concerne uniquement la tâche cible.
  • Pour executer une tâche avec un autre utilisateur que root (become simple) ou celui de connexion (sans become) on le précise en ajoutant à become: yes, become_user: username

Variables Ansible

Ansible utilise en arrière plan un dictionnaire contenant de nombreuses variables.

Pour s’en rendre compte on peut lancer : ansible <hote_ou_groupe> -m debug -a "msg={{ hostvars }}"

Ce dictionnaire contient en particulier:

  • des variables de configuration ansible (ansible_user par exemple)
  • des facts c’est à dire des variables dynamiques caractérisant les systèmes cible (par exemple ansible_os_family) et récupéré au lancement d’un playbook.
  • des variables personnalisées (de l’utilisateur) que vous définissez avec vos propre nom généralement en snake_case.

Jinja2 et variables dans les playbooks et rôles (fichiers de code)

La plupart des fichiers Ansible (sauf l’inventaire) sont traités avec le moteur de template python JinJa2.

Ce moteur permet de créer des valeurs dynamiques dans le code des playbooks, des roles, et des fichiers de configuration.

  • Les variables écrites au format {{ mavariable }} sont remplacées par leur valeur provenant du dictionnaire d’exécution d’Ansible.

  • Des filtres (fonctions de transformation) permettent de transformer la valeur des variables: exemple : {{ hostname | default('localhost') }} (Voir plus bas)

Jinja2 et les variables dans les fichiers de templates

Les fichiers de templates (.j2) utilisés avec le module template, généralement pour créer des fichiers de configuration peuvent contenir des variables et des filtres comme les fichier de code (voir au dessus) mais également d’autres constructions jinja2 comme:

  • Des if : {% if nginx_state == 'present' %}...{% endif %}.
  • Des boucles for : {% for host in groups['appserver'] %}...{% endfor %}.
  • Des inclusions de templates {% include 'autre_fichier_template.j2' %}

Définition des variables

On peut définir et modifier la valeur des variables à différents endroits du code ansible:

  • La section vars: du playbook.
  • Un fichier de variables appelé avec var_files:
  • L’inventaire : variables pour chaque machine ou pour le groupe.
  • Dans des dossier extension de l’inventaire group_vars, host_bars
  • Dans le dossier defaults des roles (cf partie sur les roles)
  • Dans une tache avec le module set_facts.
  • A runtime au moment d’appeler la CLI ansible avec --extra-vars "version=1.23.45 other_variable=foo"

Lorsque définies plusieurs fois, les variables ont des priorités en fonction de l’endroit de définition. L’ordre de priorité est plutôt complexe: https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable

En résumé la règle peut être exprimée comme suit: les variables de runtime sont prioritaires sur les variables dans un playbook qui sont prioritaires sur les variables de l’inventaire qui sont prioritaires sur les variables par défaut d’un role.

  • Bonne pratique: limiter les redéfinitions de variables en cascade (au maximum une valeur par défaut, une valeur contextuelle et une valeur runtime) pour éviter que le playbook soit trop complexe et difficilement compréhensible et donc maintenable.

Remarques de syntaxe

  • groups.all et groups['all'] sont deux syntaxes équivalentes pour désigner les éléments d’un dictionnaire.

variables spéciales

https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html

Les plus utiles:

  • hostvars: dictionaire de toute les variables rangées par hote de l’inventaire.
  • ansible_host: information utilisée pour la connexion (ip ou domaine).
  • inventory_hostname: nom de la machine dans l’inventaire.
  • groups: dictionnaire de tous les groupes avec la liste des machines appartenant à chaque groupe.

Pour explorer chacune de ces variables vous pouvez utiliser le module debug en mode adhoc ou dans un playbook:

ansible <hote_ou_groupe> -m debug -a "msg={{ ansible_host }}"

ou encore:

ansible <hote_ou_groupe> -m debug -a "msg={{ groups.all }}"

Facts

Les facts sont des valeurs de variables récupérées au début de l’exécution durant l’étape gather_facts et qui décrivent l’état courant de chaque machine.

  • Par exemple, ansible_os_family est un fact/variable décrivant le type d’OS installé sur la machine. Elle n’existe qu’une fois les facts récupérés.

! Lors d’une commande adhoc ansible les facts ne sont pas récupérés : la variable ansible_os_family ne sera pas disponible.

La liste des facts peut être trouvée dans la documentation et dépend des plugins utilisés pour les récupérés: https://docs.ansible.com/ansible/latest/user_guide/playbooks_vars_facts.html

Structures de controle Ansible (et non JinJa2)

La directive when

Elle permet de rendre une tâche conditionnelle (une sorte de if)

- name: start nginx service
  systemd:
    name: nginx
    state: started
  when: ansible_os_family == 'RedHat'

Sinon la tache est sautée (skipped) durant l’exécution.

La directive loop:

Cette directive permet d’executer une tache plusieurs fois basée sur une liste de valeur:

https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html

exemple:

- hosts: localhost
  tasks:
    - name: exemple de boucle
      debug:
        msg: "{{ item }}"
      loop:
        - message1
        - message2
        - message3

On peut également controler cette boucle avec quelques paramètres:

- hosts: localhost
  vars:
    messages:
      - message1
      - message2
      - message3

  tasks:
    - name: exemple de boucle
      debug:
        msg: "message numero {{ num }} : {{ message }}"
      loop: "{{ messages }}"
      loop_control:
        loop_var: message
        index_var: num
    

Cette fonctionnalité de boucle était anciennement accessible avec le mot clé with_items: qui est maintenant déprécié.

Filtres Jinja

Pour transformer la valeur des variables à la volée lors de leur appel on peut utiliser des filtres (jinja2) :

La liste complète des filtres ansible se trouve ici : https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html

Debugger un playbook.

Avec Ansible on dispose d’au moins trois manières de debugger un playbook:

  • Rendre la sortie verbeuse (mode debug) avec -vvv.

  • Utiliser une tache avec le module debug : debug msg="{{ mavariable }}".

  • Utiliser la directive debugger: always ou on_failed à ajouter à la fin d’une tâche. L’exécution s’arrête alors après l’exécution de cette tâche et propose un interpreteur de debug.

Les commandes et l’usage du debugger sont décris dans la documentation: https://docs.ansible.com/ansible/latest/user_guide/playbooks_debugger.html

Cours 3 - Organiser un projet

Organisation d’un dépot de code Ansible

Voici, extrait de la documentation Ansible sur les “Best Practice”, l’une des organisations de référence d’un projet ansible de configuration d’une infrastructure:

production                # inventory file for production servers
staging                   # inventory file for staging environment

group_vars/
   group1.yml             # here we assign variables to particular groups
   group2.yml
host_vars/
   hostname1.yml          # here we assign variables to particular systems
   hostname2.yml


site.yml                  # master playbook
webservers.yml            # playbook for webserver tier
dbservers.yml             # playbook for dbserver tier

roles/
    common/               # this hierarchy represents a "role"
        ...               # role code

    webtier/              # same kind of structure as "common" was above, done for the webtier role
    monitoring/           # ""
    fooapp/               # ""

Plusieurs remarques:

  • Chaque environnement (staging, production) dispose d’un inventaire ce qui permet de préciser à runtime quel environnement cibler avec l’option --inventaire production.
  • Chaque groupe de serveurs (tier) dispose de son playbook
    • qui s’applique sur le groupe en question.
    • éventuellement définit quelques variables spécifiques (mais il vaut mieux les mettre dans l’inventaire ou les dossiers cf suite).
    • Idéalement contient un minimum de tâches et plutôt des roles (ie des tâches rangées dans une sorte de module)
  • Pour limiter la taille de l’inventaire principal on range les variables communes dans des dossiers group_vars et host_vars. On met à l’intérieur un fichier <nom_du_groupe>.yml qui contient un dictionnaire de variables.
  • On cherche à modulariser au maximum la configuration dans des roles c’est à dire des modules rendus génériques et specifique à un objectif de configuration.
  • Ce modèle d’organisation correspond plutôt à la configuration de base d’une infrastructure (playbooks à exécuter régulièrement) qu’à l’usage de playbooks ponctuels comme pour le déploiement. Mais, bien sur, on peut ajouter un dossier playbooks ou operations pour certaines opérations ponctuelles. (cf cours 4)
  • Si les modules de Ansible (complétés par les commandes bash) ne suffisent pas on peut développer ses propre modules ansible.
    • Il s’agit de programmes python plus ou moins complexes
    • On les range alors dans le dossier library du projet ou d’un role et on le précise éventuellement dans ansible.cfg.
  • Observons le role Common : il est utilisé ici pour rassembler les taches de base des communes à toutes les machines. Par exemple s’assurer que les clés ssh de l’équipe sont présentes, que les dépots spécifiques sont présents etc.

Roles Ansible

Objectif:

  • Découper les tâches de configuration en sous ensembles réutilisables (une suite d’étapes de configuration).

  • Ansible est une sorte de langage de programmation et l’intéret du code est de pouvoir créer des fonction regroupées en librairies et les composer. Les roles sont les “librairies/fonction” ansible en quelque sorte.

  • Comme une fonction un role prend généralement des paramètres qui permettent de personnaliser son comportement.

  • Tout le nécessaire doit y être (fichiers de configurations, archives et binaires à déployer, modules personnels dans library etc.)

  • Remarque ne pas confondre modules et roles : file est un module geerlingguy.docker est un role. On doit écrire des roles pour coder correctement en Ansible, on peut écrire des modules mais c’est largement facultatif car la plupart des actions existent déjà.

  • Présentation d’un exemple de role : https://github.com/geerlingguy/ansible-role-docker

    • Dans la philosophie Ansible on recherche la généricité des roles. On cherche à ajouter des paramètres pour que le rôle s’adapte à différents cas (comme notre playbook flask app).
    • Une bonne pratique: préfixer le nom des paramètres par le nom du role exemple docker_edition.
    • Cependant la généricité est nécessaire quand on veut distribuer le role ou construire des outils spécifiques qui serve à plus endroit de l’infrastructure mais elle augmente la complexité.
    • Donc pour les roles internes on privilégie la simplicité.
    • Les roles contiennent idéalement un fichier README en décrire l’usage et un fichier meta/main.yml qui décrit la compatibilité et les dépendanice en plus de la licence et l’auteur.
    • Il peuvent idéalement être versionnés dans des dépots à part et installé avec ansible-galaxy

Structure d’un rôle

Un role est un dossier avec des sous dossiers conventionnels:

roles/
    common/               # this hierarchy represents a "role"
        tasks/            #
            main.yml      #  <-- tasks file can include smaller files if warranted
        handlers/         #
            main.yml      #  <-- handlers file
        templates/        #  <-- files for use with the template resource
            ntp.conf.j2   #  <------- templates end in .j2
        files/            #
            foo.sh        #  <-- script files for use with the script resource
        vars/             #
            main.yml      #  <-- variables associated with this role
        defaults/         #
            main.yml      #  <-- default lower priority variables for this role
        meta/             #
            main.yml      #  <-- role dependencies
        library/          # roles can also include custom modules
        module_utils/     # roles can also include custom module_utils
        lookup_plugins/

On constate que les noms des sous dossiers correspondent souvent à des sections du playbook. En fait le principe de base est d’extraire les différentes listes de taches ou de variables dans des sous-dossier

  • Remarque : les fichier de liste doivent nécessairement s’appeler main.yml" (pas très intuitif)

  • Remarque2 : main.yml peut en revanche importer d’autre fichiers aux noms personnalisés (exp role docker de geerlingguy)

  • Le dossier defaults contient les valeurs par défaut des paramètres du role. Ces valeurs ne sont jamais prioritaires (elles sont écrasées par n’importe quelle redéfinition)

  • Le fichier meta/main.yml est facultatif mais conseillé et contient des informations sur le role

    • auteur
    • license
    • compatibilité
    • version
    • dépendances à d’autres roles.
  • Le dossier files contient les fichiers qui ne sont pas des templates (pour les module copy ou sync, script etc).

Ansible Galaxy

C’est le store de roles officiel d’Ansible : https://galaxy.ansible.com/

C’est également le nom d’une commande ansible-galaxy qui permet d’installer des roles et leurs dépendances depuis internet. Un sorte de gestionnaire de paquet pour ansible.

Elle est utilisée généralement sour la forme ansible install -r roles/requirements.yml -p roles <nom_role> ou plus simplement ansible-galaxy install <role> mais installe dans /etc/ansible/roles.

Tous les rôles ansible sont communautaires (pas de roles officiels) et généralement stockés sur github.

Mais on peut voir la popularité la qualité et les tests qui garantissement la plus ou moins grande fiabilité du role

Il existe des roles pour installer un peu n’importe quelle application serveur courante aujourd’hui. Passez du temps à explorer le web avant de développer quelque chose avec Ansible

Installer des roles avec requirements.yml

Conventionnellement on utilise un fichier requirements.yml situé dans roles pour décrire la liste des roles nécessaires à un projet.

- src: geerlingguy.repo-epel
- src: geerlingguy.haproxy
- src: geerlingguy.docke
# from GitHub, overriding the name and specifying a specific tag
- src: https://github.com/bennojoy/nginx
  version: master
  name: nginx_role
  • Ensuite pour les installer on lance: ansible-galaxy install -r roles/requirements.yml -p roles.

Cours 4 - Sécurité et Cloud

Sécurité

Les problématiques de sécurité linux ne sont pas résolue magiquement par Ansible. Tous le travail de réflexion et de sécurisation reste identique mais peut comme le reste être mieux controllé grace à l’approche déclarative de l’infrastructure as code.

Si cette problématique des liens entre Ansible et sécurité vous intéresse : Security automation with Ansible

Il est à noter tout de même qu’Ansible est généralement apprécié d’un point de vue sécurité car il n’augmente pas (vraiment) la surface d’attaque de vos infrastructure : il est basé sur ssh qui est éprouvé et ne nécessite généralement pas de réorganisation des infrastructures.

Pour les cas plus spécifiques et si vous voulez éviter ssh, Ansible est relativement agnostique du mode de connexion grâce aux plugins de connexions (voir ci-dessous).

Authentification et SSH

Un bonne pratique importante : changez le port de connexion ssh pour un port atypique. Ajoutez la variable ansible_ssh_port=17728 dans l’inventaire.

Il faut idéalement éviter de créer un seul compte ansible de connexion pour toutes les machines:

  • difficile à bouger
  • responsabilité des connexions pas auditable (auth.log + syslog)

Il faut utiliser comme nous avons fait dans les TP des logins ssh avec les utilisateurs humain réels des machines et des clés ssh. C’est à dire le même modèle d’authentification que l’administration traditionnelle.

Les autres modes de connexion

Le mode de connexion par défaut de Ansible est SSH cependant il est possible d’utiliser de nombreux autres modes de connexion spécifiques :

  • Pour afficher la liste des plugins disponible lancez ansible-doc -t connection -l.

  • Une autre connexion courante est ansible_connection=local qui permet de configurer la machine locale sans avoir besoin d’installer un serveur ssh.

  • Citons également les connexions ansible_connexion=docker et ansible_connexion=lxd pour configurer des conteneurs linux ainsi que ansible_connexion= pour les serveurs windows

  • Les questions de sécurités de la connexion se posent bien sur différemment selon le mode de connexion utilisés (port, authentification, etc.)

  • Pour débugger les connexions et diagnotiquer leur sécurité on peut afficher les détails de chaque connection ansible avec le mode de verbosité maximal (network) en utilisant le paramètre -vvvv.

Variables et secrets

Le principal risque de sécurité lié à Ansible comme avec Docker et l’IaC en général consiste à laisser trainer des secrets (mot de passe, identités de clients, api token, secret de chiffrement / migration etc.) dans le code ou sur les serveurs (moins problématique).

Attention : les dépôt git peuvent cacher des secrets dans leur historique. Pour chercher et nettoyer un secret dans un dépôt l’outil le plus courant est BFG : https://rtyley.github.io/bfg-repo-cleaner/

Désactiver le logging des informations sensibles

Ansible propose une directive no_log: yes qui permet de désactiver l’affichage des valeurs d’entrée et de sortie d’une tâche.

Il est ainsi possible de limiter la prolifération de données sensibles.

Ansible vault

Pour éviter de divulguer des secrets par inadvertance, il est possible de gérer les secrets avec des variables d’environnement ou avec un fichier variable externe au projet qui échappera au versionning git, mais ce n’est pas idéal.

Ansible intègre un trousseau de secret appelé , Ansible Vault permet de chiffrer des valeurs variables par variables ou des fichiers complets. Les valeurs stockées dans le trousseaux sont déchiffrée à l’exécution après dévérouillage du trousseau.

  • ansible-vault create /var/secrets.yml
  • ansible-vault edit /var/secrets.yml ouvre $EDITOR pour changer le fichier de variables.
  • ansible-vault encrypt_file /vars/secrets.yml pour chiffrer un fichier existant
  • ansible-vault encrypt_string monmotdepasse permet de chiffrer une valeur avec un mot de passe. le résultat peut être ensuite collé dans un fichier de variables par ailleurs en clair.

Pour déchiffrer il est ensuite nécessaire d’ajouter l’option --ask-vault-pass au moment de l’exécution de ansible ou ansible-playbook

Il existe également un mode pour gérer plusieurs mots de passe associés à des identifiants.

Ansible dans le cloud

L’automatisation Ansible fait d’autant plus sens dans un environnement d’infrastructures dynamique:

  • L’agrandissement horizontal implique de résinstaller régulièrement des machines identiques
  • L’automatisation et la gestion des configurations permet de mieux contrôler des environnements de plus en plus complexes.

Il existe de nombreuses solutions pour intégrer Ansible avec les principaux providers de cloud (modules ansible, plugins d’API, intégration avec d’autre outils d’IaC Cloud comme Terraform ou Cloudformation).

Inventaires dynamiques

Les inventaires que nous avons utilisés jusqu’ici implique d’affecter à la main les adresses IP des différents noeuds de notre infrastructure. Cela devient vite ingérable.

La solution ansible pour le pas gérer les IP et les groupes à la main est appelée inventaire dynamique ou inventory plugin. Un inventaire dynamique est simplement un programme qui renvoie un JSON respectant le format d’inventaire JSON ansible, généralement en contactant l’api du cloud provider ou une autre source.

$ ./inventory_terraform.py
{
  "_meta": {
    "hostvars": {
      "balancer0": {
        "ansible_host": "104.248.194.100"
      },
      "balancer1": {
        "ansible_host": "104.248.204.222"
      },
      "awx0": {
        "ansible_host": "104.248.204.202"
      },
      "appserver0": {
        "ansible_host": "104.248.202.47"
      }
    }
  },
  "all": {
    "children": [],
    "hosts": [
      "appserver0",
      "awx0",
      "balancer0",
      "balancer1"
    ],
    "vars": {}
  },
  "appservers": {
    "children": [],
    "hosts": [
      "balancer0",
      "balancer1"
    ],
    "vars": {}
  },
  "awxnodes": {
    "children": [],
    "hosts": [
      "awx0"
    ],
    "vars": {}
  },
  "balancers": {
    "children": [],
    "hosts": [
      "appserver0"
    ],
    "vars": {}
  }
}%  

On peut ensuite appeler ansible-playbook en utilisant ce programme plutôt qu’un fichier statique d’inventaire: ansible-playbook -i inventory_terraform.py configuration.yml

Étendre et intégrer Ansible

La bonne pratique : utiliser un plugin d’inventaire pour alimenter

Bonne pratique : Normalement l’information de configuration Ansible doit provenir au maximum de l’inventaire. Ceci est conforme à l’orientation plutôt déclarative d’Ansible et à son exécution descendante (master -> nodes). La méthode à privilégier pour intégrer Ansible à des sources d’information existantes est donc d’utiliser ou développer un plugin d’inventaire.

https://docs.ansible.com/ansible/latest/plugins/inventory.html

On peut cependant alimenter le dictionnaire de variable Ansible au fur et à mesure de l’exécution, en particulier grâce à la directive register et au module set_fact.

Exemple:

# this is just to avoid a call to |default on each iteration
- set_fact:
    postconf_d: {}

- name: 'get postfix default configuration'
  command: 'postconf -d'
  register: postconf_result
  changed_when: false

# the answer of the command give a list of lines such as:
# "key = value" or "key =" when the value is null
- name: 'set postfix default configuration as fact'
  set_fact:
    postconf_d: >
            {{ postconf_d | combine(dict([ item.partition('=')[::2]map'trim') ])) }}
  loop: postconf_result.stdout_lines

On peut explorer plus facilement la hiérarchie d’un inventaire statique ou dynamique avec la commande:

ansible-inventory --inventory <inventory> --graph

Principaux type de plugins possibles pour étendre Ansible

https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html

  • Ansible modules
  • Inventory plugins
  • Connection plugins

Intégration Ansible et AWS

Pour les VPS de base Amazon EC2 : utiliser un plugin d’inventaire AWS et les modules adaptés.

Intégration Ansible Nagios

Possibilité 1 : Gérer l’exécution de tâches Ansible et le monitoring Nagios séparément, utiliser le module nagios pour désactiver les alertes Nagios lorsqu’on manipule les ressources monitorées par Nagios.

Possibilité 2 : Laisser le contrôle à Nagios et utiliser un plugin pour que Nagios puisse lancer des plays Ansible en réponse à des évènements sur les sondes.

TP1 - Mise en place et Ansible ad-hoc

Installation de Ansible

  • Installez Ansible au niveau du système avec apt en lançant:
$ sudo apt update
$ sudo apt install software-properties-common
$ sudo apt-add-repository --yes --update ppa:ansible/ansible
$ sudo apt install ansible
  • Affichez la version pour vérifier que c’est bien la dernière stable.
ansible --version
=> 2.8.x
  • Traditionnellement lorsqu’on veut vérifier le bon fonctionnement d’une configuration on utilise ansible all -m ping. Que signifie-t-elle ?
Réponse :
  • Lancez la commande précédente. Que ce passe-t-il ?
Réponse :
  • Utilisez en plus l’option -vvv pour mettre en mode très verbeux. Ce mode est très efficace pour débugger lorsqu’une erreur inconnue se présente. Que se passe-t-il avec l’inventaire ?
Réponse :
  • Testez l’installation avec la commande ansible en vous connectant à votre machine localhost et en utilisant le module ping.
Réponse :
  • Ajoutez la ligne hotelocal ansible_host=127.0.0.1 dans l’inventaire par défaut (le chemin est indiqué dans). Et pinguer hotelocal.
Réponse :

Explorer LXD

LXD est une technologie de conteneurs actuellement promue par canonical (ubuntu) qui permet de faire des conteneur linux orientés systèmes plutôt qu’application. Par exemple systemd est disponible à l’intérieur des conteneurs contrairement aux conteneurs Docker.

LXD est déjà installé et initialisé sur notre ubuntu (sinon apt install snapd + snap install lxd + ajouter votre utilisateur courant au group unix lxd).

Il faut cependant l’initialiser avec : lxd init

  • Cette commande vous pose un certain nombre de questions pour la configuration et vous pouvez garder TOUTES les valeurs par défaut en fait ENTER simplement à chaque question.

  • Affichez la liste des conteneurs avec lxc list. Aucun conteneur ne tourne.

  • Maintenant lançons notre premier conteneur centos avec lxc launch images:centos/7/amd64 centos1.

  • Listez à nouveau les conteneurs lxc.

  • Ce conteneur est un centos minimal et n’a donc pas de serveur SSH pour se connecter. Pour lancez des commandes dans le conteneur on utilise une commande LXC pour s’y connecter lxc exec <non_conteneur> -- <commande>. Dans notre cas nous voulons lancer bash pour ouvrir un shell dans le conteneur : lxc exec centos1 -- bash.

  • Nous pouvons installer des logiciels dans le conteneur comme dans une VM. Pour sortir du conteneur on peut simplement utiliser exit.

  • Un peu comme avec Docker, LXC utilise des images modèles pour créer des conteneurs. Affichez la liste des images avec lxc image list. Trois images sont disponibles l’image centos vide téléchargée et utilisée pour créer centos1 et deux autres images préconfigurée ubuntu_ansible et centos_ansible. Ces images contiennent déjà la configuration nécessaire pour être utilisée avec ansible (SSH + Python + Un utilisateur + une clé SSH).

  • Supprimez la machine centos1 avec lxc stop centos1 && lxc delete centos1

Facultatif : Configurer un conteneur pour Ansible manuellement

Facultatif :

Récupérer les images de correction depuis un remote LXD

Pour avoir tous les mêmes images de base récupérons les depuis un serveur dédié à la formation. Un serveur distant LXD est appelé un remote.

  • Ajoutez le remote tp-images avec la commande:
lxc remote add tp-images https://lxd-images.dopl.uk --protocol lxd
  • Le mot de passe est: formation_ansible.

  • Copiez ensuite les images depuis ce remote dans le dépot d’image local avec :

lxc image copy tp-images:centos_ansible local: --copy-aliases --auto-update
lxc image copy tp-images:ubuntu_ansible local: --copy-aliases --auto-update

Lancer et tester les conteneurs

Créons à partir des images du remotes un conteneur ubuntu et un autre centos:

lxc launch ubuntu_ansible ubu1
lxc launch centos_ansible centos1
  • Pour se connecter en SSH nous allons donc utiliser une clé SSH appelée id_stagiaire qui devrait être présente dans votre dossier ~/.ssh/. Vérifiez cela en lançant ls -l /home/stagiaire/.ssh.

  • Déverrouillez cette clé ssh avec ssh-add ~/.ssh/id_stagiaire et le mot de passe devops101 (le ssh-agent doit être démarré dans le shell pour que cette commande fonctionne si ce n’est pas le cas eval $(ssh-agent)).

  • Essayez de vous connecter à ubu1 et centos1 en ssh pour vérifier que la clé ssh est bien configurée et vérifiez dans chaque machine que le sudo est configuré sans mot de passe avec sudo -i.

Créer un projet de code Ansible

Lorsqu’on développe avec Ansible il est conseillé de le gérer comme un véritable projet de code :

  • versionner le projet avec Git
  • Ajouter tous les paramètres nécessaires dans un dossier pour être au plus proche du code. Par exemple utiliser un inventaire inventory.cfg ou hosts et une configuration locale au projet ansible.cfg

Nous allons créer un tel projet de code pour la suite du tp1

  • Créez un dossier projet tp1 sur le Bureau.
Facultatif :
  • Ouvrez Visual Studio Code.
  • Installez l’extension Ansible dans VSCode.
  • Ouvrez le dossier du projet avec Open Folder...

Un projet Ansible implique généralement une configuration Ansible spécifique décrite dans un fichier ansible.cfg

  • Ajoutez à la racine du projet un tel fichier ansible.cfg avec à l’intérieur:
[defaults]
inventory = ./inventory.cfg
roles_path = ./roles
host_key_checking = false # nécessaire pour les labs ou on créé et supprime des machines constamment avec des signatures SSH changées.
  • Créez le fichier d’inventaire spécifié dans ansible.cfg et ajoutez à l’intérieur notre nouvelle machine hote1.
  • Il faut pour cela lister les conteneurs lxc lancés.
lxc list # récupérer l'ip de la machine

Créez et complétez le fichier inventory.cfg d’après ce modèle:

ubu1 ansible_host=<ip>

[all:vars]
ansible_user=<votre_user>

Contacter nos nouvelles machines

Ansible cherche la configuration locale dans le dossier courant. Conséquence: on lance généralement toutes les commandes ansible depuis la racine de notre projet.

  • Dans le dossier du projet, essayez de relancer la commande ad-hoc ping sur cette machine.

  • Ansible implique le cas échéant (login avec clé ssh) de déverrouiller la clé ssh pour se connecter à chaque hôte. Lorsqu’on en a plusieurs il est donc nécessaire de la déverrouiller en amont avec l’agent ssh pour ne pas perturber l’exécution des commandes ansible. Pour cela : ssh-add.

  • Créez un groupe adhoc_lab et ajoutez les deux machines ubu1 et centos1.

Réponse :
  • Lancez ping sur les deux machines.
Réponse :
  • Nous avons jusqu’à présent utilisé une connexion ssh par clé et précisé l’utilisateur de connexion dans le fichier ansible.cfg. Cependant on peut aussi utiliser une connexion par mot de passe et préciser l’utilisateur et le mot de passe dans l’inventaire ou en lançant la commande.

En précisant les paramètres de connexion dans le playbook il et aussi possible d’avoir des modes de connexion différents pour chaque machine.

Installons nginx avec quelques modules et commandes ad-hoc

  • Modifiez l’inventaire pour créer deux sous-groupes de adhoc_lab, centos_hosts et ubuntu_hosts avec deux machines dans chacun. (utilisez pour cela [adhoc_lab:children])
[all:vars]
ansible_user=<votre_user>

[ubuntu_hosts]
ubu1 ansible_host=<ip>

[centos_hosts]
centos1 ansible_host=<ip>

[adhoc_lab:children]
ubuntu_hosts
centos_hosts

Dans un inventaire ansible on commence toujours par créer les plus petits sous groupes puis on les rassemble en plus grands groupes.

  • Pinguer chacun des 3 groupes avec une commande ad hoc.

Nous allons maintenant installer nginx sur les 2 machines. Il y a plusieurs façons d’installer des logiciels grâce à Ansible: en utilisant le gestionnaire de paquets de la distribution ou un gestionnaire spécifique comme pip ou npm. Chaque méthode dispose d’un module ansible spécifique.

  • Si nous voulions installer nginx avec la même commande sur des machines centos et ubuntu à la fois impossible d’utiliser apt car centos utilise yum. Pour éviter ce problème on peut utiliser le module package qui permet d’uniformiser l’installation (pour les cas simples).
    • Allez voir la documentation de ce module
    • utilisez --become pour devenir root avant d’exécuter la commande (cf élévation de privilège dans le cours2)
    • Utilisez le pour installer nginx
Réponse :
  • Pour résoudre le problème installez epel-release sur la machine centos.
Réponse :
  • Relancez la commande d’installation de nginx. Que remarque-t-on ?
Réponse :
  • Utiliser le module systemd et l’option --check pour vérifier si le service nginx est démarré sur chacune des 2 machines. Normalement vous constatez que le service est déjà démarré (par défaut) sur la machine ubuntu et non démarré sur la machine centos.
Réponse :
  • L’option --check à vérifier l’état des ressources sur les machines mais sans modifier la configuration`. Relancez la commande précédente pour le vérifier. Normalement le retour de la commande est le même (l’ordre peu varier).

  • Lancez la commande avec state=stopped : le retour est inversé.

  • Enlevez le --check pour vous assurer que le service est démarré sur chacune des machines.

  • Visitez dans un navigateur l’ip d’un des hôtes pour voir la page d’accueil nginx.

Ansible et les commandes unix

Il existe trois façon de lancer des commandes unix avec ansible:

  • le module command utilise python pour lancez la commande.

    • les pipes et syntaxes bash ne fonctionnent pas.
    • il peut executer seulement les binaires.
    • il est cependant recommandé quand c’est possible car il n’est pas perturbé par l’environnement du shell sur les machine et donc plus prévisible.
  • le module shell utilise un module python qui appelle un shell pour lancer une commande.

    • fonctionne comme le lancement d’une commande shell mais utilise un module python.
  • le module raw.

    • exécute une commande ssh brute.
    • ne nécessite pas python sur l’hote : on peut l’utiliser pour installer python justement.
    • ne dispose pas de l’option creates pour simuler de l’idempotence.
  • Créez un fichier dans /tmp avec touch et l’un des modules précédents.

  • Relancez la commande. Le retour est toujours changed car ces modules ne sont pas idempotents.

  • Relancer l’un des modules shell ou command avec touch et l’option creates pour rendre l’opération idempotente. Ansible détecte alors que le fichier témoin existe et n’exécute pas la commande.

ansible adhoc_lab --become -m "command touch /tmp/file" -a "creates=/tmp/file"

TP2 - Créer un playbook de déploiement d'application flask

Création du projet

  • Créez un nouveau dossier tp2_flask_deployment.
  • Créez le fichier ansible.cfg comme précédemment.
[defaults]
inventory = ./inventory.cfg
roles_path = ./roles
host_key_checking = false
  • Créez deux machines ubuntu app1 et app2.
lxc launch ubuntu_ansible app1
lxc launch ubuntu_ansible app2
  • Créez l’inventaire statique inventory.cfg.
$ lxc list # pour récupérer les adresses ip 

[all:vars] ansible_user=

[appservers] app1 ansible_host=10.x.y.z app2 ansible_host=10.x.y.z


- Ajoutez à l'intérieur les deux machines dans un groupe `appservers`.
- Pinguez les machines.

ansible all -m ping



Facultatif :
## Premier playbook : installer les dépendances Le but de ce projet est de déployer une application flask, c'est a dire une application web python. Le code (très minimal) de cette application se trouve sur github à l'adresse: [https://github.com/e-lie/flask_hello_ansible.git](https://github.com/e-lie/flask_hello_ansible.git). - N'hésitez pas consulter extensivement la documentation des modules avec leur exemple ou d'utiliser la commande de doc `ansible-doc <module>` - Créons un playbook : ajoutez un fichier `flaskhello_deploy.yml` avec à l'intérieur: ```yaml - hosts: <hotes_cible> tasks: - name: ping ping:
  • Lancez ce playbook avec la commande ansible-playbook <nom_playbook>.

  • Commençons par installer les dépendances de cette application. Tous nos serveurs d’application sont sur ubuntu. Nous pouvons donc utiliser le module apt pour installer les dépendances. Il fournit plus d’option que le module package.

  • Avec le module apt installez les applications: python3-dev, python3-pip, python3-virtualenv, virtualenv, nginx, git. Donnez à cette tache le nom: ensure basic dependencies are present. Ajoutez, pour devenir root, la directive become: yes au début du playbook.

    - name: Ensure apt dependencies are present
      apt:
        name:
          - python3-dev
          - python3-pip
          - python3-virtualenv
          - virtualenv
          - nginx
          - git
        state: present
  • Lancez ce playbook sans rien appliquer avec la commande ansible-playbook <nom_playbook> --check --diff. La partie --check indique à Ansible de ne faire aucune modification. La partie --diff nous permet d’afficher ce qui changerait à l’application du playbook.

  • Relancez bien votre playbook à chaque tache : comme Ansible est idempotent il n’est pas grave en situation de développement d’interrompre l’exécution du playbook et de reprendre l’exécution après un échec.

  • Ajoutez une tâche systemd pour s’assurer que le service nginx est démarré.

    - name: Ensure nginx service started
      systemd:
        name: nginx
        state: started
  • Ajoutez une tache pour créer un utilisateur flask et l’ajouter au groupe www-data. Utilisez bien le paramètre append: yes pour éviter de supprimer des groupes à l’utilisateur.
    - name: Add the user running webapp
      user:
        name: "flask"
        state: present
        append: yes # important pour ne pas supprimer les groupes d'un utilisateur existant
        groups:
          - "www-data"

Récupérer le code de l’application

  • Pour déployer le code de l’application deux options sont possibles.

    • Télécharger le code dans notre projet et le copier sur chaque serveur avec le module sync qui fait une copie rsync.
    • Utiliser le module git.
  • Nous allons utiliser la deuxième option (git) qui est plus cohérente pour le déploiement et la gestion des versions logicielles. Allez voir la documentation comment utiliser ce module.

  • Utilisez le pour télécharger le code source de l’application (branche master) dans le dossier /home/flask/hello mais en désactivant la mise à jour (au cas ou le code change).

    - name: Git clone/update python hello webapp in user home
      git:
        repo: "https://github.com/e-lie/flask_hello_ansible.git"
        dest: /home/flask/hello
        version: "master"
        clone: yes
        update: no
  • Lancez votre playbook et allez vérifier sur une machine en ssh que le code est bien téléchargé.

Installez les dépendances python de l’application

Le langage python a son propre gestionnaire de dépendances pip qui permet d’installer facilement les librairies d’un projet. Il propose également un méchanisme d’isolation des paquets installés appelé virtualenv. Normalement installer les dépendances python nécessite 4 ou 5 commandes shell.

  • La liste de nos dépendances est listée dans le fichier requirements.txt à la racine du dossier d’application.

  • Nous voulons installer ces dépendances dans un dossier venv également à la racine de l’application.

  • Nous voulons installer ces dépendance en version python3 avec l’argument virtualenv_python: python3.

Avec ces informations et la documentation du module pip installez les dépendances de l’application.

    - name: Install python dependencies for the webapp in a virtualenv
      pip:
        requirements: /home/flask/hello/requirements.txt
        virtualenv: /home/flask/hello/venv
        virtualenv_python: python3

Changer les permission sur le dossier application

Notre application sera executée en tant qu’utilisateur flask pour des raisons de sécurité. Pour cela le dossier doit appartenir à cet utilisateur or il a été créé en tant que root (à cause du become: yes de notre playbook).

  • Créez une tache file qui change le propriétaire du dossier de façon récursive.
    - name: Change permissions of app directory
      file:
        path: /home/flask/hello
        state: directory
        owner: "flask"
        recurse: true

Module Template : configurer le service qui fera tourner l’application

Notre application doit tourner comme c’est souvent le cas en tant que service (systemd). Pour cela nous devons créer un fichier service adapté hello.service dans le le dossier /etc/systemd/system/.

Ce fichier est un fichier de configuration qui doit contenir le texte suivant:

[Unit]
Description=Gunicorn instance to serve hello
After=network.target

[Service]
User=flask
Group=www-data
WorkingDirectory=/home/flask/hello
Environment="PATH=/home/flask/hello/venv/bin"
ExecStart=/home/flask/hello/venv/bin/gunicorn --workers 3 --bind unix:hello.sock -m 007 app:app

[Install]
WantedBy=multi-user.target

Pour gérer les fichier de configuration on utilise généralement le module template qui permet à partir d’un fichier modèle situé dans le projet ansible de créer dynamiquement un fichier de configuration adapté sur la machine distante.

  • Créez un dossier templates, avec à l’intérieur le fichier app.service.j2 contenant le texte précédent.

  • Utilisez le module template pour le copier au bon endroit avec le nom hello.service.

  • Utilisez ensuite systemd pour démarrer ce service (state: restarted ici pour le cas ou le fichier à changé).

Configurer nginx

  • Comme précédemment créez un fichier de configuration hello.test.conf dans le dossier /etc/nginx/sites-available à partir du fichier modèle:

nginx.conf.j2

server {
    listen 80;

    server_name hello.test;

    location / {
        include proxy_params;
        proxy_pass http://unix:/home/flask/hello/hello.sock;
    }
}
  • Utilisez file pour créer un lien symbolique de ce fichier dans /etc/nginx/sites-enabled (avec l’option force:yes pour écraser le cas échéant).

  • Ajoutez une tache pour supprimer le site /etc/nginx/sites-enabled/default.

  • Ajouter une tache de redémarrage de nginx.

  • Ajoutez hello.test dans votre fichier /etc/hosts pointant sur l’ip d’un des serveur d’application.

  • Visitez l’application dans un navigateur et debugger le cas échéant.

Correction intermédiaire

flaskhello_deploy.yml

Code de correction :
Facultatif :

Améliorer notre playbook avec des variables.

Variables

Ajoutons des variables pour gérer dynamiquement les paramètres de notre déploiement:

  • Ajoutez une section vars: avant la section tasks: du playbook.

  • Mettez dans cette section la variable suivante (dictionnaire):

  app:
    name: hello
    user: flask
    domain: hello.test
  • Remplacez dans le playbook précédent et les deux fichiers de template:

    • toutes les occurence de la chaine hello par {{ app.name }}
    • toutes les occurence de la chaine flask par {{ app.user }}
    • toutes les occurence de la chaine hello.test par {{ app.domain }}
  • Relancez le playbook : toutes les tâches devraient renvoyer ok à part les “restart” car les valeurs sont identiques.

Facultatif :
  • Pour la correction clonez le dépôt de base à l’adresse https://github.com/e-lie/ansible_tp_corrections.
  • Renommez le clone en tp2_before_handlers.
  • ouvrez le projet avec VSCode.
  • Activez la branche tp2_before_handlers_correction avec git checkout tp2_before_handlers_correction.

Le dépot contient également les corrigés du TP3 et TP4 dans d’autre branches.

Vous pouvez consultez la correction également directement sur le site de github.

Ajouter un handler pour nginx et le service

Pour le moment dans notre playbook, les deux tâches de redémarrage de service sont en mode restarted c’est à dire qu’elles redémarrent le service à chaque exécution (résultat: changed) et ne sont donc pas idempotentes. En imaginant qu’on lance ce playbook toutes les 15 minutes dans un cron pour stabiliser la configuration, on aurait un redémarrage de nginx 4 fois par heure sans raison.

On désire plutôt ne relancer/recharger le service que lorsque la configuration conrespondante a été modifiée. c’est l’objet des taches spéciales nommées handlers.

Ajoutez une section handlers: à la suite

  • Déplacez la tâche de redémarrage/reload de nginx dans cette section et mettez comme nom reload nginx.

  • Ajoutez aux deux taches de modification de la configuration la directive notify: <nom_du_handler>.

  • Testez votre playbook. il devrait être idempotent sauf le restart de hello.service.

  • Testez le handler en ajoutant un commentaire dans le fichier de configuration nginx.conf.j2.

    - name: template nginx site config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ app.domain }}.conf
      notify: reload nginx

      ...

  handlers:
    - name: reload nginx
      systemd:
        name: "nginx"
        state: reloaded

# => penser aussi à supprimer la tâche de restart de nginx précédente

Rendre le playbook dynamique avec une boucle.

Plutôt qu’une variable app unique on voudrait fournir au playbook une liste d’application à installer (liste potentiellement définie durant l’exécution).

  • Identifiez dans le playbook précédent les tâches qui sont exactement communes aux deux installations.

!!! il s’agit des taches d’installation des dépendances apt et de vérification de l’état de nginx (démarré)

  • Créez un nouveau fichier deploy_app_tasks.yml et copier à l’intérieur la liste de toutes les autres taches mais sans les handlers que vous laisserez à la fin du playbook.

!!! Il reste donc dans le playbook seulement les deux premières taches et les handlers, les autres taches (toutes celles qui contiennent des parties variables) sont dans deploy_app_tasks.yml.

  • Ce nouveau fichier n’est pas à proprement parlé un playbook mais une liste de taches. utilisez include_tasks: pour importer cette liste de tâche à l’endroit ou vous les avez supprimées.

  • Vérifiez que le playbook fonctionne et est toujours idempotent.

  • Ajoutez une tâche debug: msg={{ app }} au début du playbook pour visualiser le contenu de la variable.

  • Ensuite remplacez la variable app par une liste flask_apps de deux dictionnaires (avec name, domain, user différents les deux dictionnaires et repository et version identiques).

flask_apps:
  - name: hello
    domain: "hello.test"
    user: "flask1"
    version: master
    repository: https://github.com/e-lie/flask_hello_ansible.git

  - name: hello2
    domain: "hello2.test"
    user: "flask2"
    version: master
    repository: https://github.com/e-lie/flask_hello_ansible.git
  • Utilisez les directives loop et loop_control+loop_var sur la tâche include_tasks pour inclure les taches pour chacune des deux applications.

  • Créez le dossier group_vars et déplacez le dictionnaire flask_apps dans un fichier group_vars/appservers.yml. Comme son nom l’indique ce dossier permet de définir les variables pour un groupe de serveurs dans un fichier externe.

  • Testez en relançant le playbook que le déplacement des variables est pris en compte correctement.

Correction

Le dépot contient également les corrigés du TP3 et TP4 dans d’autre branches.

Vous pouvez consultez la correction également directement sur le site de github.

Bonus

Pour ceux ou celles qui sont allé-es vite, vous pouvez tenter de créer une nouvelle version de votre playbook portable entre centos et ubuntu. Pour cela utilisez la directive when: ansible_os_family == 'Debian' ou RedHat.

Bonus 2 pour pratiquer

Essayez de déployer une version plus complexe d’application flask avec une base de donnée mysql: https://github.com/miguelgrinberg/microblog/tree/v0.17

Il s’agit de l’application construite au fur et à mesure dans un magnifique tutoriel python. Ce chapitre indique comment déployer l’application sur linux.

TP3 - Structurer le projet avec des roles

Ajouter un provisionneur d’infra maison pour créer les machines automatiquement

  • Clonez la correction du TP2 (lien à la fin du TP2) et renommez là en tp3_provisionner_roles.
  • Chargez ce dossier dans VSCode (vous pouvez fermer le tp2).

Dans notre infra virtuelle, nous avons trois machines dans deux groupes. Quand notre lab d’infra grossit il devient laborieux de créer les machines et affecter les ip à la main. En particulier détruire le lab et le reconstruire est pénible. Nous allons pour cela introduire un playbook de provisionning qui va créer les conteneurs lxd en définissant leur ip à partir de l’inventaire.

  • modifiez l’inventaire comme suit:
[all:vars]
ansible_user=<votre_user>

[appservers]
app1 ansible_host=10.x.y.121 container_image=ubuntu_ansible node_state=started
app2 ansible_host=10.x.y.122 container_image=ubuntu_ansible node_state=started

[dbservers]
db1 ansible_host=10.x.y.131 container_image=ubuntu_ansible node_state=started
  • Remplacez x et y dans l’adresse IP par celle fournies par votre réseau virtuel lxd (faites lxc list et copier simple les deux chiffre du milieu des adresses IP)

  • Ajoutez un playbook provision_lxd_infra.yml dans un dossier provisionners contenant:

- hosts: localhost
  connection: local

  tasks:
    - name: Setup linux containers for the infrastructure simulation
      lxd_container:
        name: "{{ item }}"
        state: "{{ hostvars[item]['node_state'] }}"
        source:
          type: image
          alias: "{{ hostvars[item]['container_image'] }}"
        profiles: ["default"]
        config:
          security.nesting: 'true' 
          security.privileged: 'false' 
        devices:
          # configure network interface
          eth0:
            type: nic
            nictype: bridged
            parent: lxdbr0
            # get ip address from inventory
            ipv4.address: "{{ hostvars[item].ansible_host }}"

        # Comment following line if you installed lxd using apt
        url: unix:/var/snap/lxd/common/lxd/unix.socket
        wait_for_ipv4_addresses: true
        timeout: 600

      register: containers
      loop: "{{ groups['all'] }}"
    

    # Uncomment following if you want to populate hosts file pour container local hostnames
    # AND launch playbook with --ask-become-pass option

    # - name: Config /etc/hosts file accordingly
    #   become: yes
    #   lineinfile:
    #     path: /etc/hosts
    #     regexp: ".*{{ item }}$"
    #     line: "{{ hostvars[item].ansible_host }}    {{ item }}"
    #     state: "present"
    #   loop: "{{ groups['all'] }}"
  • Etudions le playbook (explication démo).

  • Lancez le playbook avec sudo car lxd se contrôle en root sur localhost: sudo ansible-playbook provision_lxd_infra (c’est le seul cas exceptionnel ou ansible-playbook doit être lancé avec sudo, pour les autre playbooks ce n’est pas le cas)

  • Lancez lxc list pour afficher les nouvelles machines de notre infra et vérifier que le serveur de base de données a bien été créé.

Facultatif: Ajouter une machine mysql simple avec un role externe

Facultatif :

Transformer notre playbook en role

  • Si ce n’est pas fait, créez à la racine du projet le dossier roles dans lequel seront rangés tous les roles (c’est une convention ansible à respecter).
  • Créer un dossier flaskapp dans roles.
  • Ajoutez à l’intérieur l’arborescence:
flaskapp
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── tasks
│   ├── deploy_app_tasks.yml
│   └── main.yml
└── templates
    ├── app.service.j2
    └── nginx.conf.j2
  • Les templates et les listes de handlers/tasks sont a mettre dans les fichiers correspondants (voir plus bas)
  • Le fichier defaults/main.yml permet de définir des valeurs par défaut pour les variables du role. Mettez à l’intérieur une application par défaut:
flask_apps:
  - name: defaultflask
    domain: defaultflask.test
    repository: https://github.com/e-lie/flask_hello_ansible.git
    version: master
    user: defaultflask

Ces valeurs seront écrasées par celles fournies dans le dossier group_vars (la liste de deux applications du TP2). Elle est présente pour éviter que le role plante en l’absence de variable (valeurs de fallback).

  • Copiez les tâches (juste la liste de tiret sans l’intitulé de section tasks:) contenues dans le playbook appservers dans le fichier tasks/main.yml.

  • De la même façon copiez le handler dans handlers/main.yml sans l’intitulé handlers:.

  • Copiez également le fichier deploy_flask_tasks.yml dans le dossier tasks.

  • Déplacez vos deux fichiers de template dans le dossier templates du role (et non celui à la racine que vous pouvez supprimer).

  • Pour appeler notre nouveau role, supprimez les sections tasks: et handlers: du playbook appservers.yml et ajoutez à la place:

  roles:
    - flaskapp
  • Votre role est prêt : lancez appservers.yml et debuggez le résultat le cas échéant.

Facultatif: Ajouter un paramètre d’exécution à notre rôle pour mettre à jour l’application.

Facultatif :

Correction

Il contient également les corrigés du TP2 et TP4 dans d’autre branches.

Bonus

Essayez différents exemples de projets de Geerlingguy accessibles sur github à l’adresse https://github.com/geerlingguy/ansible-for-devops.

TP4 - Orchestration, Serveur de contrôle et Cloud

Cloner le projet modèle

  • Pour simplifier le démarrage, clonez le dépôt de base à l’adresse https://github.com/e-lie/ansible_tp_corrections.
  • Renommez le clone en tp4.
  • ouvrez le projet avec VSCode.
  • Activez la branche tp4_correction avec git checkout tp4_correction.

Facultatif: Infrastructure dans le cloud avec Terraform et Ansible

Facultatif :

Infrastructure multi-tiers avec load balancer

Pour configurer notre infrastructure:

  • Installez les roles avec ansible-galaxy install -r roles/requirements.yml -p roles.

  • Si vous n’avez pas fait la partie Terraform:

    • complétez l’inventaire statique (inventory.cfg)
    • changer dans ansible.cfg l’inventaire en ./inventory.cfg comme pour les TP précédents
    • Supprimez les conteneurs app1 et app2 du TP précédent puis lancez le playbook de provisionning lxd : sudo ansible-playbook provisionner/provision_lxd_infra.yml
  • Lancez le playbook global site.yml

  • Utilisez la commande ansible-inventory --graph pour afficher l’arbre des groupes et machines de votre inventaire

  • Utilisez la de même pour récupérer l’ip du balancer0 (ou balancer1) avec : ansible-inventory --host=balancer0

  • Ajoutez hello.test et hello2.test dans /etc/hosts pointant vers l’ip de balancer0.

  • Chargez les pages hello.test et hello2.test.

  • Observons ensemble l’organisation du code Ansible de notre projet.

    • Nous avons rajouté à notre infrastructure un loadbalancer installé à l’aide du fichier balancers.yml
    • Le playbook upgrade_apps.yml permet de mettre à jour l’application en respectant sa haute disponibilité. Il s’agit d’une opération d’orchestration simple en les 3 serveurs de notre infrastructure.
    • Cette opération utilise en particulier serial qui permet de d’exécuter séquentiellement un play sur un fraction des serveurs d’un groupe (ici 1 à la fois parmis les 2).
    • Notez également l’usage de delegate qui permet d’exécuter une tache sur une autre machine que le groupe initialement ciblé. Cette directive est au coeur des possibilités d’orchestration Ansible en ce qu’elle permet de contacter un autre serveur ( déplacement latéral et non pas master -> node ) pour récupérer son état ou effectuer une modification avant de continuer l’exécution et donc de coordonner des opérations.
    • notez également le playbook exclude_backend.yml qui permet de sortir un backend applicatif du pool. Il s’utilise avec des variables en ligne de commande
  • Désactivez le noeud qui vient de vous servir la page en utilisant le playbook exclude_backend.yml:

ansible-playbook --extra-vars="backend_name=<noeud a desactiver> backend_state=disabled" playbooks/exclude_backend.yml
  • Rechargez la page: vous constatez que c’est l’autre backend qui a pris le relais.

  • Nous allons maintenant mettre à jour

Falcultatif : ajoutons un serveur de control AWX (/ Ansible Tower)

Facultatif :

Explorer AWX

  • Identifiez vous sur awx avec le login admin et le mot de passe précédemment configuré.

  • Dans la section modèle de projet, importez votre projet. Un job d’import se lance. Si vous avez mis le fichier requirements.yml dans roles les roles devraient être automatiquement installés.

  • Dans la section crédentials, créez un crédential de type machine. Dans la section clé privée copiez le contenu du fichier ~/.ssh/id_ssh_tp que nous avons configuré comme clé ssh de nos machines. Ajoutez également la passphrase que vous avez configuré au moment de la création de cette clé.

  • Créez une ressource inventaire. Créez simplement l’inventaire avec un nom au départ. Une fois créé vous pouvez aller dans la section source et choisir de l’importer depuis le projet, sélectionnez inventory.cfg que nous avons configuré précédemment. Bien que nous utilisions AWX les ip n’ont pas changé car AWX est en local et peut donc se connecter au reste de notre infrastructure LXD.

  • Pour tester tout cela vous pouvez lancez une tâche ad-hoc ping depuis la section inventaire en sélectionnant une machine et en cliquant sur le bouton executer.

  • Allez dans la section modèle de job et créez un job en sélectionnant le playbook site.yml.

  • Exécutez ensuite le job en cliquant sur la fusée. Vous vous retrouvez sur la page de job de AWX. La sortie ressemble à celle de la commande mais vous pouvez en plus explorer les taches exécutées en cliquant dessus.

  • Modifiez votre job, dans la section Plannifier configurer l’exécution du playbook site.yml toutes les 15 minutes.

  • Allez dans la section plannification. Puis visitez l’historique des Jobs.

Bibliographie

Ansible

  • Jeff Geerling - Ansible for DevOps - Leanpub
Pour aller plus loin :
  • Keating2017 - Mastering Ansible - Second Edition - Packt
Ansible pour des thématiques sépcifiques
  • Ratan2017 - Practical Network Automation: Leverage the power of Python and Ansible to optimize your network
  • Madhu, Akash2017 - Security automation with Ansible 2
  • https://iac.goffinet.org/ansible-network/
Cheatsheet

Linux et Bash

Slides 1 - Introduction à Linux

Introduction à Linux


Hello, world!


Disclaimers

  • L’informatique technique, c’est compliqué
    • ignorez les cryptonerds qui prétendent que c’est intuitif et trivial
  • Soyez patients, méthodiques, attentifs !

On est là pour apprendre :

  • Trompez-vous !
  • Sortez des sentiers battus !
  • Cassez des trucs !
  • Interagissez, posez des questions !

0. Les origines de (GNU/)Linux

(ou plus largement de l’informatique contemporaine)


La préhistoire de l’informatique

  • ~1940 : Ordinateurs électromécaniques, premiers ordinateurs programmables
  • ~1950 : Transistors
  • ~1960 : Circuits intégrés

…Expansion de l’informatique…


1970 : UNIX

  • Définition d’un ‘standard’ pour les OS
  • Multi-utilisateur, multi-tâche
  • Design modulaire, simple, élégant, efficace
  • Adopté par les universités américaines
  • Ouvert (évidemment)
  • (Écrit en assembleur)


1970 : UNIX


1975 : Le langage C

  • D. Ritchie et K. Thompson définissent un nouveau langage : le C ;
  • Le C rend portable les programmes ;
  • Ils réécrivent une version d’UNIX en C, ce qui rend UNIX portable ;

1990 : Linux se développe…

  • Linus Torvalds met Linux sous licence GPL
  • Support des processeurs Intel
  • Système (kernel + programmes) libre et ouvert
  • Compatibles avec de nombreux standard (POSIX, SystemV, BSD)
  • Intègre des outils de développement (e.g. compilateurs C)
  • Excellent support de TCP/IP
  • Création de Debian en 1993

… L’informatique et Internet se démocratisent …

En très résumé :

  • Linux remporte le marché de l’infrastructure (routeur, serveurs, ..)
  • Windows remporte le marché des machines de bureau / gaming
  • Google remporte le marché des smartphones


L’informatique contemporaine


Linux aujourd’hui

  • Très présent dans les routeurs, les serveurs et les smartphones
  • Indépendant de tout constructeur
  • Evolutif mais très stable
  • Le système est fait pour être versatile et personnalisable selon son besoin
  • Pratiques de sécurités beaucoup plus saines et claires que Microsoft

Les distributions

Un ensemble de programmes “packagés”, préconfigurés, intégré pour un usage ~précis ou suivant une philosophie particulière

  • Un noyau (Linux)
  • Des programmes (GNU, …)
  • Des pré-configurations
  • Un gestionnaire de paquet
  • Un (ou des) environnements graphiques (Gnome, KDE, Cinnamon, Mate, …)
  • Une suite de logiciel intégrée avec l’environnement graphique
  • Des objectifs / une philosophie

Les distributions

  • Debian : réputé très stable, typiquement utilisé pour les serveurs
  • Ubuntu, Mint : grand public
  • CentOS, RedHat : pour les besoins des entreprises
  • Archlinux : un peu plus technicienne, très à jour avec les dernières version des logiciels
  • Kali Linux : orientée sécurité et pentesting
  • Android : pour l’embarqué (téléphone, tablette)
  • YunoHost : auto-hébergement grand-public

Les distributions

Et bien d’autres : Gentoo, LinuxFromScratch, Fedora, OpenSuse, Slackware, Alpine, Devuan, elementaryOS, …


Linux, les environnement

  • Gnome
  • Cinnamon, Mate
  • KDE
  • XFCE, LXDE
  • Tiling managers (awesome, i3w, …)

Linux, les environnements (Gnome)


Linux, les environnements (KDE)


Linux, les environnements (Cinnamon)


Linux, les environnements (XFCE)


Linux, les environnements (Awesome)


1. Installer une distribution

Linux Mint

  • (Choix arbitraire du formateur)
  • Distribution simple, sobre, pas spécialement controversée (?)
  • Profite de la stabilité de Debian et de l’accessibilité d’Ubuntu

–>

1. Rappels sur l’informatique


« Informatique »



1. Rappels sur l’informatique

Architecture d’un ordinateur


1. Rappels sur l’informatique

Le rôle d’un OS

User Programs Operating System Hardware

L’OS :

  • sais communiquer avec le hardware pour exploiter les ressources
  • créer des abstractions pour les programmes (e.g. fichiers)
  • partage le temps de calcul entre les programmes
  • s’assure que les opérations demandées sont légales

1. Rappels sur l’informatique

Architecture d’Internet

  • Décentralisé / distribué / “organique”
  • Intelligence à l’extérieur


1. Rappels sur l’informatique

Architecture d’Internet

  • IP : routage des paquets “au mieux”
  • TCP : tunnel fiable pour communiquer (IP+accusés de réception)

1. Rappels sur l’informatique

Architecture d’Internet

Le web : un protocole parmis d’autre pour échanger de l’information, dans un format précis (pages web) Le mail : un autre protocole(s) pour échanger de l’information, dans un autre format (les courriers)

Autres protocoles : DNS, SSH, IRC, torrent, …


1. Rappels sur l’informatique

Architecture d’Internet

  • Programmes
  • Protocole
  • TCP
  • IP
  • Cables

Modèle client / serveur



2. La ligne de commande


Structure d’une commande

  evince  --fullscreen     presentation.pdf
   |     '------------'    '------------'
   |           |                      |
   v           v                      v
  nom       options              arguments

Exemples

Une commande peut être simple :

cd

ou assez complexe :

dnsmasq -x /run/dnsmasq/dnsmasq.pid -u dnsmasq -7 /etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new --local-service

passwd - Changer son password


pwd - Afficher le dossier courant

Print current working directory


cd - Naviguer dans les dossiers

cd  /un/dossier   # Change de dossier courant
cd                # Revient dans le home
cd ..             # Remonte d'un dossier (par exemple /home si on était dans /home/alex)
cd -              # Retourne dans le dossier où on était juste avant

N.B : On ne peut pas faire cd /un/fichier ! Ça n’a pas de sens !


ls - Liste les fichiers d’un dossier

ls            # Liste les fichiers du repertoire courant
ls  /usr/bin  # Liste les fichiers du repertoire /usr/bin
ls  -a        # (ou --all) Liste les fichiers (y compris cachés)
ls  -l        # Avec des détails (type, permissions, proprio, date de modif)
ls  -t        # Trie par date de modification
ls  -h        # (ou --human-readable) Tailles lisibles comme '24K' ou '3G'
ls  *.py      # Liste tous les fichiers du repertoire courant qui se finissent par `.py`

(on peut combiner les options et arguments)


  • Utiliser ls et cd, c’est comme naviguer avec un explorateur de fichier graphique !

  • Un bon Jedi est toujours être attentif à :

    • où il est
    • ce qu’il cherche à faire
    • ce qu’il tape
    • ce que la machine renvoie

Nettoyer son terminal

  • clean efface tout ce qui est affiché dans le terminal
  • reset permet de réinitialiser le terminal (utile pour certaines situation où le terminal est “cassé”)
  • exit permet de fermer un terminal
  • (logout est similaire à exit)

Obtenir de l’aide sur des commandes

man nom_de_commande

(navigation avec les fleches, /mot pour chercher un mot, q pour quitter)

Ou :

nom_de_comande --help

Annuler / arrêter une commande en cours d’execution

  • Si une commande prends trop longtemps, il est possible de l’annuler avec [Ctrl]+C
alex@shadow:~$ sleep 30
[...]
[Ctrl]+C
alex@shadow:~$
  • [Ctrl]+C est à utiliser avec parcimonie ! Interrompre certaines commande peut causer des problèmes…
  • (N.B. : [Ctrl]+C / [Ctrl]+V ne fais pas copier/coller dans la console !)

Raccourcis et astuces de ninja

[Tab]

  • [Tab] x1 permet d’autocompléter les noms de commande et les noms de fichier (si pas d’ambiguité)
  • [Tab] x2 permet de suggérer les différentes possibilités
  • Double-effect kisscool : utiliser [Tab] vous permet de valider au fur à mesure que la commande et le fichier existe !

Historique

  • Vous pouvez utiliser ↑ pour retrouver les commandes précédentes
  • Ou aussi : history

Utilisez [Tab] !


Utilisez [Tab] !


Utilisez [Tab] !


Utilisez [Tab] !


Utilisez [Tab] !


Le système de fichier

Sous UNIX / Linux

“Tout est fichier”

  • fichiers ordinaires (-) : données, configuration, …
  • répertoire (directory, d) : gérer l’aborescence, …
  • spéciaux :
    • devices (c, b) (clavier, souris, disque, …)
    • sockets (s), named pipe (p) (communication entre programmes)
    • links (l) (‘alias’ de fichiers, ~comme les raccourcis sous Windows)

Le système de fichier

Un fichier

  • Un inode (numéro unique représentant le fichier)
  • Des noms (chemins d’accès)
    • Un même fichier peut être à plusieurs endroits en meme temps (hard link)
  • Des propriétés
    • Taille
    • Permissions
    • Date de création, modification

Le système de fichier

Nommage des fichiers

  • Noms sensibles à la casse
  • (Eviter d’utiliser des espaces)
  • Un fichier commençant par . est “caché”
  • Les extensions de fichier sont purement indicatives : un vrai mp3 peut s’apeller musique.jpg et vice-versa
  • Lorsqu’on parle d’un dossier, on l’ecrit plutôt avec un / à la fin pour expliciter sa nature

Le système de fichier

Arborescence de fichier

coursLinux/
├── dist/
│   ├── exo.html
│   └── presentation.html
├── exo.md
├── img/
│   ├── sorcery.jpg
│   └── tartiflette.png
├── presentation.md
└── template/
    ├── index.html
    ├── remark.min.js
    └── style.scss

Le système de fichier

Filesystem Hierarchy Standard

  • / : racine de toute la hierarchie
  • /bin/, /sbin/ : programmes essentiels (e.g. ls)
  • /boot/ : noyau et fichiers pour amorcer le système
  • /dev/, /sys : périphériques, drivers
  • /etc/ : fichiers de configuration
  • /home/ : répertoires personnels des utilisateurs
  • /lib/ : librairies essentielles
  • /proc/, /run : fichiers du kernel et processus en cours
  • /root/ : répertoire personnel de root
  • /tmp/ : fichiers temporaires
  • /usr/ : progr. et librairies “non-essentielles”, doc, données partagées
  • /var/ : fichiers / données variables (e.g. cache, logs, boîtes mails)

Le système de fichier

Répertoires personnels

  • Tous les utilisateurs ont un répertoire personnel
  • Classiquement /home/<user>/ pour les utilisateurs “normaux”
  • Le home de root est /root/
  • D’autres utilisateurs ont des home particulier (/var/mail/, …)

Le système de fichier

Filesystem Hierarchy Standard


Le système de fichier

Designation des fichiers

“Rappel” :

  • . : désigne le dossier actuel
  • .. : désigne le dossier parent
  • ~ : désigne votre home

Un chemin peut être :

  • Absolu : /home/alex/dev/yunohost/script.sh
  • Relatif : ../yunohost/script.sh (depuis /home/alex/dev/apps/)

Un chemin relatif n’a de sens que par rapport à un dossier donné… mais est souvent moins long à écrire














Le système de fichier

Chemins relatifs

  • d’exemples, tous équivalents (depuis /home/alex/dev/apps/)
  • /home/alex/dev/yunohost/script.sh
  • ~/dev/yunohost/script.sh
  • ../yunohost/script.sh
  • ./../yunohost/script.sh
  • ./wordpress/../../yunohost/script.sh
  • ../.././music/.././../barbara/.././alex/dev/ynh-dev/yunohost/script.sh

Le système de fichier

Manipuler des fichiers (1/4)

  • ls : lister les fichiers
  • cat <fichier> : affiche le contenu d’un fichier dans la console
  • wc -l <fichier> : compte le nombre de lignes dans un fichier

Exemples :

ls /usr/share/doc/                       # Liste les fichiers de /usr/share/doc
wc -l /usr/share/doc/nano/nano.html      # 2005 lignes !

Le système de fichier

Manipuler des fichiers (2/4)

  • head <fichier>, tail <fichier> : affiche les quelques premières ou dernières ligne du fichier
  • less <fichier> : regarder le contenu d’un fichier de manière “interactive”
    • ↑, ↓, ⇑, ⇓ pour se déplacer
    • /mot pour chercher un mot
    • q pour quitter
tail -n 30 /usr/share/doc/nano/nano.html # Affiche les 30 dernieres lignes du fichier
less /usr/share/doc/nano/nano.html       # Regarder interactivement le fichier

Le système de fichier

Manipuler des fichiers (3/4)

  • touch <fichier> : créer un nouveau fichier, et/ou modifie sa date de modification
  • nano <fichier> : éditer un fichier dans la console
    • (nano créera le fichier si besoin)
    • [Ctrl]+X pour enregistrer+quitter
    • [Ctrl]+W pour chercher
    • [Alt]+Y pour activer la coloration syntaxique
  • vim <fichier> : alternative à nano
    • plus puissant (mais plus complexe)

Le système de fichier

Manipuler des fichiers (4/4)

  • cp <source> <destination> : copier un fichier
  • rm <fichier> : supprimer un fichier
  • mv <fichier> <destination> : déplace (ou renomme) un fichier

Exemple

cp cours.html coursLinux.html  # Créée une copie avec un nom différent
cp cours.html ~/bkp/linux.bkp  # Créée une copie de cours.html dans /home/alex/bkp/
rm cours.html                  # Supprime cours.html
mv coursLinux.html linux.html  # Renomme coursLinux.html en linux.html
mv linux.html ~/archives/      # Déplace linux.html dans ~/archives/

Le système de fichier

Manipuler des dossiers (1/3)

  • pwd : connaître le dossier de travail actuel
  • cd <dossier> : se déplacer vers un autre dossier

Le système de fichier

Manipuler des dossiers (2/3)

  • mkdir <dossier> : créer un nouveau dossier
  • cp -r <source> <destination> : copier un dossier et l’intégralité de son contenu

Exemples :

mkdir ~/dev           # Créé un dossier dev dans /home/alex
cp -r ~/dev ~/dev.bkp # Créé une copie du dossier dev/ qui s'apelle dev.bkp/
cp -r ~/dev /tmp/     # Créé une copie de dev/ et son contenu dans /tmp/

Le système de fichier

Manipuler des dossiers (3/3)

  • mv <dossier> <destination> : déplace (ou renomme) un dossier
  • rmdir <dossier> : supprimer un dossier vide
  • rm -r <dossier> : supprimer un dossier et tout son contenu récursivement

Exemples :

mv dev.bkp  dev.bkp2   # Renomme le dossier dev.bkp en dev.bkp2
mv dev.bkp2 ~/trash/   # Déplace dev.bkp2 dans le dossier ~/trash/
rm -r ~/trash          # Supprime tout le dossier ~/trash et son contenu

Le système de fichier

  • ln <source> <destination>
  • Le même fichier … à plusieurs endroits !
  • Supprimer une instance de ce fichier ne supprime pas les autres

Le système de fichier

  • ln -s <cible> <nom_du_lien>
  • Similaire à un “raccourci”, le fichier n’est pas vraiment là .. mais comme si
  • Supprimer le fichier pointé par le symlink “casse” le lien

Le système de fichier

  • Dans ce exemple, le lien a été créé avec
    • ln -s ../../../conf/ynh.txt conf.json
  • conf.json est “le raccourci” : on peut le supprimer sans problème
  • ynh.txt est la cible : le supprimer rendra inopérationnel le raccourci

Utilisateurs et groupes

Généralités

  • une entité / identité (!= être humain) qui demande des choses au système
  • possède des fichiers, peut en créer, modifier, naviguer, …
  • peut lancer des commandes / des processus

Utilisateurs et groupes

Répertoire des utilisateurs

Classiquement, les utilisateurs sont répertoriés dans /etc/passwd

alex:x:1000:1000:Zee Aleks:/home/alex:/bin/bash
  • identifiant / login
  • x (historique)
  • uid (id utilisateur)
  • gid (id de groupe)
  • commentaire
  • répertoire home
  • shell de démarrage

Utilisateurs et groupes

root

  • uid=0, gid=0
  • Dieu sur la machine
  • With great power comes great responsabilities
    • Si un attaquant devient root, l’OS est entièrement compromis (à jamais)


Utilisateurs et groupes

Passer root (ou changer d’utilisateur)

su          # Demande à ouvrir un shell en tant que root
su barbara  # Demande à ouvrir un shell en tant que barbara
exit        # Quitter un shell

Utilisateurs et groupes

Sudo

  • On peut autoriser les utilisateurs à faire des choses en root en leur donnant les droits ‘sudo’
su -c "ls /root/"   # Executer 'ls /root/' en tant que root (de manière ephemere)
sudo ls /root/      # Meme chose mais avec sudo
sudo whoami         # Renvoie "root"
sudo su             # Ouvrir un shell root via sudo...
  • Suivant la commande demandée, le mot de passe n’est pas le même…
    • su : mot de passe root
    • sudo : mot de passe utilisateur

Utilisateurs et groupes

Les groupes

  • Chaque user à un groupe associé qui possède le même nom
  • Des groupes supplémentaires peuvent être créés
  • Ils permettent ensuite de gérer d’accorder des permissions spécifiques

Exemples :

  • students
  • usb
  • power

Utilisateurs et groupes

Mot de passe

  • Autrefois dans /etc/passwd (accessibles à tous mais hashés)
  • Maintenant dans /etc/shadow (accessibles uniquement via root)
alex:$6$kncRwIMqSb/2PLv3$x10HgX4iP7ZImBtWRChTyufsG9XSKExHyg7V26sFiPx7htq0VC0VLdUOdGQJBJmN1Rn34LRVAWBdSzvEXdkHY.:0:0:99999:7:::

(Parenthèse sur le hashing)

$ md5sum coursLinux.html
458aca9098c96dc753c41ab1f145845a

…Je change un caractère…

$ md5sum coursLinux.html
d1bb5db7736dac454c878976994d6480

(Parenthèse sur le hashing)

Hasher un fichier (ou une donnée) c’est la transformer en une chaîne :

  • de taille fixe
  • qui semble “aléatoire” et chaotique (mais déterministe !)
  • qui ne contient plus l’information initiale

Bref : une empreinte caractérisant une information de manière très précise


Utilisateurs et groupes

Commandes utiles

whoami                  # Demander qui on est...!
groups                  # Demander dans quel groupe on est
id                      # Lister des infos sur qui on est (uid, gid, ..)
passwd <user>           # Changer son password (ou celui de quelqu'un si on est root)
who                     # Lister les utilisateurs connectés
useradd <user>          # Créé un utilisateur
userdel <user>          # Supprimer un utilisateur
groupadd <group>        # Ajouter un groupe
usermod -a -G <group> <user>  # Ajouter un utilisateur à un groupe

Permissions


Permissions

Généralités

  • Chaque fichier a :
    • un utilisateur proprietaire
    • un groupe proprietaire
    • des permissions associés
  • (root peut tout faire quoi qu’il arrive)
  • Système relativement minimaliste mais suffisant pour pas mal de chose
    • (voir SELinux pour des mécanismes avancés)
$ ls -l coursLinux.html
-rw-r--r-- 1 alex alex 21460 Sep 28 01:15 coursLinux.html

    ^         ^     ^
    |         |     '- groupe proprio
    |          '- user proprio
    les permissions !

Permissions


Permissions


Permissions

Permissions des fichiers

  • r : lire le fichier
  • w : écrire dans le fichier
  • x : executer le fichier

Permissions

Permissions des dossiers

  • r : lire le contenu du dossier
  • w : créer / supprimer des fichiers
  • x : traverser le répertoire

(On peut imager que les permissions d’un dossier soient r-- ou --x)


Permissions

Gérer les propriétaires

(Seul root peut faire ces opérations !!)

chown <user> <cible>          # Change l'user proprio d'un fichier
chown <user>:<group> <cible>  # Change l'user et groupe proprio d'un fichier
chgrp <group> <cible>         # Change juste le groupe d'un fichier

Exemples :

chown barbara:students coursLinux.md  # "Donne" coursLinux.md à barbara et au groupe students
chown -R barbara /home/alex/dev/      # Change le proprio récursivement !

Permissions

Gérer les permissions

chmod <changement> <cible>   # Change les permissions d'un fichier

Exemples

chmod u+w   coursLinux.html  # Donne le droit d'ecriture au proprio
chmod g=r   coursLinux.html  # Remplace les permissions du groupe par "juste lecture"
chmod o-rwx coursLinux.html  # Enlève toutes les permissions aux "others"
chmod -R +x ./bin/           # Active le droit d'execution pour tout le monde et pour tous les fichiers dans ./bin/

Permissions

Représentation octale


Permissions


Permissions

Gérer les permissions .. en octal !

chmod <permissions> <cible>

Exemples

chmod 700 coursLinux.html  # Fixe les permissions à rwx------
chmod 644 coursLinux.html  # Fixe les permissions à rw-r--r--
chmod 444 coursLinux.html  # Fixe les permissions à r--r--r--

Permissions

Chown vs. chmod


Permissions

Lorsque l’on fait :

$ /etc/passwd

On tente d’executer le fichier !

Obtenir comme réponse

-bash: /etc/passwd: Permission denied

ne signifie pas qu’on a pas les droits de lecture sur le fichier, mais bien que l’on a “juste” pas le droit de l’executer (car ça n’a en fait pas de sens de chercher à l’executer)


Processus


Processus

Généralités

  • Un processus est une instance d’un programme en cours d’éxécution

  • (Un même programme peut tourner plusieurs fois sous la forme de plusieurs processus)

  • Un processus utilise des ressources :

    • code qui s’execute dans le CPU, ou en attente en cache/RAM
    • données du processus en cache/RAM
    • autres ressources (port, fichiers ouverts, …)
  • Un processus a des attributs (iidentifiant, proprio, priorité, …)


Processus

Execution (1/2)

La machine comprends seulement du code machine (“binaire”).

Un programme est donc soit :

  • compilé (par ex. un programme en C)
  • interprété par un autre programme, qui lui est compilé (par ex. un programme en python, interprété par l’interpreteur python)

Rappel : UNIX est multi-tâche, multi-utilisateur

  • partage de temps, execution parallèle
  • coordonnées par le kernel

Processus

Execution (2/2)

Un processus est lancé soit :

  • en interactif (depuis un shell / la ligne de commande)
  • de manière automatique (tâche programmées, c.f. at et jobs cron)
  • en tant que daemon/service

En mode interactif, on peut interragir directement avec le processus pendant qu’il s’execute


Processus

Attributs

  • Propriétaire
  • PID (processus ID)
  • PPID (processus ID du parent !)
  • Priorité d’execution
  • Commande / programme lancé
  • Entrée, sortie

Processus

Lister les processus et leurs attributs (1/2)

ps aux            # Liste tous les processus
ps ux -U alex     # Liste tous les processus de l'utilisateur alex
ps -ef --forest   # Liste tous les processus, avec des "arbres de parenté"
pstree            # Affiche un arbre de parenté entre les processus

Exemple de ps -ef --forest

  935   927  0 Sep25 ?      00:00:52  \_ urxvtd
 3839   935  0 Sep26 pts/1  00:00:00      \_ -bash
16076  3839  0 00:49 pts/1  00:00:49      |   \_ vim coursLinux.html
20796   935  0 Sep27 pts/2  00:00:00      \_ -bash
 2203 20796  0 03:10 pts/2  00:00:00      |   \_ ps -ef --forest
13070   935  0 00:27 pts/0  00:00:00      \_ -bash
13081 13070  0 00:27 pts/0  00:00:00          \_ ssh dismorphia -t source getIrc.sh

Processus

Lister les processus et leurs attributs (2/2)

Et aussi :

top               # Liste les processus actif interactivement
  -> [shift]+M    #    trie en fonction de l'utilisation CPU
  -> [shift]+P    #    trie en fonction de l'utilisation RAM
  -> q            # Quitte

Processus

Priorité des processus (1/2)

  • Il est possible de régler la priorité d’execution d’un processus
  • “Gentillesse” (niceness) entre -20 et 19
    • -20 : priorité la plus élevée
    • 19 : priorité la plus basse
  • Seul les process du kernel peuvent être “méchant”
    • niceness négative, et donc les + prioritaires

Processus

Priorité des processus (2/2)

nice -n <niceness> <commande> # Lancer une commande avec une certaine priorité
renice <modif> <PID>       # Modifier la priorité d'un process

Exemples :

# Lancer une création d'archive avec une priorité faible
nice 5 tar -cvzf archive.tar.gz /home/
# Redéfinir la priorité du processus 9182
renice +10 9182

Processus

Gérer les processus interactif

<commande>            # Lancer une commande de façon classique
<commande> &          # Lancer une commande en arrière plan
[Ctrl]+Z  puis 'bg'   # Passer la commande en cours en arrière-plan
fg                    # Repasser une commande en arrière-plan en avant-plan
jobs                  # Lister les commandes en cours d'execution

Processus

Tuer des processus

kill <PID>     # Demande gentillement à un processus de finir ce qu'il est en train de faire
kill -9 <PID>  # Tue un processus avec un fusil à pompe
pkill <nom>    # (pareil mais via un nom de programme)
pkill -9 <nom> # (pareil mais via un nom de programme)

Exemples

kill 2831
kill -9 2831
pkill java
pkill -9 java

Slides 2 - Administration Linux

Administration Linux


L’écosystème Linux

  • Système d’exploitation
  • Interagir avec le système en ligne de commande
  • Un système de fichiers
  • Des utilisateurs, des permissions
  • Des Processus

Objectifs

  • installer et gérer une distribution
  • acquérir des bases de réseau et de sécurité
  • administrer un serveur à distance
  • configurer et gérer des services
  • déployer un serveur web

Plan

  1. Le gestionnaire de paquet (et les archives)
  2. Notions de réseau
  3. Notions de cryptographie
  4. Se connecter et gérer un serveur avec SSH
  5. Services et sécurité basique d’un serveur
  6. Déployer un site “basique” avec nginx
  7. Automatiser avec at et les cron jobs

2. Le gestionnaire de paquet

(et les archives)


2. Le gestionnaire de paquet

Motivation

Historiquement, c’est très compliqué d’installer un programme :

  • le télécharger et le compiler
  • la compilation (ou le programme lui-même) requiert des dependances
  • il faut télécharger et compiler les dépendances
  • qui requiert elles-mêmes des dépendances …

2. Le gestionnaire de paquet

Le travail d’une distribution (entre autre)

  • créer et maintenir un ensemble de paquet cohérents
  • … et le gestionnaire de paquet qui va avec
  • les (pre)compiler pour fournir des binaires

2. Le gestionnaire de paquet

Paquet ~ programmes ou librairies

Le gestionnaire de paquet c’est :

  • La “clef de voute” d’une distribution ?
  • un système unifié pour installer des paquets … ;
  • et les mettre à jour ! ;
  • le tout en gérant les dépendances et les conflits ;
  • et via une commaunauté qui s’assure que les logiciels ne font pas n’importe quoi.

2. Le gestionnaire de paquet

Comparaison avec Windows

Sous Windows

  • téléchargement d’un .exe par l’utilisateur …
  • … depuis une source obscure ! (critical security risk !)
  • procédure d’installation spécifique
  • … qui tente de vous refiler des toolbar bloated, et/ou des CGU obscures
  • système de mise à jour spécifique
  • nécessité d’installer manuellement des dépendances

2. Le gestionnaire de paquet

One package to rule them all

One package to find them

One package to download them all

and on the system bind them

In the land of GNU/Debian where the penguin lie


2. Le gestionnaire de paquet

Sous Debian

Format .deb

apt : couche “haut niveau”

  • dépot,
  • authentification,

dpkg : couche “bas niveau”

  • gestion des dépendances,
  • installation du paquet,

2. Le gestionnaire de paquet

Parenthèse sur apt-get

  • Historiquement, apt-get (et apt-cache, apt-mark, ..) étaientt utilisés
  • Syntaxe inutilement complexe ?
  • apt fourni une meilleur interface (UI et UX)

2. Le gestionnaire de paquet

Utilisation de apt

  • apt install <package> : télécharge et installe le paquet et tout son arbre de dépendances
  • apt remove <package> : désinstaller le paquet (et les paquet dont il dépends !)
  • apt autoremove : supprime les paquets qui ne sont plus nécessaires

2. Le gestionnaire de paquet

Mettre à jour les paquets

  • apt update : récupère la liste des paquets depuis les dépots
  • apt dist-upgrade : calcule et lance la mise à jour de tous les paquets
  • (apt upgrade : mise à jour “safe”, sans installer/supprimer de nouveaux paquets)

2. Le gestionnaire de paquet

Les dépots

Les dépots de paquets sont configurés via /etc/apt/sources.list et les fichiers du dossier /etc/apt/sources.list.d/.

Exemple :

deb http://ftp.debian.fr/debian/ stretch main contrib
  • stretch est le nom de la distribution
  • main et contrib sont des composantes à utiliser

2. Le gestionnaire de paquet

Les versions de Debian

Debian vise un système libre et très stable

  • stable : paquets éprouvés et très stable (bien que souvent un peu vieux)
  • testing : paquets en cours de test, comportant encore quelques bugs
  • unstable (sid) : pour les gens qui aiment vivre dangereusement

Les versions tournent tous les ~2 ans environ

  • l’ancienne testing devient la nouvelle stable
  • le passage de version peut être un peu douloureux …

2. Le gestionnaire de paquet

Gérer des archives

tar (tape archive) permet de créer des archives (non compressées) qui rassemblent des fichiers.

# Créer une archive monarchive.tar
tar -cvf monarchive.tar file1 file2 folder2/ folder2/

# Désassembler une archive
tar -xvf monarchive.tar

2. Le gestionnaire de paquet

Gérer des archives

gzip (gunzip) permet de compresser des fichiers (similaire aux .zip, .rar, …)

# Compresser zblorf.scd
gzip zblorf.scd

# [...] le fichier a été compressé et renommé zblorf.scd.gz

# Decompresser le fichier :
gzip -d zblorf.scd.gz

2. Le gestionnaire de paquet

Gérer des archives

tar peut en fait être invoqué avec -z pour générer une archive compressée

# Créer une archive compressée
tar -cvzf monarchive.tar.gz file1 file2 folder2/ folder2/

# Désassembler une archive
tar -xvzf monarchive.tar.gz

2. Le gestionnaire de paquet

Gérer des archives


3. Notions de réseau


3. Notions de réseau

“Réseau”

Tout ce qui permet la communication entre les machines (et les programmes)


3. Notions de réseau

Objectifs

  • Comprendre et savoir se représenter les différentes couches
  • Savoir faire quelques des tests “de base”
  • … et les commandes associées

3. Notions de réseau

  • DNS : domaine, résolution, …
  • Protocoles, HTTP, modèle client/serveur
  • TCP : ports, NAT
  • IP : adresses, routage, DHCP
  • Physique : interfaces réseau


3. Notions de réseau

Couche physique (1/2)

  • Ethernet, wifi, 4G, …
  • Votre ordinateur dispose d'interface réseau
  • Elles permettent de communiquer sur un support (cable, onde)
  • Chaque interface réseau possède une adresse MAC
  • Il existe typiquement une interface lo (loopback, la boucle locale - 127.0.0.1)

3. Notions de réseau

Couche physique (2/2)

  • ip a permet d’obtenir des informations sur les interfaces
  • Historiquement, les noms étaient “simple” : eth0, wlan0, …
  • Aujourd’hui les noms sont un peu plus complexes / arbitraires
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP>
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s25: <NO-CARRIER,BROADCAST,MULTICAST,UP>
    link/ether 33:0e:d8:3f:65:7e
3: wlp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP>
    link/ether 68:a6:2d:9f:ad:07

3. Notions de réseau

IP : Internet Protocol (1/2)

  • IP fait parler des machines !
  • Protocole de routage des paquets
  • “Best-effort”, non fiable !
  • Les routeurs discutent entre eux pour optimiser l’acheminement
  • Les adresses sont comme des numéros de telephone, ou des positions GPS
    • IPv4, par exemple 92.93.127.10 (4.3 milliards d’adresse)
    • IPv6, par exemple 2a04:7260:9088:6c00::1 (10^38 addresses)

3. Notions de réseau

IP : Internet Protocol (2/2)

$ ip a
enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP>
 link/ether 40:8d:5c:f3:3e:35
 inet 91.225.41.29/32 scope global enp3s0
 inet6 2a04:7202:8008:60c0::1/56 scope global

Voir aussi : ifconfig (deprecated) et ipconfig (sous windows!)


3. Notions de réseau

IP : ping teste la connexion entre deux machines

$ ping 91.198.174.192
PING 91.198.174.192 (91.198.174.192) 56(84) bytes of data.
64 bytes from 91.198.174.192: icmp_seq=1 ttl=58 time=51.5 ms
64 bytes from 91.198.174.192: icmp_seq=2 ttl=58 time=65.3 ms
^C
--- 91.198.174.192 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 3ms
rtt min/avg/max/mdev = 51.475/58.394/65.313/6.919 ms

3. Notions de réseau

IP : whois pour obtenir des infos sur le(s) proprio(s) d’une ip

$ whois 91.198.174.192
[...]
organisation:   ORG-WFI2-RIPE
org-name:       Wikimedia Foundation, Inc
[...]
mnt-by:         RIPE-NCC-HM-MNT
mnt-by:         WIKIMEDIA-MNT

3. Notions de réseau

IP : traceroute permet d’étudier la route prise par les paquets

$ traceroute 91.198.174.192
 1  _gateway (192.168.0.1)  4.212 ms  6.449 ms  6.482 ms
 2  * 10.13.25.1 (10.13.25.1)  248.615 ms *
 3  211-282-253-24.rev.numericable.fr (211.282.253.24)  251.263 ms  251.332 ms  251.408 ms
 4  172.19.132.146 (172.19.132.146)  251.493 ms ip-65.net-80-236-3.static.numericable.fr (80.236.3.65)  251.569 ms  251.619 ms
 5  prs-b7-link.telia.net (62.115.55.45)  251.692 ms  251.769 ms  251.979 ms
 6  prs-bb4-link.telia.net (62.115.120.30)  252.026 ms prs-bb3-link.telia.net (62.115.121.96)  17.989 ms prs-bb4-link.telia.net (213.155.134.228)  1069.536 ms
 7  adm-bb4-link.telia.net (213.155.136.167)  1070.116 ms  1242.772 ms adm-bb3-link.telia.net (213.155.136.20)  1242.839 ms
 8  adm-b3-link.telia.net (62.115.122.179)  1243.006 ms adm-b3-link.telia.net (62.115.122.191)  1242.879 ms  1243.082 ms
[...]

3. Notions de réseau

TCP : Transmission Control Protocol (1/2)

  • TCP fait communiquer des programmes
  • Découpage des messages en petits paquets pour IP
  • Fiabilité avec des accusés de réception / renvois

3. Notions de réseau

TCP : Transmission Control Protocol (2/2)

  • TCP fourni un “tuyau de communication” entre deux programmes
  • Notion de ‘port’
  • Analogie avec les différents “departement” à l’intérieur d’une entreprise
  • Par exemple : votre navigateur web (port 56723) qui discute qui discute avec le serveur web (port 80)
    • côté A : 183.92.18.6:56723 (un navigateur web)
    • côté B : 91.198.174.192:80 (un serveur web)

3. Notions de réseau

TCP : lsof -i pour lister les connexions active

$ lsof -i
ssh        3231 alex IPv4 shadow.local:34658->142.114.82.73.rev.sfr.net:ssh (ESTABLISHED)
thunderbi  3475 alex IPv4 shadow.local:59424->tic.mailoo.org:imap (ESTABLISHED)
thunderbi  3475 alex IPv4 shadow.local:57312->tic.mailoo.org:imap (ESTABLISHED)
waterfox  12193 alex IPv4 shadow.local:54606->cybre.space:https (ESTABLISHED)
waterfox  12193 alex IPv4 shadow.local:32580->cybre.space:https (ESTABLISHED)

3. Notions de réseau

TCP : nc -zv pour tester si un port est ouvert

ACHTUNG : ne pas abuser de cela..

$ nc -zv 44.112.42.13 22
Connection to 44.112.42.13 22 port [tcp/ssh] succeeded!

nc -zv ynh-forge.netlib.re 53

3. Notions de réseau

TCP : tcpdump pour regarder l’activité sur le réseau


3. Notions de réseau

TCP : et aussi : wireshark


3. Notions de réseau

Modèle client/serveur

Un serveur (au sens logiciel) est un programme. Comme un serveur dans un bar (!) :

  • il écoute et attends qu’on lui demande un service
  • par exemple : fournir la page d’acceuil d’un site
  • le serveur écoute sur un port : par exemple : 80

Le client est celui qui demande le service

  • il toque à la porte
  • transmet sa demande
  • le serveur lui réponds (on espère)

3. Notions de réseau

Modèle client/serveur : netstat

netstat -tulpn permet de lister les programmes qui écoutent et attendent

 > netstat -tulpn | grep LISTEN | grep "80\|25"
tcp     0.0.0.0:80  LISTEN      28634/nginx: master
tcp     0.0.0.0:25  LISTEN      1331/master
tcp6    :::80       LISTEN      28634/nginx: master
tcp6    :::25       LISTEN      1331/master

3. Notions de réseau

Protocoles (1/2)

  • Un protocole = une façon de discuter entre programmes
  • Conçus pour une finalité particulière
  • Ont généralement un port “par défaut” / conventionnel
    • 80/http : le web (des “vitrines” pour montrer et naviguer dans du contenu)
    • 443/https : le web (mais en chiffré)
    • 25/smtp : le mail (pour relayer les courriers électroniques)
    • 993/imap : le mail (synchroniser des boites de receptions)
    • 587/smtps : le mail (soumettre un courrier à envoyer)
    • 22/ssh : lancer des commandes à distance
    • 53/dns : transformer des noms en ip
    • 5222/xmpp : messagerie instantannée
    • 6667/irc : salons de chat

3. Notions de réseau

Protocoles (2/2)

Par exemple, HTTP :

  • On envoie GET / et on reçoit 200 + la page d’acceuil
  • On envoie GET /chaton.jpg et on reçoit 200 + une image (si elle existe)
  • On envoie GET /meaningoflife.txt et on reçoit 404 (si la page n’existe pas)
  • On peut ajouter des Headers aux requetes (c.f. debugger firefox)
  • Il existe d’autres requetes : POST, PUT, DELETE, …

3. Notions de réseau

DNS : Domain name server (1/5)

  • Retenir cinquante numéros de telephone (ou coordonées GPS) par coeur, c’est pas facile
  • On invente l’annuaire et les adresses postales
  • wikipedia.org -> 91.198.174.192
  • On peut acheter des noms chez des registrars (OVH, Gandi, …)
  • Composant critique d’Internet (en terme fonctionnel)

3. Notions de réseau

DNS : Domain name server (2/5)

  • Il existe des résolveurs DNS à qui on peut demander de résoudre un nom via le protocole DNS (port 53)

  • Par exemple :

    • 8.8.8.8, le resolveur de Google
    • 9.9.9.9, un nouveau service qui “respecte la vie privée”
    • 89.234.141.66, le resolveur de ARN
    • 208.67.222.222, OpenDNS
  • Choix critique pour la vie privée !!

  • Generalement, vous utilisez (malgré vous) le resolveur de votre FAI, ou bien celui de Google


3. Notions de réseau

DNS : Domain name server (3/5)

  • Sous Linux, le resolveur DNS se configure via un fichier /etc/resolv.conf
$ cat /etc/resolv.conf
nameserver 89.234.141.66

3. Notions de réseau

DNS : Domain name server (4/5)

ping fonctionne aussi avec noms de domaine

host permet sinon de connaître l’ip associée

$ host wikipedia.org
wikipedia.org has address 91.198.174.192
wikipedia.org has IPv6 address 2620:0:862:ed1a::1
wikipedia.org mail is handled by 50 mx2001.wikimedia.org.
wikipedia.org mail is handled by 10 mx1001.wikimedia.org.

3. Notions de réseau

DNS : Domain name server (5/5)

  • On peut outrepasser / forcer la résolution DNS de certains domaine avec le fichier /etc/hosts
 > cat /etc/hosts
127.0.0.1	localhost
127.0.1.1	shadow
::1	localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

127.0.0.1 google.com
127.0.0.1 google.fr
127.0.0.1 www.google.com
127.0.0.1 www.google.fr
127.0.0.1 facebook.com
127.0.0.1 facebook.fr




3. Notions de réseau

Réseau local et NAT (1/6)

  • En pratique, on est peu souvent “directement” connecté à internet
    • MachinBox
    • Routeur de l’entreprise
  • Pas assez d’IPv4 pour tout le monde
    • nécessité de sous-réseaux “domestique” / des réseau “local”
    • basé sur les NAT
    • typiquement avec des IP en 192.168.x.y ou 10.0.x.y



3. Notions de réseau

Réseau local et NAT (4/6)

  • C’est le routeur qui m’attribue une IP via le DHCP
  • Le routeur agit comme “gateway” (la “passerelle” vers les internets)
    • (c.f. ip route, et la route par défaut)
  • Depuis l’extérieur du réseau local, il n’est pas possible de parler “simplement” à une machine
  • Example : Je ne peux apriori pas parler à la machine 192.168.0.12 de mon réseau local chez moi depuis le centre de formation…
  • Egalement : Difficulté de connaître sa vraie IP “globale” ! Il faut forcément demander à une autre machine … c.f whatsmyip.com

3. Notions de réseau

Réseau local et NAT (5/6)

La situation se complexifie avec Virtualbox :

  • Typiquement Virtualbox créé un NAT à l’intérieur de votre machine
  • Les différentes VM ont alors des adresses en 10.0.x.y


5. Se connecter et gérer un serveur avec SSH


5. SSH et les serveurs

À propos des serveurs

Serveur (au sens matériel)

  • machine destinée à fournir des services (e.g. un site web)
  • allumée et connectée 24/7
  • typiquement sans interface graphique
  • … et donc administrée à distance

5. SSH et les serveurs

À propos des serveurs

Serveur (au sens logiciel)

  • aussi appelé “daemon”, ou service
  • programme qui écoute en permanence et attends qu’un autre programme le contacte
    • par ex. : un serveur web attends des clients
  • écoute typiquement sur un ou plusieurs port
    • par ex. : 80 pour HTTP

5. SSH et les serveurs

Serveurs : quel support matériel ?


5. SSH et les serveurs

Serveurs : quel support matériel ?


5. SSH et les serveurs


… Plot twist !


5. SSH et les serveurs

“Virtual” Private Server (VPS)

VPS = une VM dans un datacenter


5. SSH et les serveurs

“Virtual” Private Server (VPS)

… qui tourne quelque part sur une vraie machine


5. SSH et les serveurs


5. SSH et les serveurs


5. SSH et les serveurs

SSH : Secure Shell

  • Un protocole client-serveur, par défaut sur le port 22
  • Prendre le contrôle d’une machine à distance via un shell
  • Sécurisé grâce à du chiffrement asymétrique
    • le serveur a un jeu de clef publique/privé
    • le client peut aussi en avoir un (sinon : mot de passe)
  • Outil “de base” pour administrer des serveurs

5. SSH et les serveurs

Syntaxe : ssh utilisateur@machine

$ ssh admin@ynh-forge.netlib.re
The authenticity of host 'ynh-forge.netlib.re (46.101.221.117)' can't be established.
RSA key fingerprint is SHA256:CuPd7AtmqS0UE6DwDDG68hQ+qIT2tQqZqm8pfo2oBE8.
Are you sure you want to continue connecting (yes/no)? █

5. SSH et les serveurs

Syntaxe : ssh utilisateur@machine

$ ssh admin@ynh-forge.netlib.re
The authenticity of host 'ynh-forge.netlib.re (46.101.221.117)' can't be established.
RSA key fingerprint is SHA256:CuPd7AtmqS0UE6DwDDG68hQ+qIT2tQqZqm8pfo2oBE8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'ynh-forge.netlib.re' (RSA) to the list of known hosts.
Debian GNU/Linux 9
admin@ynh-forge.netlib.re's password: █

5. SSH et les serveurs

Syntaxe : ssh utilisateur@machine

$ ssh admin@ynh-forge.netlib.re
The authenticity of host 'ynh-forge.netlib.re (46.101.221.117)' can't be established.
RSA key fingerprint is SHA256:CuPd7AtmqS0UE6DwDDG68hQ+qIT2tQqZqm8pfo2oBE8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'ynh-forge.netlib.re' (RSA) to the list of known hosts.
Debian GNU/Linux 9
admin@ynh-forge.netlib.re's password:

Last login: Thu Oct  4 08:52:07 2018 from 90.63.229.46
admin@ynh-forge:~$ █

5. SSH et les serveurs

SSH : se logguer

  • ACHTUNG : Soyez attentif à dans quel terminal vous tapez !!!
  • En se connectant la première fois, on vérifie la clef publique du serveur
  • On a besoin du mot de passe pour se connecter
  • … mais la bonne pratique est d’utiliser nous-aussi une clef

5. SSH et les serveurs

SSH : avec une clef

… mais pourquoi ?

  • Pas de mot de passe qui se balade sur le réseau
  • Pas nécessaire de retaper le mot de passe à chaque fois
  • Possibilité d’automatiser des tâches (clef sans mot de passe)
  • (Plusieurs personnes peuvent avoir accès à un meme utilisateur sans devoir se mettre d’accord sur un mot de passe commun)

5. SSH et les serveurs

SSH : avec une clef

1 - Générer avec ssh-keygen -t rsa -b 4096 -C "commentaire ou description"

$ ssh-keygen -t rsa -b 4096 -C "Clef pour la formation"

5. SSH et les serveurs

SSH : avec une clef

1 - Générer avec ssh-keygen -t rsa -b 4096 -C "commentaire ou description"

$ ssh-keygen -t rsa -b 4096 -C "Clef pour la formation"
Generating public/private rsa key pair.
Enter file in which to save the key (/home/alex/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):   # Mot de passe
Enter same passphrase again:                  # (again)
Your identification has been saved in /home/alex/.ssh/id_rsa.
Your public key has been saved in /home/alex/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:ZcAKHVtTXUPz3ipqia4i+soRHZQ4tYsDGfc5ieEGWcY "Clef pour la formation"

5. SSH et les serveurs

SSH : avec une clef

2 - Configurer la clef sur le serveur

  • soit depuis le client avec
ssh-copy-id -i chemin/vers/la/clef user@machine
  • soit depuis le serveur en rajoutant la clef dans ~/.ssh/authorized_keys
    • (generalement, l’admin vous demande votre clef)

5. SSH et les serveurs

SSH : avec une clef

3 - Utiliser la clef pour se connecter

$ ssh -i ~/.ssh/ma_clef alex@jaimelecafe.com
Enter passphrase for key '/home/alex/.ssh/ma_clef': █

5. SSH et les serveurs

SSH : avec une clef

3 - Utiliser la clef pour se connecter

$ ssh -i ~/.ssh/ma_clef alex@jaimelecafe.com
Enter passphrase for key '/home/alex/.ssh/ma_clef':

Last login: Mon Oct  8 19:46:32 2018 from 11.22.33.44
user@jaimelecafe.com:~$ █
  • Le système peut potentiellement se souvenir du mot de passe pour les prochaines minutes, comme avec sudo
  • Il peut ne pas y avoir de mot de passe (utilisation dans des scripts)

5. SSH et les serveurs

SSH : configuration côté client

  • Le fichier ~/.ssh/config peut être édité pour définir des machines et les options associées
Host jaimelecafe
    User alex
    Hostname jaimelecafe.com
    IdentityFile ~/.ssh/ma_clef
  • On peut ensuite écrire simplement : ssh jaimelecafe

5. SSH et les serveurs

SCP : copier des fichiers

scp <source> <destination> permet de copier des fichiers entre le client et le serveur

  • Le chemin d’un fichier distant s’écrit machine:/chemin/vers/fichier
  • ou (avec un user) : utilisateur@une.machine.com:/chemin/vers/ficier

Exemples :

$ scp slides.html bob@dismorphia.info:/home/alex/
$ scp bob@dismorphia.info:/home/alex/.bashrc ./

5. SSH et les serveurs

Divers

  • Client SSH sous Windows : MobaXterm
  • sshfs pour monter des dossiers distants
  • ssh -D pour créer des tunnels chiffrés (similaires à des VPNs)


Slides 3 - Shell Scripting

Bash Scripting

et commandes “avancées”



8. Personnaliser son environnement


8. Personnaliser son environnement

Variables d’envionnement

Lorsque vous êtes dans un shell, il existe des variables d’environnement qui définissent certains comportements.

Par exemple, la variable ‘HOME’ contient /home/padawan et corresponds à l’endroit où cd retourne par défaut (si pas de dossier donné en argument)

Autre exemples :

SHELL : /bin/bash (généralement)
LANG, LC_ALL, ... : langue utilisée par les messages
USER, USERNAME : nom d'utilisateur

8. Personnaliser son environnement

Changer une variable d’envionnement

Exemple :

HOME=/tmp/

Lister les variables d’envionnement

env permet de lister les variables d’environnement

$ env
LC_ALL=en_US.UTF-8
HOME=/home/alex
LC_MONETARY=fr_FR.UTF-8
TERM=rxvt-unicode-256color
[...]

8. Personnaliser son environnement

Définir des aliases

Un alias est un nom “custom” pour une commande et des options

alias ll='ls -l'
alias rm='rm -i'
alias ls='ls --color=auto'

On peut connaître les alias existants avec juste alias

(Mauvaise blague : définir alias cd='rm -r' !)


8. Personnaliser son environnement

Les fichiers de profil

  • Le fichier ~/.bashrc est lu à chaque lancement de shell
  • Il permet de définir des commandes à lancer à ce moment
  • Par exemple, des alias à définir ou des variables à changer…
  • Pour appliquer les modifications, il faut faire source ~/.bashrc

Autres fichiers de profils : ~/.profile et /etc/bash_profile


9. Commandes avancées

9.1 - Redirections, assemblages


9.1 - Redirections, assemblages

Schema fonctionnel d’une commande

  • Une commande est une boîte avec des entrées / sorties
  • et un code de retour ($?)
    • 0 : tout s’est bien passé
    • 1 (ou toute valeur différente de 0) : problème !


9.1 - Redirections, assemblages

Entrées / sorties

  • arguments : donnés lors du lancement de la commande (ex: /usr/ dans ls /usr/)
  • stdin : flux d’entrée (typ. viens du clavier)
  • stdout : flux de sortie (typ. vers le terminal)
  • stderr : flux d’erreur (typ. vers le terminal aussi !)

9.1 - Redirections, assemblages

Code de retour

$ ls /toto
ls: cannot access '/toto': No such file or directory
$ echo $?
2

9.1 - Redirections, assemblages

Rediriger les entrées/sorties (1/3)

  • cmd > fichier : renvoie stdout vers un fichier (le fichier sera d’abord écrasé !)
  • cmd >> fichier : ajoute stdout à la suite du fichier
  • cmd < fichier : utiliser ‘fichier’ comme stdin pour la commande
  • cmd <<< "chaine" : utiliser ‘chaine" comme stdin pour la commande

Exemples

ls -la ~/ > tous_mes_fichiers.txt  # Sauvegarde la liste de tous les fichiers dans le home
echo "manger" >> todo.txt          # Ajoute "manger" a la liste des choses à faire
wc < "une grande phrase"           # Compte le nomde de mot d'une chaine

9.1 - Redirections, assemblages

Rediriger les entrées/sorties (2/3)

  • commande 2> fichier : renvoie stderr vers un fichier (le fichier sera d’abord écrasé !)
  • commande 2>&1 : renvoie stderr vers stdout !

Exemples :

ls /* 2> errors # Sauvegarde les erreurs dans 'errors'
ls /* 2>&1 > log # Redirige les erreurs vers stdout (la console) et stdout vers 'log'
ls /* > log 2>&1 # Redirige tout vers 'log' !

9.1 - Redirections, assemblages

Rediriger les entrées/sorties (3/3)

Fichiers speciaux :

  • /dev/null : puit sans fond (trou noir)
  • /dev/urandom : generateur aleatoire (trou blanc)


9.1 - Redirections, assemblages

Rediriger les entrées/sorties (3/3)

Fichiers speciaux :

  • /dev/null : puit sans fond (trou noir)
  • /dev/urandom : generateur aleatoire (trou blanc)
ls /* 2> /dev/null           # Ignore stderr
mv ./todo.txt /dev/null      # Façon originale de supprimer un fichier !
head -c 5 < /dev/urandom     # Affiche 5 caractères de /dev/urandom
cat /dev/urandom > /dev/null # Injecte de l'aleatoire dans le puit sans fond

9.1 - Redirections, assemblages

Assembler des commandes

Executer plusieurs commandes à la suite :

  • cmd1; cmd2 : execute cmd1 puis cmd2
  • cmd1 && cmd2 : execute cmd1 puis cmd2 mais seulement si cmd1 reussie !
  • cmd1 || cmd2 : execute cmd1 puis cmd2 mais seulement si cmd1 a échoué
  • cmd1 && (cmd2; cmd3) : “groupe” cmd2 et cmd3 ensemble

Exercice en live :

que fait cmd1 && cmd2 || cmd3


9. Commandes avancées

9.2 - Pipes et boîte à outils


Pipes ! (1/3)

  • cmd1 | cmd2 permet d’assembler des commandes de sorte à ce que le stdout de cmd1 devienne le stdin de cmd2 !

Exemple : cat /etc/login.defs | head -n 3

  • (Attention, par défaut stderr n’est pas affecté par les pipes !)

Pipes ! (2/3)

Lorsqu’on utilise des pipes, c’est generalement pour enchaîner des opérations comme :

  • générer ou récupérer des données
  • filtrer ces données
  • modifier ces données à la volée

Pipes ! (3/3)

Precisions techniques

  • La transmission d’une commande à l’autre se fait “en temps réel”. La première commande n’a pas besoin d’être terminée pour que la deuxieme commence à travailler.
  • Si la deuxieme commande a terminée, la première peut être terminée prématurément (SIGPIPE).
    • C’est le cas par exemple pour cat tres_gros_fichier | head -n 3

Boîte à outils : tee

tee permet de rediriger stdout vers un fichier tout en l’affichant quand meme dans la console

tree ~/documents | tee arbo_docs.txt  # Affiche et enregistre l'arborescence de ~/documents
openssl speed | tee -a tests.log      # Affiche et ajoute la sortie de openssl à la suite de tests.log

Boîte à outils : grep (1/3)

grep permet de trouver des lignes qui contiennent un mot clef (ou plus generalement, une expression)

$ ls -l | grep r2d2
-rw-r--r--  1 alex alex        0 Oct  2 20:31 r2d2.conf
-rw-r--r--  1 r2d2 alex     1219 Jan  6  2018 zblorf.scd
$ cat /etc/login.defs | grep TIMEOUT
LOGIN_TIMEOUT		60

(on aurait aussi pu simplement faire : grep TIMEOUT /etc/login.defs)


Boîte à outils : grep (2/3)

Une option utile (parmis d’autres) : -v permet d’inverser le filtre

$ ls -l | grep -v "alex alex"
total 158376
d---rwxr-x  2 alex droid    4096 Oct  2 15:48 droidplace
-rw-r--r--  1 r2d2 alex     1219 Jan  6  2018 zblorf.scd

On peut créer un “ou” avec : r2d2\|c3p0

$ ps -ef | grep "alex\|r2d2"
# Affiche seulement les lignes contenant alex ou r2d2

Boîte à outils : grep (3/3)

On peut faire référence à des débuts ou fin de ligne avec ^ et $ :

$ cat /etc/os-release | grep "^ID"
ID=manjaro

$ ps -ef | grep "bash$"
alex      5411   956  0 Oct02 pts/13   00:00:00 -bash
alex      5794   956  0 Oct02 pts/14   00:00:00 -bash
alex      6164   956  0 Oct02 pts/15   00:00:00 -bash
root      6222  6218  0 Oct02 pts/15   00:00:00 bash




10. Bash scripts


10. Bash scripts

10.0 Écrire et executer des scripts


10.0 Écrire / executer

Des scripts

  • bash (/bin/bash) est un interpreteur
  • Plutôt que de faire de l’interactif, on peut écrire une suite d’instruction qu’il doit executer (un script)
  • Un script peut être considéré comme un type de programme, caractérisé par le fait qu’il reste de taille modeste

10.0 Écrire / executer

Utilité des scripts bash

Ce que ça ne fait généralement pas :

  • du calcul scientifique
  • des interfaces graphiques / web
  • des manipulations ‘fines’ d’information

Ce que ça fait plutôt bien :

  • prototypage rapide
  • automatisation de tâches d’administration (fichiers, commandes, ..)
  • rendre des tâches parametrables ou interactives

10.0 Écrire / executer

Ecrire un script (1/2)

#!/bin/bash

# Un commentaire
cmd1
cmd2
cmd3
...

exit 0    # (Optionnel, 0 par defaut)

10.0 Écrire / executer

Ecrire un script (2/2)

#!/bin/bash

echo "Hello, world !"
echo "How are you today ?"

10.0 Écrire / executer

exit

  • exit permet d’interrompre le script immédiatement
  • exit 0 quitte et signale que tout s’est bien passé
  • exit 1 (ou une valeur différente de 0) quitte et signale un problème

10.0 Écrire / executer

Executer un script (1/3)

Première façon : avec l’interpreteur bash

  • bash script.sh execute script.sh dans un processus à part
  • on annonce explicitement qu’il s’agit d’un script bash
    • dans l’absolu, pas besoin d’avoir mis #!/bin/bash

10.0 Écrire / exécuter

Exécuter un script (2/3)

Deuxième façon : avec source

  • source script.sh execute le script dans le terminal en cours
  • 95% du temps, ce n’est pas source qu’il faut utiliser pour votre cas d’usage !
  • Cas d’usage typique de source : recharger le .bashrc
  • (Autre cas : source venv/bin/activate pour les virtualenv python)

10.0 Écrire / exécuter

Exécuter un script (3/3)

Troisième façon : en donnant les permissions d’execution à votre script

chmod +x script.sh   # À faire la première fois seulement
./script.sh
  • l’interpreteur utilisé sera implicitement celui défini après le #! à la première ligne
  • (dans notre cas : #!/bin/bash)

10.0 Écrire / executer

Parenthèse sur la variable PATH (1/2)

La variable d’environnement PATH définit où aller chercher les programmes

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/sbin

$ which ls
/usr/bin/ls

$ which script.sh
which: no script.sh in (/usr/local/bin:/usr/bin:/bin:/usr/local/sbin

10.0 Écrire / executer

Parenthèse sur la variable PATH (2/2)

$ ./script.sh  # Fonctionnera (si +x activé)
$ script.sh    # Ne fonctionnera a priori pas

Néanmoins il est possible d’ajouter des dossiers à PATH :

PATH="$PATH:/home/padawan/my_programs/"

Ensuite, vous pourrez utiliser depuis n’importe où les programmes dans ~/my_programs !


10.0 Écrire / executer

Résumé

  • bash script.sh est la manière “explicite” de lancer un script bash
  • ./script.sh lance un executable (+x) via un chemin absolu ou relatif
  • source script.sh execute le code dans le shell en cours !
  • script.sh peut être utilisé seulement si le script est dans un des dossier de PATH

10. Bash scripts

10.1 Les variables


10.1 Les variables

De manière générale, une variable est :

  • un contenant pour une information
  • une façon de donner un nom à cette information

Initialiser une variable en bash (attention à la syntaxe) :

PI="3.1415"

Utiliser une variable :

echo "Pi vaut (environ) $PI"

N.B. : différence contenu/contenant sans trop d’ambiguité


10.1 Les variables

On peut modifier une variable existante :

$ HOME="/home/alex"
$ HOME="/var/log"

10.1 Les variables

Initialiser une variable à partir du résultat d’une autre commande

NB_DE_LIGNES=$(wc -l < /etc/login.defs)

Syntaxe équivalente avec des backquotes (ou backticks) (historique, dépréciée)

NB_DE_LIGNES=`wc -l < /etc/login.defs`

10.1 Les variables

On peut également initialiser une variable en composant avec d’autres variables :

MY_HOME="/home/$USER"

ou encore :

FICHIER="/etc/login.defs"
NB_DE_LIGNES=$(wc -l < $FICHIER)
MESSAGE="Il y a $NB_DE_LIGNES lignes dans $FICHIER"
echo "$MESSAGE"

10.1 Les variables

Notes diverses (1/5)

  • En bash, on manipule du texte !
$ PI="3.14"

$ NOMBRE="$PI+2"

$ echo $NOMBRE
3.14+2           # littéralement !

10.1 Les variables

Notes diverses (2/5)

  • Lorsqu’on utilise une variable, il faut mieux l’entourer de quotes :
$ FICHIER="document signé.pdf"

$ ls -l $FICHIER
ls: cannot access 'document': No such file or directory
ls: cannot access 'signé.pdf': No such file or directory

$ ls -l "$FICHIER"
-rw-r--r-- 1 alex alex 106814 Mar  2  2018 'document signé.pdf'

10. Bash scripts

10.2 Paramétrabilité / interactivité


10.2 Paramétrabilité / interactivité

  • Le comportement d’un script peut être paramétré via des options ou des données en argument
  • On peut également créer de l’interactivité, c’est à dire demander des informations à l’utilisateur pendant l’exécution du programme

10.2 Paramétrabilité / interactivité

Les paramètres

  • $0 contient le nom du script
  • $1 contient le premier argument
  • $2 contient le deuxieme argument
  • et ainsi de suite …
  • $# contient le nombre d’arguments total
  • $@ corresponds à “tous les arguments” (en un seul bloc)

10.2 Paramétrabilité / interactivité

#!/bin/bash

echo "Ce script s'apelle $0 et a eu $# arguments"
echo "Le premier argument est : $1"
echo "Le deuxieme argument est : $2"
$ ./monscript.sh coucou "les gens"
Ce script s'apelle monscript.sh et a eu 2 arguments
Le premier argument est : coucou
Le deuxieme argument est : les gens

10.2 Paramétrabilité / interactivité

Interactivité

Il est possible d’attendre une entrée de l’utilisateur avec read :

echo -n "Comment tu t'appelles ? "
read NAME
echo "OK, bonjour $NAME !"

Exercices - partie 1

Feuille d’exercices

0. Création de la machine

  • Installer Virtualbox
  • Créer une machine virtuelle
    • choisissez comme type Linux / Other-Linux (64 bit)
    • 2048 Mo de RAM devraient suffir
    • au moment de spécifier le disque dur virtuel, utiliser l’image OS Boxes Linux Mint (fichier VDI)
  • Démarrer la machine

1. Démarrer et se logguer

  • Observer le démarrage de la machine
  • Au lieu de se connecter depuis l’interface graphique, utiliser l’interface tty (faire Ctrl+Alt+F2 ou F3, F4, …)
  • Se logger depuis le tty
    • login: osboxes.org
    • password: osboxes.org
    • (attention, il se peut que le clavier soit configuré en qwerty, on pourra y remédier sous l’interface graphique)

2. Premier contact avec la ligne de commande commandes

  • Changer le mot de passe en tapant passwd puis Entrée et suivre les instructions
  • Taper pwd puis Entrée et observer
  • Taper ls puis Entrée et observer
  • Taper cd /var puis Entrée et observer
  • Taper pwd puis Entrée et observer
  • Taper ls puis Entrée et observer
  • Taper ls -l puis Entrée et observer
  • Taper echo 'Je suis dans la matrice' puis Entrée et observer

3. La ligne de commande

  • 3.1 - Rendez-vous dans /usr/bin et listez le contenu du dossier
  • 3.2 - Y’a-t-il des fichiers cachés dans votre répertoire personnel ?
  • 3.3 - Quand a été modifié le fichier /etc/shadow ?
  • 3.4 - Identifiez à quoi sert l’option -h de la commande ls via son man.
  • 3.5 - Identifiez ce que fait la commande sleep via son man.
  • 3.6 - Lancer sleep 30 et arrêter l’execution de la commande avant qu’elle ne se termine.
  • 3.7 - Lister successivement et le plus rapidement possible le contenu des dossier /usr, /usr/share, /usr/share/man et /usr/share/man/man1 grâce à [Tab] et ↑.
  • 3.8 - Se renseigner sur ce que font date et cal
  • 3.9 - Afficher le calendrier pour l’année 2019, puis juste le mois de Février 2019
  • 3.10 - Se renseigner sur ce que fait la commande free, et interpreter la sortie de free -h
  • 3.11 - Se renseigner sur ce que fait la commande ping et interpreter la sortie de ping 8.8.8.8

4. Le système de fichier

  • 4.1 - En utilisant mkdir et touch, créez dans votre répertoire personnel l’arborescence suivante :
documents/
├── notes_a_propos_des_commandes/
│   ├── ls.txt
│   ├── cd.txt
│   └── pwd.txt
├── img/
│   ├── pikachu.jpg
│   └── carapuce.jpg
└── coursLinux.pdf
  • 4.2 - Remplissez ls.txt, cd.txt et pwd.txt avec du texte en utilisant nano (par exemple, résumez l’utilité de la commande et ses options / cas d’usage)

  • 4.3 - Vérifiez que le contenu de ces fichiers a bien été modifié avec cat.

  • 4.4 - Affichez le contenu du fichier /etc/os-release

  • 4.4 - Aller dans ~/documents/notes_a_propos_des_commandes puis, en utilisant uniquement des chemins relatifs et en vous aidant de la touche [Tab], déplacez-vous successivement vers :

    • ~/documents/img
    • /usr/share/doc/
    • ~/.nano
    • ~/documents/img
  • 4.5 - Affichez le contenu de /etc/motd et /etc/login.defs

  • 4.6 - En utilisant less, checher LOGIN_TIMEOUT dans le fichier /etc/login.defs. Même chose, mais cette fois en utilisant nano.

  • 4.7 - Combien de ligne fait le fichier /etc/login.defs ?

  • 4.8 - Créez le fichier dracaufeu.jpg dans le dossier ~/documents/notes_a_propos_des_commandes/… Vous réalisez ensuite que vous auriez voulu mettre ce fichier dans ~/documents/img ! Utilisez alors la commande mv pour déplacer dracaufeu.jpg vers le bon dossier.

  • 4.9 - Renommez ~/documents/img en ~/documents/pokemons

  • 4.10 - Créez un nouveau dossier ~/mybins et copiez dedans les fichier /bin/ls et /bin/pwd.

  • 4.11 - Créez un dossier ~/bkp/ et créer une copie de ~/documents/notes_a_propos_des_commandes qui s’apelle ~/bkp/cmd_bkp

  • 4.12 - Supprimez ~/bkp/cmd_bkp/pwd.txt

  • 4.13 - Supprimez tout le dossier ~/bkp/ récursivement

  • 4.14 - Tentez de supprimer /etc/passwd (en tant que padawan !)

  • 4.15 - Inspectez les sorties de df -h et lsblk

5. Utilisateurs et groupes

  • 5.1 - Ouvrir un shell root avec sudo, su, ou via un autre tty
  • 5.2 - Créez un utilisateur r2d2
  • 5.3 - Créez un groupe droid
  • 5.4 - Ajoutez r2d2 au groupe droid
  • 5.5 - À l’aide de su, lancez un shell en tant que r2d2 et regarder le résultat de whoami, id et groups
  • 5.6 - Définir un mot de passe avec passwd
  • 5.7 - Ouvrir plusieurs tty et se logger avec différents utilisateurs, puis observer ce que who retourne
  • 5.8 - Vérifiez que les infos de r2d2 sont bien dans /etc/passwd et /etc/shadow
  • 5.9 - Que se passe-t-il si vous définissez /bin/false comme shell par défaut pour r2d2 ?

6. Permissions

  • 6.1 - Créez un fichier xwing.conf que seul vous et votre groupe pouvez lire
  • 6.2 - Créez un fichier private et supprimer toutes les permissions dessus
  • 6.3 - Ajoutez successivement à private le droit de lecture au propriétaire, le droit d’écriture au groupe et au proprietaire, et les droits d’execution pour tout le monde.
  • 6.4 - Resupprimez toutes les permissions de private
  • 6.5 - Remettez les mêmes permissions qu’avant mais avec une seule commande
  • 6.6 - Modifier les permissions de votre répertoire personnel pour que seul vous ayez le droit d’écriture et de traverse (x) dessus
  • 6.7 - Interdisez à tous les “autres” utilisateurs de fouiller et modifier les fichier dans ~/documents, avec une seule commande qui aura un effet récursif
  • 6.8 - Créez un répertoire personnel pour r2d2
  • 6.9 - Définir r2d2 comme proprietaire de son dossier personnel + s’assurer que les permissions lui permettent (à lui et à lui seul) de lire, ecrire et entrer dans son repertoire.
  • 6.10 - Créez un fichier droid.conf dans son dossier personnel, le définir comme propriétaire, et définir le groupe comme ‘droid’.
  • 6.11 - Créez des fichier beep.wav, boop.wav et blop.wav que seul r2d2 peut executer.
  • 6.12 - Êtes-vous capable de créer un dossier qui contient des fichiers qu’il est possible de lire, mais pas de lister ?
  • 6.13 - En tant qu’utilisateur padawan, arrivez-vous à donner un de vos fichier à r2d2 ?

7. Processus

  • 7.1 - Lancer sleep 30, puis mettre la commande en arrière-plan. Vérifier avec jobs qu’elle continue de s’executer, et qu’elle finie bien par se terminer.
  • 7.2 - Même chose, mais en remettant la commande en avant-plan avec qu’elle ne se termine.
  • 7.3 - Lancer sleep 30 directement en arrière plan (avec &) puis tuez le processus avant qu’il ne se termine
  • 7.4 - Lancer encore sleep 30 dans un terminal, puis regarder depuis un autre terminal avec une commande comme ps que le processus est bien là
  • 7.5 - Identifiez ainsi quel processus (son parent) corresponds au shell qui a lancé le sleep 30
  • 7.6 - Connaissant le PID de ce shell, tenter de tuer le shell gentillement (ou brutalement si il résiste)
  • 7.7 - Identifiez avec top le processus consommant en ce moment le plus de CPU, et celui consommant le plus de mémoire
  • 7.8 - Lancer la commande openssl speed -multi 4 - puis refaite le test
  • 7.11 - Comment pouvez-vous tuer d’un seul coup tous les processus openssl ?
  • 7.12 - Lancez une session screen puis une commande longue dans cette session, comme par exemple sleep 30. Détachez la session puis ré-attachez-la depuis un autre tty.
  • 7.13 - Dans une autre console, identifiez via ps le PID de la session screen et tentez de tuer ce processus.

8. Personnaliser son environnement

  • 8.1 - Personnaliser l’apparence de votre invite de commande (syntaxe, couleurs) en modifiant la variable PS1.
  • 8.2 - Ajouter la personnalisation de l’invite à votre .bashrc et propagez ces changements sur vos shells ouverts.
  • 8.3 - Ajouter aussi un message de bienvenue comme “May the source be with you” qui s’affichera à chaque ouverture d’un shell.
  • 8.4 - Changer le .bashrc de root pour que son invite de commande soit en rouge !
  • 8.5 - S’assurer que vous disposez de l’alias ll (pour ls -l), et que --color=auto est activé implicitement lorsque vous utilisez ls.
  • 8.6 - Créer des alias suls et sucat qui permettent de lister les fichiers d’un dossier, ou d’afficher le contenu d’un fichier en activant automatiquement sudo. Tester ces alias en tapant suls /root et sucat /etc/shadow en tant que padawan.
  • 8.7 - Créer un alias r2d2 qui permet d’ouvrir un shell en tant que r2d2 avec sudo et su.
  • 8.8 - Se renseigner sur LS_COLORS et personnaliser cette variable.
  • 8.9 - En utilisant echo, comment faire pour faire en sorte que la commande ‘ls’ retourne systématiquement ‘J’ai pas envie’ au lieu de son comportement normal ?

Administration Linux - feuille d'exercice n.1

Le gestionnaire de paquet (et les archives)

Gestionnaire de paquet

  • 1.11 Suite à l’installation de votre système, vous voulez vous assurer qu’il est à jour.

    • Lancez la commande apt update. Quels dépôts sont contactés pendant cette opération ?
    • À l’aide de apt list --upgradable, identifiez si firefox, libreoffice, linux-firmware et apt peuvent être mis à jour - et identifiez l’ancienne version et la nouvelle version.
    • Lancez la mise à jour avec apt dist-upgrade. Pendant le déroulement de la mise à jour, identifiez les trois parties clefs du déroulement : liste des tâches et validation par l’utilisateur, téléchargement des paquets, et installation/configuration.
  • 1.12 - Cherchez avec apt search si le programme sl est disponible. (Utiliser grep pour vous simplifiez la tâche). À quoi sert ce programme ? Quelles sont ses dépendances ? (Vous pourrez vous aider de apt show). Finalement, installez ce programme en prêtant attention aux autres paquets qui seront installés en même temps.

  • 1.13 - Même chose pour le programme lolcat

  • 1.15 - Parfois, il est nécessaire d’ajouter un nouveau dépôt pour installer un programme (parce qu’il n’est pas disponible, ou bien parce qu’il n’est pas entièrement à jour dans la distribution utilisée). Ici, nous prendrons l’exemple de docker qui n’est disponible que via un dépôt précis maintenu par Docker.

    • Regarder avec apt search et apt show (et grep !) si le paquet docker est disponible et quelle est la version installable.
    • Exécuter la commande suivante qui va ajouter le dépôt de Docker:
    sudo add-apt-repository \
     "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
     $(lsb_release -cs) \
     stable"
    
    • Faire apt update. Que se passe-t-il ? Quels serveurs votre machine a-t-elle essayé de contacter ? Pourquoi cela produit-il une erreur ?
    • Ajoutez la clef d’authentification des paquets avec curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -.
    • Vérifiez l’empreinte de la clé ajoutée qui devrait être 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88. Pour cela, cherchez les 8 derniers caractères de cette clé, comme ceci :
$ sudo apt-key fingerprint 0EBFCD88

pub   rsa4096 2017-02-22 [SCEA]
      9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
uid           [ unknown] Docker Release (CE deb) <docker@docker.com>
sub   rsa4096 2017-02-22 [S]
  • Refaire apt update. Est-ce que ça fonctionne ?
  • Regarder avec apt search et apt show (et grep !) si le paquet docker-ce est disponible et quelle est la version installable.
  • Installer le paquet. Depuis où a-t-il été téléchargé ?

  • 1.16 - Regardez le contenu de /var/cache/apt/archives. À quoi ces fichiers correspondent-ils ?
  • 1.17 - Utilisez aptitude why pour trouver la raison pour laquelle le paquet libxcomposite1 est installé
  • 1.18 - Utilisez apt-rdepends pour afficher la liste des dépendances de libreoffice.
  • 1.19 - Identifiez l’utilité de la commande apt moo

Gestion des archives

  • 1.20 - Créez une archive (non-compressée !) de votre répertoire personnel avec tar.
  • 1.21 - En utilisant gzip, produisez une version compressée de l’archive de la question précédente
  • 1.22 - Recommencez mais produisant une version compressée directement
  • 1.23 - En fouillant dans les options de tar, trouvez un moyen de lister le contenu de l’archive
  • 1.24 - Créez un dossier test_extract dans /tmp/, déplacez l’archive dans ce dossier puis décompressez-là dedans.
  • 1.25 - Trouvez un ou des fichiers .gz dans /var/log (ou ailleurs ?) et cherchez comment combiner cat et gzip pour lire le contenu de ce fichier sans créer de nouveau fichier.

Administration Linux - feuille d'exercice n.2

3 - Notions de réseau

IP locale, globale, pings

  • 3.1 - Dans votre VM, identifiez les interfaces réseaux, leur nom, leur adresse MAC, et leur adresse IP locale à l’aide de ip a. Tapez également ip route et identifiez l’adresse IP de passerelle / gateway utilisée (cela correspond à la route “default”)
  • 3.2 - Ouvrir une invite de commande Windows (Menu Démarrer, puis taper ‘cmd’), utilisez ipconfig pour identifiez votre adresse IP locale et l’adresse IP de la passerelle.
  • 3.3 - Dessinez un schéma de votre compréhension de l’agencement et des relations entre ces différentes entités (internet, le routeur du centre de formation, votre machine Windows, vos VMs)
  • 3.4 - Faites plusieurs tests de “ping” entre toutes ces différentes machines :
    • Testez de pinguer les VM entres elles
    • Testez de pinguer l’hôte Windows depuis une VM, et vice-versa
    • Testez de pinger la gateway des VM depuis la VM … et depuis Windows
    • Testez de pinger la gateway de l’hôte Windows depuis Windows … et depuis la VM
  • 3.5 - Essayez de pinguer les machines de vos voisins. Demandez-leur leur IP : êtes-vous capable de pinguer leur machine Windows ? Leur machines virtuelles ? Tentez de lister les IPs présentent sur le réseau local en tapant arp -a dans une invite de commande sur l’hôte Windows.
  • 3.6 - Récupérez votre IP globale depuis Windows et depuis votre machine virtuelle, via whatsmyip.com ou ip.yunohost.org. Comparez avec votre voisin. Comparez avec votre smartphone.
  • 3.7 - Dans la configuration de votre machine virtuelle, passez l’interface réseau en mode ‘Bridge’ (ou ‘Pont’) plutôt que NAT. Désactivez ensuite la connection filaire pour forcer la VM à se reconnecter au réseau. Quelle est la nouvelle adresse IP ? Refaites quelques-un des tests précédents. Tentez de scanner les IP du réseau avec sudo arp-scan --localnet. Êtes-vous capable de pinguer les machines de vos voisins ?
  • 3.8 - Arrivez-vous à pinger 89.234.141.68 ? Utilisez whois pour identifier l’entité propriétaire de cette IP.
  • 3.9 - (Avancé) Tentez des traceroute vers l’IP d’un voisin, vers wikipedia.org, google.com, yunohost.org et yoloswag.team.

TCP, ports et protocoles

  • 3.10 - Utilisez lsof -i pour lister les connexions actives. Arrivez-vous à identifier à quoi elle correspondent ?
  • 3.11 - Testez avec nc -zv <adresse> <port> si certains ports sont ouverts pour une IP de votre choix. Par exemple, tester les ports 22, 53, 80, 443 et 6667.
  • 3.12 - Dans une console, lancez telnet yoloswag.team 80 puis dans le sous-shell ainsi ouvert, tapez “GET /”. Que voyez-vous apparaître ? Qu’avez-vous fait ?
  • 3.13 - (~Avancé) Installez le paquet wireshark. Lancez cet outil en root et lancer une analyse de traffic. Vous voyez ensuite défiler les différent paquet. Ajouter un filtre pour montrer seulement le protocole HTTP. Pendant que l’analyse tourne, connectez-vous à un site en HTTP (pas HTTPS !) comme yoloswag.team, et regardez les paquets trouvés par wireshark. Êtes-vous capable de trouver le code source de la page en analysant ces paquets ?

DNS et /etc/hosts

  • 3.14 - À l’aide de host, récupérez l’IP des machines wikipedia.fr et lemonde.fr. Testez aussi avec dig +short <machine>
  • 3.15 - Dans votre fichier /etc/hosts, ajoutez une ligne 127.0.0.1 google.fr. Quel effet cela produit-il ?
  • 3.16 - (~Avancé) Analysez où sont envoyées les requêtes DNS (port 53) avec Wireshark. En déduire quel est le résolveur DNS utilisé par le système. Remplacez le contenu du /etc/resolv.conf par nameserver 8.8.8.8 et refaites des requêtes DNS. Confirmez avec wireshark que ces requêtes sont bien envoyées vers le nouveau résolveur.

Administration Linux - feuille d'exercice n.3

5 - Se connecter et gérer un serveur avec SSH

  • Récupérez l’adresse IP de votre serveur distant auprès du formateur, récupérez aussi la clé privée permettant de vous y connecter (il est peut-être nécessaire d’activer le copier-coller entre Windows et la VM dans les options de la VM Virtualbox)
  • pinguez votre serveur, puis tentez de vous connecter à votre serveur en utilisant la clef (ssh -i clef_privee_formateur user@machine)
  • La clé ssh fournie par le formateur est commune à toutes les machines. Générons une clef SSH qui vous est propre. Depuis votre machine de bureau (VM) :
    • générez une clef SSH pour votre utilisateur avec ssh-keygen -t rsa -b 4096 -C "un_commentaire";
    • identifiez le fichier correspondant à la clef publique créé (généralement ~/.ssh/un_nom.pub) ;
    • utilisez ssh-copy-id -i clef_publique user@machine ;
    • (notez que sur le serveur, il y a maintenant une ligne dans ~/.ssh/authorized_keys)
    • tentez de vous reconnecter à votre serveur en utilisant votre propre clé ssh cette fois-ci (ssh -i clef_privee user@machine)
    • modifiez sur votre serveur le fichier ~/.ssh/authorized_keys pour interdire d’accès les gens utilisant la clé ssh donnée par le formateur !
  • Depuis votre machine de bureau, configurez ~/.ssh/config avec ce template. Vous devriez ensuite être en mesure de pouvoir vous connecter à votre machine simplement en tapant ssh nom_de_votre_machine
Host nom_de_votre_machine
    User votre_utilisateur
    Hostname ip_de_votre_machine
    IdentityFile chemin_vers_clef_privee

Dans une autre console, constater qu’il y a maintenant une entrée correspondant à votre serveur dans ~/.ssh/known_hosts.

  • Dans votre session SSH, familiarisez-vous avec le système :
    • de quelle distribution s’agit-il ? (lsb_release -a ou regarder /etc/os-release)
    • quelle est la configuration en terme de CPU, de RAM, et d’espace disque ? (cat /proc/cpuinfo, free -h et df -h)
    • quelle est son adresse IP locale et globale ?
  • Donnez un nom à votre machine avec hostnamectl set-hostname <un_nom>
  • Créer un utilisateur destiné à être utilisé plutôt que de se connecter en root.
  • Créez-lui un répertoire personnel et donnez-lui les permissions dessus.
  • Définissez-lui un mot de passe.
  • Ajoutez votre clé publique dans le dossier ~/.ssh/authorized_keys de l’utilisateur créé
  • Ajoutez-le au groupe ssh.
  • Assurez-vous qu’il a le droit d’utiliser sudo.
  • Connectez-vous en ssh avec le nouvel utilisateur.

Créez quelques fichiers de test pour confirmer que vous avez le droit d’écrire dans votre home.

  • Définissons maintenant un vrai nom de domaine “public” pour cette machine :
    • allez sur netlib.re et se connecter avec les identifiants fourni par le formateur ;
    • créer un nouveau nom de domaine (en .netlib.re ou .codelib.re). (Ignorez les nom déjà créé, ce sont ceux de vos collègues !) ;
    • une fois créé, cliquez sur le bouton ‘Details’ puis (en bas) ajoutez un nouvel enregistrement de type ‘A’ avec comme nom ‘@’ et comme valeur l’IP globale(!) de votre serveur ;
    • de retour dans une console, tentez de résoudre et pinger le nom de domaine à l’aide de host et ping ;
    • modifiez votre ~/.ssh/config pour remplacer l’ip de la machine par son domaine, puis tentez de vous reconnecter en SSH.
  • 10.9 - Depuis votre machine de bureau (VM), récupérez sur internet quelques images de chat ou de poney et mettez-les dans un dossier. Utilisez scp pour envoyer ce dossier sur le serveur.

Exercices avancés

  • Activer le plugin VSCode SSH dans Visual Studio Code et se connecter à votre serveur de cette façon

  • Installez MobaXterm sous Windows et essayez de vous connecter à votre serveur avec cet outil.

  • Utilisez sshfs pour monter le home de votre utilisateur dans un dossier de votre répertoire personnel.

  • Utilisez ssh -D pour créer un tunnel avec votre serveur, et configurez Firefox pour utiliser ce tunnel pour se connecter à Internet. Confirmez que les changements fonctionnent en vérifiant quelle semble être votre IP globale depuis Firefox.

Introduction à git

Git - première partie

Des dépôts de code à partager


Comment gérer du code logiciel ?

Plusieurs difficultées :

  1. Suivre le code avec précision :

    • Comme on l’a vu chaque lettre compte : une erreur = un bug qui peut être grave
    • Mémoire : comment savoir ou l’on en était quand on revient sur le projet d’il y a deux mois
  2. Collaboration : Si on travaille à 15 sur un même programme:

    • Comment partager nos modifications ?
    • Comment faire si deux personnes travaillent sur le même fichier = conflits

Comment gérer du code logiciel ?

  1. Version du logiciel :
    • Le développement est un travail itératif = contruction petit à petit = pleins de versions !
    • On veut ajouter une nouvelle fonctionnalité à un logiciel, mais continuer à distribuer l’ancienne version et l’améliorer.
    • On veut créer une version de test pour que des utilisateurs avancés trouve des bugs

Solution : un gestionnaire de versions

1. Suit chaque modification faite à des fichiers textes (souvent de code mais peut-être autre chose).


Solution : un gestionnaire de versions

2. Permet de stocker plusieurs version des même fichiers et passer d’un version à l’autre.


Solution : un gestionnaire de versions

3. Permet suivre qui a fait quelle modification, partager les modifications avec les autres, régler les conflits


Git !


Git !

git est un petit programme en ligne de commande. Qui fait tout ce dont on vient de parler:

  • Suit les fichiers
  • Gère les modifications successives et leurs auteurs
  • Fait cohabiter plusieurs versions
  • Aide à résoudre les conflits de code

Écosystème git :

!! à ne pas confondre !!

  • git : le gestionnaire de version = le coeur de l’écosystème = en ligne de commande
  • les interfaces/GUI de git : VSCode, tig, meld, gitkraken, etc
    • Pour faciliter l’utilisation de git et visualiser plus facilement
    • communique avec git sans le remplacer
  • les forges logicielles basée sur git comme github ou framagit:
    • des plateformes web pour accéder au dépot / mettre son code sur les internets.
    • faciliter la collaboration sur un projet
    • tester et déployer le code automatiquement comme dans la démarche DevOps (plus avancé)

On va utiliser les trois car c’est nécessaires pour bien comprendre comment on travaille avec git sur un projet.


On va utiliser

  • git en ligne de commande souvent = il faut absolument connaître les fonctions de base pour travailler sur un projet aujourd’hui
  • VSCode : un éditeur de texte qui a des fonctions pratiques pour visualiser les modifications git et l’historique d’un projet, afficher les conflits.
  • framagit : une forge logicielle qui défend le logiciel libre (basée sur gitlab). On va l’utiliser pour collaborer sur le TP de jeux vidéos.

Git, fonctionnement de base


Warning: git est à la fois simple et compliqué.

Mémoriser les commandes prend du temps :

Utilisez votre memento !

  • On va utilisez les commandes de base durant les prochains jours pour se familiariser avec le fonctionnement normal.

  • En entreprise on utilise tout le temps git avec une routine simple. On y reviendra.

  • Même les ingénieurs avec de l’expérience ne connaissent pas forcément les fonctions avancées.


1. Créer un nouveau dépôt git, valider une premiere version du code

vous êtes dans un dossier avec du code:

  • git init créé un dépôt dans ce dossier
  • git add permet de suivre certains fichier
  • git commit permet valider vos modifications pour créer ce qu’on appelle un commit c’est-à-dire une étape validée du code.
  • git push envoie vos commits dans la branche distante
  • git pull récupère des commits depuis la branche distante
  • git status et git log permettent de suivre l’état du dépôt et la liste des commits.

Le commit

to commit signifier s’engager

  • Idéalement, lorsque vous faites un commit, le code devrait être dans un état à peu près cohérent.

Toujours mettre un message de commit

  • Les commits sont des étapes du développement du logiciel. Lire la liste de ces étapes devrait permettre à un.e developpeur de comprendre l’évolution du code.

Créer un nouveau dépot : Démonstration !


#Git cycle des fichiers



git rm fichier pour désindexer.
Tracked = suivi
Staged = inclus (dans le prochain commit)


Jenkins

TODO Jenkins

Add examples of public Jenkins Servers, eg https://jenkins.linuxcontainers.org/

Cours 1 - Le testing et l'automatisation logicielle

La CI/CD

  • CI : intégration continue
  • CD : Livraison (delivery) et / ou Déploiement continus

Le principe est de construire une automatisation autour du développement logiciel pour pouvoir:

  1. Accompagner le développeur pour écrire du code fiable et maintenable.
  2. Assembler, documenter et tester des logiciels complexes.
  3. Être agile dans la livraison du logiciel

Ce principe de test test systématique est comme nous l’avons vu dans l’intro au coeur du DevOps:

  • déployer un logiciel jusqu’en production à chaque commit validé dans la branche master ?
  • déployer automatiquement des parties d’infrastructure (as Code) dès que leur description change dans un commit dans master.
  • Valider les opérations as code comme du logiciel avant de les rendre disponible pour application.

Comment intégrer et réutiliser des composants logiciels ?

  • Pour cela on sépare le code entre parties spécifiques à l’application et parties génériques.
  • Les parties génériques sont ce qu’on appelle des librairies (par exemple une librairie pour gérer le cache de donnée d’une application web)
  • Elles exposent des interfaces bien définies pour la compatibilité (les composants logiciel doivent pouvoir se faire confiance car ils dépendent les uns des autres)
  • Problème: comme faire lorsque vous avez codé un composant logiciel / une vaste librairie pour garantir que vous maintenez la compatibilité ?

La base : gérer finement les versions du code.

  • On veut que chaque version du code soit identifiable par un numéro pour savoir exactement de quel artefact logiciel on parle.
  • Le cerveau humain ne peut pas le faire seul (impossible de vérifier manuellement des centaines de fichiers pour chercher si il y a des modifications)
  • C’est pourquoi on utilise un outil qu’on appelle gestionnaire de version. Le principal est bien sur git.
  • Une installatino de CI/CD s’intègre autour d’une forge logicielle : une plateforme qui gère des dépôts versionnés de code et encadre un workflow de développement (Cf cours sur git) que doivent suivre les développeurs.

Intégrer des composants logiciels … de façon fiable et sécurisée

  • Le problème du code est qu’il doit évoluer.
  • Mais la moindre modification dans la réponse d’une fonction peut créer un bug dans un programme utilisant la librairie.
  • Si votre librairie est très utilisée l’impact peut être énorme (et ça arrive d’introduire un bug qui impacte des milliers de personnes Cf les issues github des gros projets)
  • Il faut pouvoir contrôler précisément le code développé par une équipe et avoir une façon fiable de vérifier que l’interface (le “contrat” d’usage du composant) est stable.
  • Le cerveau humain est mal adapté à la vérification systématique de l’interface d’un composant logiciel ou pire des chemins d’exécution d’un programme.

Réutiliser des composants logiciels

Les tests automatiques

  • Pour automatiser la vérification d’un programme et surtout des libraires pour garantir que la réponse de leurs fonctions est bien ce qu’elle doit être (que le contrat de leur interface est respecté) on ajoute des tests automatiques.

  • Les tests sont conçus pour utiliser un ou plusieurs composants logiciels toujours de la même façon et vérifier qu’il se comportent comme d’habitude.

  • Il y a plusieurs types de tests:

    • unitaire (composant par composant, fonction par fonction)
    • d’intégration (plusieurs composants ensembles)
    • fonctionnel (plus ou moins de bout en bout de l’application)
  • Une installation de CI intègre des tests systématiques dès que le code change.

Intégrer (assembler) des composants, vérifier leur intégration

Avoir des interfaces standards entre les composants

Pour construire des logiciels sans perdre de temps ils faut des interfaces facilement utilisables:

  • Spécification d’interfaces standard en Java (architecture standard JEE ou encore l’interface des composants Spring)
  • API (Application Programming Interface) REST : basé sur HTTP universel et simple.

Interfaces bien documentées:

  • Une usine logicielle doit gérer la construction automatique de la documentation des composants et de leurs interfaces.

  • Exemple: construire la doc à partir des commentaires docstrings.

  • Exemple2: Utiliser un constructeur d’API REST comme Swagger qui auto documente l’API.

Construire facilement des gros logiciels avec des composants hétérogènes.

  • On ne peut pas compiler chaque classe d’un projet Java à la main. On va automatiser.
  • Mais on ne veut pas devoir mettre à jour le script de construction à chaque fois qu’on déplace un fichier.
  • Il faut un système intelligent et flexible pour la construction d’un projet logiciel: un système de build

Système de build

  • En java il existe deux systèmes de build standards et très proches: maven et gradle (Cf cours sur les systèmes de build)
  • Le système de build construit un artefact (un .jar en java ou une image docker par exemple) avec tous les éléments de l’application (le code compilé, les images et css par exemple pour une application web, un manifest qui décrit l’application, etc)
  • Il permet également de déclencher des tests et des taches comme la construction de la documentation.
  • Une CI va également se charger d’assembler automatiquement les différents composants de notre application à chaque modification soumise par un développeur.

Test fonctionnels et d’intégration

  • Les tests d’intégration testent les composants par groupe ( par exemple vérifie que l’application communique bien avec la base de données ou que Vue et Controlleur fonctionnent ensemble pour toutes les routes ).

  • Les tests fonctionnels testent le logiciel du point vue de l’utilisateur (métier ou final). Ils déclenchent les fonctions soit par l’API publique de l’application soit carrément en “cliquant virtuellement” sur l’interface (Cf Selenium par exemple).

  • Une CI devrait pouvoir déclencher également des tests fonctionnels et d’intégration régulièrement.

  • Cependant ces tests sont plus long que les tests unitaires et seront généralement déclenchés à certaines étapes dans certains environnement (environnement de staging par exemple).

Qualité logicielle

Malgré les tests il est courant que du code vite fait et mal fait s’accumule au fil des années dans une entreprise. C’est ce qu’on appelle couramment la dette technique.

  • Pour permettre de diagnostiquer et de résorber cette dette on utilise des outils de mesure de la qualité du code. Ex: Sonarcube.
  • Ces outils intégrés dans la CI peuvent même interdire à un développeur de proposer du code ne remplissant pas certains critères.

Sécurité

Il est également courant pour des développeurs en particulier s’ils ne sont pas formés en sécurité informatique d'introduire certains bugs dans un logiciel qui peuvent consituter des failles (Par exemple des comportements mémoire inadéquats qui permettent d’exploiter l’application)

  • La revue automatique du code va également permettre de détecter une partie de ces failles grâce à une analyse de sécurité statique:

    • teste au maximum tous les chemins d’exécution du programme pour trouver ceux menant à des crash
    • examine l’usage de la mémoire et sa sécurisation pour mettre en valeur les possibilité d’injection.
  • Ces tests n’éliminent pas les failles de sécurités humaines et dynamiques.

Déploiement/Livraison continue. Être agile dans la livraison du logiciel

  • Qu’il soit sur une plateforme en ligne (simple) ou installé sur la machine d’utilisateur (plus complexe), on veut pouvoir contrôler la distribution d’une nouvelle version du logicielle à l’utilisateur.

  • Un des principes centraux du DevOps est la livraison voir le déploiement continu.

  • La livraison continue implique d’ajouter une nouvelle couche de tests dit d'acceptance. Ce sont des tests fonctionnels* sur des **parties critiques** de l’application.

  • S’ils ne passent pas la livraison doit être annulée et le logiciel réexaminé.

  • Et…, pour ne pas avoir de surprise lors du déploiement proprement dit:

    • on déploie souvent.
    • de façon automatique.
  • On peut utilise toute la panoplie des technologies de fiabilisation d’infrastructure:

    • ansible (infra as code prédictible)
    • docker (boite immutables autonomes)
    • orchestration (gestion automatiques des processus et version du logiciel)

L’Usine logicielle

.col-8[]

Cours 2 - Jenkins Intro

Couteau suisse d’automatisation

Un serveur d’automatisation

  • Permet de lancer depuis un serveur central différent type d’opérations

    • tester un logiciel
    • construire une image docker
    • provisionner un infrastructure
    • n’importe quoi de scriptable
  • De fait Jenkins est devenu Un outil très populaire de CI/CD: il permet d’automatiser la plupart des étapes du processus CI/CD de façon flexible.

Jobs et pipelines

Jenkins permet donc d’automatiser n’importe quelle tâche sur forme d’un job ou tache ponctuelle.

  • planifié ou non
  • récurrent ou non

Mais la plupart des jobs (dans le cadre de la CI/CD) sont des pipelines

Un pipeline est une suite d’étapes appelées stages conditionnées les unes aux autres (qui peuvent rater ou réussir).

Jenkins les pipelines de CI/CD

  • Dans une entreprise avec une équipe de développement, Jenkins va être utilisé pour automatiser la plupart des tâches d’intégration continue:
    • Lancer la construction des artefacts logiciels à chaque push et les mettre sur un serveur spécial
    • Lancer les tests sur les artefacts : déclenchés à chaque push pour les tests unitaires et plus rarement pour les tests plus longs.
    • Scanner la qualité du code et bloquer les régressions logicielles.
    • Provisionner des infrastructures (Ansible, plus rare).
    • Déployer des conteneurs sur ces infrastructures “orchestrées”.

Jenkins

  • agnostique du langage: Jenkins a beaucoup de plugins qui supportent la plupart des langages et frameworks.

  • Open source et extensible par des plugins: Jenkins a une grosse communauté et beaucoup de plugins.

  • Portable: Jenkins est écrit Java donc il peut tourner sur la plupart des systèmes.

  • Supporte la plupart des gestionnaires de version et systèmes de build.

  • Distribué et scalable: Jenkins inclus un mode master/slave, qui permet de distribuer son exécution sur plusieurs serveurs.

  • Simplicité: Jenkins est simple seulement pour faire des trucs simples.

  • Orienté Code: Les pipelines Jenkins peuvent être définis “as code” et Jenkins lui même peut se configurer en XML ou Groovy. (cf TP2)

Des plugins pour tout faire

  • Jenkins est très limité sans plugins.
  • Chaque fonctionnalité, même les plus centrales comme les pipelines ou les noeuds dockers sont basées sur des plugins.

Installation de Jenkins

Jenkins s’installe généralement sur un serveur dédié:

  • Soit en java directement (historiquement). Comme Java est portable, Jenkins est un logiciel très portable

  • Soit à l’aide d’un conteneur docker (Avantage pour avoir de multiple Jenkins masters, on peut alors utiliser un système d’orchestration comme Kubernetes)

Une architecture distribuée et/ou haute disponibilité

  • Jenkins fonctionne soit sur un seul serveur soit avec une architecture master/runner:

    • Le master centralise et affiche les informations sur les builds.
    • Les runners se répartissent les builds lancés depuis le master à partir de divers critères (compatibilité, taille, type)
  • Idéalement il faut également dupliquer le noeud master pour fournir une haute disponibilité garantie: si l’un des master cesse de fonctionner pour une quelconque raison le deuxième récupère toute la charge. Le service n’est pas interrompu.

Un pipeline complet classique

  • Build/Compile (facultatif: seulement pour les langages compilés comme Java)
  • Unit test (Lance les tests de chaque fonctions de notre application)
  • Docker / Artefact build: construit l’application pour produire un artefact : jar ou image docker ou autre.
  • Publish (sur un serveur d’artefacts): Docker Hub, Artifactory, Votre serveur privé d’entreprise.
  • Staging Deploy: Installer l’application dans un environnement de validation.
  • Acceptance testing : Faire des tests fonctionnels dans l’environnement Staging.

TP1 Jenkins - Pipeline de test et déploiement avec Jenkins et Kubernetes

Installer Jenkins avec Docker (version simple)

Nous avons utilisé l’installation kubernetes du TP3 kubernetes (la config docker ci dessous est conservée à tire informatif)

  • Créer un dossier tp_jenkins.

  • Cherchez sur hub.docker.com l’image blueocean de jenkins.

  • Créez dans tp_jenkins un dossier jenkins_simple.

  • Ouvrez jenkins_simple avec VSCode. Pour lancer jenkins nous allons utiliser docker compose.

  • Créez un fichier docker-compose.yml avec à l’intérieur:

version: "2"
services:
  jenkins:
    image: <image_blueocean>
    user: root
    ports:
      - "<port_jenkins_standard>"
    volumes:
      ...
  • Pour le mapping de port choissez 8080 pour le port hote (le port http par défaut voir cours sur le réseau). Le port de jenkins est 8080.

  • Créez dans jenkins_simple les dossiers home et jenkins_data

  • La section volumes permet de monter des volumes docker :

    • les données de jenkins se retrouverons dans le dossier jenkins_data et survivrons à la destruction du conteneur. A l’intérieur du conteneur le dossier data est /var/jenkins
    • Le dossier ./home peut également être monté à l’emplacement /home pour persister les données de build.
    • Enfin pour que jenkins puisse utiliser Docker il doit pouvoir accéder au socket docker de l’hôte qui permet de controller la runtime docker. Il faut pour cela monter /var/run/docker.sock au même emplacement (/var/run/docker.sock) côté conteneur.
  • Après avoir complété le fichier et ajouté les 3 volumes, lancez jenkins avec docker-compose up -d.

  • Pour vérifier que le conteneur est correctement démarré utilisez la commande docker-compose logs

  • Quand le logiciel est prêt la commande précédente affiche des triple lignes d’étoiles *. Entre les deux est affiché un token du type: 05992d0046434653bd253e85643bae12. Copiez ce token.

  • Visitez l’adresse http://localhost:8080. Vous devriez voir une page jenkins s’afficher. Activez le compte administrateur avec le token précédemment récupéré.

  • Cliquez sur Installer les plugins recommandés

  • Créez un utilisateur à votre convenance. Jenkins est prêt.

Créer un premier pipeline

  • Cliquez sur créer un nouvel item, sélectionnez le type pipeline.
  • Dans le vaste formulaire qui s’ouvre remplissez la section nom avec hello
  • Laissez tout le reste tel quel sauf la section script en bas ou il faut coller la description du pipeline:
pipeline {
    agent any
    stages {
        stage("Hello") {
            steps {
                echo 'Hello World'
            }
        }
    }
}
  • Sauvegardez le pipeline. Retournez sur la page d’accueil de Jenkins et lancez votre tache.
  • Cliquez le sur le job qui se lance #1 ou #2 pour suivre son déroulement puis cliquez sur Console Output dans le menu de gauche.
  • Vous devriez voir quelque chose comme:
Started by user elie
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on docker-slave-41a6ab3a5327 in /home/jenkins/workspace/hello
[Pipeline] {
[Pipeline] stage
[Pipeline] { (Hello)
[Pipeline] echo
Hello World
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
  • L’interface douteuse que vous venez de visiter est celle de jenkins traditionnelle. Nous allons maintenant voir BlueOcean qui est plus simple et élégante.
  • Cliquez sur Open Blue Ocean
  • Affichez simplement les logs de notre pipeline précédent. La mise en forme est plus épurée et claire.
  • Pour accéder directement à la page d’accueil visitez http://localhost:8080/blue.
  • Cliquez sur le job hello est relancez le. Un nouveau pipeline démarre qui s’exécute en une seconde.

Passons maintenant à un vrai pipeline de test. Pour cela nous devons d’abord avoir une application à tester et un jeu de tests à appliquer. Nous allons comme dans les TPs précédent utiliser une application python flask.

Tester une application flask avec Jenkins

  • Dans tp_jenkins créer un dossier flask_app.
  • Ouvrez ce dossier avec une nouvelle instance de VSCode.
  • Créez à l’intérieur .gitignore, app.py, requirements.txt et test.py.

.gitignore

__pycache__
*.pyc
venv

app.py

#!/usr/bin/env python3
from flask import Flask
app = Flask(__name__)

@app.route('/')
@app.route('/hello/')
def hello_world():
   return 'Hello World!\n'

@app.route('/hello/<username>') # dynamic route
def hello_user(username):
   return 'Hello %s!\n' % username

if __name__ == '__main__':
   app.run(host='0.0.0.0') # open for everyone

requirements.txt

Click==7.0
Flask==1.0.2
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
Werkzeug==0.14.1
xmlrunner==1.7.7

test.py

#!/usr/bin/env python3

import unittest
import app

class TestHello(unittest.TestCase):

   def setUp(self):
       app.app.testing = True
       self.app = app.app.test_client()

   def test_hello(self):
       rv = self.app.get('/')
       self.assertEqual(rv.status, '200 OK')
       self.assertEqual(rv.data, b'Hello World!\n')

   def test_hello_hello(self):
       rv = self.app.get('/hello/')
       self.assertEqual(rv.status, '200 OK')
       self.assertEqual(rv.data, b'Hello World!\n')

   def test_hello_name(self):
       name = 'Simon'
       rv = self.app.get(f'/hello/{name}')
       self.assertEqual(rv.status, '200 OK')
       self.assertIn(bytearray(f"{name}", 'utf-8'), rv.data)

if __name__ == '__main__':
   unittest.main()
  • Pour essayer nos tests sur l’application lancez:
    • virtualenv -p python3 venv
    • source venv/bin/activate
    • pip install -r requirements.txt
    • chmod +x app.py test.py
    • ./test.py --verbose

résultat:

test_hello (__main__.TestHello) ... ok
test_hello_hello (__main__.TestHello) ... ok
test_hello_name (__main__.TestHello) ... ok

----------------------------------------------------------------------
Ran 3 tests in 0.014s

OK

Créons maintenant un conteneur docker avec cette application. Précréer une image Docker permet de tester plus facilement les applications dans un contexte d’automatisation CI/CD car l’application est encapsulée et facile à déployer pour Jenkins (ou autre ex gitlab).

  • Créez le Dockerfile suivant dans flask_app:
FROM python:3.7

# Ne pas lancer les app en root dans docker
RUN useradd flask
WORKDIR /home/flask

#Ajouter tout le contexte sauf le contenu de .dockerignore
ADD . .

# Installer les déps python, pas besoin de venv car docker
RUN pip install -r requirements.txt
RUN chmod a+x app.py test.py && \
    chown -R flask:flask ./

# Déclarer la config de l'app
ENV FLASK_APP app.py
EXPOSE 5000

# Changer d'user pour lancer l'app
USER flask

CMD ["./app.py"]
  • Ajoutez également le .dockerignore:
__pycache__
venv
Dockerfile
  • Construisez l’image : docker build -t flask_hello .
  • Lancez la pour tester son fonctionnement: docker run --rm --name flask_hello -p 5000:5000 flask_hello
  • Pour lancez les test il suffit d’écraser au moment de lancer le conteneur la commande par défaut ./app.py par la commande ./test.py:
docker run --rm --name flask_hello -p 5000:5000 flask_hello ./test.py --verbose
  • Lancez un registry docker en local sur le port 4000 avec docker run -p 4000:5000 registry LAISSEZ LE TOURNER.
  • tagguez l’image pour la poussez sur ce registry : docker tag flask_hello localhost:4000/flask_hello
  • poussez la avec docker push localhost:4000/flask_hello

Installer Jenkins dans Kubernetes

Nous allons installer Jenkins avec Helm et le chart Jenkins Stable. Cela va permettre d’utiliser des pods k8s comme agents Jenkins. Pour cela :

  • Commencez par récupérer le fichier des valeurs de configuration du chart Jenkins stable avec : wget https://raw.githubusercontent.com/helm/charts/master/stable/jenkins/values.yaml

Ce fichier contient plein d’options sur comment configurer l’installation de Jenkins et Jenkins lui même:

  • sur quel port exposer le service

  • quels plugins Jenkins installer

  • le nom et mot de passe de l’admin

  • etc

  • Modifiez: adminUser et adminPassword pour mettre les votre

  • Modifiez serviceType: NodePort

  • Ajoutez nodePort: 32000 à la ligne juste en dessous de servicetype

Maintenant lançons l’installation avec :

  • kubectl create namespace jenkins

  • kubectl config set-context --current --namespace=jenkins

  • helm repo add stable https://kubernetes-charts.storage.googleapis.com/

  • helm repo update

  • helm install --name-template jenkins -f values.yaml stable/jenkins

  • Chargez la page localhost:32000 pour accéder à Jenkins et utilisez le login configuré.

  • Installez le plugin blue ocean dans l’administration de Jenkins

Tester notre application avec un pipeline

Pour tester notre application nous allons créer un pipeline as code c’est à dire ici un fichier Jenkinsfile à la racine de l’application flask_app qui décrit notre test automatique.

Jenkinsfile


pipeline {
  agent {
    kubernetes {
      // this label will be the prefix of the generated pod's name
      label 'jenkins-agent-my-app'
      yaml """
apiVersion: v1
kind: Pod
metadata:
  labels:
    component: ci
spec:
  containers:
    - name: python
      image: python:3.7
      command:
        - cat
      tty: true
"""
    }
  }

  stages {
    stage('Test python') {
      steps {
        container('python') {
          sh "pip install -r requirements.txt"
          sh "python test.py"
        }
      }
    }
  }

}
  • Créez un commit pour le code de l’application.

  • Créez un nouveau projet github flask_hello_jenkins et poussez le code.

  • Copiez l’adresse SSH de votre nouveau dépot (menu clone de la page d’accueil).

  • Allez dans Blue Ocean et créez un nouveau pipeline de type git (pas github ou autre) en collant l’adresse SSH précédent.

  • Blue Ocean vous présente une clé SSH et vous propose de l’ajouter à votre dépot pour l’authentification.

    • Cliquez sur copy to clipboard
    • Allez dans github > settings > SSH keys et ajoutez la clé
    • retournez sur la page jenkins et faite Créer le pipeline
  • A partir d’ici le pipeline démarre

    • d’abord (étape 1) Jenkins s’authentifie en SSH et clone le dépôt du projet automatiquement
    • puis il lit le Jenkinsfile et créé les étapes (steps) nécessaires à partir de leur définition
    • agent { kubernetes ... containers: ... - python:3.7.2' } indique que Jenkins doit utiliser un pod kubernetes basé sur l’image docker python pour exécuter les test
    • l’étape Test Python installe les requirements nécessaire et lance simplement le fichier de test précédemment créé
  • Observez comment Blue ocean créé des étapes d’une chaine (le pipeline) et vous permet de consulter les logs détaillés de chaque étape.

Ajouter le déclenchement automatique du pipeline à chaque push.

Ajoutons un trigger Jenkins pour déclencher automatiquement le pipeline dès que le code sur le dépot change. Une façon simple de faire cela est d’utiliser le trigger pollSCM:

  • Ajouter en dessous de la ligne agent du Jenkinsfile les trois lignes suivantes:
  triggers {
      pollSCM('* * * * *')
  }

Cet ajout indique à Jenkins de vérifier toute les minute si le dépôt à été mis à jour.

  • Créez un commit et poussez le code.
  • Pour que le trigger soit pris en compte il faut d’abord relancer le build manuellement : allez sur l’interface et relancez le build. Normalement les tests sont toujours fonctionnels.

Utilisons notre Dockerfile pour construire un artefact

Dans le Jenkinsfile précédent nous avons demandé à Jenkins de partir de l’image python:3.7.2 et d’installer les requirements.

Mais sous avons aussi un Dockerfile qui va nous permettre de construire l’image et la pousser automatiquement sur notre registry. Ajoutez le stage build suivant

    stage('Build image') {
      steps {
        container('docker') {
          sh "docker build -t localhost:4000/pythontest:latest ."
          sh "docker push localhost:4000/pythontest:latest"
        }
      }
    }

Pour builder l’image le contexte du pipeline doit avoir docker disponible. Pour cela nous allons ajouter un deuxième conteneur et un volume à notre pod jenkins agent. Ajoutez un deuxième conteneur à la liste spec: containers: de la configuration de l’agent kubernetes comme suit:

    - name: docker
      image: docker
      command:
        - cat
      tty: true
      volumeMounts:
        - mountPath: /var/run/docker.sock
          name: docker-sock
  volumes:
    - name: docker-sock
      hostPath:
        path: /var/run/docker.sock
  • Commitez, poussez cette version et relancez le pipeline.
  • Observez les logs en particulier la partie build de l’image : Jenkins utilise docker pour relancer le build

Déployer notre application dans Kubernetes

Nous allons enfin ajouter un stage Deploy pour lancer notre application dans le cluster et pouvoir la tester.

créez un dossier kubernetes avec à l’intérieur deux fichiers:

  • deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pythontest 
  labels:
    app: pythontest
spec:
  selector:
    matchLabels:
      app: pythontest
  strategy:
    type: Recreate
  replicas: 1
  template:
    metadata:
      labels:
        app: pythontest
    spec:
      containers:
      - image: localhost:4000/pythontest:latest
        name: pythontest
        ports:
        - containerPort: 5000
          name: microport
  • service.yaml
apiVersion: v1
kind: Service
metadata:
  name: pythontest
  labels:
    app: pythontest
spec:
  ports:
    - port: 5000
      nodePort: 31000
  selector:
    app: pythontest 
  type: NodePort

Ajoutez un stage Deploy au pipeline Jenkinsfile comme suit:

    stage('Deploy') {
      steps {
        container('kubectl') {
          sh "kubectl apply -f ./kubernetes/deployment.yaml"
          sh "kubectl apply -f ./kubernetes/service.yaml"
        }
      }
    }

Pour exécuter ces commande il nous faut kubectl dans le pod. Ajoutons pour cela un conteneur kubectl dans le pod à la suite du conteneur docker précédemment ajouté et avant la section volumes::

    - name: kubectl
      image: lachlanevenson/k8s-kubectl:v1.17.2 # use a version that matches your K8s version
      command:
        - cat
      tty: true
  • Poussez ces modification et lancez un build à nouveau.

Correction

Ajoutez une branche feature et du TDD

Nous allons maintenant ajouter un test pour une fonctionnalité hypothétique non encore existante. Le test va donc échouer. Ecrire le test à l’avance fait partie d’une méthode appelée TDD ou développement dirigé par les tests.

  • Normalement on ne fait pas ça (ajouter un test en échec) dans la branche master mais dans une branche feature (sinon on casse la branche stable). Basculez sur une branche feature1 avec git checkout -b <branche>

  • Ajoutez le code suivant à test.py après test_hello_name:

    def test_new_route(self):
        rv = self.app.get(f'/feature/{name}')
        self.assertEqual(rv.status, '200 OK')
  • Committez et pousser votre code.

  • Rendez vous sur l’interface de Blue Ocean. Au bout de 2 minutes, vous pourrez observer un nouveau pipeline en échec.

    • Le pipeline de test s’est lancé automatiquement sur la nouvelle branche feature1.
    • La sortie indique: Ran 4 tests in 0.015s FAILED (errors=1)
  • Ajoutez une route python flask dans app.py pour que le test fonctionne:

    • indice1: dupliquez la route /hello/<username> et sa fonction
    • indice2: changez la route en avec /feature.
  • Vous pouvez tester votre ajout en local en relançant : ./test.py

  • Une fois que le test est corrigé, poussez votre “feature” sur framagit et observez le pipeline de test.

  • Normalement, le pipeline il est toujours en échec. Expliquez ce qui s’est passé.

  • Corrigez le problème dans la branche feature en remettant l’agent dockerfile true comme précédemment. Mettez à jours le projet sur framagit.

  • Le pipeline devrait maintenant passer à nouveau.

Python Intermédiaire

Introduction

Plan

Partie 1 : Révisions? élémentaires

  • Variables, fonctions
  • Structures de contrôle (conditions, boucles)
  • Debugging
  • setup de dev

Partie 2 :

  • Structures de données (listes, dictionnaires, …)
  • Fichiers
  • exceptions
  • librairies
  • XML en python

Partie 3 : Programmation Orientée Objet

  • Classes et méthodes
  • Héritage et Polymorphisme
  • Encapsulation
  • Stockage de données?

Partie 4 : Python Object Model

  • Python Object Model, méthodes spéciales
  • Itérateurs, Décorateurs, Design Patterns
  • Modules et Packages, script CLI, documentation
  • Testing
  • +? Parallélisme, ?

Méthode

Alternance entre :

  • Des explications théoriques sur une notion donnée et présentation de syntaxes Python
  • Exercices pratiques que nous ferons ensemble pas à pas.

Tous est décrit sur le site.

Votre profil et vos attentes ?

Langage Python et programmation.

« L’Informatique »

Cuisiner de l’information

  • Préparer des outils et des ingrédients
  • Donner des instructions
  • … parfois en utilisant des “fonctions”
    • « monter des oeufs en neige »
    • « cuire à thermostat 6 pendant 20 minutes »

Langage de programmation

Comme un vrai langage !

  1. Concepts (mots, verbes, phrases …)
  2. Grammaire et syntaxe
  3. Vocabulaire
  4. Organiser sa rédaction et ses idées : structurer correctement son code et ses données

Le langage Python

  • “Moyen-niveau” : équilibre entre performance, flexibilité et simplicité d’écriture
  • Syntaxe légère, lisible, facile à prendre en main
  • Interprété, “scripting”, prototypage rapide
  • Flexible (typage dynamique, …)
  • Grande communauté, de plus en plus répandu…

Nous récapitulerons en conclusion les caractéristiques du langage, ses avantages et ses inconvénients.

Python history

« … In December 1989, I was looking for a “hobby” programming project that would keep me occupied during the week around Christmas. My office … would be closed, but I had a home computer, and not much else on my hands. I decided to write an interpreter for the new scripting language I had been thinking about lately: a descendant of ABC that would appeal to Unix/C hackers. I chose Python as a working title for the project, being in a slightly irreverent mood (and a big fan of Monty Python’s Flying Circus). » — Guido van Rossum

Some programming mindset

Remarque Meme
La programmation c’est compliqué
Il n’y a pas de honte à prendre du temps pour comprendre
Cassez des trucs !
Explorez !

Développement Logiciel

  • Jusqu’ici nous avons parlé du langage python et de la façon dont il permet d’exprimer un programme. Il s’agit donc de programmation.

  • Mais l’activité de coder va au delà de l’expression d’une logique dans un langage. Il s’agit d’organiser la production d’un ensemble d’élément de programme, un logiciel et pour cela on parle plutôt de développement logiciel.

Quatre grands thèmes de l’activité de développement

Algorithme / Langage / Architecture / Qualité Logicielle

Algorithme vs Langage

Lorsqu’on programme il est utile de faire la distinction en ce qui relève de :

  • L’algorithme c’est à dire de la logique de résolution de problème (indépendant du langage).
  • L’expression élégante de cet algorithme dans le langage qu’on veut utiliser.

Pour trouver un algorithme il vaut mieux dessiner et écrire sur un papier ce que l’on cherche à faire !

Architecture

Attitude du développeur

  • Il est important avoir des connaissances fondamentales (ce que je vous raconte ici notamment) et des connaissances techniques.

  • Il faut également avoir la recherche web facile pour pouvoir faire le tri dans la junglede tétails techniques.

Deux sources web classiques de l’information pertinente

  • github
  • stackoverflow.

Lire des livres

Lire des (bons) livres plutôt que des (mauvais) tutoriels. Cf Bibliographie

Communauté

Bonne nouvelle le Python est un écosystème informatique plutôt sain: culture libriste et passion de l’informatique dans la communauté python. N’hésitez pas à aller rencontrer d’autre développeurs.

Après cette formation

Le sujet est très vaste, le métier de développeur est long à intégrer. Il faut “passer plusieurs couches de peintures”. Nous allons parcourir pas mal de distance (en profondeur) et vous pourrez (devriez ?) creuser en largeur par la suite grâce aux références indiquées.

Mémo Syntaxe Python

Récapitulation des principales syntaxes Python

Demander et afficher des informations

Syntaxe Description
print("message") Affiche “message” dans la console
v = input("message") Demande une valeur et la stocke dans v

Calculs

Syntaxe Description
a + b Addition de a b
a - b Soustraction de a et b
a / b Division de a par b
a * b Multiplication de a par b
a % b Modulo (reste de division) de a par b
a ** b Exponentiation de a par b

Toutes ces opérations peuvent être appliquées directement sur une variable via la syntaxe du type a += b (additionner b à a et directement modifier la valeur de a avec le résultat).

Types de variable et conversion

Syntaxe Description
type(v) Renvoie le type de v
int(v) Converti v en entier
float(v) Converti v en float
str(v) Converti v en string

Chaînes de caractères

Syntaxe Description
chaine1 + chaine2 Concatène les chaînes de caractères chaine1 et chaine2
chaine[n:m] Retourne les caractères de chaine depuis la position n à m
chaine * n Retourne chaine concaténée n fois avec elle-meme
len(chaine) Retourne la longueur de chaine
chaine.replace(a, b) Renvoie chaine avec les occurences de a remplacées par b
chaine.split(c) Créé une liste à partir de chaine en la séparant par rapport au caractère c
chaine.strip() “Nettoie” chaine en supprimant les espaces et \n au début et à la fin
\n Représentation du caractère ‘nouvelle ligne’

Fonctions

def ma_fonction(toto, tutu=3):
    une_valeur = toto * 6 + tutu
    return une_valeur

Cette fonction :

  • a pour nom ma_fonction ;
  • a pour argument toto et tutu ;
  • tutu est un argument optionnel avec comme valeur par défaut l’entier 3 ;
  • une_valeur est une variable locale à la fonction ;
  • elle retourne une_valeur ;

Conditions

if condition:
    instruction1
    instruction2
elif autre_condition:
    instruction3
elif encore_une_autre_condition:
    instruction4
else:
    instruction5
    instruction6

Opérateurs de conditions

Syntaxe Description
a == b Egalité entre a et b
a != b Différence entre a et b
a > b a supérieur (strictement) à b
a >= b a supérieur ou égal à b
a < b a inférieur (strictement) à b
a <= b a inférieur ou égal à b
cond1 and cond2 cond1 et cond2
cond1 or cond2 cond1 ou cond2
not cond négation de la condition cond
a in b a est dans b (chaîne, liste, set..)

Inline ifs

parite = "pair" if n % 2 == 0 else "impair"

Exception, assertions

try/except permettent de tenter des instructions et d’attraper les exceptions qui peuvent survenir pour ensuite les gérer de manière spécifique :

try:
   instruction1
   instruction2
except FirstExceptionTime:
   instruction3
except Exception as e:
   print("an unknown exception happened ! :" + e.str)

Les assertions permettent d’expliciter et de vérifier des suppositions faites dans le code :

def une_fonction(n):
   assert isinstance(n, int) and is_prime(n), "Cette fonction fonctionne seulement pour des entiers premiers !"

Boucles

Syntaxe Description
for i in range(0, 10) Itère sur i de 0 à 9
for element in iterable Itère sur tous les elements de iterable (liste, set, dict, …)
for key, value in d.items() Itère sur toutes les clefs, valeurs du dictionnaire d
while condition Répète un jeu d’instruction tant que condition est vraie
break Quitte immédiatement une boucle
continue Passe immédiatement à l’itération suivante d’une boucle

Structures de données

Syntaxe Description
L = ["a", 2, 3.14 ] Liste (suite ordonnée d’éléments)
S = { "a", "b", 3 } Ensemble (éléments unique, désordonné)
D = { "a": 2, "b": 4 } Dictionnaire (ensemble de clé-valeurs, avec clés uniques)
T = (1,2,3) Tuple (suite d’élément non-mutables)
Syntaxe Description
L[i] i-eme element d’une liste ou d’une tuple
L[i:] Liste de tous les éléments à partir du i-eme
L[i] = e Remplace le i-eme element par e dans une liste
L.append(e) Ajoute e à la fin de la liste L
S.add(e) Ajoute e dans le set S
L.insert(i, e) Insère e à la position i dans la liste L
chaine.join(L) Produit une string à partir de L en intercallant la string chaine entre les elements

Fichiers

Ouvrir et lire un fichier :

# Créé un contexte dans lequel le fichier
# est ouvert en lecture en tant que 'f', 
# et met son contenu dans 'content'

with open("/un/fichier", "r") as f:  
    content = f.readlines()          
                                     

Ecrire dans un fichier :

# Créé un contexte dans lequel le fichier
# est ouvert en ré-écriture complète et
# écrit le contenu de 'content' dedans.

with open("/un/fichier", "w") as f:  
    f.write(content)                 
                                     

(Le mode 'a' (append) au lieu de 'w' permet d’ouvrir le fichier pour ajouter du contenu à la fin plutôt que de le ré-écrire)

Partie 1

Cours 1

0. Setup de développement Python

Notre premier outil développement est bien sur l’interpréteur python lui même utilisé pour lancer un fichier de code.

Installation

  • Sur linux installer le paquet python3 (généralement déjà installé parce que linux utilise beaucoup python)
  • Sur Windows installer depuis python.org ou depuis un outil comme chocolatey
  • Sur MacOs déjà installé mais pour gérer un version plus à jours on peut le faire manuellement depuis python.org ou avec homebrew.

Python 2 vs Python 3

  • Python 2 existe depuis 2000
  • Python 3 existe depuis 2008
  • Fin de vie de Python 2 en 2020
  • … mais encore la version par défaut dans de nombreux système … (c.f. python --version)

Généralement il faut lancer python3 explicitement ! (et non python) pour utiliser python3

Différences principales

  • print "toto" ne fonctionnera pas en Python 3 (utiliser print("toto")
  • Nommage des paquets debian (python-* vs python3-*)
  • Gestion de l’encodage
  • range, xrange
  • Disponibilité des librairies ?

Il existe des outils comme 2to3 pour ~automatiser la transition.

Executer un script explicitement avec python

$ python3 hello.py

ou implicitement (shebang)

#!/usr/bin/env python3

print("Hello, world!")

puis on rend le fichier executable et on l’execute

$ chmod +x hello.py
$ ./hello.py

En interactif

$ python3
>>> print("Hello, world!")

ipython3 : alternative à la console python ‘classique’

$ sudo apt install ipython3
$ ipython3
In [1]: print("Hello, world!")

Principaux avantages:

  • Complétion des noms de variables et de modules avec TAB
  • Coloré pour la lisibilité
  • Plus explicite parfois
  • des commandes magiques comme %cd, %run script.py,

Inconvénients:

  • Moins standard
  • à installer en plus de l’interpréteur python.
pour quitter : exit

Les éditeurs de code

VSCode est un éditeur de code récent et très à la mode, pour de bonnes raisons:

  • Il est simple ou départ et fortement extensible: à l’installation seules les fonctionnalités de base sont disponibles
    • Éditeur de code avec coloration et raccourcis pratiques
    • Navigateur de fichier (pour manipuler une grande quantité de fichers et sous dossier sans sortir de l’éditeur)
    • Recherche et remplacement flexible avec des expressions régulières (très important pour trouver ce qu’on cherche et faire de refactoring)
    • Terminal intégrée (On a plein d’outils de développement à utiliser dans le terminal)
    • Une interface git assez simple très bien faite (git on s’y perd facilement, une bonne interface aide à s’y retrouver)

Indépendamment du logiciel choisi on trouve en général toutes ces fonctionnalités dans un éditeur de code.

Observons un peu tout ça avec une démo de VSCode et récapitulons l’importance des ces fonctions.

Installer des extensions pertinentes

Au sein de l’éditeur nous voulons coder en Python et également:

  • Pouvoir détecter les erreurs de syntaxe.
  • Pouvoir explorer le code python réparti dans plusieurs fichiers (sauter à la définition d’une fonction par exemple).
  • Complétion automatique des noms de symboles (ça peut être pénible parfois).
  • Pouvoir debugger le code python de façon agréable.
  • Pouvoir refactorer (changer le nom de variables ou fonctions partout automatiquement).

Installez l’extension Python (et affichez la documentation si vous êtes curieux) en allant dans la section Extensions (Icone de gauche avec 4 carrés dont un détaché)

Nous allons également utiliser git sérieusement donc nous allons installer une super extension git appelée Gitgraph pour pouvoir mieux explorer l’historique d’un dépôt git.

Enfin vous pouvez installer d’autres extensions pour personnaliser l’éditeur comme l’extension VIM si vous aimez habituellement utiliser cet éditeur.

Opensource et extensibilité : ne pas s’enfermer dans un environnement de travail

  • VSCode est développé par Microsoft et partiellement opensource (Le principal code est accessible mais pas tout)
  • VSCodium est la version opensource communautaire de VSCode mais certaines fonctions puissantes et pratiques sont seulement dans VSCode (les environement distant Docker et SSH par exemple)
  • Un fork récent et complètement opensource de VSCode qui peut fonctionner directement dans le navigateur (Cf. gitpod.io). Moins mature.

Ces trois logiciels sont très proches et vous pouvez coder vos extensions (compatibles avec les 3) pour étendre ces éditeur.

Il me semble important pour choisir un outil de se demander si on possède l’outil ou si l’outil nous possède (plus ou moins les deux en général). Pour pouvoir gérér la complexité du développement moderne on dépend de pas mal d’outils. Savoir choisir des outils ouverts et savoir utiliser également les outils en ligne commande (git, pylint, etc cf. suite du cours) est très important pour ne pas s’enfermer dans un environnement limitant et possessif.

1. Les variables

1.1. Exemple

message = "Je connais la réponse à l'univers, la vie et le reste"
reponse = 6 * 7

print(message)
print(reponse)

sorcery

1.2. Principe

  • Les variables sont des abstractions de la mémoire
  • Une étiquette collée apposée sur une partie de la mémoire : nom pointe vers un contenu
  • Différent du concept mathématique

1.3. Déclaration, utilisation

  • En python : déclaration implicite
  • Ambiguité : en fonction du contexte, x désigne soit le contenant, soit le contenu…
x = 42     # déclare (implicitement) une variable et assigne une valeur
x = 3.14   # ré-assigne la variable avec une autre valeur
y = x + 2  # déclare une autre variable y, à partir du contenu de x
print(y)   # affichage du contenu de y

Nommage

  • Caractères autorisés : caractères alphanumériques (a-zA-Z0-9) et _.
  • Les noms sont sensibles à la casse : toto n’est pas la même chose que Toto!
  • (Sans commencer par un chiffre)

Comparaison de différentes instructions

Faire un calcul sans l’afficher ni le stocker nul part:

6*7

Faire un calcul et l’afficher dans la console:

print(6*7)

Faire un calcul et stocker le résultat dans une variable r pour le réutiliser plus tard

r = 6*7

Opérations mathématiques

2 + 3   # Addition
2 - 3   # Soustraction
2 * 3   # Multiplication
2 / 3   # Division
2 % 3   # Modulo
2 ** 3  # Exponentiation

Calcul avec réassignation

x += 3   # Équivalent à x = x + 3
x -= 3   # Équivalent à x = x - 3
x *= 3   # Équivalent à x = x * 3
x /= 3   # Équivalent à x = x / 3
x %= 3   # Équivalent à x = x % 3
x **= 3  # Équivalent à x = x ** 3

Types

42            # Entier / integer               / int
3.1415        # Réel                           / float
"Marius"        # Chaîne de caractère (string)   / str
True / False  # Booléen                        / bool
None          # ... "rien" / aucun (similar à `null` dans d'autres langages)

Connaître le type d’une variable : type(variable)

Conversion de type

int("3")      -> 3
str(3)        -> "3"
float(3)      -> 3.0
int(3.14)     -> 3
str(3.14)     -> "3.14"
float("3.14") -> 3.14
int(True)     -> 1
int("trois")  -> Erreur / Exception

2. Chaînes de caractères

Syntaxe des chaînes

  • Entre simple quote (') ou double quotes ("). Par exemple: "hello"
  • print("hello") affiche le texte Hello
  • print(hello) affiche le contenu d’une variable qui s’apellerait Hello

Longueur

m = "Hello world"
len(m)        # -> 11

Extraction

m[:5]    # -> 'Hello'
m[6:8]   # -> 'wo'
m[-3:]   # -> 'rld'

Multiplication

"a" * 6    # -> "aaaaaa"

Concatenation

"Cette phrase" + " est en deux morceaux."
name = "Marius"
age = 28
"Je m'appelle " + name + " et j'ai " + str(age) + " ans"

Construction à partir de données, avec %s

"Je m'appelle %s et j'ai %s ans" % ("Marius", 28)

Construction à partir de données, avec format

"Je m'appelle {name} et j'ai {age} ans".format(name=name, age=age)

Substitution

"Hello world".replace("Hello", "Goodbye")   # -> "Goodbye world"

Chaînes sur plusieurs lignes

  • \n est une syntaxe spéciale faisant référence au caractère “nouvelle ligne”
"Hello\nworld"     # -> Hello <nouvelle ligne> world

Interactivité basique avec input

En terminal, il est possible de demander une information à l’utilisateur avec input("message")

reponse = input("Combien font 6 fois 7 ?")

N.B. : ce que renvoie input() est une chaîne de caractère !

Et bien d’autres choses !

c.f. documentation, e.g https://devdocs.io/python~3.7/library/stdtypes#str

3. Les fonctions

Principe

Donner un nom à un ensemble d’instructions pour créer de la modularité et de la sémantique

def ma_fonction(arg1, arg2):
    instruction1
    instruction2
    ...
    return resultat

On peut ensuite utiliser la fonction avec les arguments souhaitées et récupérer le resultat :

mon_resultat = ma_fonction("pikachu", "bulbizarre")
autre_resultat = ma_fonction("salameche", "roucoups")
Calculs mathématiques
sqrt(2)        -> 1.41421 (environ)
cos(3.1415)    -> -1 (environ)
Générer ou aller chercher des données
nom_du_departement(67)        -> "Bas-rhin"
temperature_actuelle("Lyon")  -> Va chercher une info sur internet et renvoie 12.5
Convertir, formatter, filtrer, trier des données …
int("3.14")                     -> 3
normalize_url("toto.com/pwet/") -> https://toto.com/pwet
sorted(liste_de_prenoms)     -> renvoie la liste triée alphabétiquement
**Afficher / demander des données **
print("un message")
input("donne moi un chiffre entre 1 et 10 ?")

Exemples concrets

def aire_triangle(base, hauteur):
    return base * hauteur / 2

A1 = aire_triangle(3, 5)      # -> A1 vaut 15 !
A2 = aire_triangle(4, 2)      # -> A2 vaut 8 !


def aire_disque(rayon):
    rayon_carree = rayon ** 2
    return 3.1415 * rayon_carree

A3 = aire_disque(6)           # -> A3 vaut (environ) 113 !

def aire_triangle(base, hauteur):
    return base * hauteur / 2

A1 = aire_triangle(3, 5)      # -> A1 vaut 15 !
A2 = aire_triangle(4, 2)      # -> A2 vaut 8 !


def aire_disque(rayon):
    rayon_carree = rayon ** 2
    return 3.1415 * rayon_carree

A3 = aire_disque(6)           # -> A3 vaut (environ) 113


def volume_cylindre(rayon, hauteur):
    return hauteur * aire_disque(rayon)

V1 = volume_cylindre(6, 4)   # -> A4 vaut (environ) 452

Écrire une fonction

Éléments de syntaxe

def aire_disque(rayon):
    rayon_carree = rayon ** 2
    return 3.1415 * rayon_carree
  • def, :
  • des instructions indentées !!
  • des arguments (ou pas!)
  • return (ou pas)

Les arguments

def aire_disque(rayon):
    # [ ... ]
  • Une fonction est un traitement générique. On ne connait pas à l’avance la valeur précise qu’aura un argument, et généralement on appelle la fonction pleins de fois avec des arguments différents…
  • En définissant la fonction, on travaille donc avec un argument “abstrait” nommé rayon
  • Le nom rayon en tant qu’argument de la fonction n’a de sens qu’a l’intérieur de cette fonction !
  • En utilisant la fonction, on fourni la valeur pour rayon, par exemple: aire_disque(6).

Les variables locales

def aire_disque(rayon):
    rayon_carree = rayon ** 2
    # [ ... ]
  • Les variables créées dans la fonction sont locales: elles n’ont de sens qu’a l’intérieur de la fonction
  • Ceci dit, cela ne m’empêche pas d’avoir des variables aussi nommées rayon ou rayon_carree dans une autre fonction ou dans la portée globale (mais ce ne sont pas les mêmes entités)

Le return

def aire_disque(rayon):
    rayon_carree = rayon ** 2
    return 3.1415 * rayon_carree
  • return permet de récupérer le résultat de la fonction
  • C’est ce qui donne du sens à A = aire_disque(6) (il y a effectivement un résultat à mettre dans A)
  • Si une fonction n’a pas de return, elle renvoie None
  • return quitte immédiatement la fonction

Erreur classique:

Utiliser print au lieu de return

Ce programme n’affiche rien

def aire_disque(rayon):
    rayon_carree = rayon ** 2
    return 3.1415 * rayon_carree

A = aire_disque(6)      # A vaut bien quelque chose
                        # mais nous ne demandons pas de l'afficher ...

Solution naive : remplacer le return par un print

def aire_disque(rayon):
    rayon_carree = rayon ** 2
    print(3.1415 * rayon_carree)    # Affiche le résultat dans la console

A = aire_disque(6)   # Mais maintenant A vaut None
                     # car la fonction n'a pas utilisé `return`
“Bonne” solution
def aire_disque(rayon):
    rayon_carree = rayon ** 2
    return 3.1415 * rayon_carree

A = aire_disque(6)   # Stocker le résultat dans A
print(A)             # Demander d'afficher A dans la console

Ceci dit, il peut être tout à fait légitime de mettre des print dans une fonction, par exemple pour la débugger…!

Appel de fonction avec arguments explicites

def aire_triangle(base, hauteur):
    return base * hauteur / 2

A1 = aire_triangle(3, 5)
A2 = aire_triangle(4, hauteur=8)
A3 = aire_triangle(hauteur=6, base=2)
A4 = aire_triangle(hauteur=3, 2)    # < Pas possible !

N.B. : cette écriture est aussi plus explicite / lisible / sémantique:

aire_triangle(base=3, hauteur=5)

que juste

aire_triangle(3, 5)

On peut se retrouver dans des situations comme:

base = 3
hauteur = 5

A1 = aire_triangle(base=base, hauteur=hauteur)

Dans l’appel de la fonction :

  • le premier base est le nom de l’argument de la fonction aire_triangle,
  • le deuxième base corresponds au contenu de la variable nommée base.

Arguments optionnels

Les arguments peuvent être rendu optionnels si ils ont une valeur par défaut :

def distance(dx, dy=0, dz=0):
    [...]

Dans ce cas, tous ces appels sont valides :

distance(5)
distance(2, 4)
distance(5, 8, 2)
distance(9, dy=5)
distance(0, dz=4)
distance(1, dy=1, dz=9)
distance(2, dz=4, dy=7)

Exemple réaliste

subprocess.Popen(args,
                 bufsize=0,
                 executable=None,
                 stdin=None,
                 stdout=None,
                 stderr=None,
                 preexec_fn=None,
                 close_fds=False,
                 shell=False,
                 cwd=None,
                 env=None,
                 universal_newlines=False,
                 startupinfo=None,
                 creationflags=0)

c.f. https://docs.python.org/2/library/subprocess.html#subprocess.Popen

4. Conditions et branchements conditionnels

Pour pouvoir écrire des applications il faut des techniques permettant de contrôler le déroulement du programme dans différentes directions, en fonction des circonstances. Pour cela, nous devons disposer d’instructions capables de tester une certaine condition et modifier le comportement du programme en conséquence.

La principale instruction conditionnelle est, en python comme dans les autres langages impératifs, le if (Si condition alors …) assorti généralement du else (Sinon faire …) et en python de la contraction elif de else if (Sinon, Si condition alors …)e

Syntaxe générale

if condition:
    instruction1
    instruction2
elif (autre condition):
    instruction3
elif (encore autre condition):
    instruction4
else:
    instruction5
    instruction6

Attention à l’indentation !

Tout n’est pas nécessaire, par exemple on peut simplement mettre un if :

if condition:
    instruction1
    instruction2

Exemple

a = 0
if a > 0 :
    print("a est positif")
elif a < 0 :
    print("a est négatif")
else:
    print("a est nul")

Lien avec les booléens

Les conditions comme a > 0 sont en fait transformées en booléen lorsque la ligne est interprétée.

On aurait pu écrire :

a_est_positif = (a > 0)

if a_est_positif:
    [...]
else:
    [...]

Écrire des conditions

angle == pi      # Égalité
angle != pi      # Différence
angle > pi       # Supérieur
angle >= pi      # Supérieur ou égal
angle < pi       # Inférieur
angle <= pi      # Inférieur ou égal

Combiner des conditions

x = 2

print("x > 0:", x > 0) # vrai 
print("x > 0 and x == 2:", x > 0 and x == 2) # vrai et vrai donne vrai
print("x > 0 and x == 1:", x > 0 and x == 2) # vrai et faux donne faux
print("x > 0 or x == 1:", x > 0 or x == 1) # vrai ou faux donne vrai
print("not x == 1:", not x == 1) # non faux donne vrai
print("x > 0 or not x == 1:", x > 0 or not x == 1) # vrai ou (non faux) donne vrai ou vrai donne vrai

Conditions “avancées”

Chercher des choses dans des chaînes de caractères

"Jack" in nom           # 'nom' contient 'Jack' ?
nom.startswith("Jack")  # 'nom' commence par 'Jack' ?
nom.endswith("ack")     # 'nom' fini par 'row' ?

Remarque: l’opérateur in est très utile et générale en Python: il sert à vérifier qu’un élément existe dans une collection. Par exemple si l’entier 2 est présent dans une liste d’entier ou comme ici si un mot est présent dans une chaine de caractère.

‘Inline’ ifs

On peut rassembler un if else sur une ligne comme suit:

parite = "pair" if n % 2 == 0 else "impair"

Tester si une variable a une valeur de façon “pythonique”.

En python pour tester si une variable contient une valeur vide ou pas de valeur (c-à-d valeur None) on aime bien, par convention “pythonique”, écrire simplement if variable: :

reste_division = a % 2

if reste_division:
    print("a est pair parce que le reste de sa division par 2 est nul")
else:
    print("a est impair")

Pareil pour tester si unt chaîne de caractère est vide ou nulle:

texte = input()

if texte:
    print("vous avez écrit: ", texte)
else:
    print("pas de texte")
    print("texte is None :", texte is None)
    print("texte == \"\" (chaine vide) :", texte == "")

Remarque: dans notre dernier cas il n’est pas forcément important de savoir si texte est None ou une chaîne vide mais plutôt de savoir si on a effectivement une valeur “significative” à afficher. C’est souvent le cas et c’est pour cela qu’on privilégie if variable pour simplifier la lecture du code.

Vraisemblance (truthiness) d’un valeur

L’usage de if variable: comme précédemment est basé sur la truthiness ou vraisemblance de la variable. On dit que a est vraisemblable si la conversion de a en booléen donne True : bool(3) donne True on dit que 3 est truthy, bool(None) donne False donc None est falsy. TODO Nous verrons dans la partie sur le Python Data Model que cela implique des choses pour nos classes de programmation orientée objet en python (en Résumé on veut que if monObjet: soit capable de tester si l’objet est initialisé et utilisable) Autrement dit en python on aime utiliser la vraisemblance implicite des variables pour tester si leur valeur est significative/initialisée ou non.

5. Les boucles

Répéter des opération est le coeur de la puissance de calcul des ordinateur. On peut pour cela utiliser des boucles ou des appels récusifs de fonctions. Les deux boucles python sont while et for.

La boucle while

while <condition>: veut dire “tant que la condition est vraie répéter …”. C’est une boucle simple qui teste à chaque tour (avec une sorte de if) si on doit continuer de boucler.

Exemple:

a = 0
while (a < 10) # On répète les deux instructions de la boucle tant que a est inférieur à 7
    a = a + 1 # A chaque tour on ajoute 1 à la valeur de a
    print(a)

La boucle for et les listes

La boucle for en Python est plus puissante et beaucoup plus utilisée que la boucle while car elle “s’adapte aux données” et aux objets du programme grâce à la notion d’itérateur que nous détaillerons plus loin. (De ce point de vue, la boucle for python est très différente de celle du C/C++ par exemple)

On peut traduire la boucle Python for element in collection: en français par “Pour chaque élément de ma collection répéter …”. Nous avons donc besoin d’une “collection” (en fait un iterateur) pour l’utiliser. Classiquement on peut utiliser une liste python pour cela:

ma_liste = [7, 2, -5, 4]

for entier in ma_liste:
    print(entier)

Pour générer rapidement une liste d’entiers et ainsi faire un nombre défini de tours de boucle on utilise classiquement la fonction range()

print(range(10))

for entier in range(10):
    print(entier) # Afficher les 10 nombres de 0 à 9
for entier in range(1, 11):
    print(entier) # Afficher les 10 nombres de 1 à 10
for entier in range(2, 11, 2):
    print(entier) # Afficher les 5 nombres pairs de 2 à 10 (le dernier paramètre indique d'avancer de 2 en 2)

continue et break

continue permet de passer immédiatement à l’itération suivante

break permet de sortir immédiatement de la boucle

for i in range(0,10):
    if i % 2 == 0:
        continue

    print("En ce moment, i vaut " + str(i))

-> Affiche le message seulement pour les nombres impairs

for i in range(0,10):
    if i == 7:
        break

    print("En ce moment, i vaut " + str(i))

-> Affiche le message pour 0 à 6

6. Principes de développement - Partie 1

Écrire un programme … pour qui ? pour quoi ?

Le fait qu’un programme marche n’est pas suffisant voire parfois “secondaire” !

  • … Mieux vaut un programme cassé mais lisible (donc débuggable)
  • … qu’un programme qui marche mais incompréhensible (donc fragile et/ou qu’on ne saura pas faire évoluer)
  • … et donc qui va surtout faire perdre du temps aux futurs développeurs

Autrement dit : la lisibilité pour vous et vos collègues a énormément d’importance pour la maintenabilité et l’évolution d’un projet

Posture de développeur et bonnes pratiques

  • Lorsqu’on écrit du code, la partie “tester” et “debugger” fait partie du job.

On écrit pas un programme qui marche au premier essai

  • Il faut tester et débugger au fur et à mesure, pas tout d’un seul coup !

Le debugging interactif : pdb, ipdb, VSCode

  • PDB = Python DeBugger

  • Permet (entre autre) de définir des “break points” pour rentrer en interactif

    • import ipdb; ipdb.set_trace()
    • en 3.7 : breakpoint() Mais fait appel à pdb et non ipdb ?
  • Une fois en interactif, on peut inspecter les variables, tester des choses, …

  • On dispose aussi de commandes spéciales pour executer le code pas-à-pas

  • Significativement plus efficace que de rajouter des print() un peu partout !

Commandes pdb et ipdb

  • l(ist) : affiche les lignes de code autour de code (ou continue le listing precedent)

  • c(ontinue) : continuer l’execution normalement (jusqu’au prochain breakpoint)

  • s(tep into) : investiguer plus en détail la ligne en cours, possiblement en descendant dans les appels de fonction

  • n(ext) : passer directement à la ligne suivante

  • w(here) : print the stack trace, c.a.d. les différents sous-appels de fonction dans lesquels on se trouve

  • u(p) : remonte d’un cran dans les appels de la stacktrace

  • d(own) : redescend d’un cran dans les appels de la stacktrace

  • pp <variable> : pretty-print d’une variable (par ex. une liste, un dict, ..)

Debug VSCode

  • Dans VSCode on peut fixer des breakpoints (points rouges) directement dans le code en cliquant sur la colonne de gauche de l’éditeur.
  • Il faut ensuite aller dans l’onglet debug et sélectionner une configuration de debug ou en créer une plus précise (https://code.visualstudio.com/docs/python/python-tutorial)
  • Ensuite on lance le programme en mode debug et au moment de l’arrêt il est possible d’explorer les valeurs de toutes les variables du programme (Démo)

Bonnes pratiques pour la lisibilité, maintenabilité

  • Keep It Simple

  • Sémantique : utiliser des noms de variables et de fonctions qui ont du sens

  • Architecture : découper son programme en fonction qui chacune résolvent un sous-problème précis

  • Robustesse : garder ses fonctions autant que possibles indépendantes, limiter les effets de bords

    • lorsque j’arose mes plantes, ça ne change pas la température du four
  • Lorsque mon programme évolue, je prends le temps de le refactoriser si nécessaire

    • si je répète plusieurs fois les mémes opérations, il peut être intéressant d’introduire une nouvelle fonction
    • si le contenu d’une variable ou d’une fonction change, peut-être qu’il faut modifier son nom
    • si je fais pleins de petites opérations bizarre, peut-être qu’il faut créer une fonction

Quelques programmes réels utilisant Python

Dropbox

Atom

Eve online

Matplotlib

Blender

OpenERP / Odoo

Tartiflette

Corrections 1

Exercice 1

result_1 = 567 * 72
result_2 = 33**4
result_3 = 98.2 / 6
result_4 = (7 * 9)**4 / 6

print(result_1)
print(result_2)
print(result_3)
print(result_4)

Exercice 2

annee = int(input('Quelle est votre année de naissance ?\n'))
age = 2019 - annee + 2
print("Dans deux ans vous aurez {} ans.".format(age))

3.1 Compter les lettres

mot = input("Donnez moi un mot.\n")
print("Ce mot fait {} caractères (espaces inclus).".format(len(mot)))

3.2 Encadrer le mot avec

mot_encadre = '#### ' + mot + ' ####'
print("mot encadré: {}".format(mot_encadre))

Exercice 4. fonctions de centrage

def centrer(mot, largeur=80):
    
	nb_espaces = largeur - len(mot) - 2
    nb_espaces_gauche = nb_espaces // 2     # division entière:  25 // 2 -> 12
    nb_espaces_droite = nb_espaces - nb_espaces_gauche

    resultat = "|" + nb_espaces_gauche * " " + mot + nb_espaces_droite * " " + "|"

    return resultat
    
    
def encadrer(mot, largeur=80, caractere='@'):
	ligne1 = caractere * largeur
    ligne2 = centrer(mot, largeur)
    
    return "{}\n{}\n{}".format(ligne1, ligne2, ligne1)
	

print(centrer("Pikachu"))
print(len(centrer("Pikachu")) # 80
print(centrer("Pikachu", 40))

print(encadrer("Pikachu"))
print(encadrer("Pikachu", 37))
print(encadrer("Pikachu", 71, "#"))

Exercice 5. conditions

def centrer(mot, largeur=80):
	nb_espaces = largeur - len(mot) - 2
    nb_espaces_gauche = nb_espaces // 2     # division entière:  25 // 2 -> 12
    nb_espaces_droite = nb_espaces - nb_espaces_gauche

    resultat = "|" + nb_espaces_gauche * " " + mot + nb_espaces_droite * " " + "|"

    return resultat
    
    
def encadrer(mot, largeur=80, caractere='@'):
	if largeur == -1:
    	largeur = len(mot) + 4
	
    if caractere == '':
    	return centrer(mot, largeur)
    
    longueur_max = largeur - 4
    if len(mot) > longueur_max:
    	mot = mot[:longueur_max]
        
 	ligne1 = caractere * largeur
    ligne2 = centrer(mot, largeur).replace("|", caractere)
    
    return "{}\n{}\n{}".format(ligne1, ligne2, ligne1)
	

print(encadrer("Pikachu", -1))
print(encadrer("Pikachu", 34, ''))
print(encadrer("Pikachu", 8, '@'))

Exercice 6

from timeit import default_timer as timer
from functools import lru_cache as cache

def fib_rec_naive(n):
    """
    fib_rec_naive calcule le Ne terme de la suite de fibonacci
    En utilisant une approche récursive naive de complexité exponentielle
    """
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib_rec_naive(n-1) + fib_rec_naive(n-2)

@cache()
def fib_rec_naive_cache(n):
    """
    fib_rec_naive calcule le Ne terme de la suite de fibonacci
    En utilisant une approche récursive naive, mais en ajoutant 
    un décorateur de memoïzation qui stocke l'état de la pile d'éxecution entre les appels
    """
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib_rec_naive_cache(n-1) + fib_rec_naive_cache(n-2)

liste_termes_calculés = [0,1]
def fib_rec_liste(n):
    """
    fib_rec_liste calcule le Ne terme de la suite de fibonacci
    En utilisant une approche récursive correcte de complexité linéaire
    en utilisant une mémoire sous forme de liste
    """
    if n < len(liste_termes_calculés):
        return liste_termes_calculés[n]
    else:
        liste_termes_calculés.append(fib_rec_liste(n-1) + fib_rec_liste(n-2))
        return liste_termes_calculés[n]

def fib_iter(n):
    """
    fib_iter calcule le Ne terme de la suite de fibonacci
    En utilisant une approche itérative de complexité linéaire
    """
    ancien_terme, nouveau_terme = 0, 1
    if n == 0:
        return 0

    for i in range(n-1):
        ancien_terme, nouveau_terme = nouveau_terme, ancien_terme + nouveau_terme

    return nouveau_terme


if __name__ == "__main__":

    # Temps avec 35 termes

    start = timer()
    fib_rec_naive(35)
    stop = timer()
    print( "fib_rec_naive(35) execution time: ", stop - start )

    start = timer()
    fib_rec_naive_cache(35)
    stop = timer()
    print( "fib_rec_naive_cache(35) execution time: ", stop - start )

    start = timer()
    fib_rec_liste(35)
    stop = timer()
    print( "fib_rec_list(35) execution time: ", stop - start )

    start = timer()
    fib_iter(35)
    stop = timer()
    print( "fib_iter(35) execution time: ", stop - start )

    # Temps avec 38 termes

    start = timer()
    fib_rec_naive(38)
    stop = timer()
    print( "fib_rec_naive(38) execution time: ", stop - start )

    start = timer()
    fib_rec_naive_cache(38)
    stop = timer()
    print( "fib_rec_naive_cache(38) execution time: ", stop - start )

    start = timer()
    fib_rec_liste(38)
    stop = timer()
    print( "fib_rec_list(38) execution time: ", stop - start )

    start = timer()
    fib_iter(38)
    stop = timer()
    print( "fib_iter(38) execution time: ", stop - start )

Bonus 2

    
def fibonacci_generator():
    a, b = 0, 1
    yield a
    yield b
    
    while True:
        a, b = (b, a+b)
        yield b

for n in fibonacci_generator():
    if n > 500:
        break
    print(n)

Exos 1

1. Calculs dans l’interpréteur

  • À l’aide de python, calculer le résultat des opérations suivantes :
    • 567×72
    • 33⁴
    • 98.2/6
    • ((7×9)⁴)/6
    • vrai et non (faux ou non vrai)

2. Interactivité

  • Demander l’année de naissance de l’utilisateur, puis calculer et afficher l’âge qu’il aura dans deux ans (approximativement, sans tenir compte du jour et mois de naissance…).

3. Chaînes de caractères

  • Demander un mot à l’utilisateur. Afficher la longueur du mot avec une message tel que "Ce mot fait X caractères !"

  • Afficher le mot encadré avec des ####. Par exemple:

##########
# Python #
##########

4. Fonctions

  • Ecrire une fonction centrer prend en argument une chaîne de caractère, et retourne une nouvelle chaîne centrée sur 40 caractères. Par exemple print(centrer("Python")) affichera :
|                Python                |
  • Ajouter un argument optionnel pour gérer la largeur au lieu du 40 “codé en dur”. Par exemple print(centrer("Python", 20)) affichera :
|      Python      |
  • Créer une fonction encadrer qui utilise la fonction centrer pour produire un texte centré et encadré avec des ####. Par exemple, print(encadrer("Python", 20)) affichera :
####################
|      Python      |
####################

5. Conditions

  • Reprendre la fonction annee_naissance et afficher un message d’erreur et sortir immédiatement de la fonction si l’argument fourni n’est pas un nombre entre 0 et 130. Valider le comportement en appelant votre fonction avec comme argument -12, 158, None ou "toto".

  • Inspecter l’execution du code pas à pas à l’aide du debugger VSCode.

  • Reprendre la fonction centrer de l’exercice 4.1 et gérer le cas où la largueur demandée est -1 : dans ce cas, ne pas centrer. Par exemple, print(encadrer("Python", -1)) affichera :

##########
# Python #
##########

6. Performances et debugging : plusieurs implémentations de la suite de fibonacci

La célèbre suite de Fibonacci, liée au nombre d’or, est une suite d’entiers dans laquelle chaque terme est la somme des deux termes qui le précèdent. Mais elle est également un exercice classique d’algorithmique.

  • Écrire une fonction fibonacci_rec_naive(n) qui calcule de façon récursive la suite de fibonacci.

  • Créez une autre fonction fibonacci_iter(n) qui calcule de façon iterative la suite de fibonacci.

  • Calculez le 40e terme de la suite avec chacune des implémentation précédente.

  • Debuggez les deux implémentations. Que se passe-t-il ?

  • A l’aide de la librairie timeit et de sa fonction timer (from timeit import default_timer as timer) qui renvoie le temps processeur courant, mesurez le temps d’exécution des deux fonctions.

  • Écrire une fonction fibonacci_rec_liste(n) qui calcule récursivement la suite de fibonacci en utilisant une liste comme mémoire pour ne pas recalculer les terme déjà calculés.

  • Bonus 1: Utilisons un décorateur de “caching” de fonction (from functools import lru_cache as cache) sur fibonacci_rec_naive(n) pour l’optimiser sans changer le code.

  • Bonus 2: Écrivons une implémentation pythonique de fibonacci utilisant un générateur

Partie 2

Cours 2

7. Structures de données

Les structures de données permettent de stocker des séries d’information et d’y accéder (plus ou moins) facilement et rapidement.

Les listes

Une collection d’éléments ordonnés référencé par un indice

animaux_favoris = [ "girafe", "chenille", "lynx" ]
fibonnaci = [ 1, 1, 2, 3, 5, 8 ]
stuff = [ 3.14, 42, "bidule", ["a", "b", "c"] ]

Accès à element particulier ou a une “tranche”

animaux_favoris[1]      ->  "chenille"
animaux_favoris[-2:]    ->  ["chenille", "lynx"]

Longueur

len(animaux_favoris)    -> 3

Tester qu’un élément est (ou n’est pas) dans une liste

"lynx" in animaux_favoris   # -> True
"Mewtwo" not in animaux_favoris   # -> True
animaux_favoris = [ "girafe", "chenille", "lynx" ]

Iteration

for animal in animaux_favoris:
    print(animal + " est un de mes animaux préférés !")

Iteration avec index

print("Voici la liste de mes animaux préférés:")
for i, animal in enumerate(animaux_favoris):
    print(str(i+1) + " : " + animal)
animaux_favoris = [ "girafe", "chenille", "lynx" ]

Modification d’un élément

animaux_favoris[1] = "papillon"

Ajout à la suite, contatenation

animaux_favoris.append("coyote")

Insertion, concatenation

animaux_favoris.insert(1, "sanglier")
animaux_favoris += ["lion", "moineau"]

Exemple de manip classique : filtrer une liste pour en construire une nouvelle

animaux_favoris = [ "girafe", "chenille", "lynx" ]

# Création d'une liste vide
animaux_starting_with_c = []

# J'itère sur la liste de pokémons favoris
for animal in animaux_favoris:

   # Si le nom de l'animal actuel commence par c
   if animal.startswith("c"):

      # Je l'ajoute à la liste
      animaux_starting_with_B.append(animal)

À la fin, animaux_starting_with_c contient:

["girafe"]

Transformation de string en liste

"Hello World".split()    -> ["Hello", "World"]

Transformation de liste en string

' | '.join(["a", "b", "c"])      -> "a | b | c"

Les dictionnaires

Une collection non-ordonnée (apriori) de clefs a qui sont associées des valeurs

phone_numbers = { "Alice":   "06 93 28 14 03",
                  "Bob":     "06 84 19 37 47",
                  "Charlie": "04 92 84 92 03"  }

Accès à une valeur

phone_numbers["Charlie"]        -> "04 92 84 92 03"
phone_numbers["Marius"]           -> KeyError !
phone_numbers.get("Marius", None) -> None

Modification d’une entrée, ajout d’une nouvelle entrée

phone_numbers["Charlie"] = "06 25 65 92 83"
phone_numbers["Deborah"] = "07 02 93 84 21"

Tester qu’une clef est dans le dictionnaire

"Marius" in phone_numbers    # -> False
"Bob" not in phone_numbers # -> False
phone_numbers = { "Alice":   "06 93 28 14 03",
                  "Bob":     "06 84 19 37 47",
                  "Charlie": "04 92 84 92 03"  }

Iteration sur les clefs

for prenom in phone_numbers:     # Ou plus explicitement: phone_numbers.keys()
    print("Je connais le numéro de "+prenom)

Iteration sur les valeurs

for phone_number in phone_numbers.values():
    print("Quelqu'un a comme numéro " + phone_number)

Iterations sur les clefs et valeurs

for prenom, phone_number in phone_numbers.items():
    print("Le numéro de " + prenom + " est " + phone_number)

Construction plus complexes

Liste de liste, liste de dict, dict de liste, dict de liste, …

contacts = { "Alice":  { "phone": "06 93 28 14 03",
                         "email": "alice@megacorp.eu" },

             "Bob":    { "phone": "06 84 19 37 47",
                         "email": "bob.peterson@havard.edu.uk" },

             "Charlie": { "phone": "04 92 84 92 03" } }
contacts = { "Alice":  { "phone": "06 93 28 14 03",
                         "email": "alice@megacorp.eu" },

             "Bob":    { "phone": "06 84 19 37 47",
                         "email": "bob.peterson@harvard.edu.uk" },

             "Charlie": { "phone": "04 92 84 92 03" } }

Recuperer le numero de Bob

contacts["Bob"]["phone"]   # -> "06 84 19 37 47"

Ajouter l’email de Charlie

contacts["Charlie"]["email"] = "charlie@orange.fr"

Ajouter Deborah avec juste une adresse mail

contacts["Deborah"] = {"email": "deb@hotmail.fr"}

Les sets

Les sets sont des collections d’éléments unique et non-ordonnée

chat = set(["c", "h", "a", "t"])        # -> {'h', 'c', 'a', 't'}
chien = set(["c", "h", "i", "e", "n")   # -> {'c', 'e', 'i', 'n', 'h'}
chat - chien                            # -> {'a', 't'}
chien - chat                            # -> {'i', 'n', 'e'}
chat & chien                            # -> {'h', 'c'}
chat | chien                            # -> {'c', 't', 'e', 'a', 'i', 'n', 'h'}
chat.add("z")                           # ajoute `z` à `chat`

Les tuples

Les tuples permettent de stocker des données de manière similaire à une liste, mais de manière non-mutable. Generalement itérer sur un tuple n’a pas vraiment de sens…

Les tuples permettent de grouper des informations ensembles. Typiquement : des coordonnées de point.

xyz = (2,3,5)
xyz[0]        # -> 2
xyz[1]        # -> 3
xyz[0] = 5    # -> Erreur!

Autre exemple dictionnaire.items() renvoie une liste de tuple (clef, valeur) :

[ (clef1, valeur1), (clef2, valeur2), ... ]

List/dict comprehensions

Les “list/dict comprehensions” sont des syntaxes particulière permettant de rapidement construire des listes (ou dictionnaires) à partir d’autres structures.

Syntaxe (list comprehension)

[ new_e for e in liste if condition(e) ]

Exemple (list comprehension)

Carré des entiers impairs d’une liste

[ e**2 for e in liste if e % 2 == 1 ]

List/dict comprehensions

Les “list/dict comprehensions” sont des syntaxes particulière permettant de rapidement construire des listes (ou dictionnaires) à partir d’autres structures.

Syntaxe (dict comprehension)

{ new_k:new_v for k, v in d.items() if condition(k, v) }

Exemple (dict comprehension)

{ nom: age-20 for nom, age in ages.items() if age >= 20 }

Générateurs

(Pas vraiment une structure de données, mais c’est lié aux boucles …)

  • Une fonction qui renvoie des résultats “au fur et à mesure” qu’ils sont demandés …
  • Se comporte comme un itérateur
  • Peut ne jamais s’arrêter …!
  • Typiquement, évite de créer des listes intermédiaires

exemple SANS generateur

mes_animaux = { "girafe": 300,    "coyote": 50,
                 "chenille": 2,       "cobra": 45
                 # [...]
               }

def au_moins_un_metre(animaux):

    output = []
    for animal, taille in animaux.items():
        if taille >= 100:
            output.append(animal)

    return output

for animal in au_moins_un_metre(mes_animaux):
   ...

exemple AVEC generateur

mes_animaux = { "girafe": 300,    "coyote": 50,
                 "chenille": 2,       "cobra": 45
                 # [...]
               }

def au_moins_un_metre(animaux):

    for animal, taille in animaux.items():
        if taille >= 100:
            yield animal

for animal in au_moins_un_metre(mes_animaux):
   ...

Il n’est pas nécessaire de créer la liste intermédiaire output

Un autre exemple

def factorielle():

   n = 1
   acc = 1

   while True:
       acc *= n
       n += 1

       yield acc

8. Programmation et algorithmique - récapituler

Programmation impérative / procédurale

  • Comme une recette de cuisine qui manipule de l’information
  • Une suite d’opération à effectuer
  • Différents concepts pour construire ces opérations:
    • des variables
    • des fonctions
    • des conditions
    • des boucles
    • des structures de données (listes, dictionnaires)

Variables

x = "Toto"
x = 40
y = x + 2
print("y contient " + str(y))

Fonctions

def aire_triangle(base, hauteur):
    calcul = base * hauteur / 2
    return calcul

A1 = aire_triangle(3, 5)      # -> A1 vaut 15 !
A2 = aire_triangle(4, 2)      # -> A2 vaut 8 !
  • Indentation
  • Arguments (peuvent être optionnels si on spécifie une valeur par défaut)
  • Variables locales
  • return pour pouvoir récupérer un résultat depuis l’extérieur
  • Appel de fonction

Conditions

def aire_triangle(base, hauteur):

    if base < 0 or hauteur < 0:
        print("Il faut donner des valeurs positives!")
        return -1

    calcul = base * hauteur / 2
    return calcul
  • Indentation
  • Opérateurs (==, !=, <=, >=, and, or, not, in, …)
  • Mot clefs if, elif, else

Listes, dictionnaires et boucles

breakfast = ["Spam", "Eggs", "Bacon", "Spam"]
breakfast.append("Coffee")

print("Au petit dej' je mange: ")
for stuff in breakfast:
    print(stuff)
ingredients_gateau = {"farine": 200,
                      "beurre": 100,
                      "chocolat": 150}

for ingredient, qty in ingredients_gateau.items():
    print("J'ai besoin de " + str(qty) + "g de " + ingredient)

Algorithmes simples : max

def max(liste_entiers):
    if liste_entiers == []:
        print("Erreur, peut pas calculer le max d'une liste vide")
        return None

    m = liste_entiers[0]
    for entier in liste_entiers:
        if m < entier:
            m = entier

    return m

Algorithmes simples : filtrer une liste

def pairs(liste_entiers):

    resultat = []

    for entier in liste_entiers:
        if entier % 2 == 0:
            resultat.append(entier)

    return resultat

9. Erreurs et exceptions

En Python, lorsqu’une erreur se produit ou qu’un cas particulier empêche (a priori) la suite du déroulement normal d’un programme ou d’une fonction, une exception est déclenchée

Attention : différent des erreurs de syntaxe

Exemple d’exceptions

  • Utiliser une variable qui n’existe pas

  • Utiliser int() sur quelque chose qui ne peut pas être converti en entier

  • Diviser un nombre par zero

  • Diviser un nombre par une chaine de caractère

  • Tenter d’accéder à un élément d’une liste qui n’existe pas

  • Tenter d’ouvrir un fichier qui n’existe pas ou qu’on ne peut pas lire

  • Tenter de télêcharger des données sans être connecté à internet

  • etc…

  • Une exception a un type (c’est un objet d’un classe d’exception -> cf. Partie 3):

    • Exception, ValueError, IndexError, TypeError, ZeroDivisionError, …
  • Lorsqu’une exception interrompt le programme, l’interpréteur affiche la stacktrace (TraceBack) qui contient des informations pour comprendre quand et pourquoi l’exception s’est produite.

Traceback (most recent call last):
  File "coucou.py", line 3, in <module>
    print(coucou)
NameError: name 'coucou' is not defined
# python3 test_int.py

Tapez un entier entre 1 et 3: truc

Traceback (most recent call last):
  File "test_int.py", line 8, in <module>
    demander_nombre()
  File "test_int.py", line 4, in demander_nombre
    r = int(input("Tape un entier entre 1 et 3: "))
ValueError: invalid literal for int() with base 10: 'truc'

Souvent une exception est due à une entrée utilisateur incorrecte (comme ici) mais pas toujours.

raise

Il est possible de déclencher ses propres exceptions à l’aide de raise

def max(liste_entiers):
    if liste_entiers == []:
        raise Exception("max() ne peut pas fonctionner sur une liste vide!")

(Ici, le type utilisé est le type générique Exception)

Autre exemple:

def envoyer_mail(destinataire, sujet, contenu):
    if '@' not in destinataire:
        raise Exception('Une adresse mail doit comporter un @ !')

(Ici, le type utilisé est le type générique Exception)

try/except

De manière générale dans un programme, il peut y’avoir beaucoup de manipulation dont on sait qu’elles peuvent échouer pour un nombre de raisons trop grandes à lister …

Par exemple : écrire dans un fichier

  • Est-ce que le programme a la permission d’écrire dans ce fichier ?
  • Est-ce qu’aucun autre programme n’est en train d’écrire dans ce fichier ?
  • Est-ce qu’il y a assez d’espace disque libre ?
  • Si je commence à écrire, peut-être vais-je tomber sur un secteur disque deffectueux

Autre exemple : aller chercher une information sur internet

  • Est-ce que je suis connecté à Internet ?
  • Est-ce que la connection est suffisament stable et rapide ?
  • Est-ce que le programme a le droit d’effectuer d’envoyer des requêtes ?
  • Est-ce qu’un firewall va bloquer ma requête ?
  • Est-ce que le site que je veux contacter est disponible actuellement ?
  • Est-ce que le certificat SSL du site est à jour ?
  • Quid de si la connexion est perdue en plein milieu de l’échange ?

En Python, il est courant d'« essayer » des opérations puis de gérer les exceptions si elles surviennent.

On utilise pour cela des try: ... except: ....

Exemple

reponse = input("Entrez un entier svp !")

try:
    n = int(reponse)
except:
    raise Exception("Ce n'est pas un entier !")

Utilisation différente

reponse = input("Entrez un entier svp !")

try:
    n = int(reponse)
except:
    n = -1
while True:
    reponse = input("Entrez un entier svp !")

    try:
        n = int(reponse)
        break
    except:
        # Faire en sorte de boucler pour reposer la question à l'utilisateur ...
        print("Ce n'est pas un entier !")
        continue

Autre exemple (inhabituel):

On peut utiliser les exception comme une sorte de if ou inversement

def can_be_converted_to_int(stuff):
    try:
        int(stuff)
    except:
        return False

    return True

can_be_converted_to_int("3")    # -> True
can_be_converted_to_int("abcd") # -> False

The “python way”

« Better to ask forgiveness than permissions »

Traduction “on essaye et puis on voit et on gère les dégats”. (ça se discute)

Assertions

Il est possible d’utiliser des assertions pour expliciter certaines hypothèses faites pendant l’écriture du code. Si elles ne sont pas remplies, une exception est déclenchée.

Un peu comme un "if not condition raise error".

def max(liste_entiers):
    assert liste_entiers != [], "max() ne peut pas fonctionner sur une liste vide!"

(assert toto est équivalent à if not toto: raise Exception())

def distance(x=0, y=0):
    assert isinstance(x, (int, float)), "Cette fonction ne prends que des int ou float en argument !"
    assert isinstance(y, (int, float)), "Cette fonction ne prends que des int ou float en argument !"

    return racine_carree(x*x + y*y)
def some_function(n):
    assert n, "Cette fonction n'accepte pas 0 ou None comme argument !"
    assert n % 2 == 0, "Cette fonction ne prends que des entiers pairs en argument !"

    [...]

Assertions et tests unitaires

En pratique, l’une des utilisations les plus courantes de assert est l’écriture de tests unitaires qui permettent de valider qu’une fonction marche dans tous les cas (et continue à marcher si on la modifie)

Dans votre application:

def trier(liste_entiers):
    # on définie le comportement de la fonction

Dans les tests (fichier à part):

assert trier([15, 8, 4, 42, 23, 16]) == [4, 8, 15, 16, 23, 42]
assert trier([0, 82, 4, -21, 2]) == [-21, 0, 2, 4, 82]
assert trier([-7, -3, 0]) == [-7, -3, 0]
assert trier([]) == []

Cf. Chapitre 19

Calcul du max d’un liste d’entier : plusieurs approches !

Attention : dans les exemples suivant je dois penser au cas où resultat peut valoir None

Je soupçonne fortemment que ma_liste puisse ne pas être une liste ou puisse être vide

Soit je teste explicitement avant pour être sur (moins pythonique) !

if not isinstance(ma_liste, list) or ma_liste == []:
    resultat = None
else:
    resultat = max(ma_liste)

Ça devrait marcher, mais j’ai un doute …

Soit j’essaye et je gère les cas d’erreur (plus pythonique)!

try:
    resultat = max(ma_liste)
except ValueError as e:
    print("Warning : peut-etre que ma_liste n'etait pas une liste non-vide ?")
    resultat = None

Soit j’assert le test pour laisser la fonction appelante le soin de gérer l’entrée correctement

Normalement ma_liste est une liste non-vide, sinon il y a un très gros problème avant dans le programme…

assert isinstance(ma_liste, list) and ma_liste != []

resultat = max(ma_liste)

Dans ce cas la fonction

10. Fichiers

Lire “brutalement”

mon_fichier = open("/etc/passwd", "r")
contenu_du_fichier = mon_fichier.readlines()
mon_fichier.close()

for ligne in contenu_du_fichier:
    print(ligne)

Attention à bien distinguer:

  • le nom du fichier (passwd) et son chemin d’accès absolu (/etc/passwd)
  • le vrai fichier qui existe sur le disque
  • la variable / objet Python (dans l’exemple, nommée f) qui est une interface pour interagir avec ce fichier

Lire, avec une “gestion de contexte”

with open("/etc/passwd", "r") as mon_fichier:
    contenu_du_fichier = mon_fichier.readlines()

for ligne in contenu_du_fichier:
    print(ligne)

Explications

  • open("fichier", "r") ouvre un fichier en lecture
  • with ... as ... ouvre un contexte, à la fin duquel le fichier sera fermé automatiquement
  • f.readlines() permet d’obtenir une liste de toutes les lignes du fichier

Lire

  • f.readlines() renvoie une liste contenant les lignes une par une

  • f.readline() renvoie une ligne du fichier à chaque appel.

  • f.read() renvoie une (grande) chaĩne contenant toutes les lignes concaténées

  • Attention, si je modifie la variable contenu_du_fichier … je ne modifie pas vraiment le fichier sur le disque ! Pour cela, il faut explicitement demander à écrire dans le fichier.

Ecrire

En remplacant tout !

with open("/home/alex/test", "w") as f:
    f.write("Plop")

À la suite (« append »)

with open("/home/alex/test", "a") as f:
    f.write("Plop")

Fichiers et exceptions

try:
    with open("/some/file", "r") as f:
        lines = f.readlines()
except:
    raise Exception("Impossible d'ouvrir le fichier en lecture !")

Un autre exemple

try:
    with open("/etc/shadow", "r") as f:
        lines = f.readlines()
except PermissionError:
    raise Exception("Pas le droit d'ouvrir le fichier !")
except FileNotFoundError:
    raise Exception("Ce fichier n'existe pas !")

Note “technique” sur la lecture des fichiers

  • Il y a un “curseur de lecture”. On peut lire petit morceaux par petit morceaux … une fois arrivé au bout, il n’y a plus rien à lire, il faut replacer le curseur si on veut de nouveau lire.
f = open("/etc/passwd")
print(f.read())  # ---> Tout plein de choses
print(f.read())  # ---> Rien !
f.seek(0)        # On remet le curseur au début
print(f.read())  # ---> Tout plein de choses !

11. Librairies

L’une des puissances de python vient de l’écosystème de librairie disponibles.

Librairie / bibliothèque / module : un ensemble de fonctionnalité déjà pensés et éprouvées, prêtes à l’emploi.

Syntaxes d’import

import un_module          # -> Importer tout un module
un_module.une_fonction()  # -> Appeler la fonction une_function()
                          #    du module

Exemple

import math

math.sqrt(2)   # -> 1.4142135623730951

Importer juste des choses précises

from un_module import une_fonction, une_autre

une_fonction(...)

Exemple

from math import sqrt, sin, cos

sqrt(2)   # -> 1.4142135623730951

Exemple : json

Le JSON est un format de fichier qui permet de décrire des données numériques complexe et imbriquées pour le stocker ou le transférer. Il s’agit du format de données dominant aujourd’hui sur le web. Il est utilisé dans tous les langages et Python intègre à l’installation une librairie pour le manipuler.

A noter également qu’il est quasiment isomorphe à un dictionnaire Python.

{
    "mailman": {
        "branch": "master",
        "level": 2,
        "state": "working",
        "url": "https://github.com/yunohost-apps/mailman_ynh",
        "flags": [ "mailing-list", "lightweight" ]
    },
    "mastodon": {
        "branch": "master",
        "level": 3,
        "state": "inprogress",
        "url": "https://github.com/YunoHost-Apps/mastodon_ynh",
        "flags": [ "social network", "good-UX" ]
    }
}

La fonction principale de la librairie est loads() qui tranforme une chaîne de caractère au format JSON en dictionnaire.

import json

# Ouvrir, lire et interpreter un fichier json
with open("applications.json") as f:
    j = json.loads(f.read())


# Trouver l'état de l'application mailman
j["mailman"]["state"]     # -> "working"

Exemple : requests pour un besoin web simple (bas niveau)

Envoyer une requête HTTP et récuperer la réponse (et potentiellement le contenu d’une page).

import requests

r = requests.get("https://en.wikipedia.org/wiki/Python", timeout=30)

print(r.status_code)    # -> 200 si ça a marché
print(r.text)           # -> Le contenu de la page

Exemple : csv

import csv

# Ouvrir et lire les lignes d'un fichier csv
with open("table.csv") as f:
    table = csv.reader(f, delimiter='|')
    for row in table:
        print(row[1]) # Afficher le 2eme champ
        print(row[3]) # Afficher le 4eme champ

with open("newtable.csv", "w") as f:
    newtable = csv.write(f, delimiter=",")
    newtable.writerow(["Alice", 32, "Lyon"])
    newtable.writerow(["Bob", 29, "Bordeaux"])

Exemple : sys

permet d’interagir / de s’interfacer avec le systeme (librairie système commune à toutes les plateforme)

Par exemple:

import sys

sys.stdout   # La sortie standard du programme
sys.path     # Les chemins depuis lesquels sont chargés les imports
sys.argv     # Tableau des arguments passés en ligne de commande
sys.exit(1)  # Sortir du programme avec un code de retour de 1

Exemple : os

os permet d’interagir avec le système d’exploitation pour réaliser différent type d’action… Certaines étant spécifiques à l’OS en question (Linux, Windows, …)

Quelques exemples :

import os
os.listdir("/etc/")            # Liste les fichiers dans /etc/
os.path.join("/etc", "passwd") # Génère un chemin à partir de plusieurs parties
os.system("touch /etc/toto")   # (à éviter) Execute une commande "brute"

Voir aussi : copie ou suppression de fichiers, modification des permissions, …

Exemple : argparse

  • Du vrai parsing d’argument en ligne de commande
  • (Un peu long à initialiser mais puissant)

Exemple concurrent: docopt

Sert à la même chose que argparse mais beaucoup plus rapide à utiliser ! Docopt analyse la documentation du module pour deviner les arguments !

"""Naval Fate.

Usage:
  naval_fate.py ship new <name>...
  naval_fate.py ship <name> move <x> <y> [--speed=<kn>]
  naval_fate.py ship shoot <x> <y>
  naval_fate.py mine (set|remove) <x> <y> [--moored | --drifting]
  naval_fate.py (-h | --help)
  naval_fate.py --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=<kn>  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.

"""
from docopt import docopt


if __name__ == '__main__':
    arguments = docopt(__doc__)
    print(arguments)

Ensuite python naval_fate.py ship new monbateau --speed=15 renvoie un dictionnaire d’arguments du type:

{'--drifting': False,    'mine': False,
 '--help': False,        'move': True,
 '--moored': False,      'new': True,
 '--speed': '15',        'remove': False,
 '--version': False,     'set': False,
 '<name>': ['Guardian'], 'ship': True,
 '<x>': '100',           'shoot': False,
 '<y>': '150'}

On peut les utiliser pour paramétrer le programme CLI !

Exemple : subprocess

subprocess peut typiquement être utilisé pour lancer des commandes en parallèle du programme principal et récupérer leur résultat.

out = subprocess.check_output(["echo", "Hello World!"])
print(out)    # -> Affiche 'Hello World'
  • check_output : recupère la sortie d’une commande
  • check_call : verifie que la commande a bien marché (code de retour ‘0’) ou declenche une exception
  • Popen : méthode plus bas niveau

Cf. Partie sur l’execution concurrente en Python

Moar ?

  • Debian packages : python-*
  • Python package manager : pip

Exemples

  • JSON, XML, HTML, YAML, …
  • Regular expressions
  • Logging, Parsing d’options, …
  • Internationalisation
  • Templating
  • Plots, LDAP, …

Gestionnaire de paquet pip

  • Gestionnaire de paquet / modules Python
  • PIP : “Pip Install Packages”
  • PyPI : Python Package Index : visitez https://pypi.org

(à ne pas confondre avec Pypy un interpreter python écrit en Python)

  • Installer un paquet :
    • pip3 install <paquet>
  • Rechercher un paquet :
    • pip3 search <motclef>
  • Installer une liste de dépendances :
    • pip3 install -r requirements.txt
  • Lister les paquets installés
    • pip3 list, pip3 freeze
  • Les paquets installés sont dans /usr/lib/python*/dist-packages/

Virtualenv

  • Environnement virtuel
  • Isoler des paquets / dépendances pour utiliser des versions spécifiques
# La premiere fois :
sudo apt install python3-virtualenv virtualenv

# Creation d'un virtualenv 'venv'
virtualenv -p python3 venv
source venv/bin/activate

# Installation de dependances
pip3 install <une dependance...>
pip3 install <une autre dependance...>


# On développe, on teste, etc....


# Si on a fini et/ou que l'on veut "sortir" du virtualenv
deactivate

Documentation pour toutes les plateformes : https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/

Outils plus récents Pipenv et Conda

Pip et les virtualenv sont les outils classique pour gérer les dépendances en Python mais il existe également de nouvelles solutions moins classique

  • Pipenv un outil rassemblant pip et virtualenv pour simplifier le processus de travail.
  • Conda un gestionnaire de dépendances multiplateforme.

Installer Pip et Virtualenv sur Windows

12. Principes de développement - Partie 2

Documentation

Pour les librairies (et Python en général) :

  • docs.python.org
  • devdocs.io
  • stack overflow …
  • doc strings !!

Pour votre code :

  • nom de variables, fonctions, argument !!!
  • commentaires, doc strings
  • gestionnaire de version
  • generation de doc automatique ?

Faire du “bon code”

La lisibilité est la priorité numéro 1

Un programme est vivant et évolue. Mieux vaut un programme cassé mais lisible (donc débuggable) qu’un programme qui marche mais incompréhensible (donc fragile et/ou qu’on ne saura pas faire évoluer)

(c.f. Guido van Rossum chez Dropbox)

Autrement dit : la lisibilité pour vous et vos collègues a énormément d’importance pour la maintenabilité et l’évolution du projet

  • Keep It Simple
  • Sémantique : utiliser des noms de variables et de fonctions concis et pertinents
  • Commentaires : lorsque c’est nécessaire, pour démystifier ce qu’il se passe
  • Modularité : découper son programme en fonctions qui chacune résolvent un sous-problème
  • Couplage faible : garder ses fonctions autant que possibles indépendantes, limiter les effets de bords
  • Prendre le temps de refactoriser quand nécessaire
    • si je répète plusieurs fois les mémes opérations, peut-être définir une nouvelle fonction
    • si le contenu d’une variable ou d’une fonction change, peut-être changer son nom
  • Ne pas abuser des principes précédents
    • trop d’abstractions tue l’abstraction
    • tout ça viens avec le temps et l’expérience

How to write good code

Conventions de nommages des variables, fonctions et classes

Variables et fonctions en snake case : nom_de_ma_variable

Constantes globales en macro case: NOM_DE_MA_CONSTANTE

Nom de classes en upper camel case : NomDeMaClasse

Syntaxe, PEP8, linters

  • Le style d’écriture de python est standardisé via la norme PEP8
  • Il existe des “linter” pour détecter le non-respect des conventions (et également certaines erreurs logiques)
    • Par exemple flake8, pylint
  • Intégration possible dans vim et autres IDE…
  • autopep8 ou black permettent de corriger un bon nombre de problème automatiquement

Bonus. Manipuler du XML en Python

XML : eXtensible Markup Language

  • Format très général pour structurer des informations dans un fichier texte
  • Défini et géré par le W3C (Consortium de standardisation et developpement du Web)
  • (X)HTML est un cas particulier de XML
  • ~historique ?… à tendance à être remplacé par JSON, YAML, bases SQL / noSQL, …

Quelques exemple courants:

  • un XML assez standard:
<?xml version="1.0" encoding="UTF-8"?>
<data>
    <apps>
        <app name="mailman" state="working" level="5" />
        <app name="wekan" state="inprogress" level="3" />
        <app name="nextcloud" state="working" level="7" />
        <app name="wordpress" state="working" level="7" />
        <app name="plex" state="notworking" />
    </apps>
</data>
  • du html:
<html>
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="style.css">
        <script src="lib.js"></script>
    </head>
    <body>
        <p class="text-bold">Un morceau de texte</p>
        <p class="text-emph">Un autre paragraphe</p>
    </body>
</html>
  • Un documents LibreOffice
<?xml version="1.0" encoding="UTF-8"?>
<office:document-content office:version="1.2">
    [...]
    <office:body>
        <office:text>
            <text:p text:style-name="P1">
            Hello <text:span text:style-name="T1">world!</text:span>
            </text:p>
        </office:text>
    </office:body>
</office:document-content>

Un peu de vocabulaire

<html>
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="style.css">
        <script src="lib.js"></script>
    </head>
    <body>
        <p class="text-bold">Un morceau de texte</p>
        <p class="text-emph">Un autre paragraphe</p>
    </body>
</html>
  • Balises : ouvrantes et fermantes, e.g. <p class="red"> et </p>
  • Attributs : par exemple class="red"
  • Noeuds éléments : caractérisés et délimités par des balises : head, body, script, p, …
    • peut contenir d’autres noeuds (éléments, texte, …) et donc créer un arbre
  • Noeuds texte : e.g. "Un morceau de texte"

XML : Approche DOM v.s. SAX

DOM : Document Object Model

  • Lecture et chargement initial de tout le document (peut être lourd pour les gros documents !)
  • Puis accès à tous les noeuds de l’arbre (~AST)
  • Approche classique et répandue (c.f. Javascript)

SAX : Simple API for XML

  • Lecture et analyse “au fur et à mesure”
  • Pas besoin de tout charger en mémoire
  • Adaptée aux gros documents

ElementTree

  • Best of both world ? (Mais moins de fonctionnalités avancées)
  • Simple à utiliser comme DOM, peut être aussi rapide que SAX

Quelques exemples de librairies

  • xml.tree.ElementTree : ElementTree API, inclue de base dans Python
  • lxml : Très complète, support de nombreux standard
  • BeautifulSoup : Interface simple, conçu pour parser du HTML contenant des erreurs
  • (et pleins d’autres …)
<html>
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="style.css">
        <script src="lib.js"></script>
    </head>
    <body>
        <p class="text-bold">Un morceau de texte</p>
        <p class="text-emph">Un autre paragraphe</p>
    </body>
</html>

xml.tree.ElementTree

Parser / lire

from xml.etree import ElementTree as ET

root = ET.parse("monfichier.html")
body = root.find("body")

print(body[0])        # --> <Element 'p' at 0x12345>
print(body[0].tag)    # --> p
print(body[0].attrib) # --> {'class': 'text-bold'}
print(body[0].text)   # --> Un morceau de texte
print(list(body[0]))  # --> []  (pas d'elements fils)

# Trouver tout les <p> dans le body
tous_les_p = body.findall("p")

Construire / ecrire

from xml.etree import ElementTree as ET

root = ET.parse("monfichier.html")
body = root.find("body")

# Ajout d'un nouvel element dans <body>
# <p class="text-underline" id="new">Du texte en plus</p>

nouveau_p = ET.SubElement(body, "p", clas="text-underline", id="new")

root.write("monfichier_2.xml")

Parsing itératif avec lxml.etree.iterparse

  • ET.parse("fichier.xml") explose la RAM pour les gros fichiers.
  • Besoin d’une technique plus efficace
  • iterparse fourni un iterateur pour parser au fur et à mesure, qui plus est seulement sur des tags specifiques
from lxml import etree

iterator = etree.iterparse("fichier.xml", tag="p")

for event, element in iterator:
    # [...] traiter l'element

Ça consomme toujours de la RAM … besoin d’un trick en + … c.f. https://stackoverflow.com/questions/12160418

from lxml import etree

def clear_elem_and_ancestors(elem):
    elem.clear()
    for ancestor in elem.xpath('ancestor-or-self::*'):
        while ancestor.getprevious() is not None:
            del ancestor.getparent()[0]

iterator = etree.iterparse("fichier.xml", tag="p")

for event, element in iterator:
    # [...] traiter l'element
    clear_elem_and_ancestors(element)

Exercices Partie 2

Correction - Exercice 2.1

2.1 - Fichiers, JSON et dictionnaires

  • Écrire une fonction qui prends un nom de fichier en argument et retourne le contenu si elle a été capable de le récupérer. Sinon, elle doit déclencher une exception qui explique en français pourquoi elle n’a pas pu.
correction
  • Écrire une fonction qui remplace un mot par un autre dans un fichier. On pourra pour cela se servir de une_chaine.replace("mot", "nouveau_mot") qui renvoie une version modifiée de une_chaine en ayant remplacé “mot” par “nouveau mot”.
correction
  • Télécharger le fichier https://app.yunohost.org/community.json (avec votre navigateur ou wget par exemple). Écrire une fonction qui lit ce fichier, le charge en tant que données json et renvoie un dictionnaire Python. Écrire une autre fonction capable de filtrer le dictionnaire pour ne garder que les apps d’un level supérieur ou égal à un level n donné en argument. Essayez votre fonction avec le niveau 8.
correction
  • Améliorez le programme précédent pour récupérer la liste directement depuis le programme avec requests. Gérer les différentes exceptions qui pourraient se produire (afficher un message en français) : syntaxe json incorrecte, erreur 404, time-out du serveur, erreur SSL
correction

Correction exercice 2.2 - Utilisation de la librairie XML intégrée `ElementTree`

  • En utilisant le module ElementTree de Python, charger le fichier country.xml fourni par le formateur. Boucler sur les différents éléments country et afficher pour chaque élément la valeur du gdppc et le nom des voisins.

  • Ajouter un element country pour la France et l’Espagne en suivant la même structure.

  • Sauvegarder la version modifiée en country_extended.xml

correction

Exercice 2.1 - Fichiers, JSON et dictionnaires

2.1 - Fichiers, JSON et dictionnaires

  • Écrire une fonction qui prends un nom de fichier en argument et retourne le contenu si elle a été capable de le récupérer. Sinon, elle doit déclencher une exception qui explique en français pourquoi elle n’a pas pu.

  • Écrire une fonction qui remplace un mot par un autre dans un fichier. On pourra pour cela se servir de une_chaine.replace("mot", "nouveau_mot") qui renvoie une version modifiée de une_chaine en ayant remplacé “mot” par “nouveau mot”.

  • Télécharger le fichier https://app.yunohost.org/community.json (avec votre navigateur ou wget par exemple). Écrire une fonction qui lit ce fichier, le charge en tant que données json et renvoie un dictionnaire Python. Écrire une autre fonction capable de filtrer le dictionnaire pour ne garder que les apps d’un level supérieur ou égal à un level n donné en argument. Essayez votre fonction avec le niveau 8.

  • Améliorez le programme précédent pour récupérer la liste directement depuis le programme avec requests. Gérer les différentes exceptions qui pourraient se produire (afficher un message en français) : syntaxe json incorrecte, erreur 404, time-out du serveur, erreur SSL

Exercice 2.2 - Utilisation de la librairie XML intégrée ElementTree

  • En utilisant le module ElementTree de Python, charger le fichier countries.xml fourni par le formateur. Boucler sur les différents éléments country et afficher pour chaque élément la valeur du gdppc (PIB par habitant) et le nom des voisins.

  • Ajouter un element country pour la France en suivant la même structure.

  • Sauvegarder la version modifiée en countries_extended.xml

Correction exercice 2.3 - Lecture itérative avec la library externe lxml

  • Installez lxml grâce à pip3, et récupérez le “gros” fichier XML, copyright.xml à l’adresse https://dl.google.com/rights/books/renewals/google-renewals-20080516.zip. Attention à ne pas tenter d’ouvrir “brutalement” ce fichier avec un éditeur ou avec la méthode utilisée en 1 : cela consommera beaucoup trop de RAM !

  • En utilisant des commandes comme head -n 50 copyright.xml, analyser visuellement la structure du fichier d’après ses premières lignes.

  • Initialiser un itérateur destiné à itérer sur ce fichier, et en particulier sur les tags Title. Créer une boucle à partir de cet itérateur et afficher tous les titres qui contiennent la chaîne "Pyth". On prendra soin de nettoyer les éléments trouvés avant de passer à chaque nouvelle itération sous peine de remplir la RAM très vite !

  • Pour chaque titre trouvé, remonter au parent ‘Record’ pour trouver le ‘Holder Name’ correspondant à ce titre. S’aider du debug VSCode, ipython et/ou ipdb pour tester et expérimenter en interactif.

correction

Exercice 2.3 - Lecture itérative avec la library externe lxml

  • Installez lxml grâce à pip3, et récupérez le “gros” fichier XML, copyright.xml à l’adresse https://dl.google.com/rights/books/renewals/google-renewals-20080516.zip. Attention à ne pas tenter d’ouvrir “brutalement” ce fichier avec un éditeur ou avec la méthode utilisée en 1 : cela consommera beaucoup trop de RAM !

  • En utilisant des commandes comme head -n 50 copyright.xml, analyser visuellement la structure du fichier d’après ses premières lignes.

  • Initialiser un itérateur destiné à itérer sur ce fichier, et en particulier sur les tags Title. Créer une boucle à partir de cet itérateur et afficher tous les titres qui contiennent la chaîne "Pyth". On prendra soin de nettoyer les éléments trouvés avant de passer à chaque nouvelle itération sous peine de remplir la RAM très vite !

  • Pour chaque titre trouvé, remonter au parent ‘Record’ pour trouver le ‘Holder Name’ correspondant à ce titre. S’aider du debug VSCode, ipython et/ou ipdb pour tester et expérimenter en interactif.

Partie 3 - POO

Cours 3

13. POO - Classes, attributs et méthodes

L’orienté objet est un paradigme de programmation inventé dans les années 80 et popularisé dans les années 90. Aujourd’hui il est incontournable bien qu’il commence aussi à être critiqué.

Il permet d’organiser un programme de façon standard et ainsi d’éviter des erreurs d’architectures comme le spaghetti code

Principe de base

Regrouper les variables et fonctions en entités (“objets”) cohérentes qui appartiennent à des classes

  • attributs (les variables décrivant l’état de l’objet)
  • méthodes (les fonctions appliqubles à l’objet)

De cette façon on fabrique des sorte types de données spécifique à notre programme utilisables de façon pratique et consistante.

Exemple

Les cercles (classe)

ont un centre, un rayon, une couleur, une épaisseur de trait : ce sont des attributs.

On peut : déplacer le cercle, l’agrandir, calculer son aire, le dessiner sur l’écran : ce sont des méthodes.

Un petit cercle rouge (objet, ou instance)

centre = (3, 5), rayon = 2, couleur = “red”, épaisseur = 0.1

Un grand cercle bleu (autre objet, instance)

centre = (-4, 2), rayon = 6, couleur = “blue”, épaisseur = 1

Exemple en Python

class Cercle:

   def __init__(self, centre, rayon, couleur="black", epaisseur=0.1):
       self.centre = centre
       self.rayon = rayon
       self.couleur = couleur
       self.epaisseur = epaisseur

   def deplacer(self, dx=0, dy=0):
       self.centre = (self.centre[0]+dx, self.centre[1]+dy)


cercle1 = Cercle((3, 5), 2, "red")
cercle2 = Cercle((-4, 2), 6, "blue", epaisseur=1)

cercle1.deplacer(dy=2)
print(cercle1.centre)
  • __init__ est le constructeur C’est la fonction qui est appelée à la création de l’objet.

  • On instancie un objet en faissant mon_objet = Classe(...) ce qui appelle __init__

  • self correspond à l’objet en train d’être manipulé. Il soit être passé en paramètre de toutes les fonctions de la classe (les méthodes)

Les attributs sont les variables internes qui décrivent l’état et régisse le fonctionnement de l’objet.

  • self.centre, self.rayon, self.couleur, self.epaisseur sont ici les attributs. Si on lit littéralement la syntaxe python on comprend self.centre comme “le centre de l’objet en cours(le cercle en cours)”

Toutes les fonctions incluses dans la classe sont appelées des méthodes.

  • __init__ et deplacer sont des méthodes. Elles agissent généralement sur les attributs de l’objet mais pas nécessairement.

Les attributs et méthodes de la classe sont “dans” chaque instance d’objet (ici chaque cercle). On dit que la classe est un namespace (ou espace de nom). Chaque variable centre est isolée dans son cercle et on peut donc réutiliser plusieurs fois le nom centre pour chaque cercle. Par contre pour y accéder on doit préciser le cercle concerné avec la syntaxe cercle1.centre.

  • De même on utilise les methodes en faisant un_objet.la_methode(...)

Attention à l’indentation !!

Spaghetti code, variables globales et refactoring

Lorsqu’on enchaine simplement des instructions sans trop de structure dans un programme on arrive vite à quelque chose d’assez imprévisible et ingérable.

On commence généralement à définir des variables globales accessibles partout pour maintenir l’état de notre programme. Plusieurs fonctions viennent modifier de façon concurrente ces variables globales (pensez au score dans un jeu par exemple) pouvant mener à des bugs complexes.

On arrive aussi à beaucoup de code dupliqué et il devient très difficile dans ce contexte de refactorer un programme:

  • dès qu’on tire un spaghetti tout casse
  • dès qu’on veut changer un endroit il faut modifier beaucoup de choses
  • la compréhension du programme devient difficile pour le développeur initial et encore plus pour ses collègues.

On peut voir la programmation orientée objet comme une façon d’éviter le code spaghetti.

Intérets de la POO

La POO est critique pour garder un code structuré et compréhensible quand la complexité d’un projet augmente.

  • Rassembler ce qui va ensemble pour s’y retrouver
  • Maintenir les variables isolées à l’intérieur d’un “scope” pour évitées qu’elles ne soient modifiée n’importe quand et n’importe comment et qu’il y ai des conflits de nom.
  • Fournir une façon d’architecturer un programme que tout le monde connait à peu près
  • Fournir un moyen efficace de programmer en évitant la répétition et favorisant la réutilisation
  • Créer des “boîtes noires” utilisables sans connaître leur fonctionnement interne (bien et pas bien à la fois). C’est à dire une façon de se répartir le travail entre développeurs (chacun sa boîte qu’on maîtrise).

DRY don’t repeat yourself et couplage

La POO permet d’appliquer le principe DRY -> identifier ce qui se ressemble et le rassembler dans une méthode ou une classe.

Cela permet ensuite de modifier le code à un seul endroit pour tout changer -> puissant.

Il s’agit plus d’un ideal que d’un principe. Il ne faut pas l’appliquer à outrance parfois un peu de répétition est mieux car plus simple.

Si on factorise tout en POO on arrive souvent à un code fortement coupler qui empêche le refactoring et le programme finit par devenir fragile.

À retenir

  • __init__ est le constructeur
  • __init__ et deplacer sont des méthodes
  • self correspond à l’objet en train d’être manipulé
  • Toutes les méthodes ont au moins self comme premier argument
  • On utilise les methodes en faisant un_objet.la_methode(...)
  • self.centre, self.rayon, self.couleur, self.epaisseur sont des attributs
  • On instancie un objet en faissant mon_objet = Classe(...)

14. Héritage et polymorphisme

Héritage

Une classe peut hériter d’une autre pour étendre ses fonctionnalités. Inversement, cela permet de factoriser plusieurs classes ayant des fonctionnalités communes.

Par exemple, les cercles, les carrés et les étoiles sont trois types de figures géométriques.

En tant que figure géométriques, elles ont toutes un centre, une couleur et une épaisseur utilisés pour le dessin. On peut les déplacer, et on peut calculer leur aire.

  • L’héritage permet d’ordonner des objets proches en les apparentant pour s’y retrouver.
  • Il permet également de factoriser du code en repérant des comportements génériques utilisés dans plusieurs contextes et en les mettant dans un parent commun.
class FigureGeometrique:

    def __init__(self, centre, couleur="black", epaisseur=0.1):
        raise NotImplementedError("La classe fille doit implémenter cette fonction!")
        self.centre = centre
        self.couleur = couleur
        self.epaisseur = epaisseur

    def deplacer(self, dx=0, dy=0):
        self.centre = (self.centre[0]+dx, self.centre[1]+dy)

    def aire(self):
        raise NotImplementedError("La classe fille doit implémenter cette fonction!")

class Cercle(FigureGeometrique):

    def __init__(self, centre, rayon, couleur="black", epaisseur=0.1):
        self.rayon = rayon
        super().__init__(centre, couleur, epaisseur)

    def aire(self):
        return 3.1415 * self.rayon * self.rayon

class Carre(FigureGeometrique):

    def __init__(self, centre, cote, couleur="black", epaisseur=0.1):
        self.cote = cote
        super().__init__(centre, couleur, epaisseur)

    def aire(self):
        return self.cote ** 2


cercle_rouge = Cercle((3, 5), 2, "red")
carre_vert  = Carre((5, -1), 3, "green", epaisseur=0.2)

cercle_rouge.deplacer(dy=2)
carre_vert.deplacer(dx=-3)

print(carre_vert.centre) # -> affiche (2, -1)
print(carre_vert.aire()) # -> affiche 9
  • Les cercles et les carrés “descendent” ou “héritent” de la classe FigureGeometrique avec la syntaxe class Carre(FigureGeometrique).

  • La méthode deplacer de la classe mère est disponible automatiquement dans les classes filles

Ainsi pour factoriser du code on peut repèrer un comportement commun à plusieurs éléments de notre programme et on créé une classe mère avec une méthode exprimant ce comportement de façon générique. Tous les classes fille pourront utiliser ce comportement. Si on le change plus tard il sera changé dans tout le programme (puissant pour refactoriser le code)

Cependant il est rare qu’un comportement soit exactement identique entre deux classes. On veut souvent changer légèrement ce comportement selon la classe utilisée. Pour cela on utilise le polymorphisme.

Polymorphisme

Surcharge de fonction

Dans le cas de l’aire de nos figures, chaque figure doit pouvoir calculer son aire mais le calcul est différent pour chaque type de figure concrête.

  • On définit une méthode abstraite aire dans la classe mère pour indiquer que chaque figure a une méthode aire. Comme une figure en général n’a pas de calcul d’aire la méthode abstraite déclenche une exception (Utilisez ici NotImplementedError() qui est faite pour ça)

  • On redéfinit la méthode aire dans chaque classe fille. La méthode aire fille écrase ou surcharge celle de la classe mère et sera appelée à la place de celle-ci dès qu’on veut l’aire d’une figure géométrique.

Découper le travail en méthode mère et fille avec super().methode()

Souvent on veut quand même utiliser la méthode de la classe même pour faire une partie du travail (commun à toute les classes filles) et ensuite spécialiser le travail en ajoutant des actions suplémentaires dans la méthode fille qui surcharge la méthode mère. A cause de la surcharge la méthode mère n’est pas du tout appelée automatiquement donc il faut le faire “manuellement”.

Exemple ci-dessus: pour créer un carré:

  • on appelle d’abord le constructeur de la classe mère qui initialise centre, couleur et epaisseur avec super().__init__()
  • Puis on initialise cote qui est un attribut du carré (mais pas du cercle donc pas dans le constructeur général)

De façon générale super() renvoie une instance de la classe mère.

Classe Abstraite

Une classe abstraite est une classe dont on ne peut pas créer d’instance. Elle est simplement là pour définir un modèle minimal que toutes les classes fille doivent suivre (et étendre).

En Python on créé généralement une classe abstraite en levant l’exception NotImplementedError dans le constructeur __init__.

Travailler avec la classe mère

On parle de polymorphisme quand on utilise la classe abstraite pour gérer uniformément plusieurs type d’objets de classe différente et qu’on laisse le langage choisir le comportement en fonction du contexte.

Par exemple on peut faire une liste de FigureGeometrique de différents types et afficher les aire de chacune. Python devinera automatiquement quelle méthode appeler :

formes = [Cercle((3, 5), 2, "red"),
          Carre((5, -1), 3, "green"),
          Cercle((-2, 4), 5, "yellow"),
          Carre((4, -2), 2, "purple")]

for forme in formes:
    print(forme.aire())

(c.f. aussi autre exemple sur stack overflow)

Le polymorphisme est puissant car il permet d’économiser beaucoup de if et autre branchements:

On aurait pu écrire l’exemple précédent avec des if isinstance(figure, Cercle): par exemple mais cela aurait été beaucoup moins élégant.

À retenir

  • class Cercle(FigureGeometrique) fais hériter Cercle de FigureGeometrique
  • super().__init__(...) permet d’appeler le constructeur de la classe mère
  • Les classes filles disposent des méthodes de la classe mère mais peuvent les surcharger (c.f. exemple avec aire)
  • super().une_methode(...) permet d’appeler une_methode telle que définie dans la classe mère.
  • isinstance verifie l’heritage ! isinstance(cercle_rouge, FigureGeometrique) vaut True !

Tester la classe pour s’adapter

Souvent pour adapter le comportement d’un programme on veut savoir de quel type est un objet:

  • isinstance verifie l’heritage ! isinstance(cercle_rouge, FigureGeometrique) vaut True !

15. Encapsulation et attributs statiques

D’abord quelques astuces

  • dir(un_objet) : listes tous les attributs / methodes d’un objet (ou module)
  • Il existe aussi un_objet.__dict__
  • MaClasse.__subclasses__() : lister toutes les classes filles d’une classe

Attributs ‘statiques’ (partagés par tous les objets d’une classe)

Les attributs qui sont définits dans le corps de la classe et non dans le constructeurs sont statiques en python. C’est à dire que leur valeur est commune à toutes les instances de la classe en cours d’utilisation dans le programme. Cela peut être très pratique pour maintenir une vision globale de l’état du programme de façon sécurisée.

class FormeGeometrique():

    nb_instances = 0

    def __init__(self):
        FormeGeometrique.nb_instances += 1

forme1 = FormeGeometrique()
forme2 = FormeGeometrique()
forme3 = FormeGeometrique()

print(FormeGeometrique.nb_instances)
# -> affiche 3

Méthodes statiques et méthodes de classe

… Sont deux types de méthodes rattachées à une classe mais non à une instance de la classe (un objet). On les fabrique en ajoutant les décorateurs @staticmethod ou @classmethod sur une méthode de la classe.

La méthode statique est complètement indépendante de la classe même si rangée à l’intérieur alors que la méthode de classe récupère implicitement sa classe comme premier argument ce qui permet de construire des objet de la classe dans le corps de la méthode

Exemple d’utilisation d’un méthode de classe

class MaCollectionDeLettre: # Réimplétation de String

    def __init__(self, astring): # Build an object from a string
      self._string = astring

    @classmethod
    def build_from_list(cls, alist): # Alternative constructor to build from a list of lettres
      x = cls('') # L'argument implicite cls permet de construire un objet de la classe
      x._string = ','.join(str(s) for s in alist)
      return x

L’encapsulation

Nous avons évoqué dans le cours 13 qu’un des intérêts de la POO est de sécuriser les variables dans un contexte isolé pour éviter qu’elles soient accédées à tort et à travers par différents programmeurs ce qui a tendance à créer des bugs mythiques.

Pour éviter cela on essaye au maximum d’encapsuler les attributs et les méthodes internes qui servent à faire fonctionner une classe pour éviter que les utilisateurs de la classe (ignorants son fonctionnement) puissent pas les appeler directement et “casser” le fonctionnement de la classe.

On parle d’attributs et méthodes privés quand ils sont internes et inaccessibles.

En Python les attributs et méthodes d’un objet sont “publiques” par défaut : on peut y accéder quand on veut et donc il faut donc une façon de pouvoir interdire leur usage:

  • On utilise un underscore _ devant le nom de l’attribut ou méthode pout indiquer qu’il est privé et ne doit pas être utilisé.

Exemple: self._valeurinterne = 50 ou def _mamethodeprivee(self, arg): ...

En réalité l’attribut/méthode est toujours accessible, il s’agit d’une convention mais il faut la respecter !! Par défaut les editeurs de code vous masqueront les elements privés lors de l’autocomplétion par exemple.

Accesseurs (getters) et mutateurs (setters)

Même lorsque qu’un attribut d’objet devrait être accessible à l’utilisateur (par exemple le rayon d’un cercle), on voudrait pouvoir contrôler l’accès à cet attribut pour que tout ce passe bien.

Par exemple éviter que l’utilisateur puisse définir un rayon négatif !!

Pour cela on créé des attributs privés et on définit des méthodes “publique”

On veut donc généralement pouvoir y donner accès à l’utilisateur de la classe selon certaines conditions.

Pour cela un définit une méthode d’accès (getter/accesseur) qui décrit comment récupérer la valeur ou une méthode de modification (setter/mutateur) qui contrôle comment on peut modifier la valeur (et qui vous envoie balader si vous définissez un rayon négatif).

Exemple (non pythonique !)

class Cercle:

   def __init__(self, centre, rayon, couleur="black", epaisseur=0.1):
       self.centre = centre
       self._rayon = rayon
       self._couleur = couleur

    def get_couleur(self):
        print("on accède à la couleur")
        return self._couleur

    def set_rayon(self, rayon)
        assert rayon > 0, "Le rayon doit être supérieur à 0 !"
        self._rayon = rayon



cercle1 = Cercle((3, 5), 2, "red")
cercle1.get_couleur()
cercle1.set_rayon(1)
cercle1.set_rayon(-1) # Erreur

Cependant en Python on ne fait généralement pas directement comme dans cet exemple !

Des attributs “dynamiques” avec @property

Le décorateur @property ajouté à une méthode permet de l’appeler comme un attribut (sans parenthèses)

class Carre(FigureGeometrique):

    # [ ... ]

    @property
    def aire(self):
        return self.cote * self.cote


carre_vert  = Carre((5, -1), 3, "green", epaisseur=0.2)
print(carre_vert.aire) # N.B. : plus besoin de mettre de parenthèse ! Se comporte comme un attribut

Autre exemple avec @property

class Facture():

    def __init__(self, total):
        self.montant_total = total
        self.montant_deja_paye = 0

    @property
    def montant_restant_a_payer(self):
        return montant_total - montant_deja_paye


ma_facture = Facture(45)
ma_facture.montant_deja_paye += 7

print("Il reste %s à payer" % ma_facture.montant_restant_a_payer)
# -> Il reste 38 à payer

La façon pythonique de faire des getters et setters en python est donc la suivante:

        @property
        def toto(self):
            return self.__toto

        @toto.setter
        def toto(self, value):
            self.__toto = value   # ... ou tout autre traitement

On peut ensuite accéder et modifier l’attribut toto de manière transparente :

monobjet = Objet()

monobjet.toto = "nouvelle_valeur"
print(monobjet.toto)

16. Stockage de données et ORM

Enregistrer des objets avec pickle

pickle permet de “sérialiser” et “déserialiser” des objets (ou de manière générale des structure de données) en un flux binaire (!= texte).

Sauvegarde

import pickle

ma_facture = Facture(45)

f = open("save.bin", "wb")   # the 'b' in 'wb' is important !
pickle.dump(ma_facture, f)

Puis recuperation

import pickle

f = open("save.bin", "rb")
ma_facture = pickle.load(f)

Un exemple courant de POO : les ORM (Object Relationnal Mapper)

Rappels (?) sur SQL

  • Base de données : stocker des informations en masse et de manière efficace
  • On manipule des tables (des lignes, des colonnes) …
  • Les colonnes sont fortement typées et on peut poser des contraintes (unicité, …)
  • Relations entres les tables, écritures concurrentes, …
  • Exemple de requête :
# Create a table
CREATE TABLE members (username text, email text, memberSince date, balance real)

# Add a record
INSERT INTO members VALUES ('alice', 'alice@gmail.com', '2017-11-05', 35.14)

# Find records
SELECT * FROM members WHERE balance>0;

Orienté objet : ORM

SQL “brut” en Python

import sqlite3
conn = sqlite3.connect('example.db')

c = conn.cursor()

# Create a table
c.execute('''CREATE TABLE members
             (username text, email text, memberSince date, balance real)''')

# Add a record
c.execute("INSERT INTO members VALUES ('alice', 'alice@gmail.com', '2017-11-05', 35.14)")

# Save (commit) the changes and close the connection
conn.commit()
conn.close()

Définition - Object Relational Mapping

  • Sauvegarder et charger des objets dans une base de donnée de type SQL de manière “transparente”
  • Simplifie énormément l’interface entre Python et SQL
    • Python <-> base SQL
    • classes (ou modèle) <-> tables
    • objets <-> lignes
    • attributs <-> colonnes
  • Gère aussi la construction et execution des requêtes (query)
  • Syntaxe spéciale pour définir les types et les contraintes (en fonction de la lib utilisée)
  • Librairie populaire et efficace : SQLAlchemy (on utilisera la surcouche ActiveAlchemy)

Exemple de classe / modèle

from active_alchemy import ActiveAlchemy

db = ActiveAlchemy('sqlite:///members.db')

class Member(db.Model):
	username    = db.Column(db.String(25), nullable=False, unique=True)
	email       = db.Column(db.String(50), nullable=True)
	memberSince = db.Column(db.Date,       nullable=False)
    balance     = db.Column(db.Float,      nullable=False, default=0.0)
    active      = db.Column(db.Boolean,    nullable=False, default=True)

Créer des tables et des objets

# Supprimer toutes les tables (attention ! dans la vraie vie on fait des migrations...)
db.drop_all()
# Initialiser toutes les tables dont il y a besoin
db.create_all()

# Créer des utilisateurs
alice   = Member(name="Alice",   memberSince=datetime.date(day=5, month=11, year=2017))
bob     = Member(name="Bob",     memberSince=datetime.date.today(), balance=15)
camille = Member(name="Camille", memberSince=datetime.date(day=7, month=10, year=2018), balance=10)

# Dire qu'on veut les enregistrer
db.session.add(alice)
db.session.add(bob)
db.session.add(camille)

# Commiter les changements
db.session.commit()

Exemple de requete (query)

all_members = Member.query().all()

active_members = Member.query()
                .filter(Member.active == True)
                .order_by(Member.memberSince)

for member in active_members:
    print(user.name)

Exercices Partie 3

Correction 3.1 - Cercles et Cylindres

Dans cet exercice nous allons représenter des objets et calculs géométriques simples en coordonnées entières. Utilisez des annotations de types : int, -> None, -> int et : Tuples[int ...] dès que possible. Testez régulièrement la consistance de ces types avec mypy fichier.py.

  • Implémenter une classe Cercle avec comme attributs un rayon rayon et les coordonnées x et y de son centre. Par exemple on pourra instancier un cercle avec mon_cercle = Cercle(5, (3,1))

  • Dans la classe Cercle, implémenter une propriété aire dépendante du rayon qu’on peut appeler avec mon_cercle.aire.

  • Implémenter une classe Cylindre, fille de Cercle, qui est caractérisée par un rayon rayon, une hauteur hauteur et des coordonnées x, y et z. On écrira le constructeur de Cylindre en appelant le constructeur de Cercle.

  • Dans la classe Cercle, implémenter une méthode intersect qui retourne True ou False suivant si deux cercles se touchent. Exemple d’utilisation : c1.intersect(c2)

  • Surcharger la méthode intersect pour la classe Cylindre, en se basant sur le résultat de la méthode de la classe mère.

Correction

Correction 3.1

Correction exercice 3.2 - Jeu de carte

Une classe Carte pour représenter les éléments d’un jeu

  • Dans un fichier carte.py, créer une classe Carte. Une carte dispose d’une valeur (1 à 10 puis VALET, DAME et ROI) et d’une couleur (COEUR, PIQUE, CARREAU, TREFLE). Par exemple, on pourra créer des cartes en invoquant Carte(3, 'COEUR') et Carte('ROI', 'PIQUE').

  • Implémenter la méthode points pour la classe Carte, qui retourne un nombre entre 1 et 13 en fonction de la valeur de la carte. Valider ce comportement depuis un fichier main.py qui importe la classe Carte.

  • Implémenter la méthode __repr__ pour la classe Carte, de sorte à ce que print(Carte(3, "COEUR")) affiche <Carte 3 de COEUR>.

c = Carte("DAME", "PIQUE")

print(c.couleur)
# Affiche PIQUE

print(c.points)
# Affiche 12

print(c)
# Affiche <Carte DAME de PIQUE>
Correction 3.2 `carte.py`

Encapsulation et validation des valeurs de carte possibles

Pour sécuriser l’usage ultérieur de notre jeu de carte on aimerait que les cartes ne puissent être crées et modifiées qu’avec des valeurs correctes (les 4 couleurs et 13 valeurs précisées)

  • Modifiez le constructeur pour valider que les données fournies sont valides. Sinon levez une exception (on utilise conventionnellement le type d’exception ValueError pour cela ou un type d’exception personnalisé).

  • Modifiez également les paramètres couleur et valeur pour les rendre privés, puis créer des accesseurs et mutateurs qui permettent d’y accéder en mode public et de valider les données à la modification.

Correction 3.2 `carte.py`

La classe Paquet, une collection de cartes

  • Dans un nouveau fichier paquet.py, créer une classe Paquet correspondant à un paquet de 52 cartes. Le constructeur devra créer toute les cartes du jeu et les stocker dans une liste ordonnée. Vous aurez probablement besoin d’importer la classe Carte. Testez le comportement de cette classe en l’important et en l’utilisant dans main.py.

  • Implémenter la méthode melanger pour la classe Paquet qui mélange l’ordre des cartes.

  • Implémenter la méthode couper qui prends un nombre aléatoire du dessus du paquet et les place en dessous.

  • Implémenter la méthode piocher qui retourne la Carte du dessus du paquet (eticla l’enlève du paquet)

1.0 : Implémenter la méthode distribuer qui prends en argument un nombre de carte et un nombre de joueurs (e.g. p.distribuer(joueurs=4, cartes=5)), pioche des cartes pour chacun des joueurs à tour de rôle, et retourne les mains correspondantes.

p = Paquet()
p.melanger()

main_alice, main_bob = p.distribuer(joueurs=2, cartes=3)

print(main_alice)
# affiche par exemple [<Carte 3 de PIQUE>, <Carte VALET de CARREAU>, <Carte 1 de trefle>]

print(p.pioche())
# affiche <Carte 9 de CARREAU>

print(main_alice[1].points())
# affiche 11
Correction 3.2 `paquet.py`
Correction 3.2 `main.py`

Correction exercice 3.3 - Introduction aux ORM avec ActiveAlchemy

On se propose de reprendre le jeu de données des apps Yunohost (Exos part 2, fichier app.yunohost.org/community.json) et d’importer ces données dans une base SQL (plus précisémment SQLite)

  • Installer active_alchemy à l’aide de pip3

  • Créer un fichier mydb.py qui se contente de créer une base db (instance de ActiveAlchemy) de type sqlite. Dans la suite, on importera l’objet db depuis mydb.py dans les autres fichiers si besoin.

  • Créer un fichier models.py et créer dedans une classe (aussi appellé modèle) App. On se limitera aux attributs (aussi appellés champs / colonnes) suivants :

    • un nom qui est une chaîne de caractère unique parmis toutes les App ;
    • un niveau qui est un entier (ou vide) ;
    • une adresse qui est une chaîne de caractère unique parmis toutes les App ;
  • Créer un fichier nuke_and_reinit.py dont le rôle est de détruire et réinitialiser les tables, puis de les remplir avec les données du fichier json. On utilisera pour ce faire db.drop_all() et db.create_all(). Puis, itérer sur les données du fichier json pour créer les objets App correspondant. Commiter les changements à l’aide de db.session.add et commit.

  • Créer un fichier analyze.py qui cherche et affiche le nom de toutes les App connue avec un niveau supérieur ou égal à n. En utilisant l’utilitaire bash time (ou bien avec time.time() en python), comparer les performances de analyze.py avec un script python équivalent mais qui travaille à partir du fichier community.json directement (en local, pas via requests.get)

mydb.py

from active_alchemy import ActiveAlchemy

db = ActiveAlchemy('sqlite:///apps.db')

models.py

from mydb import db

class App(db.Model):
    name = db.Column(db.String(20), unique=True, nullable=False)
    level = db.Column(db.Integer, nullable=True)
    url = db.Column(db.String(50), unique=True, nullable=False)
    
    def __repr__(self):
        return "<App " + self.name + ">"

nuke_and_reinit

import json
from mydb import db
from models import App

db.drop_all()
db.create_all()

with open("apps.json") as f:
    apps_from_json = json.loads(f.read())

for app, infos in apps_from_json.items():
    a = App(name=app, level=infos["level"], url=infos["git"]["url"])
    db.session.add(a)

db.session.commit()

apps_level_3 = App.query().filter(App.level == 3)
for app in apps_level_3:
    print(app.name)

Exercice 3.1 - Cercles et Cylindres

Dans cet exercice nous allons représenter des objets et calculs géométriques simples en coordonnées entières. Utilisez des annotations de types : int, -> None, -> int et : Tuples[int ...] dès que possible. Testez régulièrement la consistance de ces types avec mypy fichier.py.

  • Implémenter une classe Cercle avec comme attributs un rayon rayon et les coordonnées x et y de son centre. Par exemple on pourra instancier un cercle avec mon_cercle = Cercle(5, (3,1))

  • Dans la classe Cercle, implémenter une propriété aire dépendante du rayon qu’on peut appeler avec mon_cercle.aire.

  • Implémenter une classe Cylindre, fille de Cercle, qui est caractérisée par un rayon rayon, une hauteur hauteur et des coordonnées x, y et z. On écrira le constructeur de Cylindre en appelant le constructeur de Cercle.

  • Dans la classe Cercle, implémenter une méthode intersect qui retourne True ou False suivant si deux cercles se touchent. Exemple d’utilisation : c1.intersect(c2)

  • Surcharger la méthode intersect pour la classe Cylindre, en se basant sur le résultat de la méthode de la classe mère.

Exercice 3.2 - Jeu de carte

Une classe Carte pour représenter les éléments d’un jeu

  • Dans un fichier carte.py, créer une classe Carte. Une carte dispose d’une valeur (1 à 10 puis VALET, DAME et ROI) et d’une couleur (COEUR, PIQUE, CARREAU, TREFLE). Par exemple, on pourra créer des cartes en invoquant Carte(3, 'COEUR') et Carte('ROI', 'PIQUE').

  • Implémenter la méthode points pour la classe Carte, qui retourne un nombre entre 1 et 13 en fonction de la valeur de la carte. Valider ce comportement depuis un fichier main.py qui importe la classe Carte.

  • Implémenter la méthode __repr__ pour la classe Carte, de sorte à ce que print(Carte(3, "COEUR")) affiche <Carte 3 de COEUR>.

c = Carte("DAME", "PIQUE")

print(c.couleur)
# Affiche PIQUE

print(c.points)
# Affiche 12

print(c)
# Affiche <Carte DAME de PIQUE>

Encapsulation et validation des valeurs de carte possibles

Pour sécuriser l’usage ultérieur de notre jeu de carte on aimerait que les cartes ne puissent être crées et modifiées qu’avec des valeurs correctes (les 4 couleurs et 13 valeurs précisées)

  • Modifiez le constructeur pour valider que les données fournies sont valides. Sinon levez une exception (on utilise conventionnellement le type d’exception ValueError pour cela ou un type d’exception personnalisé).

  • Modifiez également les paramètres couleur et valeur pour les rendre privés, puis créer des accesseurs et mutateurs qui permettent d’y accéder en mode public et de valider les données à la modification.

La classe Paquet, une collection de cartes

  • Dans un nouveau fichier paquet.py, créer une classe Paquet correspondant à un paquet de 52 cartes. Le constructeur devra créer toute les cartes du jeu et les stocker dans une liste ordonnée. Vous aurez probablement besoin d’importer la classe Carte. Testez le comportement de cette classe en l’important et en l’utilisant dans main.py.

  • Implémenter la méthode melanger pour la classe Paquet qui mélange l’ordre des cartes.

  • Implémenter la méthode couper qui prends un nombre aléatoire du dessus du paquet et les place en dessous.

  • Implémenter la méthode piocher qui retourne la Carte du dessus du paquet (et l’enlève du paquet)

1.0 : Implémenter la méthode distribuer qui prends en argument un nombre de carte et un nombre de joueurs (e.g. p.distribuer(joueurs=4, cartes=5)), pioche des cartes pour chacun des joueurs à tour de rôle, et retourne les mains correspondantes.

Exercice 3.3 - Introduction aux ORM avec ActiveAlchemy

On se propose de reprendre le jeu de données des apps Yunohost (Exos part 2, fichier app.yunohost.org/community.json) et d’importer ces données dans une base SQL (plus précisémment SQLite)

  • Installer active_alchemy à l’aide de pip3

  • Créer un fichier mydb.py qui se contente de créer une base db (instance de ActiveAlchemy) de type sqlite. Dans la suite, on importera l’objet db depuis mydb.py dans les autres fichiers si besoin.

  • Créer un fichier models.py et créer dedans une classe (aussi appellé modèle) App. On se limitera aux attributs (aussi appellés champs / colonnes) suivants :

    • un nom qui est une chaîne de caractère unique parmis toutes les App ;
    • un niveau qui est un entier (ou vide) ;
    • une adresse qui est une chaîne de caractère unique parmis toutes les App ;
  • Créer un fichier nuke_and_reinit.py dont le rôle est de détruire et réinitialiser les tables, puis de les remplir avec les données du fichier json. On utilisera pour ce faire db.drop_all() et db.create_all(). Puis, itérer sur les données du fichier json pour créer les objets App correspondant. Commiter les changements à l’aide de db.session.add et commit.

  • Créer un fichier analyze.py qui cherche et affiche le nom de toutes les App connue avec un niveau supérieur ou égal à n. En utilisant l’utilitaire bash time (ou bien avec time.time() en python), comparer les performances de analyze.py avec un script python équivalent mais qui travaille à partir du fichier community.json directement (en local, pas via requests.get)

Partie 4 - Python Object Model et modules

Cours 4

17. Python Object Model et sujets avancés

Python Object Model

Si on regarde un autre langage orienté objet avant Python il paraît étrange de mettre len(collection) au lieu de collection.len() (faire comme s’il s’agissait d’un fonction plutôt que d’une méthode). Cette apparente bizarrerie est la partie émergée d’un iceberg qui, lorsqu’il est bien compris, est la clé de ce qui est pythonique. L’iceberg est appelé le Python Object(ou Data) Model, et il décrit l’API que vous pouvez utiliser pour faire jouer vos propres objets avec les constructions idiomatiques du langage Python. (traduction d’un paragraphe du livre Fluent Python)

Cette API (application programming interface = série de fonctions qui décrivent ce qu’on peut faire) se compose d’attributs et méthodes “spéciales” qui sont encadrées par des doubles underscores (__ ) comme __add__.

Exemple 1: redéfinir l’addition avec __add__

On peut créer une méthode def __add__(self, autre_objet_de_la_classe): ... pour dans nos classe pour redéfinir le symbole + appliqué à nos objets.

Exemple un vecteur 2D:

class Vector2d:
    typecode = 'd'

    def __init__(self, x, y):
        self.x = float(x)
        self.y = float(y)

    def __add__(self, autre_vecteur):
        return Vector2d(self.x + autre_vecteur.x, self.y + autre_vecteur.y)

nouveau_vecteur = Vector2d(3, 4) + Vector2d(3, 7) # -> Vector2d(6, 11)

On parle aussi dans ce cas de surcharge d’opérateur qui est un classique dans les langage de POO.

Exemple 2: faire de notre objet un conteneur pythonique avec __setitem__ et __getitem__

class MaCollectionEnnuyeuse:
    def __init__(self, collection):
        self.mesitems = list(collection)

    def __getitem__(self, indice):
        return self.mesitems[indice] 

    def __setitem__(self, indice, item_a_ajouter):
        return self.mesitems[indice] = item_a_ajouter

print(MaCollectionEnnuyeuse("Hello")[0:1]) # -> Renvoie 'He'

Une fois qu’on a implémenté le minimum de l’interface on peut utiliser des fonctions python intégrées par exemple ici on peut faire directement

shuffle(MaCollectionEnnuyeuse('Diantre')) # -> Mélange les lettres de Diantre 

En fait, on peut dire qu’être une liste en python c’est plus ou moins avoir les méthodes spéciales qui définissent la liste. Pareil pour le dictionnaire. Un bon exemple de ce principe est l’itérable : tout objet qui peut renvoyer un iterateur avec __iter__ est utilisable dans une boucle for (puissant)

Exemple3 : les iterateurs

En python pour pouvoir utiliser la puissance de la boucle for on a besoin d’un objet itérateur ou d’un objet itérable c’est à dire un objet dont on peut tirer automatiquement un itérateur.

Une liste est itérable, ce qui veut dire qu’elle possède une fonction __iter__ qui renvoie un itérateur sur ses éléments.

Un itérateur est un objet qui:

  • possède une méthode __next__ qui renvoie l’élément suivant de l’itération
  • possède une méthode __iter__ qui renvoie un objet itérateur avec lequel continuer l’itération (souvent un simple return self)
  • déclenche une exception de type StopIteration lorsqu’il n’y a plus d’élément à itérer

Méthodes spéciales

Il existe plein de méthodes spéciales pour implémenter toutes les syntaxes, comportements sympathiques, et fonctions de base incluses dans Python (comme shuffle ou sort). Quelques autre:

  • __repr__ et __str__ : génère automatiquement une représentation de l’objet sous forme de chaîne de caractères (la première est une représentation basique pour le debug, la deuxième prioritaire est pour une représentation plus élégante de l’objet) qui permet de faire un “joli” print(mon_objet)
   def __str__(self):
      return "Cercle de couleur " + self.color + " et de rayon " + self.rayon
  • __eq__ : définir l’égalité entre deux objets. Très important pour faire des comparaison rapide et par exemple permettre de trier automatiquement vos objets dans une liste. Etc

  • __bool__: Permet de convertir votre objet en booléen et ainsi de supporter des syntaxes comme

if mon_objet:
    print("c'est bon")
else:
    print("c'est pas bon")

ETC…

Cf. le livre Fluent Python et la doc officielle

Implémenter ces différentes fonctions d’API n’est pas obligation mais surtout utile pour construire du code (souvent de librairie) qui sera agréable à utiliser pour les autre développeurs habitués à Python.

Design Patterns

En fait au delà de Python et de la POO, lorsqu’on construit des programmes on peut identifier des bonnes façon de résoudre des problèmes courants ou qui on une forme courante qu’on retrouve souvent dans les programmes. On appelle ces méthodes/forme des Design Patterns.

Par exemple l’iterateur (Pattern Iterator) est un design pattern que le langage Python implémente à sa façon et qui propose une solution pratique au parcours d’une collection d’objets.

Le Decorator est également un motif pour personnaliser le fonctionnement d’une fonction ou classe sans la modifier (et donc sans complexifier le code principal) il est implémenté en python grace à une syntaxe spécifique du langage très utilisée (Cf juste après).

Ces “motifs de conception” logicielle proviennent d’un ouvrage éponyme, influent dans les années 90, du Gang of Four (Gof). En réalité c’est même plus général que ce livre orienté POO car on peut identifier des Design Patterns dans des langages très différents par exemple fonctionnels.

Il existe pas mal d’autres Patterns non implémentés direactement dans le langage Python:

Décorateurs

Les décorateurs sont en Python des sortes d'“emballages” qu’on ajoute aux fonctions et au classes pour personnaliser leur comportement sans modifier le code principal de la fonction. Concrêtement les décorateurs sont des

En gros ça permet d’ajouter des prétraitements, des posttraitements et de modifier le comportement de la fonction elle

Programmes asynchrones en Python

Très bonne synthèse pour python >= 3.8 : https://www.integralist.co.uk/posts/python-asyncio/

Une synthèse de la synthèse (Perte d’information ;)) :

Un programme synchrone est un programme ou toutes les étapes de calculs sont éxecutées les unes à la suite des autres. Conséquence on attend la fin de chaque opération avant de continuer et si une opération prend du temps l’utilisateur attend.

Un programme asynchrone est un programme qui execute diférentes étapes de calcul sans respecter l’ordre linéraire du programme. Par exemple deux fonctions appelées en même temps et qui vont s’exécuter de façon concurrent (on les lance toutes les deux en même temps et elles se partagent les ressources de calculs).

Pour executer des morceaux de calculs de façon concurrente il y a pas mal d’approches dont:

  1. le multiprocessing : on lance plusieurs processus au niveau de l’os, un peu l’équivalent de plusieurs programme en parallèle. Ils peuvent se répartir les multiples processeurs d’une machine ou d’un cluster. C’est intéressant pour les gros calcul mais pour faire plein de petites taches c’est pas très intéressant car le changement de process prend du temps.

  2. le multithreading : on lance un processus système avec plusieurs processus “virtuels” “légers” à l’intérieur. Les différents threads peuvent aussi potentiellement utiliser plusieurs processeurs en même temps. Cependant le multithread est peu efficace en python (avec Cpython) à cause du Global Interpreter Lock. On utilise peu les threads.

  3. execution asynchrone dans un seul processus (asyncio basé sur une event loop): En gros les différents morceaux du code concurrents ne s’exécutent pas “réellement” en même temps, ils se partagent le temps d’exécution d’un seul processus de calcul en se passant la main. Cette approche n’utilise pas tous les processeurs disponibles mais est légère et facilement controlable.

Pourquoi un programme est-il lent ?

Avant de choisir une solution il faut étudier son programme pour diagnostiquer le ralentissement.

  • Très couramment à cause de blocages au niveau des entrées/sortie (IO) lorsqu’on attend qu’un serveur (sur le réseau ou autre) ou un device (le disque ou autre) réponde à une demande.
  • Parce que le calcul est très lourd et demande plein d’opérations processeur (CPU intensive) (courant mais plus rare dans les programmes réels)

Dans le premier cas il faut utiliser l’execution asynchrone (solution 3.) en coroutine (fonction commençant par async def) avec asyncio.

Dans le deuxième cas il faut utiliser le multiprocessing (solution 1.) pour maximiser les processeurs utilisés avec concurrent.futures.

On peut combiner facilement les deux approches si nécessaire.

Concrêtement avec des exemples

On commence par essayer d’accélérer son programme avec asyncio

Exemple de asyncio:

import asyncio

async def foo():
    print("Foo!")

async def hello_world():
    await foo()  # waits for `foo()` to complete
    print("Hello World!")

asyncio.run(hello_world())

Il faut s’habituer à cette façon de programmer :

  • se rappeler qu’une fonction async def peut se réveille périodiquement pour s’exécuter (le flux d’exécution est plus dur à imaginer)
  • Il faut aussi gérer la concurrence entre les coroutines (attendre un résultat dont on a besoin pour continuer le calcul d’une autre coroutine avec await par exemple)

Exemple2 avec gather pour attendre et rassembler les résultat de plusieurs taches:

gather

import asyncio


async def foo(n):
    await asyncio.sleep(5)  # wait 5s before continuing
    print(f"n: {n}!")


async def main():
    tasks = [foo(1), foo(2), foo(3)]
    await asyncio.gather(*tasks)


asyncio.run(main())

Enfin pour compléter l’approche asyncio avec du multiprocessing (au cas ou c’est le processeur qui bloque et que le programme est toujours lent) on peut utiliser concurrent.futures et un Pool de Process (ProcessPoolExecutor).

Exemple de la doc Python ou on combine asyncio et concurrent.futures.

import asyncio
import concurrent.futures


def blocking_io():
    # File operations (such as logging) can block the
    # event loop: run them in a thread pool.
    with open("/dev/urandom", "rb") as f:
        return f.read(100)


def cpu_bound():
    # CPU-bound operations will block the event loop:
    # in general it is preferable to run them in a
    # process pool.
    return sum(i * i for i in range(10 ** 7))


async def main():
    loop = asyncio.get_running_loop()

    # 1. Run in the default loop's executor:
    result = await loop.run_in_executor(None, blocking_io)
    print("default thread pool", result)

    # 2. Run in a custom thread pool:
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, blocking_io)
        print("custom thread pool", result)

    # 3. Run in a custom process pool:
    with concurrent.futures.ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, cpu_bound)
        print("custom process pool", result)


asyncio.run(main())

19. Organiser son code en modules, packages et librairies

Modules Python

Les modules Python sont le plus haut niveau d’organisation du code (plus que les classes).

Ils servent à regrouper des ensembles de classes et fonctions apparentées.

Un module est ce qu’on importe grace à import ou from ... import ....

Un module peut être un simple fichier

Si on met des fichiers python dans le même dossier ils constituent automatiquement des modules.

fichier mon_module.py:


ma_variable = 1

def ma_fonction(arg: int):
    return ma_variable + arg

fichier mon_module2.py:

from mon_module import ma_fonction

ma_variable = 2

fichier mon_programme_principal.py

import mon_module
import mon_module2


if __name__ == "__main__"
    ma_variable = 3
    print(mon_module.ma_variable) # -> 1 
    print(mon_module2.ma_variable) # -> 2
    print(ma_variable) # -> 3
    print(mon_module2.ma_fonction(ma_variable))
  • Les modules sont des namespaces pour leurs variables : mon_module.ma_variable != mon_module2.mavariables != mavariables

  • Les imports de modules sont transitifs : si on importe module2 qui importe module1 alors on a module1 disponible même si on a pas importé directement module1.

  • Le code d’un module est exécuté au moment de l’import (si ya un print qui traine dans le corps d’un module ça risque de se voir…)

Packages : quand on a beaucoup de code…

On ne s’y retrouve plus avec un seul module ou quelques fichiers à la racine du projet.

  • On met les fichiers dans plusieurs dossiers bien ordonnés

  • On ajoute des fichiers __init__.py dans chaque sous dossiers et ça fait un module

Exemple

Considérant les fichiers suivants :

├── main.py
└── mylib/
    ├── __init__.py
    └── bonjour.py      # <-- Contient "def dire_bonjour..."

Depuis main.py, je peux faire

from mylib.bonjour import dire_bonjour

dire_bonjour("Marius") # -> "Bonjour Marius !"

print(dire_bonjour)
# -> <function dire_bonjour at 0x7fb964fab668>

Considérant les fichiers suivants :

├── main.py
└── mylib/
    ├── __init__.py
    └── bonjour.py      # <-- Contient "def dire_bonjour..."

Depuis main.py, je peux aussi faire

from mylib import bonjour

bonjour.dire_bonjour("Marius") # -> "Bonjour Marius !"

print(bonjour)
# -> <module 'mylib.bonjour' from 'mylib/bonjour.pyc'>

Faire une librairie

Si on a besoin de le distribuer ou simplement pour le séparer du reste du code peut ensuite transformer son package en une librairie installable grâce à un outil nommée setuptools et/ou pip.

Cf. Exercice 4.3

19. Tester son code

Pourquoi Tester ?

“Pour éviter les régressions”

Une modification à un bout du programme peut casser un autre morceau si on y prend pas garde ! Par exemple si on a changer un nom de variable mais pas partout dans le code. Le logiciel a l’air de fonctionner.

Lorsqu’on a un gros logiciel avec une base de code python énorme on ne peut pas facilement connaître tout le code. Même sur un logiciel plus limité on ne peut pas penser à tout.

Comme un logiciel doit pouvoir être en permanence refactorisé pour resté efficace et propre on a vraiment besoin de tests pour tout logiciel d’une certaine taille.

Si vous codez une librairie pour d’autres développeurs/utilisateurs, ces utilisateurs veulent un maximum de tests pour garantir que vous ne laisserait pas des bugs dans la prochaine version et qu’ils peuvent faire confiance à votre code.

Pour anticiper les bugs avant qu’ils n’arrivent

Écrire des bons test nécessite d’imaginer les cas limites de chaque fonction. Si on a oublié de gérer le cas argument = -1 par exemple au moment des tests on peut le remarquer, le corriger et faire en sorte que le test garantisse que ce bug est évité.

Pour aider à coder le programme en réfléchissant à l’avance a ce que chaque fonction doit faire

Écrire des tests avant de coder, une pratique qu’on appelle le Test Driven Development

Deux types de tests: tests unitaires et tests d’intégrations

  • Unitaire: tester chaque fonction et chaque classe. Peur détecter les problèmes locaux à chaque fonction.

  • Intégration: tester l’application en largeur en appelant le programme ou certaines grosses partie dans un contexte plus ou moins réaliste. Pour détecter les problèmes d’intgégration entre plusieurs parties du programme mais déclenche aussi les problèmes dans les fonctions.

  • Généralement les tests unitaires sont très rapides (on peut les lancer toutes les 5 minutes puisque ça prend 4 secondes)

Généralement les tests d'intégration sont plus lent puisqu’il faut initialiser toute l’application et son contexte avant de les lancer.

Test unitaire avec Pytest

Dans mylib.py

def func(x):
    return x + 1

Dans tests.py

from mylib import func

def test_answer():
    assert func(3) == 5

Lancer Pytest

  • En précisant le fichier de test un fichier: pytest tests.py ou python3 -m pytest tests.py si on utilise un environnement virtuel python.

  • En laissant pytest trouver tous les tests du projet : les commandes pytest ou python3 -m pytest parcourt tous les fichiers python du dossier et considère comme des tests toutes les fonctions qui commencent par test_

Tests d’integration exemple avec Flask

Initialiser le contexte de test avec une fixture

(fixture = une fonction de préparation d’un contexte consistant pour les tests)

import os
import tempfile
import pytest

from web_app import web_app

@pytest.fixture
def client():
    with web_app.test_client() as client: # une application flask propose une méthode test_client() pour mettre en place un serveur web destiné aux test
        yield client # pour chaque test la fonction client() renvoie le client de test flask

def test_compute_add_5_5(client): # la fixture client est passée en paramètre de la fonction de test
    return_value = client.get('/add/5/5')
    assert b'5 + 5 = 10' in return_value.data

def test_compute_add_0_0(client):
    return_value = client.get('/add/0/0')
    assert b'0 + 0 = 0' in return_value.data

Ces deux tests s’éxecutent en montant un serveur web et en appelant la route (~page web) correspondante. On aurait pu également initialiser une base de données pour le site web avant de lancer les tests avec une fixture par exemple bdd.

Bonus. Introduction à Flask et le web

Une application web

  • On interagit avec au travers d’un navigateur web
  • Avec le navigateur, on accède à des ressources par des URL. Par exemple :
    • La racine du site : /
    • Une page avec un formulaire de contact : /contact
    • Une image stockée sur le site : /chat.jpg
  • On clique sur des liens qui vont demander d’autres ressources (GET)
  • On clique sur des boutons qui peuvent envoyer des informations (POST)

… mais pourquoi une app web ? (plutôt qu’un logiciel classique)

Pros:

  • Cross-platform
  • Mise à jour simple
  • Au niveau technique : distinction plus évidente entre le front et le back-end ?
  • Plus de possibilité et de flexibilité cosmétiques

Cons:

  • Moins de vie privée
  • Le web est un désastre au niveau CPU
  • Demnade de connaitre + de technos ? (HTML/CSS/JS)

Flask

En quelques mots

Un “micro-framework” pour faire du web, composé de plusieurs morceaux

  • Vues gérées avec Jinja (moteur de template avec une syntaxe “à la Python”)
  • Controleurs gérés avec Werkzeug (une URL <-> une fonction)
  • Modèles gérées avec SQLAlchemy (ORM : une classe <-> une table SQL)

On peut y greffer pleins d’autres modules petits modules optionnels

Pour des applications plus grosses, on préferera tout même Django qui est un framework plus complet (mais plus complexe) mais qui suis la même logique

Virtualenv “de base” pour Flask

virtualenv -p python3 venv
source venv/bin/activate

pip install Flask
pip install Flask-SQLAlchemy

Hello World en Flask

On associe l’url / à un controleur (= une fonction) qui renvoie Hello World

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

Mon controleur hello_world() doit renvoyer du texte ou une “HTTP response” (par exemple, erreur 404, ou redirection, …)

Lancer le serveur web de test :

$ export FLASK_APP=hello.py
$ flask run
 * Running on http://127.0.0.1:5000/

ensuite, je visite:

http://127.0.0.1:5000/     # -> Affichera 'Hello world'

On peut créer d’autres controleur pour d’autres URLs…

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

@app.route('/python')
def python():
    return "Le python, c'est la vie!"

ensuite :

http://127.0.0.1:5000/python    # -> Affichera 'Le python, c'est la vie!'

Créer des vues avec Jinja

Un template ressemble à :

<html>
  Bonjour {{ prenom }} !

  {% for app in apps %}
    {{ app.name }} est niveau {{ app.level }} !
  {% endfor %}
</html>

On peut l'hydrater avec par exemple ces données :

prenom = "Marius"
apps = [ { "name": "mailman", "level": 2 },
         { "name": "wordpress", "level": 7 },
         { "name": "nextcloud", "level": 8 }    ]

Rendu :

<html>
  Bonjour Marius !

  mailman est niveau 2 !
  wordpress est niveau 7 !
  nextcloud est de niveau 8 !
</html>

En supposant que le template précédent soit situé dans templates/hello.html, je peux utiliser render_template dans mon controleur générer un rendu à l’aide de mes données

from flask import render_template

@app.route('/')
def homepage():
    apps = [ { "name": "mailman", "level": 2 },
             { "name": "wordpress", "level": 7 },
             { "name": "nextcloud", "level": 8 }    ]
    return render_template('hello.html', 
                           name="Marius",
                           apps=apps)

Gérer les données avec SQL Alchemy

from flask_sqlalchemy import SQLAlchemy

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./db.sqlite'
db = SQLAlchemy()
db.init_app(app)


class App(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), unique=True, nullable=False)
    level = db.Column(db.Integer, nullable=False)
    date_last_test = db.Column(db.Date, nullable=True)

Initialiser les tables

# Supprimer toutes les tables existantes (achtung!)
db.drop_all()

# Recréer toutes les tables qui vont bien
db.create_all()

Ecrire

# Creer et ajouter une app dans la database...
mailman = App(name="mailman", level=3)
db.session.add(mailman)
db.session.commit()

Lire

# Trouver toutes les apps..
App.query.all()

# Trouver toutes les apps level 7 ...
App.query.filter_by(level=7).all()

# Trouver l'app qui s'apelle mailman
App.query.filter_by(name="mailman").first()

Dans un controleur

from flask import render_template
from my_models import App

@app.route('/')
def homepage():

    apps = App.query.all()
    
    return render_template('hello.html', 
                           prenom="Marius",
                           apps=apps)

Exercices Partie 4 - Python Object Model, modules et qualité

Exercice 4.1 - Un paquet pythonique

4.1 Utiliser les syntaxes de liste sur la classe Paquet

  • Plutôt que d’utiliser len(mon_paquet.cartes) pour avoir le nombre de carte on voudrait utiliser len(mon_paquet). Implémentez la méthode spéciale __len__ pour renvoyer la longueur du paquet. Profitez-en pour empêcher que les utilisateurs de la classe modifient directement le paquet en rendant l’attribut cartes privé. Testez votre programme en mettant à jour le code main.py

  • Maintenant que l’attribut cartes n’est plus censé être accessible hors de la classe, nous avons besoin d’un nouvelle méthode pour accéder à une carte du paquet depuis le programme principal. Implémentez la méthode spéciale __getitem__ pour pouvoir accéder à une carte avec mon_paquet[position]. Tester la dans le programme principal.

  • Notre Paquet ressemble maintenant beaucoup à une véritable liste python. Essayez dans le main.py d’utiliser la méthode shuffle classique de Python pour mélanger un paquet de carte : Il manque quelque chose.

  • Dans l’interpréteur (python3 ou ipython3) affichez la liste des méthode de la classe paquet en utilisant dir(). Les méthodes en python sont assignées dynamiquement aux classes et peuvent être modifiées au fur et à mesure du programme. Ajoutons une méthode __setitem__ directement depuis l’interpréteur (démo). Affichez à nouveau le dictionnaire dir() de mon_paquet pour voir la nouvelle méthode ajoutée.

  • Ajoutez maintenant __setitem__ dans le code de Paquet. Supprimez et remplacez la méthode melanger par shuffle dans le code du projet.

Exercice 4.2 - Un itérateur de cartes

4.2 Itérateurs de carte : génération de la suite de carte à partir d’une carte

Plutôt que de générer les 52 cartes avec une boucle for dans le constructeur du paquet on voudrait utiliser un générateur/itérateur associé à la classe carte.

  • Ajoutez à carte.py une classe IterateurDeCarte pour générer la suite des cartes à partir d’un objet carte.

    1. D’abord créez la classe IterateurDeCarte qui prend en argument une Carte à la création et qui possède des méthodes __next__(self) qui retourne la carte suivante dans l’ordre des cartes et __iter__ qui lui permet de se renvoyer lui même pour continuer l’itération.
    2. Ajoutez une méthode __iter__ à la classe carte qui renvoie un itérateur basée sur la carte courante.
  • Générez les 52 cartes du paquet à partir de notre iterateur de carte.

  • Ajoutez un paramètre facultatif carte_de_départ au contructeur de paquet pour commencer la génération du paquet à partie d’une carte du milieu de la série de carte possible.

  • Modifiez le constructeur de la classe Carte pour qu’elle prenne en argument des valeurs et couleurs possibles qui ne soit pas les valeurs classique. Testez cette fonctionnalité dans main.py en générant un jeu de “UNO” (sans les cartes “Joker” noire) à la place d’un jeu classique. Cartes de Uno

Bonus : d’autres générateurs de carte

Les listes sont des collections finies et les itérateurs de liste sont donc toujours finis. Cependant un itérateur n’a pas de taille en général et peut parfois renvoyer des valeurs indéfiniment des valeurs (grace à un générateur infini par exemple).

  • Modifiez l’itérateur de carte pour qu’il se base sur un générateur de carte infini utilisant les nombre de la suite de fibonacci et les quatre couleurs du UNO. (Voir correction de fibonacci dans la partie 1)

Exercice 4.3 - fancy operations - Packages, scripts et tests

4.3.1 Créer un script avec des paramètres documentés grâce à docopt

Le point de départ des exercices 4.3 à 4.5 est une librairie de calcul extrêment simple ennuyeuse puisqu’elle fournit des fonctions fancy_add, fancy_substract et fancy_product. Pour illustrer la réutilisation du code et des bonnes pratiques de développement, nous allons cependant la packager et l’utiliser pour contruire un outil de calcul en ligne de commande, et un autre basé sur une application web (cli_calculator.py et web_calculator).

  • Récupérez avec git clone le projet de base à l’adresse https://github.com/e-lie/python202011-exercice-fancy-ops.git. Ouvrez le dans VSCode.

  • Créez un environnement virtuel python3 dans un dossier venv pour travailler de façon isolée des autres projets et de l’environnement python du système: virtualenv -p python3 venv.

  • Activez l’environnement dans votre terminal courant : source ./venv/bin/activate (deactivate pour desactiver l’environnement).

  • Observer les fonctions de calculs présentes dans fancy_operations.py. Créez un script cli_calculator.py qui importe ces trois fonctions et les utilise pour faire des calculs simples.

  • Essayez de debugger le script dans VSCode (normalement la configuration de debug est déjà présente dans car fournit dans le fichier .vscode/launch.json du projet).

  • Installons la librairie externe docopt dans notre environnement virtuel:

    • Ajoutez docopt à un fichier requirements.txt à la racine du projet.
    • Installez cette dépendance grâce au gestionnaire de paquet pip : pip install -r requirements.txt (vérifiez bien que votre venv est activé avec source venv/bin/activate).
  • En vous inspirant du cours et de la documentation de docopt utilisez cette librairie pour faire en sorte que cli_calculator listops affiche la liste des operations disponibles dans fancy_operations.py. On pourra pour cela ajouter dans fancy_operations.py un dictionnaire fancy_operations répertoriant les operations au format { 'add': fancy_add, ... }.

4.3.2 Déplacer les fonctions de calcul dans un package de librairie

Pour ajouter une nouvelle classe vector2d à notre librairie nous allons la réorganiser en plusieurs fichiers et sous dossiers.

  • Créez un dossier computation_libs pour la librairie à la racine du projet. À l’intérieur créer un sous dossier fancy_int_operations pour ranger nos fonctions.

  • Déplacez et rangez les fonctions fancy_add, fancy_product et le dictionnaire fancy_operations à la racine de fancy_int_operations dans un fichier __init__.py de façon à pouvoir les importer dans cli_calculator.py sous la forme from computation_libs.fancy_int_operations import fancy_add, fancy_product, fancy_operations.

  • Déplacez de même fancy_substract de façon à pouvoir l’importer comme suit : from computation_libs.fancy_int_operations.more_fancy_operations import fancy_substract.

  • Vérifiez que votre script cli_calculator.py fonctionne toujours.

  • Ajoutez finalement la classe Vector2d suivante dans un fichier computation_libs/vector2d.py:

vector2d

`computation_libs/vector2d.py`
  • Documentez cette classe grâce à un doctype contenant le texte suivant A 2-dimensional vector class from the fluent python book chapter 9.

4.3.3 Finir cli_calculator

  • Ajoutez dans cli_calculator.py un deuxième cas d’usage docopt permettant d’appeler le script pour effectuer une operation comme suit: python3 cli_calculator.py substract 3 4 affichera 3 - 4 = -1. On pourra préciser le symbole -, +, * en complexifiant le dictionnaire fancy_operations pour indiquer le symbole correspondant à chaque opération.

  • Gérer les mauvaises entrées utilisateurs grâce à un try: ... except:. On pourra afficher un message d’erreur tel que Bad operation or operand (should be integers) et finir le script en erreur grâce à exit(1).

4.3.4 Créer un package python d’application web : web_calculator

  • Dans le dépot du projet récupérez la correction intermédiaire et le début du projet flask en allant sur la branche correction_inter_flask (git checkout <branche>).

  • Ajoutez la librairie web flask aux dépendances du projet et installez la avec pip.

  • Créez un script web_calculator.py avec le code d’une application web de base:

from flask import Flask, render_template

web_app = Flask(__name__)

@web_app.route('/')
def index():
    return render_template("index.html", title="Webcalculator Home")
  • Testez l’application avec flask run ou le lancement VSCode Webcalculator puis visitez http://localhost:5000 dans votre navigateur.

Maintenant que cette application minimale fonction une bonne pratique est d’en faire un package:

  • Créez un package web_app initialisant une application flask quand on l’importe avec le code :
from flask import Flask

web_app = Flask(__name__)
  • Créez un fichier routes.py dans le package avec notre route index et en important correctement les modules nécessaires.

  • Déplacez le dossier templates dans le package également et gardez dans web_calculator.py uniquement from web_app import web_app.

  • Retestez l’application comme précédemment : comment cela fonctionne-t-il au niveau de l’import ?

  • Créez une seconde route def compute(operation, int_n, int_m): en mode GET avec comme url /<operation>/<int_n>/<int_m> qui

    • utilisez la librairie fancy_int_operations pour effectuer des opérations sur des entier int_n et int_m
    • utilise le template jinja operation.html pour afficher le résultat
    • on pourra bien sur debugger l’application dans VSCode ou avec ipdb pour bien comprendre l’exécution et trouver les erreurs.
    • Testez votre application dans le navigateur.

Pour utiliser la librairie computation_libs.fancy_int_operations nous avons du déplacer le package à l’intérieur de web_app pour le rendre accessible à l’application web. Notre cli_calculator ne fonctionne plus du coup.

  • La bonne méthode pour travailler avec des packages indépendants consiste à créer un paquet pip “editable” à partir de notre package:
    • remettez computation_libs à la racine du projet.
    • ajoutez dans computation_libs un fichier de packaging setup.py utilisé par setuptools pour packer notre librairie.
    • mettez à l’intérieur:
from setuptools import setup, find_packages

setup(name='computation-libs', version='0.1', packages=find_packages())
- Installez la librairie avec `pip install -e ./computation_libs`
  • Gérez les mauvaises entrées utilisateur avec un try: except: renvoyant le cas échéant vers le template invalid.html. Testez.

4.3.4 Tester nos modules avec Pytest

  • Ecrire des tests unitaires pytest sur les 3 opérations de notre librairie.

  • Ecrire des test d’intégration sur notre application flask.

Correction:

La correction finale est dans la branche correction_finale du dépôt visible sur github ici

Exercice 4.4 - Application du design pattern observateur

4.4 Design patterns ‘Observateur’ appliquée aux chaînes Youtube

Les design patterns sont des patrons de conception qui permettent de gérer de manière des problèmes génériques qui peuvent survenir dans une grande variété de contextes. L’une d’entre elle est la design pattern “observateur”. Il définit deux types d’entités “observables” et “observateur”. Une observable peut être surveillée par plusieurs observateurs. Lorsque l’état de l’observable change, elle notifie alors tous les observateurs liés qui propage alors le changements.

Concrètement, ceci peut correspondre à des éléments d’interface graphique, des capteurs de surveillances (informatique ou physique), des systemes de logs, ou encore des comptes sur des médias sociaux lorsqu’ils postent de nouveaux messages.

(Reference plus complète : https://design-patterns.fr/observateur )

Nous proposons d’appliquer ce patron de conception pour créer un système avec des journaux / chaines youtube (observables, qui publient des articles / videos) auxquels peuvent souscrire des personnes.

  • Créer deux classes Channel (chaîne youtube) et User (suceptibles de s’abonner)

    • Chaque Channel et User a un nom.
    • La classe Channel implémente des méthodes subscribe et unsubscribe qui ajoutent/enlèvent un compte observateur donné en argument. On introduira également un attribut dans User qui liste les vidéos auxquel un compte est abonné et qui est modifié par les appel de subscribe et unsubscribe.
    • La classe Channel implémente aussi une méthode notifySubscribers qui appelle compte.actualiser() pour chaque compte abonné de la chaîne. Pour le moment, la méthode actualiser de la classe User ne fait rien (pass)
  • Ajoutons une méthode publish à la classe Channel qui permet d’ajouter une vidéo à la liste de vidéo de la chaíne. Chaque vidéo correspondra uniquement à un titre et une date de publication (gérée avec la librairie datetime). Lorsque la méthode publish est appellée, elle déclenche aussi notifySubscribers.

  • La méthode actualiser de la classe User s’occupe de parcourir toutes les chaines auxquelles le compte est abonné, et de récupérer le titre des 3 vidéos les plus récentes parmis toutes ses chaines. Ces 3 titres (et le nom du channel associé!) sont ensuite écris dans latest_videos_for_{username}.txt.

  • Tester l’ensemble du fonctionnement avec un programme tel que:


arte = Channel("ARTE")
cestpassorcier = Channel("c'est pas sorcier")
videodechat = Channel("video de chat")

alice = User("alice")
bob = User("bob")
charlie = User("charlie")

arte.subscribe(alice)
cestpassorcier.subscribe(alice)
cestpassorcier.subscribe(bob)
videodechat.subscribe(bob)
videodechat.subscribe(charlie)

cestpassorcier.publish("Le système solaire")
arte.publish("La grenouille, un animal extraordinaire")
cestpassorcier.publish("Le génie des fourmis")
videodechat.publish("Video de chat qui fait miaou")
cestpassorcier.publish("Les chateaux forts")

Correction 4.1 - Un paquet pythonique

4.1 Utiliser les syntaxes de liste sur la classe Paquet

  • Plutôt que d’utiliser len(mon_paquet.cartes) pour avoir le nombre de carte on voudrait utiliser len(mon_paquet). Implémentez la méthode spéciale __len__ pour renvoyer la longueur du paquet. Profitez-en pour empêcher que les utilisateurs de la classe modifient directement le paquet en rendant l’attribut cartes privé. Testez votre programme en mettant à jour le code main.py

  • Maintenant que l’attribut cartes n’est plus censé être accessible hors de la classe, nous avons besoin d’un nouvelle méthode pour accéder à une carte du paquet depuis le programme principal. Implémentez la méthode spéciale __getitem__ pour pouvoir accéder à une carte avec mon_paquet[position]. Tester la dans le programme principal.

  • Notre Paquet ressemble maintenant beaucoup à une véritable liste python. Essayez dans le main.py d’utiliser la méthode shuffle classique de Python pour mélanger un paquet de carte : Il manque quelque chose.

  • Dans l’interpréteur (python3 ou ipython3) affichez la liste des méthode de la classe paquet en utilisant dir(). Les méthodes en python sont assignées dynamiquement aux classes et peuvent être modifiées au fur et à mesure du programme. Ajoutons une méthode __setitem__ directement depuis l’interpréteur (démo). Affichez à nouveau le dictionnaire dir() de mon_paquet pour voir la nouvelle méthode ajoutée.

  • Ajoutez maintenant __setitem__ dans le code de Paquet. Supprimez et remplacez la méthode melanger par shuffle dans le code du projet.

`carte.py`
`paquet.py`
`main.py`

Correction 4.2 - Un itérateur de cartes

4.2 Itérateurs de carte : génération de la suite de carte à partir d’une carte

Plutôt que de générer les 52 cartes avec une boucle for dans le constructeur du paquet on voudrait utiliser un générateur/itérateur associé à la classe carte.

  • Ajoutez à carte.py une classe IterateurDeCarte pour générer la suite des cartes à partir d’un objet carte.

    1. D’abord créez la classe IterateurDeCarte qui prend en argument une Carte à la création et qui possède des méthodes __next__(self) qui retourne la carte suivante dans l’ordre des cartes et __iter__ qui lui permet de se renvoyer lui même pour continuer l’itération.
    2. Ajoutez une méthode __iter__ à la classe carte qui renvoie un itérateur basée sur la carte courante.
  • Générez les 52 cartes du paquet à partir de notre iterateur de carte.

  • Ajoutez un paramètre facultatif carte_de_départ au contructeur de paquet pour commencer la génération du paquet à partie d’une carte du milieu de la série de carte possible.

  • Modifiez le constructeur de la classe Carte pour qu’elle prenne en argument des valeurs et couleurs possibles qui ne soit pas les valeurs classique. Testez cette fonctionnalité dans main.py en générant un jeu de “UNO” (sans les cartes “Joker” noire) à la place d’un jeu classique. Cartes de Uno

`carte.py`
`paquet.py`
`main.py`

Bonus : d’autres générateurs de carte

Les listes sont des collections finies et les itérateurs de liste sont donc toujours finis. Cependant un itérateur n’a pas de taille en général et peut parfois générer indéfiniment des valeurs (grace à un générateur infini par exemple).

  • Modifiez l’itérateur de carte pour qu’elle se base sur un générateur de carte aléatoire infini.

Correction 4.4 - Application du design pattern observateur

4.4 Design patterns ‘Observateur’ appliquée aux chaînes Youtube

Les design patterns sont des patrons de conception qui permettent de gérer de manière des problèmes génériques qui peuvent survenir dans une grande variété de contextes. L’une d’entre elle est la design pattern “observateur”. Il définit deux types d’entités “observables” et “observateur”. Une observable peut être surveillée par plusieurs observateurs. Lorsque l’état de l’observable change, elle notifie alors tous les observateurs liés qui propage alors le changements.

Concrètement, ceci peut correspondre à des éléments d’interface graphique, des capteurs de surveillances (informatique ou physique), des systemes de logs, ou encore des comptes sur des médias sociaux lorsqu’ils postent de nouveaux messages.

(Reference plus complète : https://design-patterns.fr/observateur )

Nous proposons d’appliquer ce patron de conception pour créer un système avec des journaux / chaines youtube (observables, qui publient des articles / videos) auxquels peuvent souscrire des personnes.

  • Créer deux classes Channel (chaîne youtube) et User (suceptibles de s’abonner)

    • Chaque Channel et User a un nom.
    • La classe Channel implémente des méthodes subscribe et unsubscribe qui ajoutent/enlèvent un compte observateur donné en argument. On introduira également un attribut dans User qui liste les vidéos auxquel un compte est abonné et qui est modifié par les appel de subscribe et unsubscribe.
    • La classe Channel implémente aussi une méthode notifySubscribers qui appelle compte.actualiser() pour chaque compte abonné de la chaîne. Pour le moment, la méthode actualiser de la classe User ne fait rien (pass)
  • Ajoutons une méthode publish à la classe Channel qui permet d’ajouter une vidéo à la liste de vidéo de la chaíne. Chaque vidéo correspondra uniquement à un titre et une date de publication (gérée avec la librairie datetime). Lorsque la méthode publish est appellée, elle déclenche aussi notifySubscribers.

  • La méthode actualiser de la classe User s’occupe de parcourir toutes les chaines auxquelles le compte est abonné, et de récupérer le titre des 3 vidéos les plus récentes parmis toutes ses chaines. Ces 3 titres (et le nom du channel associé!) sont ensuite écris dans latest_videos_for_{username}.txt.

  • Tester l’ensemble du fonctionnement avec un programme tel que:


arte = Channel("ARTE")
cestpassorcier = Channel("c'est pas sorcier")
videodechat = Channel("video de chat")

alice = User("alice")
bob = User("bob")
charlie = User("charlie")

arte.subscribe(alice)
cestpassorcier.subscribe(alice)
cestpassorcier.subscribe(bob)
videodechat.subscribe(bob)
videodechat.subscribe(charlie)

cestpassorcier.publish("Le système solaire")
arte.publish("La grenouille, un animal extraordinaire")
cestpassorcier.publish("Le génie des fourmis")
videodechat.publish("Video de chat qui fait miaou")
cestpassorcier.publish("Les chateaux forts")
correction

Bibliographie

Bibliographie

Livres

  • Apprendre la programmation avec Python 3 (plutôt complet et orienté débutant)
  • Fluent Python (Ce que pythonique veut dire, comment utiliser Python proprement)
  • Serious Python (problématiques avancées de développement)

Tutoriels

  • Flask Mega Tutorial très long et développé : tutoriel pour coder une application web d’assez grande taille en Python de façon réaliste et illustrant pleins de point du travail de développeur et d’architecture d’application Python :
    • Bases de données
    • Structuration en package
    • Testing
    • Distribution et déploiement de l’application

Articles

Sites de références

Évènements Python

Docker

0 - Introduction à Docker

Modularisez et maîtrisez vos applications


Introduction

  • La métaphore docker : “box it, ship it”

  • Une abstraction qui ouvre de nouvelles possibilités pour la manipulation logicielle.

  • Permet de standardiser et de contrôler la livraison et le déploiement.

Retour sur les technologies de virtualisation

On compare souvent les conteneurs aux machines virtuelles. Mais ce sont de grosses simplifications parce qu’on en a un usage similaire : isoler des programmes dans des “contextes”. Une chose essentielle à retenir sur la différence technique : les conteneurs utilisent les mécanismes internes du _kernel de l’OS Linux_ tandis que les VM tentent de communiquer avec l’OS (quel qu’il soit) pour directement avoir accès au matériel de l’ordinateur.

  • VM : une abstraction complète pour simuler des machines

    • un processeur, mémoire, appels systèmes, carte réseau, carte graphique, etc.
  • conteneur : un découpage dans Linux pour séparer des ressources (accès à des dossiers spécifiques sur le disque, accès réseau).

Les deux technologies peuvent utiliser un système de quotas pour l’accès aux ressources matérielles (accès en lecture/écriture sur le disque, sollicitation de la carte réseau, du processeur)

Si l’on cherche la définition d’un conteneur :

C’est un groupe de processus associé à un ensemble de permissions.

L’imaginer comme une “boîte” est donc une allégorie un peu trompeuse, car ce n’est pas de la virtualisation (= isolation au niveau matériel).


Docker Origins : genèse du concept de conteneur

Les conteneurs mettent en œuvre un vieux concept d’isolation des processus permis par la philosophie Unix du “tout est fichier”.

chroot, jail, les 6 namespaces et les cgroups

chroot

  • Implémenté principalement par le programme chroot [change root : changer de racine], présent dans les systèmes UNIX depuis longtemps (1979 !) :

    “Comme tout est fichier, changer la racine d’un processus, c’est comme le faire changer de système”.

jail

  • jail est introduit par FreeBSD en 2002 pour compléter chroot et qui permet pour la première fois une isolation réelle (et sécurisée) des processus.

  • chroot ne s’occupait que de l’isolation d’un process par rapport au système de fichiers :

    • ce n’était pas suffisant, l’idée de “tout-est-fichier” possède en réalité plusieurs exceptions
    • un process chrooté n’est pas isolé du reste des process et peut agir de façon non contrôlée sur le système sur plusieurs aspects
  • En 2005, Sun introduit les conteneurs Solaris décrits comme un « chroot sous stéroïdes » : comme les jails de FreeBSD

Les namespaces (espaces de noms)

  • Les namespaces, un concept informatique pour parler simplement de…

    • groupes séparés auxquels on donne un nom, d’ensembles de choses sur lesquelles on colle une étiquette
    • on parle aussi de contextes
  • jail était une façon de compléter chroot, pour FreeBSD.

  • Pour Linux, ce concept est repris via la mise en place de namespaces Linux

    • Les namespaces sont inventés en 2002
    • popularisés lors de l’inclusion des 6 types de namespaces dans le noyau Linux (3.8) en 2013
  • Les conteneurs ne sont finalement que plein de fonctionnalités Linux saucissonnées ensemble de façon cohérente.

  • Les namespaces correspondent à autant de types de compartiments nécessaires dans l’architecture Linux pour isoler des processus.

Pour la culture, 6 types de namespaces :

  • Les namespaces PID : “fournit l’isolation pour l’allocation des identifiants de processus (PIDs), la liste des processus et de leurs détails. Tandis que le nouvel espace de nom est isolé de ses adjacents, les processus dans son espace de nommage « parent » voient toujours tous les processus dans les espaces de nommage enfants — quoique avec des numéros de PID différent.”
  • Network namespace : “isole le contrôleur de l’interface réseau (physique ou virtuel), les règles de pare-feu iptables, les tables de routage, etc.”
  • Mount namespace : “permet de créer différents modèles de systèmes de fichiers, ou de créer certains points de montage en lecture-seule”
  • User namespace : isolates the user IDs between namespaces (dernière pièce du puzzle)
  • “UTS” namespace : permet de changer le nom d’hôte.
  • IPC namespace : isole la communication inter-processus entre les espaces de nommage.

Les cgroups : derniers détails pour une vraie isolation

  • Après, il reste à s’occuper de limiter la capacité d’un conteneur à agir sur les ressources matérielles :

    • usage de la mémoire
    • du disque
    • du réseau
    • des appels système
    • du processeur (CPU)
  • En 2005, Google commence le développement des cgroups : une façon de tagger les demandes de processeur et les appels systèmes pour les grouper et les isoler.


Exemple : bloquer le système hôte depuis un simple conteneur

:(){ : | :& }; :

Ceci est une fork bomb. Dans un conteneur non privilégié, on bloque tout Docker, voire tout le système sous-jacent, en l’empêchant de créer de nouveaux processus.

Pour éviter cela il faudrait limiter la création de processus via une option kernel.

Ex: docker run -it --ulimit nproc=3 --name fork-bomb bash

L’isolation des conteneurs n’est donc ni magique, ni automatique, ni absolue ! Correctement paramétrée, elle est tout de même assez robuste, mature et testée.


Les conteneurs : définition

On revient à notre définition d’un conteneur :

Un conteneur est un groupe de processus associé à un ensemble de permissions sur le système.

1 container = 1 groupe de process Linux

  • des namespaces (séparation entre ces groups)
  • des cgroups (quota en ressources matérielles)

LXC (LinuX Containers)

  • En 2008 démarre le projet LXC qui chercher à rassembler :

    • les cgroups
    • le chroot
    • les namespaces.
  • Originellement, Docker était basé sur LXC. Il a depuis développé son propre assemblage de ces 3 mécanismes.


Docker et LXC

  • En 2013, Docker commence à proposer une meilleure finition et une interface simple qui facilite l’utilisation des conteneurs LXC.

  • Puis il propose aussi son cloud, le Docker Hub pour faciliter la gestion d’images toutes faites de conteneurs.

  • Au fur et à mesure, Docker abandonne le code de LXC (mais continue d’utiliser le chroot, les cgroups et namespaces).

  • Le code de base de Docker (notamment runC) est open source : l'Open Container Initiative vise à standardiser et rendre robuste l’utilisation de containers.


Bénéfices par rapport aux machines virtuelles

Docker permet de faire des “quasi-machines” avec des performances proches du natif.

  • Vitesse d’exécution.
  • Flexibilité sur les ressources (mémoire partagée).
  • Moins complexe que la virtualisation
  • Plus standard que les multiples hyperviseurs
    • notamment moins de bugs d’interaction entre l’hyperviseur et le noyau

Bénéfices par rapport aux machines virtuelles

VM et conteneurs proposent une flexibilité de manipulation des ressources de calcul mais les machines virtuelles sont trop lourdes pour être multipliées librement :

  • elles ne sont pas efficaces pour isoler chaque application
  • elles ne permettent pas la transformation profonde que permettent les conteneurs :
    • le passage à une architecture microservices
    • et donc la scalabilité pour les besoins des services cloud

Avantages des machines virtuelles

  • Les VM se rapprochent plus du concept de “boite noire”: l’isolation se fait au niveau du matériel et non au niveau du noyau de l’OS.

  • même si une faille dans l’hyperviseur reste possible car l’isolation n’est pas qu’uniquement matérielle

  • Les VM sont-elles “plus lentes” ? Pas forcément.

    • La RAM est-elle un facteur limite ? Non elle n’est pas cher
    • Les CPU pareil : on est rarement bloqués par la puissance du CPU
    • Le vrai problème c’est l’I/O : l’accès en entrée-sortie au disque et au réseau
      • en réalité Docker peut être bien plus lent pour l’implémentation de la sécurité réseau (usage du NAT et du bridging)
      • pareil pour l’accès au disque : la technologie d'overlay (qui a une place centrale dans Docker) s’améliore mais reste lente.

La comparaison VM / conteneurs est un thème extrêmement vaste et complexe.


Pourquoi utiliser Docker ?

Docker est pensé dès le départ pour faire des conteneurs applicatifs :

  • isoler les modules applicatifs.

  • gérer les dépendances en les embarquant dans le conteneur.

  • se baser sur l'immutabilité : la configuration d’un conteneur n’est pas faite pour être modifiée après sa création.

  • avoir un cycle de vie court -> logique DevOps du “bétail vs. animal de compagnie”


Pourquoi utiliser Docker ?

Docker modifie beaucoup la “logistique” applicative.

  • uniformisation face aux divers langages de programmation, configurations et briques logicielles

  • installation sans accroc et automatisation beaucoup plus facile

  • permet de simplifier l'intégration continue, la livraison continue et le déploiement continu

  • rapproche le monde du développement des opérations (tout le monde utilise la même technologie)

  • Permet l’adoption plus large de la logique DevOps (notamment le concept d’infrastructure as code)


Infrastructure as Code

Résumé

  • on décrit en mode code un état du système. Avantages :
    • pas de dérive de la configuration et du système (immutabilité)
    • on peut connaître de façon fiable l’état des composants du système
    • on peut travailler en collaboration plus facilement (grâce à Git notamment)
    • on peut faire des tests
    • on facilite le déploiement de nouvelles instances

Docker : positionnement sur le marché

  • Docker est la technologie ultra-dominante sur le marché de la conteneurisation

    • La simplicité d’usage et le travail de standardisation (un conteneur Docker est un conteneur OCI : format ouvert standardisé par l’Open Container Initiative) lui garantissent légitimité et fiabilité
    • La logique du conteneur fonctionne, et la bonne documentation et l’écosystème aident !
  • LXC existe toujours et est très agréable à utiliser, notamment avec LXD (développé par Canonical, l’entreprise derrière Ubuntu).

    • Il a cependant un positionnement différent : faire des conteneurs pour faire tourner des OS Linux complets.
  • Apache Mesos : un logiciel de gestion de cluster qui permet de se passer de Docker, mais propose quand même un support pour les conteneurs OCI (Docker) depuis 2016.

  • Podman : une alternative à Docker qui utilise la même syntaxe que Docker pour faire tourner des conteneurs OCI (Docker) qui propose un mode rootless et daemonless intéressant.

  • systemd-nspawn : technologie de conteneurs isolés proposée par systemd


1 - Manipulation des conteneurs

Terminologie et concepts fondamentaux

Deux concepts centraux :

  • Une image : un modèle pour créer un conteneur
  • Un conteneur : l’instance qui tourne sur la machine.

Autres concepts primordiaux :

  • Un volume : un espace virtuel pour gérer le stockage d’un conteneur et le partage entre conteneurs.
  • un registry : un serveur ou stocker des artefacts docker c’est à dire des images versionnées.
  • un orchestrateur : un outil qui gère automatiquement le cycle de vie des conteneurs (création/suppression).

Visualiser l’architecture Docker

Daemon - Client - images - registry


L’écosystème Docker

  • Docker Compose : Un outil pour décrire des applications multiconteneurs.

  • Docker Machine : Un outil pour gérer le déploiement Docker sur plusieurs machines depuis un hôte.

  • Docker Hub : Le service d’hébergement d’images proposé par Docker Inc. (le registry officiel)


L’environnement de développement

  • Docker Engine pour lancer des commandes docker

  • Docker Compose pour lancer des application multiconteneurs

  • Portainer, un GUI Docker

  • VirtualBox pour avoir une VM Linux quand on est sur Windows


Installer Docker sur Windows ou MacOS

Docker est basé sur le noyau Linux :

  • En production il fonctionne nécessairement sur un Linux (virtualisé ou bare metal)
  • Pour développer et déployer, il marche parfaitement sur MacOS et Windows mais avec une méthode de virtualisation :
    • virtualisation optimisée via un hyperviseur
    • ou virtualisation avec logiciel de virtualisation “classique” comme VMWare ou VirtualBox.

Installer Docker sur Windows

Quatre possibilités :

  • Solution WSL2 : on utilise Docker Desktop WSL2:

    • Fonctionne avec Windows Subsystem for Linux : c’est une VM Linux très bien intégrée à Windows
    • Le meilleur des deux mondes ?
    • Workflow similaire à celui d’un serveur Linux
  • Solution Windows : on utilise Docker Desktop for Windows:

    • Fonctionne avec Hyper-V (l’hyperviseur optimisé de Windows)
    • Casse VirtualBox/VMWare (incompatible avec la virtualisation logicielle)
    • Proche du monde Windows et de PowerShell
  • Solution VirtualBox : on utilise Docker Engine dans une VM Linux

    • Utilise une VM Linux avec VirtualBox
    • Workflow identique à celui d’un serveur Linux
    • Proche de la réalité de l’administration système actuelle
  • Solution legacy : on utilise Docker Toolbox pour configurer Docker avec le driver VirtualBox :

    • Change légèrement le workflow par rapport à la version Linux native
    • Marche sur les “vieux” Windows (sans hyperviseur)
    • Utilise une VM Linux avec bash

Installer Docker sous MacOS

  • Solution standard : on utilise Docker Desktop for MacOS (fonctionne avec la bibliothèque HyperKit qui fait de l’hypervision)
  • Solution Virtualbox / legacy : On utilise une VM Linux

Installer Docker sur Linux

Pas de virtualisation nécessaire car Docker (le Docker Engine) utilise le noyau du système natif.

  • On peut l’installer avec le gestionnaire de paquets de l’OS mais cette version peut être trop ancienne.

  • Sur Ubuntu ou CentOS la méthode conseillée est d’utiliser les paquets fournis dans le dépôt officiel Docker (vous pouvez avoir des surprises avec la version snap d’Ubuntu).


Les images et conteneurs

Les images

Docker possède à la fois un module pour lancer les applications (runtime) et un outil de build d’application.

  • Une image est le résultat d’un build :
    • on peut la voir un peu comme une boîte “modèle” : on peut l’utiliser plusieurs fois comme base de création de containers identiques, similaires ou différents.

Pour lister les images on utilise :

docker images
docker image ls

Les conteneurs

  • Un conteneur est une instance en cours de fonctionnement (“vivante”) d’une image.
    • un conteneur en cours de fonctionnement est un processus (et ses processus enfants) qui tourne dans le Linux hôte (mais qui est isolé de celui-ci)

Commandes Docker

Docker fonctionne avec des sous-commandes et propose de grandes quantités d’options pour chaque commande.

Utilisez --help au maximum après chaque commande, sous-commande ou sous-sous-commandes

docker image --help

Pour vérifier l’état de Docker

  • Les commandes de base pour connaître l’état de Docker sont :
docker info  # affiche plein d'information sur l'engine avec lequel vous êtes en contact
docker ps    # affiche les conteneurs en train de tourner
docker ps -a # affiche  également les conteneurs arrêtés

Créer et lancer un conteneur

  • Un conteneur est une instance en cours de fonctionnement (“vivante”) d’une image.
docker run [-d] [-p port_h:port_c] [-v dossier_h:dossier_c] <image> <commande>

créé et lance le conteneur

  • L’ordre des arguments est important !
  • Un nom est automatiquement généré pour le conteneur à moins de fixer le nom avec --name
  • On peut facilement lancer autant d’instances que nécessaire tant qu’il n’y a pas de collision de nom ou de port.

Options docker run

  • Les options facultatives indiquées ici sont très courantes.
    • -d permet* de lancer le conteneur en mode daemon ou détaché et libérer le terminal
    • -p permet de mapper un port réseau entre l’intérieur et l’extérieur du conteneur, typiquement lorsqu’on veut accéder à l’application depuis l’hôte.
    • -v permet de monter un volume partagé entre l’hôte et le conteneur.
    • --rm (comme remove) permet de supprimer le conteneur dès qu’il s’arrête.
    • -it permet de lancer une commande en mode interactif (un terminal comme bash).
    • -a (ou --attach) permet de se connecter à l’entrée-sortie du processus dans le container.

Commandes Docker

  • Le démarrage d’un conteneur est lié à une commande.

  • Si le conteneur n’a pas de commande, il s’arrête dès qu’il a fini de démarrer

docker run debian # s'arrête tout de suite
  • Pour utiliser une commande on peut simplement l’ajouter à la fin de la commande run.
docker run debian echo 'attendre 10s' && sleep 10 # s'arrête après 10s

Stopper et redémarrer un conteneur

docker run créé un nouveau conteneur à chaque fois.

docker stop <nom_ou_id_conteneur> # ne détruit pas le conteneur
docker start <nom_ou_id_conteneur> # le conteneur a déjà été créé
docker start --attach <nom_ou_id_conteneur> # lance le conteneur et s'attache à la sortie standard

Isolation des conteneurs

  • Les conteneurs sont plus que des processus, ce sont des boîtes isolées grâce aux namespaces et cgroups

  • Depuis l’intérieur d’un conteneur, on a l’impression d’être dans un Linux autonome.

  • Plus précisément, un conteneur est lié à un système de fichiers (avec des dossiers /bin, /etc, /var, des exécutables, des fichiers…), et possède des métadonnées (stockées en json quelque part par Docker)

  • Les utilisateurs Unix à l’intérieur du conteneur ont des UID et GID qui existent classiquement sur l’hôte mais ils peuvent correspondre à un utilisateur Unix sans droits sur l’hôte si on utilise les user namespaces.


Introspection de conteneur

  • La commande docker exec permet d’exécuter une commande à l’intérieur du conteneur s’il est lancé.

  • Une utilisation typique est d’introspecter un conteneur en lançant bash (ou sh).

docker exec -it <conteneur> /bin/bash

Docker Hub : télécharger des images

Une des forces de Docker vient de la distribution d’images :

  • pas besoin de dépendances, on récupère une boîte autonome

  • pas besoin de multiples versions en fonction des OS

Dans ce contexte un élément qui a fait le succès de Docker est le Docker Hub : hub.docker.com

Il s’agit d’un répertoire public et souvent gratuit d’images (officielles ou non) pour des milliers d’applications pré-configurées.


Docker Hub:

  • On peut y chercher et trouver presque n’importe quel logiciel au format d’image Docker.

  • Il suffit pour cela de chercher l’identifiant et la version de l’image désirée.

  • Puis utiliser docker run [<compte>/]<id_image>:<version>

  • La partie compte est le compte de la personne qui a poussé ses images sur le Docker Hub. Les images Docker officielles (ubuntu par exemple) ne sont pas liées à un compte : on peut écrire simplement ubuntu:focal.

  • On peut aussi juste télécharger l’image : docker pull <image>

On peut également y créer un compte gratuit pour pousser et distribuer ses propres images, ou installer son propre serveur de distribution d’images privé ou public, appelé registry.


En résumé

TP 1 - Installer Docker et jouer avec

Premier TD : on installe Docker et on joue avec

Installer Docker sur la VM Ubuntu dans Guacamole

  • Accédez à votre VM via l’interface Guacamole

  • Pour accéder au copier-coller de Guacamole, il faut appuyer sur Ctrl+Alt+Shift et utiliser la zone de texte qui s’affiche (réappuyer sur Ctrl+Alt+Shift pour revenir à la VM).

  • Pour installer Docker, suivez la documentation officielle pour installer Docker sur Ubuntu, depuis “Install using the repository” jusqu’aux deux commandes sudo apt-get update et sudo apt-get install docker-ce docker-ce-cli containerd.io.

    • Docker nous propose aussi une installation en une ligne (one-liner), moins sécurisée : curl -sSL https://get.docker.com | sudo sh
  • Lancez sudo docker run hello-world. Bien lire le message renvoyé (le traduire sur Deepl si nécessaire). Que s’est-il passé ?

  • Il manque les droits pour exécuter Docker sans passer par sudo à chaque fois.

    • Le daemon tourne toujours en root
    • Un utilisateur ne peut accéder au client que s’il est membre du groupe docker
    • Ajoutez-le au groupe avec la commande usermod -aG docker <user> (en remplaçant <user> par ce qu’il faut)
    • Pour actualiser la liste de groupes auquel appartient l’utilisateur, redémarrez la VM avec sudo reboot puis reconnectez-vous avec Guacamole pour que la modification sur les groupes prenne effet.

Autocomplétion

  • Pour vous faciliter la vie, ajoutez le plugin autocomplete pour Docker et Docker Compose à bash en copiant les commandes suivantes :
sudo apt update
sudo apt install bash-completion curl
sudo mkdir /etc/bash_completion.d/
sudo curl -L https://raw.githubusercontent.com/docker/docker-ce/master/components/cli/contrib/completion/bash/docker -o /etc/bash_completion.d/docker.sh
sudo curl -L https://raw.githubusercontent.com/docker/compose/1.24.1/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose

Important: Vous pouvez désormais appuyer sur la touche pour utiliser l’autocomplétion quand vous écrivez des commandes Docker


Pour vérifier l’installation

  • Les commandes de base pour connaître l’état de Docker sont :
docker info  # affiche plein d'information sur l'engine avec lequel vous êtes en contact
docker ps    # affiche les conteneurs en train de tourner
docker ps -a # affiche  également les conteneurs arrêtés

Manipuler un conteneur

Mentalité : Il faut aussi prendre l’habitude de bien lire ce que la console indique après avoir passé vos commandes.

Avec l’aide du support et de --help, et en notant sur une feuille ou dans un fichier texte les commandes utilisées :

  • Lancez simplement un conteneur Debian en mode attached. Que se passe-t-il ?
Résultat :
  • Lancez un conteneur Debian (docker run puis les arguments nécessaires, cf. l’aide --help) en mode détaché avec la commande echo "Debian container". Rien n’apparaît. En effet en mode détaché la sortie standard n’est pas connectée au terminal.

  • Lancez docker logs avec le nom ou l’id du conteneur. Vous devriez voir le résultat de la commande echo précédente.

Résultat :
  • Affichez la liste des conteneurs en cours d’exécution
Solution :
  • Affichez la liste des conteneurs en cours d’exécution et arrêtés.
Solution :
  • Lancez un conteneur debian en mode détaché avec la commande sleep 3600

  • Réaffichez la liste des conteneurs qui tournent

  • Tentez de stopper le conteneur, que se passe-t-il ?

docker stop <conteneur>

NB: On peut désigner un conteneur soit par le nom qu’on lui a donné, soit par le nom généré automatiquement, soit par son empreinte (toutes ces informations sont indiquées dans un docker ps ou docker ps -a). L’autocomplétion fonctionne avec les deux noms.

  • Trouvez comment vous débarrasser d’un conteneur récalcitrant (si nécessaire, relancez un conteneur avec la commande sleep 3600 en mode détaché).
Solution :
  • Tentez de lancer deux conteneurs avec le nom debian_container
Solution :

Le nom d’un conteneur doit être unique (à ne pas confondre avec le nom de l’image qui est le modèle utilisé à partir duquel est créé le conteneur).

  • Créez un conteneur avec le nom debian2
docker run debian -d --name debian2 sleep 500
  • Lancez un conteneur debian en mode interactif (options -i -t) avec la commande /bin/bash et le nom debian_interactif.
  • Explorer l’intérieur du conteneur : il ressemble à un OS Linux Debian normal.

Chercher sur Docker Hub

  • Visitez hub.docker.com
  • Cherchez l’image de Nginx (un serveur web), et téléchargez la dernière version (pull).
docker pull nginx
  • Lancez un conteneur Nginx. Notez que lorsque l’image est déjà téléchargée le lancement d’un conteneur est quasi instantané.
docker run --name "test_nginx" nginx

Ce conteneur n’est pas très utile, car on a oublié de configurer un port ouvert.

  • Trouvez un moyen d’accéder quand même au Nginx à partir de l’hôte Docker (indice : quelle adresse IP le conteneur possède-t-il ?).
Solution :
  • Arrêtez le(s) conteneur(s) nginx créé(s).
  • Relancez un nouveau conteneur nginx avec cette fois-ci le port correctement configuré dès le début pour pouvoir visiter votre Nginx en local.
docker run -p 8080:80 --name "test2_nginx" nginx # la syntaxe est : port_hote:port_container
  • En visitant l’adresse et le port associé au conteneur Nginx, on doit voir apparaître des logs Nginx dans son terminal car on a lancé le conteneur en mode attached.
  • Supprimez ce conteneur. NB : On doit arrêter un conteneur avant de le supprimer, sauf si on utilise l’option “-f”.

On peut lancer des logiciels plus ambitieux, comme par exemple Funkwhale, une sorte d’iTunes en web qui fait aussi réseau social :

docker run --name funky_conteneur -p 80:80 funkwhale/all-in-one:1.0.1

Vous pouvez visiter ensuite ce conteneur Funkwhale sur le port 80 (après quelques secondes à suivre le lancement de l’application dans les logs) ! Mais il n’y aura hélas pas de musique dedans :(

Attention à ne jamais lancer deux containers connectés au même port sur l’hôte, sinon cela échouera !

  • Supprimons ce conteneur :
docker rm -f funky_conteneur

Facultatif : Wordpress, MYSQL et les variables d’environnement

  • Lancez un conteneur Wordpress joignable sur le port 8080 à partir de l’image officielle de Wordpress du Docker Hub
  • Visitez ce Wordpress dans le navigateur

Nous pouvons accéder au Wordpress, mais il n’a pas encore de base MySQL configurée. Ce serait un peu dommage de configurer cette base de données à la main. Nous allons configurer cela à partir de variables d’environnement et d’un deuxième conteneur créé à partir de l’image mysql.

Depuis Ubuntu:

  • Il va falloir mettre ces deux conteneurs dans le même réseau (nous verrons plus tarde ce que cela implique), créons ce réseau :
docker network create wordpress
  • Cherchez le conteneur mysql version 5.7 sur le Docker Hub.

  • Utilisons des variables d’environnement pour préciser le mot de passe root, le nom de la base de données et le nom d’utilisateur de la base de données (trouver la documentation sur le Docker Hub).

  • Il va aussi falloir définir un nom pour ce conteneur

Résultat :
  • inspectez le conteneur MySQL avec docker inspect

  • Faites de même avec la documentation sur le Docker Hub pour préconfigurer l’app Wordpress.

  • En plus des variables d’environnement, il va falloir le mettre dans le même réseau, et exposer un port

Solution :
  • regardez les logs du conteneur Wordpress avec docker logs

  • visitez votre app Wordpress et terminez la configuration de l’application : si les deux conteneurs sont bien configurés, on ne devrait pas avoir à configurer la connexion à la base de données

  • avec docker exec, visitez votre conteneur Wordpress. Pouvez-vous localiser le fichier wp-config.php ? Une fois localisé, utilisez docker cp pour le copier sur l’hôte.

Faire du ménage

  • Lancez la commande docker ps -aq -f status=exited. Que fait-elle ?

  • Combinez cette commande avec docker rm pour supprimer tous les conteneurs arrêtés (indice : en Bash, une commande entre les parenthèses de “$()” est exécutée avant et utilisée comme chaîne de caractère dans la commande principale)

Solution :
  • S’il y a encore des conteneurs qui tournent (docker ps), supprimez un des conteneurs restants en utilisant l’autocomplétion et l’option adéquate

  • Listez les images

  • Supprimez une image

  • Que fait la commande docker image prune -a ?

Décortiquer un conteneur

  • En utilisant la commande docker export votre_conteneur -o conteneur.tar, puis tar -C conteneur_decompresse -xvf conteneur.tar pour décompresser un conteneur Docker, explorez (avec l’explorateur de fichiers par exemple) jusqu’à trouver l’exécutable principal contenu dans le conteneur.

Portainer

Portainer est un portail web pour gérer une installation Docker via une interface graphique. Il va nous faciliter la vie.

  • Lancer une instance de Portainer :
docker volume create portainer_data
docker run --detach --name portainer \
    -p 9000:9000 \
    -v portainer_data:/data \
    -v /var/run/docker.sock:/var/run/docker.sock \
    portainer/portainer-ce
  • Remarque sur la commande précédente : pour que Portainer puisse fonctionner et contrôler Docker lui-même depuis l’intérieur du conteneur il est nécessaire de lui donner accès au socket de l’API Docker de l’hôte grâce au paramètre --mount ci-dessus.

  • Visitez ensuite la page http://localhost:9000 ou l’adresse IP publique de votre serveur Docker sur le port 9000 pour accéder à l’interface.

  • il faut choisir l’option “local” lors de la configuration

  • Créez votre user admin et choisir un mot de passe avec le formulaire.

  • Explorez l’interface de Portainer.

  • Créez un conteneur.

2 - Images et conteneurs

Créer une image en utilisant un Dockerfile

  • Jusqu’ici nous avons utilisé des images toutes prêtes.

  • Une des fonctionnalités principales de Docker est de pouvoir facilement construire des images à partir d’un simple fichier texte : le Dockerfile.

Le processus de build Docker

  • Un image Docker ressemble un peu à une VM car on peut penser à un Linux “freezé” dans un état.

  • En réalité c’est assez différent : il s’agit uniquement d’un système de fichier (par couches ou layers) et d’un manifeste JSON (des méta-données).

  • Les images sont créés en empilant de nouvelles couches sur une image existante grâce à un système de fichiers qui fait du union mount.


  • Chaque nouveau build génère une nouvelle image dans le répertoire des images (/var/lib/docker/images) (attention ça peut vite prendre énormément de place)

  • On construit les images à partir d’un fichier Dockerfile en décrivant procéduralement (étape par étape) la construction.

Exemple de Dockerfile :

FROM debian:latest

RUN apt update && apt install htop

CMD ['sleep 1000']
  • La commande pour construire l’image est :
docker build [-t tag] [-f dockerfile] <build_context>
  • généralement pour construire une image on se place directement dans le dossier avec le Dockerfile et les élements de contexte nécessaire (programme, config, etc), le contexte est donc le caractère ., il est obligatoire de préciser un contexte.

  • exemple : docker build -t mondebian .


  • Le Dockerfile est un fichier procédural qui permet de décrire l’installation d’un logiciel (la configuration d’un container) en enchaînant des instructions Dockerfile (en MAJUSCULE).

  • Exemple:

# our base image
FROM alpine:3.5

# Install python and pip
RUN apk add --update py2-pip

# upgrade pip
RUN pip install --upgrade pip

# install Python modules needed by the Python app
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt

# copy files required for the app to run
COPY app.py /usr/src/app/
COPY templates/index.html /usr/src/app/templates/

# tell the port number the container should expose
EXPOSE 5000

# run the application
CMD ["python", "/usr/src/app/app.py"]

Instruction FROM

  • L’image de base à partir de laquelle est construite l’image actuelle.

Instruction RUN

  • Permet de lancer une commande shell (installation, configuration).

Instruction ADD

  • Permet d’ajouter des fichier depuis le contexte de build à l’intérieur du conteneur.
  • Généralement utilisé pour ajouter le code du logiciel en cours de développement et sa configuration au conteneur.

Instruction CMD

  • Généralement à la fin du Dockerfile : elle permet de préciser la commande par défaut lancée à la création d’une instance du conteneur avec docker run. on l’utilise avec une liste de paramètres
CMD ["echo 'Conteneur démarré'"]

Instruction ENTRYPOINT

  • Précise le programme de base avec lequel sera lancé la commande
ENTRYPOINT ["/usr/bin/python3"]

CMD et ENTRYPOINT

  • Ne surtout pas confondre avec RUN qui exécute une commande Bash uniquement pendant la construction de l’image.

L’instruction CMD a trois formes :

  • CMD ["executable","param1","param2"] (exec form, forme à préférer)
  • CMD ["param1","param2"] (combinée à une instruction ENTRYPOINT)
  • CMD command param1 param2 (shell form)

Si l’on souhaite que notre container lance le même exécutable à chaque fois, alors on peut opter pour l’usage d'ENTRYPOINT en combination avec CMD.


Instruction ENV

  • Une façon recommandée de configurer vos applications Docker est d’utiliser les variables d’environnement UNIX, ce qui permet une configuration “au runtime”.

Instruction HEALTHCHECK

HEALTHCHECK permet de vérifier si l’app contenue dans un conteneur est en bonne santé.

HEALTHCHECK CMD curl --fail http://localhost:5000/health || exit 1

Les variables

On peut utiliser des variables d’environnement dans les Dockerfiles. La syntaxe est ${...}. Exemple :

FROM busybox
ENV FOO=/bar
WORKDIR ${FOO}   # WORKDIR /bar
ADD . $FOO       # ADD . /bar
COPY \$FOO /quux # COPY $FOO /quux

Se référer au mode d’emploi pour la logique plus précise de fonctionnement des variables.

Documentation


Lancer la construction

  • La commande pour lancer la construction d’une image est :
docker build [-t <tag:version>] [-f <chemin_du_dockerfile>] <contexte_de_construction>
  • Lors de la construction, Docker télécharge l’image de base. On constate plusieurs téléchargements en parallèle.

  • Il lance ensuite la séquence des instructions du Dockerfile.

  • Observez l’historique de construction de l’image avec docker image history <image>

  • Il lance ensuite la série d’instructions du Dockerfile et indique un hash pour chaque étape.

    • C’est le hash correspondant à un layer de l’image

Les layers et la mise en cache

  • Docker construit les images comme une série de “couches” de fichiers successives.

  • On parle d'Union Filesystem car chaque couche (de fichiers) écrase la précédente.

  • Chaque couche correspond à une instruction du Dockerfile.

  • docker image history <conteneur> permet d’afficher les layers, leur date de construction et taille respectives.

  • Ce principe est au coeur de l'immutabilité des images Docker.

  • Au lancement d’un container, le Docker Engine rajoute une nouvelle couche de filesystem “normal” read/write par dessus la pile des couches de l’image.

  • docker diff <container> permet d’observer les changements apportés au conteneur depuis le lancement.


Optimiser la création d’images

  • Les images Docker ont souvent une taille de plusieurs centaines de mégaoctets voire parfois gigaoctets. docker image ls permet de voir la taille des images.

  • Or, on construit souvent plusieurs dizaines de versions d’une application par jour (souvent automatiquement sur les serveurs d’intégration continue).

    • L’espace disque devient alors un sérieux problème.
  • Le principe de Docker est justement d’avoir des images légères car on va créer beaucoup de conteneurs (un par instance d’application/service).

  • De plus on télécharge souvent les images depuis un registry, ce qui consomme de la bande passante.

La principale bonne pratique dans la construction d’images est de limiter leur taille au maximum.


Limiter la taille d’une image

  • Choisir une image Linux de base minimale:

    • Une image ubuntu complète pèse déjà presque une soixantaine de mégaoctets.
    • mais une image trop rudimentaire (busybox) est difficile à débugger et peu bloquer pour certaines tâches à cause de binaires ou de bibliothèques logicielles qui manquent (compilation par exemple).
    • Souvent on utilise des images de base construites à partir de alpine qui est un bon compromis (6 mégaoctets seulement et un gestionnaire de paquets apk).
    • Par exemple python3 est fourni en version python:alpine (99 Mo), python:3-slim (179 Mo) et python:latest (918 Mo).

Les multi-stage builds

Quand on tente de réduire la taille d’une image, on a recours à un tas de techniques. Avant, on utilisait deux Dockerfile différents : un pour la version prod, léger, et un pour la version dev, avec des outils en plus. Ce n’était pas idéal. Par ailleurs, il existe une limite du nombre de couches maximum par image (42 layers). Souvent on enchaînait les commandes en une seule pour économiser des couches (souvent, les commandes RUN et ADD), en y perdant en lisibilité.

Maintenant on peut utiliser les multistage builds.

Avec les multi-stage builds, on peut utiliser plusieurs instructions FROM dans un Dockerfile. Chaque instruction FROM utilise une base différente. On sélectionne ensuite les fichiers intéressants (des fichiers compilés par exemple) en les copiant d’un stage à un autre.

Exemple de Dockerfile utilisant un multi-stage build :

FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

Créer des conteneurs personnalisés

  • Il n’est pas nécessaire de partir d’une image Linux vierge pour construire un conteneur.

  • On peut utiliser la directive FROM avec n’importe quelle image.

  • De nombreuses applications peuvent être configurées en étendant une image officielle

  • Exemple : une image Wordpress déjà adaptée à des besoins spécifiques.

  • L’intérêt ensuite est que l’image est disponible préconfigurée pour construire ou mettre à jour une infrastructure, ou lancer plusieurs instances (plusieurs containers) à partir de cette image.

  • C’est grâce à cette fonctionnalité que Docker peut être considéré comme un outil d'infrastructure as code.

  • On peut également prendre une sorte de snapshot du conteneur (de son système de fichiers, pas des processus en train de tourner) sous forme d’image avec docker commit <image> et docker push.


Publier des images vers un registry privé

  • Généralement les images spécifiques produites par une entreprise n’ont pas vocation à finir dans un dépôt public.

  • On peut installer des registries privés.

  • On utilise alors docker login <adresse_repo> pour se logger au registry et le nom du registry dans les tags de l’image.

  • Exemples de registries :

    • Gitlab fournit un registry très intéressant car intégré dans leur workflow DevOps.

TP 2 - Images et conteneurs

Découverte d’une application web flask

  • Récupérez d’abord une application Flask exemple en la clonant :
git clone https://github.com/uptime-formation/microblog/
  • Ouvrez VSCode avec le dossier microblog en tapant code microblog ou bien en lançant VSCode avec code puis en cliquant sur Open Folder.

  • Dans VSCode, vous pouvez faire Terminal > New Terminal pour obtenir un terminal en bas de l’écran.

  • Observons ensemble le code dans VSCode.

Passons à Docker

Déployer une application Flask manuellement à chaque fois est relativement pénible. Pour que les dépendances de deux projets Python ne se perturbent pas, il faut normalement utiliser un environnement virtuel virtualenv pour séparer ces deux apps. Avec Docker, les projets sont déjà isolés dans des conteneurs. Nous allons donc construire une image de conteneur pour empaqueter l’application et la manipuler plus facilement. Assurez-vous que Docker est installé.

Pour connaître la liste des instructions des Dockerfiles et leur usage, se référer au manuel de référence sur les Dockerfiles.

  • Dans le dossier du projet ajoutez un fichier nommé Dockerfile et sauvegardez-le

  • Normalement, VSCode vous propose d’ajouter l’extension Docker. Il va nous faciliter la vie, installez-le. Une nouvelle icône apparaît dans la barre latérale de gauche, vous pouvez y voir les images téléchargées et les conteneurs existants. L’extension ajoute aussi des informations utiles aux instructions Dockerfile quand vous survolez un mot-clé avec la souris.

  • Ajoutez en haut du fichier : FROM ubuntu:latest Cette commande indique que notre image de base est la dernière version de la distribution Ubuntu.

  • Nous pouvons déjà contruire un conteneur à partir de ce modèle Ubuntu vide : docker build -t microblog .

  • Une fois la construction terminée lancez le conteneur.

  • Le conteneur s’arrête immédiatement. En effet il ne contient aucune commande bloquante et nous n’avons précisé aucune commande au lancement. Pour pouvoir observer le conteneur convenablement il fautdrait faire tourner quelque chose à l’intérieur. Ajoutez à la fin du fichier la ligne : CMD ["/bin/sleep", "3600"] Cette ligne indique au conteneur d’attendre pendant 3600 secondes comme au TP précédent.

  • Reconstruisez l’image et relancez un conteneur

  • Affichez la liste des conteneurs en train de fonctionner

  • Nous allons maintenant rentrer dans le conteneur en ligne de commande pour observer. Utilisez la commande : docker exec -it <id_du_conteneur> /bin/bash

  • Vous êtes maintenant dans le conteneur avec une invite de commande. Utilisez quelques commandes Linux pour le visiter rapidement (ls, cd…).

  • Il s’agit d’un Linux standard, mais il n’est pas conçu pour être utilisé comme un système complet, juste pour une application isolée. Il faut maintenant ajouter notre application Flask à l’intérieur. Dans le Dockerfile supprimez la ligne CMD, puis ajoutez :

RUN apt-get update -y
RUN apt-get install -y python3-pip
  • Reconstruisez votre image. Si tout se passe bien, poursuivez.

  • Pour installer les dépendances python et configurer la variable d’environnement Flask ajoutez:

COPY ./requirements.txt /requirements.txt
RUN pip3 install -r requirements.txt
ENV FLASK_APP microblog.py
  • Reconstruisez votre image. Si tout se passe bien, poursuivez.

  • Ensuite, copions le code de l’application à l’intérieur du conteneur. Pour cela ajoutez les lignes :

COPY ./ /microblog
WORKDIR /microblog

Cette première ligne indique de copier tout le contenu du dossier courant sur l’hôte dans un dossier /microblog à l’intérieur du conteneur. Nous n’avons pas copié les requirements en même temps pour pouvoir tirer partie des fonctionnalités de cache de Docker, et ne pas avoir à retélécharger les dépendances de l’application à chaque fois que l’on modifie le contenu de l’app.

Puis, dans la 2e ligne, le dossier courant dans le conteneur est déplacé à /.

  • Reconstruisez votre image. Observons que le build recommence à partir de l’instruction modifiée. Les layers précédents avaient été mis en cache par le Docker Engine.

  • Si tout se passe bien, poursuivez.

  • Enfin, ajoutons la section de démarrage à la fin du Dockerfile, c’est un script appelé boot.sh :

CMD ["./boot.sh"]
  • Reconstruisez l’image et lancez un conteneur basé sur l’image en ouvrant le port 5000 avec la commande : docker run -p 5000:5000 microblog

  • Naviguez dans le navigateur à l’adresse localhost:5000 pour admirer le prototype microblog.

  • Lancez un deuxième container cette fois avec : docker run -d -p 5001:5000 microblog

  • Une deuxième instance de l’app est maintenant en fonctionnement et accessible à l’adresse localhost:5001

Docker Hub

  • Avec docker login, docker tag et docker push, poussez l’image microblog sur le Docker Hub. Créez un compte sur le Docker Hub le cas échéant.
Solution :

Améliorer le Dockerfile

Une image plus simple

  • A l’aide de l’image python:3-alpine et en remplaçant les instructions nécessaires (pas besoin d’installer python3-pip car ce programme est désormais inclus dans l’image de base), repackagez l’app microblog en une image taggée microblog:slim ou microblog:light. Comparez la taille entre les deux images ainsi construites.

Faire varier la configuration en fonction de l’environnement

Le serveur de développement Flask est bien pratique pour debugger en situation de développement, mais n’est pas adapté à la production. Nous pourrions créer deux images pour les deux situations mais ce serait aller contre l’impératif DevOps de rapprochement du dev et de la prod.

Pour démarrer l’application, nous avons fait appel à un script de boot boot.sh avec à l’intérieur :

#!/bin/bash

# ...

set -e
if [ "$APP_ENVIRONMENT" = 'DEV' ]; then
    echo "Running Development Server"
    exec flask run -h 0.0.0.0
else
    echo "Running Production Server"
    exec gunicorn -b :5000 --access-logfile - --error-logfile - app_name:app
fi
  • Déclarez maintenant dans le Dockerfile la variable d’environnement APP_ENVIRONMENT avec comme valeur par défaut PROD.

  • Construisez l’image avec build.

  • Puis, grâce aux bons arguments allant avec docker run, lancez une instance de l’app en configuration PROD et une instance en environnement DEV (joignables sur deux ports différents).

  • Avec docker ps ou en lisant les logs, vérifiez qu’il existe bien une différence dans le programme lancé.

Exposer le port

  • Ajoutons l’instruction EXPOSE 5000 pour indiquer à Docker que cette app est censée être accédée via son port 5000.
  • NB : Publier le port grâce à l’option -p port_de_l-hote:port_du_container reste nécessaire, l’instruction EXPOSE n’est là qu’à titre de documentation de l’image.

Dockerfile amélioré

`Dockerfile` final :

L’instruction HEALTHCHECK

HEALTHCHECK permet de vérifier si l’app contenue dans un conteneur est en bonne santé.

  • Dans un nouveau dossier ou répertoire, créez un fichier Dockerfile dont le contenu est le suivant :
FROM python:alpine

RUN apk add curl
RUN pip install flask==0.10.1

ADD /app.py /app/app.py
WORKDIR /app

HEALTHCHECK CMD curl --fail http://localhost:5000/health || exit 1

CMD python app.py
  • Créez aussi un fichier app.py avec ce contenu :
from flask import Flask

healthy = True

app = Flask(__name__)

@app.route('/health')
def health():
    global healthy

    if healthy:
        return 'OK', 200
    else:
        return 'NOT OK', 500

@app.route('/kill')
def kill():
    global healthy
    healthy = False
    return 'You have killed your app.', 200


if __name__ == "__main__":
    app.run(host="0.0.0.0")
  • Observez bien le code Python et la ligne HEALTHCHECK du Dockerfile puis lancez l’app. A l’aide de docker ps, relevez où Docker indique la santé de votre app.

  • Visitez l’URL /kill de votre app dans un navigateur. Refaites docker ps. Que s’est-il passé ?

  • (Facultatif) Rajoutez une instruction HEALTHCHECK au Dockerfile de notre app microblog.


Facultatif : Décortiquer une image

Une image est composée de plusieurs layers empilés entre eux par le Docker Engine et de métadonnées.

  • Affichez la liste des images présentes dans votre Docker Engine.

  • Inspectez la dernière image que vous venez de créez (docker image --help pour trouver la commande)

  • Observez l’historique de construction de l’image avec docker image history <image>

  • Visitons en root (sudo su) le dossier /var/lib/docker/ sur l’hôte. En particulier, image/overlay2/layerdb/sha256/ :

    • On y trouve une sorte de base de données de tous les layers d’images avec leurs ancêtres.
    • Il s’agit d’une arborescence.
  • Vous pouvez aussi utiliser la commande docker save votre_image -o image.tar, et utiliser tar -C image_decompressee/ -xvf image.tar pour décompresser une image Docker puis explorer les différents layers de l’image.

  • Pour explorer la hiérarchie des images vous pouvez installer https://github.com/wagoodman/dive


Facultatif : un Registry privé

  • En récupérant la commande indiquée dans la doc officielle, créez votre propre registry.
  • Puis trouvez comment y pousser une image dessus.
  • Enfin, supprimez votre image en local et récupérez-la depuis votre registry.
Solution :

Facultatif : Faire parler la vache

Créons un nouveau Dockerfile qui permet de faire dire des choses à une vache grâce à la commande cowsay. Le but est de faire fonctionner notre programme dans un conteneur à partir de commandes de type :

  • docker run --rm cowsay Coucou !

  • docker run --rm cowsay -f stegosaurus Yo!

  • docker run --rm cowsay -f elephant-in-snake Un éléphant dans un boa.

  • Doit-on utiliser la commande ENTRYPOINT ou la commande CMD ? Se référer au manuel de référence sur les Dockerfiles si besoin.

  • Pour information, cowsay s’installe dans /usr/games/cowsay.

  • La liste des options (incontournables) de cowsay se trouve ici : https://debian-facile.org/doc:jeux:cowsay

Solution :
  • L’instruction ENTRYPOINT et la gestion des entrées-sorties des programmes dans les Dockerfiles peut être un peu capricieuse et il faut parfois avoir de bonnes notions de Bash et de Linux pour comprendre (et bien lire la documentation Docker).
  • On utilise parfois des conteneurs juste pour qu’ils s’exécutent une fois (pour récupérer le résultat dans la console, ou générer des fichiers). On utilise alors l’option --rm pour les supprimer dès qu’ils s’arrêtent.

Facultatif : Un multi-stage build

Transformez le Dockerfile de l’app dnmonster située à l’adresse suivante pour réaliser un multi-stage build afin d’obtenir l’image finale la plus légère possible : https://github.com/amouat/dnmonster/

La documentation pour les multi-stage builds est à cette adresse : https://docs.docker.com/develop/develop-images/multistage-build/

3 - Volumes et réseaux

Cycle de vie d’un conteneur

  • Un conteneur a un cycle de vie très court: il doit pouvoir être créé et supprimé rapidement même en contexte de production.

Conséquences :

  • On a besoin de mécanismes d’autoconfiguration, en particuler réseau car les IP des différents conteneur changent tout le temps.
  • On ne peut pas garder les données persistantes dans le conteneur.

Solutions :

  • Des réseaux dynamiques par défaut automatiques (DHCP mais surtout DNS automatiques)
  • Des volumes (partagés ou non, distribués ou non) montés dans les conteneurs

Réseau

Gestion des ports réseaux (port mapping)

  • L’instruction EXPOSE dans le Dockerfile informe Docker que le conteneur écoute sur les ports réseau au lancement. L’instruction EXPOSE ne publie pas les ports. C’est une sorte de documentation entre la personne qui construit les images et la personne qui lance le conteneur à propos des ports que l’on souhaite publier.

  • Par défaut les conteneurs n’ouvrent donc pas de port même s’ils sont déclarés avec EXPOSE dans le Dockerfile.

  • Pour publier un port au lancement d’un conteneur, c’est l’option -p <port_host>:<port_guest> de docker run.

  • Instruction port: d’un compose file.


Bridge et overlay

  • Un réseau bridge est une façon de créer un pont entre deux carte réseaux pour construire un réseau à partir de deux.

  • Par défaut les réseaux docker fonctionne en bridge (le réseau de chaque conteneur est bridgé à un réseau virtuel docker)

  • par défaut les adresses sont en 172.0.0.0/8, typiquement chaque hôte définit le bloc d’IP 172.17.0.0/16 configuré avec DHCP.

  • Un réseau overlay est un réseau virtuel privé déployé par dessus un réseau existant (typiquement public). Pour par exemple faire un cloud multi-datacenters.

Le réseau Docker est très automatique

  • Serveur DNS et DHCP intégré dans le “user-defined network” (c’est une solution IPAM)

  • Donne un nom de domaine automatique à chaque conteneur.

  • Mais ne pas avoir peur d’aller voir comment on perçoit le réseau de l’intérieur. Nécessaire pour bien contrôler le réseau.

  • ingress : un loadbalancer automatiquement connecté aux nœuds d’un Swarm. Voir la doc sur les réseaux overlay.

Lier des conteneurs

  • Aujourd’hui il faut utiliser un réseau dédié créé par l’utilisateur (“user-defined bridge network”)

    • avec l’option --network de docker run
    • avec l’instruction networks: dans un docker composer
  • On peut aussi créer un lien entre des conteneurs

    • avec l’option --link de docker run
    • avec l’instruction link: dans un docker composer
    • MAIS cette fonctionnalité est obsolète et déconseillée

Plugins réseaux

Il existe :

  • les réseaux par défaut de Docker
  • plusieurs autres solutions spécifiques de réseau disponibles pour des questions de performance et de sécurité
    • Ex. : Weave Net pour un cluster Docker Swarm
      • fournit une autoconfiguration très simple
      • de la sécurité
      • un DNS qui permet de simuler de la découverte de service
      • Du multicast UDP

Volumes

Les volumes Docker via la sous-commande volume

  • docker volume ls
  • docker volume inspect
  • docker volume prune
  • docker volume create
  • docker volume rm

Bind mounting

Lorsqu’un répertoire hôte spécifique est utilisé dans un volume (la syntaxe -v HOST_DIR:CONTAINER_DIR), elle est souvent appelée bind mounting (“montage lié”). C’est quelque peu trompeur, car tous les volumes sont techniquement “bind mounted”. La particularité, c’est que le point de montage sur l’hôte est explicite plutôt que caché dans un répertoire appartenant à Docker.

Exemple :

docker run -it -v /tmp/data:/data ubuntu /bin/bash

cd /data/
touch testfile
exit

ls /tmp/data/

L’instruction VOLUME dans un Dockerfile

L’instruction VOLUME dans un Dockerfile permet de désigner les volumes qui devront être créés lors du lancement du conteneur. On précise ensuite avec l’option -v de docker run à quoi connecter ces volumes. Si on ne le précise pas, Docker crée quand même un volume Docker au nom généré aléatoirement, un volume “caché”.

Partager des données avec un volume

  • Pour partager des données on peut monter le même volume dans plusieurs conteneurs.

  • Pour lancer un conteneur avec les volumes d’un autre conteneur déjà montés on peut utiliser --volumes-from <container>

  • On peut aussi créer le volume à l’avance et l’attacher après coup à un conteneur.

  • Par défaut le driver de volume est local c’est-à-dire qu’un dossier est créé sur le disque de l’hôte.

docker volume create --driver local \
    --opt type=btrfs \
    --opt device=/dev/sda2 \
    monVolume

Plugins de volumes

On peut utiliser d’autres systèmes de stockage en installant de nouveau plugins de driver de volume. Par exemple, le plugin vieux/sshfs permet de piloter un volume distant via SSH.

Exemples:

  • SSHFS (utilisation d’un dossier distant via SSH)
  • NFS (protocole NFS)
  • BeeGFS (système de fichier distribué générique)
  • Amazon EBS (vendor specific)
  • etc.
docker volume create -d vieux/sshfs -o sshcmd=<sshcmd> -o allow_other sshvolume
docker run -p 8080:8080 -v sshvolume:/path/to/folder --name test someimage

Ou via docker-compose :

volumes:
  sshfsdata:
    driver: vieux/sshfs:latest
    driver_opts:
      sshcmd: "username@server:/location/on/the/server"
      allow_other: ""

Permissions

  • Un volume est créé avec les permissions du dossier préexistant.
FROM debian
RUN groupadd -r graphite && useradd -r -g graphite graphite
RUN mkdir -p /data/graphite && chown -R graphite:graphite /data/graphite
VOLUME /data/graphite
USER graphite
CMD ["echo", "Data container for graphite"]