#!/bin/sh # Ce script sert à créer ou détruire : # - une instance PostgreSQL (PGDATA). # Il peut également gérer un serveur esclave en définissant la # variable $ESCLAVE . Dans ce cas là, mieux vaut avoir une authentification # par clef (les opérations se font par ssh) # # Attention, ce script affiche les identifiants à l'écran. # Prérequis : # - PostgreSQL # Si utilisation d'un esclave : # - une authentification pour l'utilisateur root local vers root@$ESCLAVE par clef, # - une authentification pour l'utilisateur postgres local vers postgres@$ESCLAVE par clef. # Trucs à nettoyer (si quelque chose part en vrille, ou simplement # pour désinstaller ): # - TODO # Version de PostgreSQL : 9.1 seule testée VERSION_POSTGRESQL="9.1" # Mettre ci-dessous le nom d'hôte ou l'IP du serveur esclave # (utilisé par la config PG + les connexions SSH pour l'exécution # des commandes distantes de ce script) ESCLAVE="" # idem pour le maître (vu depuis l'esclave en cas de réseau privé) # (utilisé pour la config PG) MAITRE="" # Initialisation des variables globales MODE_CREATION=0 MODE_DESTRUCTION=0 POSTGRESQL_REPLICATION_USER_NAME="repliquser" POSTGRESQL_LOCALE="fr_FR.utf8" POSTGRESQL_PORT="" # Par défaut, on arrête le script à la première erreur non "catchée" set -e # On s'exécute dans un dossier accessible à l'utilisateur postgres cd /tmp # Fonctions # affiche le message d'aide usage() { cat < -m ] [ -p ] $0 -d cluster-name [ -e -m ] $0 -h -c xxx : création d'une instance/cluster nommée 'xxx' -d xxx : destruction de l'instance nommée 'xxx' -p nnn : numéro de port à affecter au nouveau cluster (par défaut, 'auto') # Ce script peut générer la configuration nécessaire au streaming replication -e esclave : nom d'hôte ou IP de l'esclave -m maitre : nom d'hôte ou IP du serveur maître -h : ce message d'aide EOF } # Arguments # $1 : IPv4, IPv6 ou nom d'hôte # Cette fonction suffixe d'un /32 ou /128 # la chaîne si celle-ci est une adresse IP add_mask_ip() { test -n "$1" || exit 1 if [ $( echo "$1" | egrep -c "^([[:digit:]]{1,3}\.){3}[[:digit:]]{1,3}$" ) -gt 0 ]; then printf "$1/32" elif [ $( echo "$1" | egrep -c "^[[:xdigit:]:]*$" ) -gt 0 ]; then printf "$1/128" else printf "$1" fi } # Arguments # $1 : IP esclave # $2 : version PostgreSQL # $3 : nom de l'instance generate_patch_postgresqlconf_master() { test -n "$1" && test -n "$2" && test -n "$3" || exit 1 cat </dev/null 2>&1; then case "$BEGINNING" in "ERREUR"|"ERROR") tput setaf 1;; "WARN"|"WARNING") tput setaf 3;; "INFO"|"DEBUG") tput setaf 2;; esac printf "%s" "$BEGINNING" tput op printf "%s\n" "$( printf "%s" "$1" | sed '1 s/^[A-Z]\+:/:/' )" else printf "%s\n" "$1" fi } # Début du code # gestion des options de lancement while getopts c:d:e:m:p:h f; do case $f in 'c') MODE_CREATION=1 NOM_INSTANCE="$OPTARG" ;; 'd') MODE_DESTRUCTION=1 NOM_INSTANCE="$OPTARG" ;; 'e') ESCLAVE="$OPTARG" ;; 'm') MAITRE="$OPTARG" ;; 'p') if [ "$OPTARG" = "auto" ]; then unset POSTGRESQL_PORT else POSTGRESQL_PORT="$( printf "%d" "$OPTARG" )" fi ;; 'h') usage exit 0 ;; \?) usage >&2 exit 1 ;; esac done #(code inutile, mais que je garde parce qu'on ne sait jamais) #shift $( expr $OPTIND - 1 ) #DATA="$1" if [ -n "$MAITRE" ] && [ -n "$ESCLAVE" ]; then MAITRE_WITH_MASK=$( add_mask_ip "$MAITRE" ) ESCLAVE_WITH_MASK=$( add_mask_ip "$ESCLAVE" ) fi # Petite vérif. case $(( $MODE_CREATION + $MODE_DESTRUCTION )) in '0') fancylog "ERREUR: veuillez choisir entre création et destruction." >&2 usage >&2 exit 1 ;; '2') fancylog "ERREUR: tu veux créer et détruire en même temps, petit malin ?" >&2 exit 1 ;; esac # Petites vérifications préliminaires : fancylog "INFO: petites vérifications préliminaires..." # - vérifications sur le nom du clustername fourni OLD_LC_ALL="$LC_ALL" export LC_ALL=C export LANG=C if [ $( printf "$NOM_INSTANCE" | egrep -c "^[a-z][a-z0-9_]{0,14}$" ) -ne 1 ]; then fancylog "ERREUR: clustername vide ou contenant des caractères interdits : regexp : [a-z][a-z0-9_]{0,14}" >&2 exit 1 fi export LC_ALL="$OLD_LC_ALL" export LANG="$OLD_LC_ALL" # - est-ce que l'auth. SSH par clef fonctionne pour root et postgres ? if [ -n "$ESCLAVE" ]; then # - vérification de l'auth. par clefs SSH if ! ssh -o BatchMode=yes root@$ESCLAVE /bin/true; then fancylog "ERREUR: auth. par clef SSH non configurée pour l'utilisateur 'root'." exit 1 fi if ! su -c "ssh -o BatchMode=yes postgres@$ESCLAVE /bin/true" postgres; then fancylog "ERREUR: auth. par clef SSH non configurée pour l'utilisateur 'postgres'." exit 1 fi # - vérification de la présence de $MAITRE if [ ! -n "$MAITRE" ]; then fancylog "ERREUR: serveur esclave indiqué, mais pas de serveur maître (option -m)." >&2 exit 1 fi fi # - est-ce qu'on est bien sur une Debian 6 ou 7 # (c'est surtout pour éviter les mauvaises surprises, ce script n'ayant pas été testé ailleurs) if [ ! -f "/etc/debian_version" ] || [ $( egrep -c "^(6.|7.)" /etc/debian_version ) -ne 1 ]; then fancylog "ERREUR: mauvaise version de Debian détectée. Arrêt par prudence. Vérifiez le code de ce script avant de forcer." >&2 exit 1 fi if [ -n "$ESCLAVE" ] && [ "$( ssh root@$ESCLAVE 'egrep -c "^(6.|7.)" /etc/debian_version' )" -ne 1 ]; then fancylog "ERREUR: ESCLAVE: mauvaise version de Debian détectée. Arrêt par prudence. Vérifiez le code de ce script avant de forcer." >&2 exit 1 fi # - est-ce que la version attendue de PostgreSQL est bien installée ? if ! dpkg -s "postgresql-$VERSION_POSTGRESQL" >/dev/null 2>&1; then fancylog "ERREUR: PostgreSQL $VERSION_POSTGRESQL ne semble pas installée." >&2 exit 1 fi if [ -n "$ESCLAVE" ] && ! ssh root@$ESCLAVE dpkg -s "postgresql-$VERSION_POSTGRESQL" >/dev/null 2>&1; then fancylog "ERREUR: PostgreSQL $VERSION_POSTGRESQL ne semble pas installée." >&2 exit 1 fi # Fin des vérifications fancylog "INFO: fin des vérifications. Lancement des opérations." if [ "$MODE_CREATION" -eq 1 ]; then POSTGRESQL_PASSWORD=$( dd if=/dev/random 2>/dev/null bs=1 count=10 status=noxfer | base64 | sed 's#[/=]##g' ) POSTGRESQL_REPLICATION_PASSWORD=$( dd if=/dev/random 2>/dev/null bs=1 count=20 status=noxfer | base64 | sed 's#[/=]##g' ) # Création de l'instance dédiée fancylog "INFO: création du cluster PG primaire..." if [ -z "$POSTGRESQL_PORT" ]; then pg_createcluster --locale=$POSTGRESQL_LOCALE --start-conf=auto "$VERSION_POSTGRESQL" "$NOM_INSTANCE" # Détermination du port généré # TODO: récupérer ça via pg_lsclusters serait plus "propre" POSTGRESQL_PORT=$( grep "^port" /etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/postgresql.conf | sed 's/^port[[:space:]]*=[[:space:]]*\([0-9]\+\).*$/\1/' ) else pg_createcluster --locale=$POSTGRESQL_LOCALE --start-conf=auto --port "$POSTGRESQL_PORT" "$VERSION_POSTGRESQL" "$NOM_INSTANCE" fi fancylog "INFO: modification du pg_hba.conf..." if ! generate_patch_postgresql_pghbaconf "$NOM_INSTANCE" | patch -s --dry-run /etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/pg_hba.conf; then fancylog "ERREUR: modification du fichier pg_hba.conf en échec. Arrêt." exit 1 fi generate_patch_postgresql_pghbaconf "$NOM_INSTANCE" | patch -s /etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/pg_hba.conf fancylog "INFO: démarrage de l'instance PostgreSQL..." pg_ctlcluster "$VERSION_POSTGRESQL" "$NOM_INSTANCE" start # Lancement du script fourre-tout fancylog "INFO: création de l'utilisateur et de la base de données, et affinage des droits d'accès..." generate_script_postgresql_creation_db_et_user "$NOM_INSTANCE" "$POSTGRESQL_PASSWORD" "$POSTGRESQL_REPLICATION_USER_NAME" "$POSTGRESQL_REPLICATION_PASSWORD" | su -c "psql -p $POSTGRESQL_PORT -v ON_ERROR_STOP=1" postgres if [ -n "$ESCLAVE" ]; then fancylog "INFO: création de l'utilisateur de réplication..." generate_script_postgresql_creation_replication_user "$POSTGRESQL_REPLICATION_USER_NAME" "$POSTGRESQL_REPLICATION_PASSWORD" | su -c "psql -p $POSTGRESQL_PORT -v ON_ERROR_STOP=1" postgres # configuration en tant que maître fancylog "INFO: modification de la configuration BDD de l'instance primaire..." if ! generate_patch_postgresqlconf_master "$ESCLAVE" "$VERSION_POSTGRESQL" "$NOM_INSTANCE" | patch -s --dry-run /etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/postgresql.conf; then fancylog "ERREUR: modification du fichier postgresql.conf en échec. Arrêt." exit 1 fi generate_patch_postgresqlconf_master "$ESCLAVE" "$VERSION_POSTGRESQL" "$NOM_INSTANCE" | patch -s /etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/postgresql.conf # On rajoute les lignes pour la connexion streaming replication dans le pg_hba.conf echo "host replication $POSTGRESQL_REPLICATION_USER_NAME $ESCLAVE_WITH_MASK md5" >>/etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/pg_hba.conf echo "host replication $POSTGRESQL_REPLICATION_USER_NAME $MAITRE_WITH_MASK md5" >>/etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/pg_hba.conf # Création sur l'esclave # Note: on commence par créer l'arborescence de l'esclave pour # avoir le répertoire de destination des archives logs # le plus tôt possible (en tout cas, avant le pg_stop_backup() # qui envoie forcément un WAL) fancylog "INFO: création du cluster PG secondaire..." ssh "root@$ESCLAVE" "cd /tmp; pg_createcluster --locale='$POSTGRESQL_LOCALE' -p '$POSTGRESQL_PORT' --start-conf=auto '$VERSION_POSTGRESQL' '$NOM_INSTANCE'" fancylog "INFO: création du dossier de réception des archives logs et arret du backup..." su -c "mkdir '/var/lib/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/archives_from_master'" postgres su -c "ssh postgres@$ESCLAVE mkdir '/var/lib/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/archives_from_master'" postgres fancylog "INFO: redémarrage du serveur primaire..." pg_ctlcluster "$VERSION_POSTGRESQL" "$NOM_INSTANCE" restart # rsync du PGDATA vers l'esclave fancylog "INFO: écrasement du PGDATA secondaire par celui de l'instance primaire..." echo "SELECT pg_start_backup('script_mi_pg', true);" | su -c "psql -p $POSTGRESQL_PORT -v ON_ERROR_STOP=1" postgres # on attend un peu, il y a parfois des fichiers WAL qui "vanished" pendant le rsync sleep 1 # On exclut : # - postmaster.pid : raison évidente :) # - postmaster.opts : a priori, le contenu est identique mais par prudence... # - server.key/server.crt : rsync génère une erreur sur la date de modification des symlinks :-/ # - recovery.* : le recovery.done du maitre référence l'esclave, et vice versa pour le recovery.conf de l'esclave # - archives_from_master : cela écraserait tout éventuel WAL déjà envoyé # - lost+found : au cas où la partition existe déjà (pg_createcluster se bloque mais ça permet de récupérer cette ligne de commande ailleurs :) # A étudier : pas de --delete ? # test -c car problème de synchro bizarre su -c "rsync -aHc --exclude=/postmaster.pid --exclude=/postmaster.opts --exclude=/server.key --exclude=/server.crt --exclude=/recovery.conf --exclude=/recovery.done --exclude=/archives_from_master --exclude=lost+found /var/lib/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/ postgres@$ESCLAVE:/var/lib/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/" postgres echo "SELECT pg_stop_backup();" | su -c "psql -p $POSTGRESQL_PORT -v ON_ERROR_STOP=1" postgres # configuration de l'esclave fancylog "INFO: configuration de l'instance PG secondaire..." if ! ssh root@$ESCLAVE dpkg -s "postgresql-contrib-$VERSION_POSTGRESQL" >/dev/null 2>&1; then fancylog "WARNING: Le paquet postgresql-contrib-$VERSION_POSTGRESQL ne semble pas installé sur le serveur esclave, archive_cleanup_command sera désactivé." >&2 fancylog "NOTICE: cf fichier /var/lib/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/recovery.conf en cas d'installation après-coup." >&2 ARCHIVE_CLEANUP_COMMAND="" else # TODO: à rendre plus portable ? ARCHIVE_CLEANUP_COMMAND="/usr/lib/postgresql/$VERSION_POSTGRESQL/bin/pg_archivecleanup" fi generate_file_postgresqlrecoveryconf_slave "$MAITRE" "$POSTGRESQL_PORT" "$POSTGRESQL_REPLICATION_USER_NAME" "$POSTGRESQL_REPLICATION_PASSWORD" "$VERSION_POSTGRESQL" "$NOM_INSTANCE" "$ARCHIVE_CLEANUP_COMMAND" | su -c "ssh postgres@$ESCLAVE 'cat - >/var/lib/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/recovery.conf'" postgres generate_file_postgresqlrecoveryconf_slave "$ESCLAVE" "$POSTGRESQL_PORT" "$POSTGRESQL_REPLICATION_USER_NAME" "$POSTGRESQL_REPLICATION_PASSWORD" "$VERSION_POSTGRESQL" "$NOM_INSTANCE" "$ARCHIVE_CLEANUP_COMMAND" | su -c "cat - >/var/lib/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/recovery.done" postgres generate_patch_postgresqlconf_slave | ssh "root@$ESCLAVE" patch -s /etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/postgresql.conf # (note: même fichier pg_hba.conf pour tout le monde, homogénéisation avec peu de risques il me semble) scp -q /etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/pg_hba.conf root@$ESCLAVE:/etc/postgresql/$VERSION_POSTGRESQL/$NOM_INSTANCE/pg_hba.conf # démarrage des instances maître et esclave fancylog "INFO: démarrage de l'instance PostgreSQL esclave..." ssh root@$ESCLAVE pg_ctlcluster "$VERSION_POSTGRESQL" "$NOM_INSTANCE" start # Vérification de la réplication # TODO (via numéro de transaction (requête) ? processus/connexion ?) fi # Fin fancylog "INFO: Création terminée. Instance PostgreSQL allumée." fancylog "INFO: pour mémo :" fancylog "INFO: BDD: $NOM_INSTANCE, port: $POSTGRESQL_PORT, login: $NOM_INSTANCE, mdp: $POSTGRESQL_PASSWORD" TMP="$MAITRE" if [ -z "$MAITRE" ]; then TMP="localhost" fi fancylog "INFO: pgpass: $TMP:$POSTGRESQL_PORT:$NOM_INSTANCE:$NOM_INSTANCE:$POSTGRESQL_PASSWORD" fi if [ "$MODE_DESTRUCTION" -eq 1 ]; then fancylog "INFO: suppression de l'instance locale..." # Note : en guise de garde-fou, on ne met pas l'option --stop pg_dropcluster "$VERSION_POSTGRESQL" "$NOM_INSTANCE" if [ -n "$ESCLAVE" ]; then fancylog "INFO: suppression de l'instance esclave..." ssh root@$ESCLAVE pg_dropcluster "$VERSION_POSTGRESQL" "$NOM_INSTANCE" fi fi