Cluster Xen sous Debian GNU/Linux avec DRBD, Corosync et Pacemaker

Jean Baptiste FAVRE

février 2009

English version available here: Xen Cluster with Debian GNU/Linux, DRBD, Corosync and Pacemaker

Introduction

Xen est l'une des technologie de virtualisation open-source les plus avancée. Utiliser la virtualisation permet, dans une certaine mesure, de faciliter le déploiement des machines et, ce faisant, d'améliorer la disponibilité des applications. La migration à chaud des machines virtuelles permet, très simplement, de vider une machine physique si, par exemple, on détecte un souci matériel ou si l'on souhaite faire des mises à jour logicielles. Mais, in fine, tout ceci se fait manuellement. N'étant pas à l'abri d'erreurs humaines, il serait plus facile à agréable de faire toutes ces opérations de migration automatiquement. Compliqué ? Pas tellement, suivez le guide...

Principes d'un cluster

D'après Wikipédia, un cluster (ou grappe de serveurs) est un ensemble de techniques consistant à regrouper plusieurs serveurs indépendants, afin de permettre une gestion globale et de dépasser les limitations d'un ordinateur pour :

Les clusters peuvent être de taille très différentes. En fait, le terme cluster peut s'appliquer à 2 serveurs (ou plus), configurés pour fournir le même service.

Définition du cluster Xen

Dans notre exemple, nous aurons 2 serveurs Xen qui serviront à faire tourner un certain nombre de machines virtuelles. Ces 2 machines physiques se répartiront les machines virtuelles. Il faut donc:

Contraintes du cluster Xen

Notre cluster doit être capable de respecter les contraintes suivantes:

Architecture du cluster

Une fois n'est pas coutume, vous aurez droit à un schéma d'architecture ;-)

Architecture du cluster Xen
                    Internet
                   _____|______
                  |___Switch___|
                      |    |
               _______|    |_______
   10.0.1.1/24|                    |10.0.1.2/24
          ____|____            ____|____
         |         |          |         |
         | Dom0 A  |          | Dom0 B  |
         |_________|          |_________|
              |                    |
192.168.1.1/24|                    |192.168.1.2/24
              |____________________|
              DRBD+Cluster+Migration

Les 2 machines considérées disposent chacune de 2 interfaces réseau. L'une servira à accéder à l'extérieur, l'autre sera dédiée à DRBD, au management du cluster et à la migration à chaud des domU.

Installation du cluster

Pour notre cluster, nous utiliserons Corosync et Pacemaker. Le premier est chargé de la gestion des messages au sein du cluster, le second de la gestion des ressources. Ces 2 logiciels sont disponibles dans un dépôt particulier:

Fichier /etc/apt/sources.list.d/corosync-pacemaker.list
deb //people.debian.org/~madkiss/ha lenny main
Installation de corosync et pacemaker
apt-key adv --keyserver pgp.mit.edu --recv-key 1CFA3E8CD7145E30
aptitude update
aptitude install pacemaker

Une fois installé, le cluster doit être configuré. Tout d'abord, nous allons générer et déployer la clef de chiffrement pour les messages du cluster. Cette clef servira notamment à authentifier les nodes.

Configuration initiale de Corosync (sur le dom0 A seulement)
corosync-keygen
scp /etc/corosync/authkey node2:
mv ~/authkey /etc/corosync/authkey
chown root:root /etc/corosync/authkey
chmod 400 /etc/corosync/authkey

Ensuite, il faut configurer Corosync pour qu'il utilise la bonne interface réseau.

Fichier /etc/corosync/corosync.conf
totem {
	version: 2
	token: 3000
	token_retransmits_before_loss_const: 10
	join: 60
	consensus: 4320
	vsftype: none
	max_messages: 20
	clear_node_high_bit: yes
 	secauth: on
 	threads: 0
 	rrp_mode: none
 	interface {
		ringnumber: 0
		bindnetaddr: 192.168.1.0
		mcastaddr: 226.94.1.1
		mcastport: 5405
	}
}
amf {
	mode: disabled
}
service {
 	ver:       0
 	name:      pacemaker
}
aisexec {
        user:   root
        group:  root
}
logging {
    fileline: off
    to_stderr: yes
    to_logfile: no
    to_syslog: yes
	syslog_facility: daemon
        debug: off
        timestamp: on
        logger_subsys {
            subsys: AMF
            debug: off
            tags: enter|leave|trace1|trace2|trace3|trace4|trace6
        }
}

Enfin, on peut démarrer corosync sur les 2 nodes, après avoir activé Corosync à l'aide de l'option Start=yes dans le fichier /etc/default/corosync.

Activation et démarrage du cluster
/etc/init.d/corosync restart

On peut vérifier le bon fonctionnement du cluster à l'aide de la commande suivante crm_mon --one-shot -V.

Contrôle du bon fonctionnement du cluster
crm_mon --one-shot -V
crm_mon[7363]: 2009/07/26_22:05:40 ERROR: unpack_resources: No STONITH resources have been defined
crm_mon[7363]: 2009/07/26_22:05:40 ERROR: unpack_resources: Either configure some or disable STONITH with the stonith-enabled option
crm_mon[7363]: 2009/07/26_22:05:40 ERROR: unpack_resources: NOTE: Clusters with shared data need STONITH to ensure data integrity


============
Last updated: Fri Nov  6 21:03:51 2009
Stack: openais
Current DC: nodeA - partition with quorum
Version: 1.0.7-54d7869bfe3691eb723b1d47810e5585d8246b58
2 Nodes configured, 2 expected votes
0 Resources configured.
============

Online: [ nodeA nodeB ]

Le cluster nous informe qu'aucune ressource STONITH n'est définie et que c'est pourtant super important.

STONITH signifie "Shoot The Other Node In The Head". Derrière ce charmant acronyme se cache l'une des principale fonctionnalités qu'un cluster doit implémenter: la possibilité de tuer un node ou une ressource de manière à être sûr qu'elle ne tourne plus. C'est particulièrement important, par exemple dans le cas où le cluster gère des IP virtuelles. Pour nos domU, c'est également important pour éviter que le même domU ne tourne sur les 2 dom0 simultanément.

Configuration du cluster

Une fois le cluster installé et configuré, il est temps de configurer les options par défaut. Elle sont au nombre de 3:

stonith
Vous le savez maintenant, un système en production ne devrait pas se passer de Stonith. Néanmoins ici, nous ne l'utiliserons pas.
quorum
Mécanisme de vote pour permettre au cluster de déterminer s'il peut travailler, il n'est pas nécessaire pour un cluster de 2 nodes.
ressource default stickiness
Ceci permettra aux ressources de demeurer sur le node sur lequel elle se trouvent après un fail-over. Notamment, ceci les empêchera de revenir automatiquement sur le node en échec, afin de laisser le temps à l'administrateur de réaliser les tests de bon fonctionnement.

Pour appliquer cette configuration, nous allons utiliser le "shell" particulier de Corosync, invoqué à l'aide de la commande crm

Application des options par défaut du cluster
crm
crm(live)# configure
crm(live)configure# property no-quorum-policy=ignore
crm(live)configure# property stonith-enabled=false
crm(live)configure# property default-resource-stickiness=1000
crm(live)configure# commit
crm(live)configure# bye

Une fois ces options configurées, il est temps d'installer Xen.

Configuration de LVM

Notre cluster Xen repose sur l'utilisation de LVM. Là encore, un résumé des commandes LVM est disponible ici:LVM: Logical Volume Manager

Pour la suite, vous devez créer un VG appelé XenHosting. Dans ce VG, vous devez alors créer un LV cluster-ocfs.

Ces opérations doivent bien entendu être réalisées sur les 2 nodes.

Installation de DRBD

L'installation de DRBD est détaillé dans ce document: Installation de DRBD > 8.3.2 sur Debian GNU/Linux Lenny

Configuration de DRBD

Le résumé de la configuration de DRBD est détaillé dans ce document: DRBD: Distributed Replicated Block Device

En l'occurence, il faut tout d'abord mettre en place la configuration globale, puis la ressource DRBD0 qui servira à la configuration centralisée:

Fichier /etc/drbd.conf
global {
        usage-count yes;
        minor-count 16;
}
common {
        syncer {
                rate 33M;
        }
        protocol C;
}
resource cluster-ocfs {
  meta-disk internal;
  device /dev/drbd0;
  disk {
    fencing resource-only;
  }
  handlers {
    fence-peer "/usr/lib/drbd/crm-fence-peer.sh";
    after-resync-target "/usr/lib/drbd/crm-unfence-peer.sh";
  }
  on remus {
    disk /dev/XenHosting/cluster-ocfs;
    address 192.168.1.1:7800;
  }
  on romulus {
    disk /dev/XenHosting/cluster-ocfs;
    address 192.168.1.0:7800;
  }
  net {
    allow-two-primaries;
  }
  startup {
    become-primary-on both;
  }
}

Installation de OCFS2

Il existe plusieurs solution de FS en cluster. Ici, j'ai décidé d'utiliser OCFS2.

Installation de OCFS2
aptitude install ocfs2-tools

Configuration de OCFS2

Une remarque concernant OCFS2. Idéalement, il faudrait gérer OCFS2 via pacemaker. Dans ce cas précis, ce n'est pas le cas (pour l'instant).

Configuration et démarrage de OCFS2
node:
        ip_port = 7777
        ip_address = 192.168.1.1
        number = 0
        name = nodeA
        cluster = XenHosting1

node:
        ip_port = 7777
        ip_address = 192.168.1.2
        number = 1
        name = nodeB
        cluster = XenHosting1

cluster:
        node_count = 2
        name = XenHosting1
#
# This is a configuration file for automatic startup of the O2CB
# driver.  It is generated by running 'dpkg-reconfigure ocfs2-tools'.
# Please use that method to modify this file.
#

# O2CB_ENABLED: 'true' means to load the driver on boot.
O2CB_ENABLED=true

# O2CB_BOOTCLUSTER: If not empty, the name of a cluster to start.
O2CB_BOOTCLUSTER=XenHosting1

# O2CB_HEARTBEAT_THRESHOLD: Iterations before a node is considered dead.
O2CB_HEARTBEAT_THRESHOLD=31

# O2CB_IDLE_TIMEOUT_MS: Time in ms before a network connection is considered dead.
O2CB_IDLE_TIMEOUT_MS=30000

# O2CB_KEEPALIVE_DELAY_MS: Max. time in ms before a keepalive packet is sent.
O2CB_KEEPALIVE_DELAY_MS=2000

# O2CB_RECONNECT_DELAY_MS: Min. time in ms between connection attempts.
O2CB_RECONNECT_DELAY_MS=2000
/etc/init.d/ocfs2 restart
mkfs.ocfs2 /dev/drbd/by-res/cluster-ocfs

Configuration de la resource OCFS2 pour le cluster

Nous n'avons spécifié nulle part de point de montage pour la partition OCFS2 que nous venons de créer. Il y a une bonne raison à celà: c'est pacemaker qui va se charger de tout.

Réfléchissons 2 minutes. OCFS2 n'est là que pour nous éviter d'avoir à copier la configuration Xen sur chaque nœud. La réplication est assurée par DRBD. Il faut donc d'abord démarrer DRBD et ensuite seulement monter le FS sur le point de montage de notre choix. Si DRBD n'est pas démarré ou pas en état Master, alors il ne faut surtout pas monter le FS.

Nous aurons donc besoin:

Here we go:

Configuration de OCFS2 dans le cluster
crm configure
crm(live)configure# primitive Cluster-FS-DRBD ocf:linbit:drbd \
	params drbd_resource="cluster-ocfs" \
	operations $id="Cluster-FS-DRBD-ops" \
	op monitor interval="20" role="Master" timeout="20" \
	op monitor interval="30" role="Slave" timeout="20" \
	meta target-role="started"
crm(live)configure# ms Cluster-FS-DRBD-Master Cluster-FS-DRBD \
	meta resource-stickines="100" master-max="2" notify="true" interleave="true"
crm(live)configure# primitive Cluster-FS-Mount ocf:heartbeat:Filesystem \
	params device="/dev/drbd/by-res/cluster-ocfs" directory="/cluster" fstype="ocfs2" \
	meta target-role="started"
crm(live)configure# clone Cluster-FS-Mount-Clone Cluster-FS-Mount \
	meta interleave="true" ordered="true"
crm(live)configure# order Cluster-FS-After-DRBD inf: \
	Cluster-FS-DRBD-Master:promote \
	Cluster-FS-Mount-Clone:start
crm(live)configure# commit

Si le point de montage existe, vous aurez le plaisir, que dis-je la joie, de voir le FS cluster monté "automagiquement".

Vérification de routine
crm configure show
node nodeA \
	attributes standby="off"
node nodeB \
	attributes standby="off"
primitive Cluster-FS-DRBD ocf:linbit:drbd \
	params drbd_resource="cluster-ocfs" \
	operations $id="Cluster-FS-DRBD-ops" \
	op monitor interval="20" role="Master" timeout="20" \
	op monitor interval="30" role="Slave" timeout="20" \
	meta target-role="started"
primitive Cluster-FS-Mount ocf:heartbeat:Filesystem \
	params device="/dev/drbd/by-res/cluster-ocfs" directory="/cluster" fstype="ocfs2" \
	meta target-role="started"
ms Cluster-FS-DRBD-Master Cluster-FS-DRBD \
	meta resource-stickines="100" master-max="2" notify="true" interleave="true"
clone Cluster-FS-Mount-Clone Cluster-FS-Mount \
	meta interleave="true" ordered="true"
order Cluster-FS-After-DRBD inf: Cluster-FS-DRBD-Master:promote Cluster-FS-Mount-Clone:start
property $id="cib-bootstrap-options" \
	dc-version="1.0.7-54d7869bfe3691eb723b1d47810e5585d8246b58" \
	cluster-infrastructure="openais" \
	last-lrm-refresh="1266431899" \
	node-health-red="0" \
	stonith-enabled="false" \
	no-quorum-policy="ignore" \
	expected-quorum-votes="2"

Installation de Xen

Je ne détaillerai pas les étapes nécessaires dans ce document. Pour plus d'informations, vous pouvez lire: Xen: Virtualisation sous GNU/Debian Linux

Dans tous les cas, ne créez pas encore de domU. Même si la migration d'un système stand-alone vers un système cluster est faisable, il est plus simple de partir de zéro, surtout si vous découvrez les clusters.

Configuration de Xen

En ce qui concerne la configuration de Xen, la configuration du dom0 restera dans /etc/xen/. En revanche, la configuration des domU plus quelques bonus sera centralisée dans /cluster.

Arborescence de configuration
tree /cluster
/cluster
|-- CD_iso
|   |-- centos
|   |   |-- CentOS-5.4-i386-netinstall.iso
|   |   `-- CentOS-5.4-x86_64-netinstall.iso
|   `-- debian
|       |-- debian-503-amd64-netinst.iso
|       |-- debian-504-amd64-netinst.iso
|       `-- debian-504-i386-netinst.iso
|-- apt
|   |-- drbd8-module-2.6.26-2-xen-amd64_8.3.7-0+2.6.26-21lenny3_amd64.deb
|   `-- drbd8-utils_8.3.7-0_amd64.deb
`-- xen
    |-- xps-101.cfg
    |-- xps-102.cfg
    |-- xps-103.cfg
    |-- xps-104.cfg
    `-- xps-105.cfg

Comme vous le voyez, je profite du FS cluster pour stocker des iso de CD d'install (on pourrait ajouter des CD de rescue) ainsi que les paquets debian liés au cluster, comme DRBD. Enfin, la configuration des machines virtuelles est également centralisée.

Afin de ne pas interférer avec le gestionnaire de cluster, il est important de désactiver le démarrage et la sauvegarde automatiques des domU. En effet, même si les domU sont répartis entre les dom0, ceux-ci seront automatiquement migrés d'un dom0 à l'autre lors d'un reboot par exemple.

Pour cela, il faut adapter la configuration de Xen

Fichier /etc/default/xendomains
## Type: string
## Type: string
## Default: /var/lib/xen/save
#
# Directory to save running domains to when the system (dom0) is
# shut down. Will also be used to restore domains from if # XENDOMAINS_RESTORE
# is set (see below). Leave empty to disable domain saving on shutdown
# (e.g. because you rather shut domains down).
# If domain saving does succeed, SHUTDOWN will not be executed.
#
XENDOMAINS_SAVE=

## Type: string
## Default: "--all --halt --wait"
#
# After we have gone over all virtual machines (resp. all automatically
# started ones, see XENDOMAINS_AUTO_ONLY below) in a loop and sent SysRq,
# migrated, saved and/or shutdown according to the settings above, we
# might want to shutdown the virtual machines that are still running
# for some reason or another. To do this, set this variable to
# "--all --halt --wait", it will be passed to xm shutdown.
# Leave it empty not to do anything special here.
# (Note: This will hit all virtual machines, even if XENDOMAINS_AUTO_ONLY
# is set.)
# 
XENDOMAINS_SHUTDOWN_ALL=

## Type: boolean
## Default: true
#
# This variable determines whether saved domains from XENDOMAINS_SAVE
# will be restored on system startup. 
#
XENDOMAINS_RESTORE=false

## Type: string
## Default: /etc/xen/auto
#
# This variable sets the directory where domains configurations
# are stored that should be started on system startup automatically.
# Leave empty if you don't want to start domains automatically
# (or just don't place any xen domain config files in that dir).
# Note that the script tries to be clever if both RESTORE and AUTO are 
# set: It will first restore saved domains and then only start domains
# in AUTO which are not running yet. 
# Note that the name matching is somewhat fuzzy.
#
XENDOMAINS_AUTO=

## Type: boolean
## Default: false
# 
# If this variable is set to "true", only the domains started via config 
# files in XENDOMAINS_AUTO will be treated according to XENDOMAINS_SYSRQ,
# XENDOMAINS_MIGRATE, XENDOMAINS_SAVE, XENDMAINS_SHUTDOWN; otherwise
# all running domains will be. 
# Note that the name matching is somewhat fuzzy.
# 
XENDOMAINS_AUTO_ONLY=true

Enfin, juste avant de redémarrer le node pour que les modifications de la configuration Xen soient prises en compte, il faut:

Désactiver le démarrage de xendomains
update-rc.d -f xendomains remove
Removing any system startup links for /etc/init.d/xendomains ...
   /etc/rc0.d/K20xendomains
   /etc/rc1.d/K20xendomains
   /etc/rc2.d/S20xendomains
   /etc/rc3.d/S20xendomains
   /etc/rc4.d/S20xendomains
   /etc/rc5.d/S20xendomains
   /etc/rc6.d/K20xendomains

Un redémarrage plus tard, vous y êtes. il est temps de configurer notre premier domU.

Configuration d'un domU Xen

Notre exemple reposera sur un domU HVM, c'est-à-dire en virtualisation complète. Ceci permet par exemple de pouvoir virtualiser une machine Windows sur un node Linux.

Fichier /cluster/xen/xps-101.cfg
#
# HVM configuration file
# Linux
#
################################
kernel       = '/usr/lib/xen-3.2-1/boot/hvmloader'
device_model = '/usr/lib/xen-3.2-1/bin/qemu-dm'
builder      = 'hvm'
memory       = '256'
vcpus        = '1'
cpus         = '1'
localtime    = 0
serial       = 'pty'
#
# Parametrage disque
# boot on floppy (a), hard disk (c) or CD-ROM (d) or Network (n)
boot         = 'dcn'
disk         = [ 'phy:/dev/drbd/by-res/xps-101,ioemu:hda,w',
                 'file:/cluster/CD_iso/debian-504-amd64-netinst.iso,hdc:cdrom,r' ]
#
# Parametrage reseau
vif          = [ 'bridge=br-wan,type=ioemu,mac=00:16:3E:01:01:65,ip=10.0.1.101, vifname=xps-101.eth0' ]
#
# Comportement
on_poweroff  = 'destroy'
on_reboot    = 'restart'
on_crash     = 'restart'
extra = "console=tty xencons=tty clocksource=jiffies notsc pci=noapci"
#
# Parametrage VNC pour l'installation et/ou la recup
vfb = [ 'type=vnc,vnclisten=127.0.0.1,vncdisplay=1' ]
keymap       = "fr"
#
# Parametrage de la machine virtuelle
name         = 'xps-101'
hostname     = 'xps-101.mydomain.tld'

Ceci est le fichier de configuration de notre premier domU. Celui-ci disposera d'un CDRom ainsi que d'une console VNC. Le CD sera monté automatiquement au démarrage. Quant à la console, vous pouvez y accéder grâce à un tunnel SSH:

Mise en place du tunnel SSH pour accéder à la console VNC
ssh user@nodeA -f -N -L5901:127.0.0.1:5901

La console VNC est maintenant disponible sur l'interface locale de votre machine, sur le port 5901.

Vous pouvez installer dès maintenant le domU, ou préférer l'intégrer d'abord au cluster. Dans le premier cas, une fois l'installation du domU terminée, vous devez arrêter le domU avant de paramétrer Pacemaker pour qu'il le gère.

De toute façon, vous devez d'abord configurer la ressource DRBD. Les détails se trouvent là: DRBD: Distributed Replicated Block Device.

Intégration du domU dans le cluster

Nous allons configurer Pacemaker pour qu'il gère le domU. Pour cela, il faut:

Le dernier point est très important. En effet, les version 8.3.2 de DRBD s'intègrent parfaitement dans le cluster. Mais il reste possible que la ressource DRBD soit en état instable (ou split brain). Et là, seule la réparation manuelle permet de corriger proprement le problème. En attendant, il faut naturellement empêcher qu'un domU ne puisse démarrer sur un node sans DRBD master. Sinon, l'échec du démarrage du domU sera enregistré par le cluster qui bloquera toute tentative ultérieure par la définition d'un contrainte spécifique. Il vous faudra alors résoudre le problème et effacer la contrainte en question. Tant que cela n'aura pas été fait, votre cluster sera lui-même dans un état imparfait puisque qu'une ressource au moins ne pourra pas être migrée en cas de défaillance du node sur lequel elle tourne.

Configuration d'un domU Xen dans le cluster Corosync / Pacemaker
crm configure
crm(live)configure# primitive xps-101-DRBD ocf:linbit:drbd \
	params drbd_resource="xps-101" \
	operations $id="xps-101-DRBD-ops" \
	op monitor interval="20" role="Master" timeout="20" \
	op monitor interval="30" role="Slave" timeout="20" \
	meta target-role="started"
crm(live)configure# ms xps-101-MS xps-101-DRBD \
	meta resource-stickines="100" master-max="2" notify="true" interleave="true"
crm(live)configure# primitive xps-101 ocf:heartbeat:Xen \
	params xmfile="/cluster/xen/xps-101.cfg" \
	op monitor interval="10s" \
	meta target-role="started" allow-migrate="true"
crm(live)configure# colocation xps-101-Xen-DRBD inf: xps-101 xps-101-MS:Master
crm(live)configure# commit

Ceci fait, le domU devrait démarrer. Si vous avez paramétré la ressource DRBD sur le node B, vous devriez être capable de migrer la ressource. Dans le cas contraire, c'est un pré-requis ;-)

Dernière précision, mais vous l'aviez sûrement déjà compris, vous n'avez pas besoin de re-déclarer les ressources sur le node B, le cluster s'est chargé de les propager.

Lors de la définition de la ressource Xen, l'attribut meta allow-migrate="true" permet la migration à chaud du domU concerné. En son absence, si vous décider de migrer la ressource, elle sera arrêtée sur le node d'origine pour être redémarrée sur le node de destination.

Gestion des ressources du cluster

Ci dessous quelques commandes qui vous permettrons de gérer le cluster, au moins au début ;-)

Principales commandes de gestion du cluster
resource migrate xps-101
crm node standby nodeB
crm node online nodeB

Sources

Format

Ce document est disponible aux formats suivants:

À propos de Jean Baptiste FAVRE

Je suis responsable d'exploitation dans le domaine de l'hébergement. Je travaille, entre autres, sur la virtualisation et l'amélioration des performances web. De temps en temps, j'arrive à décrocher de mon clavier pour lire un bon bouquin en écoutant de la musique.

License

Creative Commons License Cette publication est publiée sous contrat Creative Common by-nc-sa

Valid XHTML 1.0 Strict |  Valid CSS |  contrat Creative Common by-nc-sa

Table des matières

  1. Introduction
  2. Principes d'un cluster
  3. Définition du cluster Xen
  4. Contraintes du cluster Xen
  5. Architecture du cluster
  6. Installation du cluster
  7. Configuration du cluster
  8. Configuration de LVM
  9. Installation de DRBD
  10. Configuration de DRBD
  11. Installation de OCFS2
  12. Configuration de OCFS2
  13. Configuration de la resource OCFS2 pour le cluster
  14. Installation de Xen
  15. Configuration de Xen
  16. Configuration d'un domU Xen
  17. Intégration du domU dans le cluster
  18. Gestion des ressources du cluster
  19. Sources
  20. Format
  21. À propos ...
  22. License