538 lines
16 KiB
Markdown
538 lines
16 KiB
Markdown
+++
|
|
template = "articles.html"
|
|
|
|
title = "Sauvegarde système de l'Orangepi sous debian/yunohost."
|
|
description = "Sauvegarder le système de sa carte ARM OrangePi avec debian sur clé usb"
|
|
date = 2019-12-09
|
|
+++
|
|
|
|
|
|
### Le problème
|
|
Ou plutôt les problèmes, car il peut y en avoir plusieurs; par exemple le
|
|
système sur la carte ne boot plus. On a fait une bêtise, on la débranché car on
|
|
a voulu la déplacer ou le fil s'est débranché par accident, et aller cette fois
|
|
là c'est la galère. C'est généralement long et fastidieux de récupérer les
|
|
données sur sa carte SD ou autre, j'ai donc fait un script qui fait une
|
|
sauvegarde globale du système sur un disque ou une clé usb que l'on devra
|
|
autoriser auparavant, histoire que dès que l'on branche une clé usb, ça n'envoie
|
|
pas les données dessus. Il y a bien évidement des solutions de sauvegarde en
|
|
ligne mais ayant une connexion avec peu de débit, ça peut prendre beaucoup de
|
|
temps, la connexion peut couper et alors c'est des complications, pour moi ce n'était pas très adapté.
|
|
|
|
|
|
### Le principe
|
|
Le principe c'est que le script a deux fonctionnalitées
|
|
Une pour ajouter les supports de stockages où la sauvegarde sera envoyé. Le script formate la partition et il ajoute l'UUID à la liste des clés ou disques autorisé a recevoir les données. Pour l'utiliser on lancera la commande ./saveonmedia create
|
|
Une pour écouter avec udev les medias branchés et réaliser la sauvegarde quand elle reconnaît L'UUID d'un disque. On lancera la commande ./saveonmedia make
|
|
Pour la liste, un bête fichier texte suffit pour ajouter les supports de stockage lignes par lignes, on se basera sur les UUID des partitions sur le disque qui sont normalement identifier de manière unique. Pour réaliser la sauvegarde le script lancera un script bash qui est généralement plus pratique si l'on veut ajouter des taches à effectuer ou personnaliser le truc un peu comme on veut.
|
|
|
|
|
|
``` python
|
|
|
|
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import pyudev
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import socket
|
|
from email.mime.text import MIMEText
|
|
from subprocess import Popen, PIPE
|
|
|
|
def print_help():
|
|
print ("""
|
|
##### saveonusb Sauvergarde sur support USB ####
|
|
! A executer avec l'utilisateur avec des droits root (sudo) ou en root !
|
|
Pour autoriser un disque (clé usb, disque usb)
|
|
Lancez la commande :
|
|
./saveonusb create
|
|
Pour effectuer l'ecoute puis la sauvegarde:
|
|
./saveonusb make
|
|
""")
|
|
|
|
def send_msg(subject, message):
|
|
# Permet d'envoyer des mail a root via sendmail.
|
|
msg = MIMEText(message)
|
|
msg["From"] = "sauvegarde@"+socket.gethostname()
|
|
msg["To"] = "root@"+socket.gethostname()
|
|
msg["Subject"] = subject
|
|
p = Popen(["/usr/sbin/sendmail", "-t", "-oi"], stdin=PIPE, universal_newlines=True)
|
|
p.communicate(msg.as_string())
|
|
|
|
|
|
def clean_df_out(df_out):
|
|
# Ici on tri les enleve les partitions de la sortir de df qui ne sont pas des supports
|
|
# de stockages
|
|
#
|
|
df_out = df_out.split('\n')
|
|
lines=[]
|
|
for line in df_out:
|
|
if line[0:4] == '/dev':
|
|
lines.append(line)
|
|
|
|
line_clean = list()
|
|
lines_clean = list()
|
|
# On crée la liste des champs de df
|
|
for line in lines:
|
|
line = line.split(' ')
|
|
for champ in line:
|
|
if champ != '':
|
|
line_clean.append(champ)
|
|
lines_clean.append(line_clean.copy())
|
|
line_clean.clear()
|
|
|
|
return lines_clean
|
|
|
|
def getMinSpaceRequirement():
|
|
# df -k A ce schema de sorti
|
|
# Filesystem | 1K-blocks | Used | Avail | Capacity | Mounted on
|
|
#
|
|
# Pour obtenir l'espace minimum on suppose que l'on veut sauvegarder toutes les
|
|
# données de tout les supports de stockages.
|
|
|
|
df = subprocess.check_output(['/bin/df', '-k']).decode('utf8')
|
|
df_list = clean_df_out(df)
|
|
print (df_list)
|
|
space_used = 0
|
|
for line in df_list:
|
|
space_used = space_used + int(line[2])
|
|
|
|
return space_used
|
|
|
|
def get_uuid(device):
|
|
cmd = subprocess.check_output(['sudo', 'blkid', device])
|
|
cmd = cmd.decode('utf-8')
|
|
for line in cmd.split(' '):
|
|
if 'UUID=' == line[0:5]:
|
|
return line[6:len(line)-1]
|
|
return ''
|
|
|
|
def make_save(save_directory, storages):
|
|
context = pyudev.Context()
|
|
monitor = pyudev.Monitor.from_netlink(context)
|
|
monitor.filter_by('block')
|
|
done = False
|
|
with open (storages, 'r') as f:
|
|
medias_save = f.read().splitlines()
|
|
|
|
while not(done):
|
|
device = monitor.poll()
|
|
if 'ID_FS_TYPE' in device and device.action == "add":
|
|
print ("device: {0} medias: {1}".format(device.get('ID_FS_UUID'), medias_save))
|
|
if "ynh_save" == device.get('ID_FS_LABEL') and device.get('ID_FS_UUID') in medias_save:
|
|
print ("La sauvegarde a commencé")
|
|
send_msg("Une sauvegarde a été lancé","Ne retirez pas votre disque ou clé usb. Merçi.")
|
|
|
|
retcode = subprocess.call("mount " + device.device_node + " "+ save_directory, shell=True)
|
|
if retcode != 0:
|
|
subject = "Bon ... ça s'est mal passé"
|
|
message = """Impossible de monter la clé USB :( """
|
|
send_msg(subject, message)
|
|
|
|
retcode = subprocess.call( save_script + " " + save_directory, shell=True)
|
|
if retcode != 0:
|
|
subject = "Bon ... ça s'est mal passé"
|
|
message = """Le script de sauvegarde a remonté une erreur,
|
|
comment dire ... je le ferai autrement moi """
|
|
send_msg(subject, message)
|
|
|
|
retcode = subprocess.call("umount " + device.device_node, shell=True)
|
|
if retcode != 0:
|
|
subject = "Bon ... ça s'est a peu près bien passé"
|
|
message = """Ça a monté le support et tout, le script pas de problèmes,
|
|
mais ca démonte pas le support correctement le script ne peut rien faire seul vous pouvez
|
|
voir ce qu'il se passe"""
|
|
else:
|
|
subject = "Confirmation de sauvegarde"
|
|
message = """Votre sauvegarde est prête vous pouvez retirer votre
|
|
support de stockage en toute sécurité. Rebranchez ce même support quand vous
|
|
souhaitez effectuer une sauvergarde. Attention! Toutes modifications de la partiton
|
|
rendra le support invalide"""
|
|
send_msg(subject, message)
|
|
|
|
|
|
def create_key_save(storages):
|
|
context = pyudev.Context()
|
|
monitor = pyudev.Monitor.from_netlink(context)
|
|
monitor.filter_by('block')
|
|
with open (storages, 'r') as f:
|
|
medias_save = f.read().splitlines()
|
|
done = False
|
|
while not(done):
|
|
device = monitor.poll()
|
|
if device.device_type == "partition" and device.action == "add":
|
|
if "ynh_save" == device.get('ID_FS_LABEL') and device.get('ID_FS_UUID') in medias_save:
|
|
subject = "Ce support de stockage est déjà initialisé"
|
|
message = "Il n'est pas nécéssaire d'initialisé ce périphérique"
|
|
send_msg(subject, message)
|
|
else:
|
|
size = subprocess.check_output("fdisk -s " +
|
|
device.device_node,
|
|
shell=True)
|
|
size = int(size)
|
|
size_requirement = getMinSpaceRequirement()
|
|
if size > size_requirement:
|
|
done = True
|
|
retcode = subprocess.call("mkfs.ext4" + " -F -L ynh_save" +
|
|
" "+ device.device_node, shell=True)
|
|
|
|
if retcode != 0:
|
|
subject = "Impossible de formatter la clé"
|
|
message = """ Il y a eu un problème durant le formatage de la clé"""
|
|
send_msg(subject, message)
|
|
# On récupere le nouvel uuid
|
|
|
|
with open (storages, 'a') as f:
|
|
f.write(get_uuid(device.device_node)+ '\n')
|
|
subject = "Autorisation d'un support de sauvegarde"
|
|
message = """ Vous venez de créer un support pour votre sauvegarde.
|
|
Cela s'est deroulé normalement. Pour effectuer une sauvegarde maintenant
|
|
rebrancher votre support de stockage; un mail vous signalera quand vous pourrez
|
|
enlevez votre support en toute sécurité."""
|
|
send_msg(subject, message)
|
|
done = True
|
|
else:
|
|
subject = "Votre support de stockage est de taille insufisante !"
|
|
message = """Il n'y a pas assez d'espace disque sur le support que
|
|
vous venez de brancher; essayer avec une media de plus grande capacité (minimum """ +
|
|
str(size_requirement/1000000) + "Gb)"
|
|
send_msg(subject, message)
|
|
|
|
|
|
save_directory = "/media/save"
|
|
save_script = "/etc/saveonusb/scripts/save.sh"
|
|
conf_path = "/etc/saveonusb/"
|
|
storages = conf_path+"storages"
|
|
|
|
if os.geteuid() != 0:
|
|
raise OSError("Ce programme doit s'executer avec les droits root")
|
|
|
|
if not(os.path.exists(save_directory)):
|
|
raise OSError("Le repertoire "+ save_directory +" n'existe pas")
|
|
|
|
if not(os.path.exists(conf_path)):
|
|
raise OSError("Le repertoire "+ conf_path +" n'existe pas")
|
|
|
|
if not(os.path.isfile(save_script)):
|
|
raise OSError("Le fichier "+ save_script +" n'existe pas")
|
|
|
|
if not(os.path.isfile(storages)):
|
|
raise OSError("Le fichier "+ storages +" n'existe pas")
|
|
|
|
if len(sys.argv) > 1:
|
|
if sys.argv[1] == 'create':
|
|
create_key_save(storages)
|
|
if sys.argv[1] == 'make':
|
|
make_save(save_directory, storages)
|
|
if sys.argv[1] == '--help' or sys.argv[1] == '-h':
|
|
print_help()
|
|
else:
|
|
print_help()
|
|
|
|
|
|
```
|
|
|
|
|
|
On installe pyudev:
|
|
|
|
``` bash
|
|
apt install python3-pyudev
|
|
```
|
|
|
|
On créé les répertoires et les fichiers nécessaires:
|
|
|
|
```bash
|
|
mkdir /media/save
|
|
mkdir -p /etc/saveonusb/scripts
|
|
touch /etc/saveonusb/storages && touch /etc/saveonusb/scripts/save.sh
|
|
chmod +x /etc/saveonusb/scripts/save.sh
|
|
```
|
|
|
|
Je me suis fait tout d'abord un petit script afin de générer
|
|
une image prête a être flasher sur une carte SD
|
|
|
|
``` bash
|
|
|
|
#!/bin/bash
|
|
|
|
set -e
|
|
#set -x
|
|
|
|
#Implementer un TRAC error avec exit qui clean quand ca foire
|
|
# Implementer des logs
|
|
|
|
|
|
#Fonction pour check les programmes nécéssaire.
|
|
#lance apt install quand les programmes manques
|
|
# modprobe loop
|
|
|
|
# Test les medias disponibles et passer en parametre le disk a monter pour mettre l'image
|
|
# Demande confirmation pour commencer tout les étapes ou a toutes les étapes
|
|
_apt_install_dep()
|
|
{
|
|
echo "on passe ici avec $1 comme argument"
|
|
case $1 in
|
|
truncate)
|
|
apt-get -y install --no-install-recommends coreutils
|
|
;;
|
|
mkfs.ext4)
|
|
apt-get -y install --no-install-recommends e2fsprogs
|
|
;;
|
|
*)
|
|
apt-get -y install --no-install-recommends $1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
check_dep()
|
|
{
|
|
bins=(dd truncate rsync parted mkfs.ext4 )
|
|
for i in "${bins[@]}"; do
|
|
if ! which "${i}" &> /dev/null; then
|
|
echo "${i} command is required"
|
|
_apt_install_dep ${i}
|
|
fi
|
|
done
|
|
echo "check_dep ok"
|
|
}
|
|
|
|
|
|
|
|
create_img() {
|
|
local folder=$1
|
|
local image=$2
|
|
|
|
# Espace utilisé sur /
|
|
local usage=$(df -BM | grep ^/dev | head -1 | awk '{print $3}' | tr -cd '[0-9]. \n')
|
|
# Espace libre
|
|
local avaible=$(df -BM $folder | grep ^/dev | head -1 | awk '{print $4}' | tr -cd '[0-9]. \n')
|
|
|
|
if [[ $usage -gt $avaible ]]; then
|
|
echo "No space left"
|
|
echo "Required: $usage MB "
|
|
echo "Free space in $folder : $avaible MB "
|
|
exit 1
|
|
fi
|
|
#On laisse 1024M d'espace libre sur l'image
|
|
local image_size=$(($usage+1024))'M'
|
|
truncate -s $image_size $folder/$image
|
|
}
|
|
|
|
|
|
prepare_part() {
|
|
local loop=$1
|
|
local folder=$2
|
|
local image=$3
|
|
local mount_image=$4
|
|
|
|
losetup $loop $folder/$image
|
|
parted -s $loop -- mklabel msdos
|
|
parted -s $loop -- mkpart primary ext4 8192s -1s
|
|
#La carte ne boot pas avec la ligne en dessous
|
|
#parted -s $loop align-check optimal 1
|
|
partprobe $loop
|
|
mkfs.ext4 $loop"p1"
|
|
mkdir $mount_image
|
|
mount $loop"p1" $mount_image
|
|
}
|
|
|
|
copy_files() {
|
|
# Stopper les services avant de copie
|
|
# Vérifier que les partitions soient montées
|
|
# Afficher le point de montage de la partition
|
|
# On redirige la sortie du programme vers /dev/null et la sortie erreur (2) sur la sortie standard
|
|
# On ne verra que les erreurs des fichiers ou la copie a posé problème
|
|
local mount_image=$1
|
|
rsync -avrltD --delete --exclude={/dev/*,/proc/*,/sys/*,/media/*,/mnt/*,/run/*,/tmp/*} / $mount_image > /dev/null 2>&1
|
|
}
|
|
|
|
#Modify fstab
|
|
change_fstab() {
|
|
local mount_image=$1
|
|
local old_uuid=$(blkid -o export `mount | grep -w / | awk '{print $1}'` | grep ^UUID)
|
|
local new_uuid=$(blkid -o export `mount | grep -w /media/$IMAGE | awk '{print $1}'` | grep ^UUID)
|
|
echo "old_uuid: $old_uuid"
|
|
echo "new_uuid: $new_uuid"
|
|
sed -i s/$old_uuid/$new_uuid/ $MOUNT_IMAGE/etc/fstab
|
|
if [ -e $MOUNT_IMAGE/boot/armbianEnv.txt ]; then
|
|
sed -i s/$old_uuid/$new_uuid/ $MOUNT_IMAGE/boot/armbianEnv.txt
|
|
fi
|
|
}
|
|
|
|
write_mbr() {
|
|
|
|
# On cherche le uboot_sunxi_with_spl.bin selon si on est sur armbian ou non
|
|
local $uboot_mbr
|
|
local $loop
|
|
loop=$1
|
|
uboot_mbr=$2
|
|
|
|
if [ ! -f $uboot_mbr ]; then
|
|
exit 1;
|
|
fi
|
|
# on Efface la zone pour le mbr
|
|
dd if=/dev/zero of=$loop bs=1k count=1023 seek=1 status=noxfer > /dev/null 2>&1;
|
|
|
|
# on flash le mbr
|
|
dd if=$uboot_mbr of=$loop bs=1024 seek=8 status=noxfer > /dev/null 2>&1;
|
|
}
|
|
|
|
|
|
clean() {
|
|
local loop=$1
|
|
local mount_image=$2
|
|
|
|
sync
|
|
umount $mount_image
|
|
losetup -d $loop
|
|
rmdir $mount_image
|
|
}
|
|
|
|
## Début du script
|
|
show_usage() {
|
|
cat << EOF
|
|
Ce script génère une image qui peut être flasher sur une carte SD pour les cartes
|
|
Olimex LIME LIME2 et orange pi pc (plus)
|
|
|
|
$(basename $0)
|
|
|
|
-d Dossier où sera stocké l'image
|
|
|
|
-u uboot_sunxi_with_spl.bin
|
|
|
|
|
|
EOF
|
|
|
|
exit 1
|
|
}
|
|
|
|
if [ ! $(id -u) = 0 ]; then
|
|
echo "L'utilisateur doit être root"
|
|
exit 1
|
|
fi
|
|
|
|
|
|
|
|
## Possibilité d'aller chercher le uboot_sunxi_with_spl.bin sur une adresse http
|
|
|
|
|
|
#Check arguments
|
|
|
|
if [ $# -eq 0 ]; then
|
|
show_usage
|
|
fi
|
|
|
|
# Gérer les options https://www.tutorialspoint.com/unix_commands/getopt.htm
|
|
|
|
|
|
while getopts ":d:u:h" opt; do
|
|
case $opt in
|
|
|
|
d)
|
|
FOLDER=$OPTARG
|
|
;;
|
|
u)
|
|
UBOOT_MBR=$OPTARG
|
|
;;
|
|
h | *)
|
|
show_usage;
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ -z $FOLDER ]; then
|
|
show_usage
|
|
fi
|
|
|
|
|
|
if [ ! -d $FOLDER ]; then
|
|
echo "$FOLDER isn't a directory";
|
|
#echo "Le dossier $FOLDER n'existe pas"
|
|
show_usage
|
|
fi
|
|
if [ -z $UBOOT_MBR ]; then
|
|
show_usage
|
|
fi
|
|
|
|
if [ ! -f $UBOOT_MBR ]; then
|
|
echo "This file $UBOOT_MBR doesn't exist";
|
|
show_usage
|
|
fi
|
|
|
|
IMAGE=snapshot_`date +%Y%m%d`.img
|
|
LOOP=$(losetup -f)
|
|
MOUNT_IMAGE="/media/"$IMAGE
|
|
|
|
check_dep
|
|
echo "check_dep : OK"
|
|
#On crée un image de la taille minimale avec 1024M de place disponible.
|
|
create_img $FOLDER $IMAGE
|
|
echo "create_img: OK"
|
|
|
|
#On crée l'interface Loop et les partitions qui vont bien
|
|
prepare_part $LOOP $FOLDER $IMAGE $MOUNT_IMAGE
|
|
echo "prepare_part: OK"
|
|
#On copie la /
|
|
copy_files $MOUNT_IMAGE
|
|
echo "copy_file: OK"
|
|
#
|
|
change_fstab $MOUNT_IMAGE
|
|
echo "change_fstab: OK"
|
|
|
|
#On écrit le MBR
|
|
write_mbr $LOOP $UBOOT_MBR
|
|
echo "write_uboot: OK"
|
|
#demonte l'image et detache l'interface loop
|
|
clean $LOOP $MOUNT_IMAGE
|
|
|
|
```
|
|
|
|
|
|
je copie ce script dans /usr/local/bin/make_img
|
|
chmod +x /usr/local/bin/make_img
|
|
|
|
Puis dans mon fichier /etc/saveonusb/script/save.sh
|
|
```bash
|
|
|
|
DIR_SAVE=$1
|
|
|
|
if [ ! -d "$DIR_SAVE" ]; then
|
|
echo "Le repertoire $DIR_SAVE n'existe pas" 1>&2;
|
|
exit 1;
|
|
fi
|
|
|
|
|
|
/usr/local/bin/make_img -f $DIR_SAVE -u /usr/lib/linux-u-boot-next-orangepipcplus_5.90_armhf/u-boot-sunxi-with-spl.bin
|
|
|
|
|
|
if [ $? -eq 1 ]; then
|
|
echo "Il y a eu une erreur durant la copie des fichiers" 1>&2;
|
|
exit 1;
|
|
fi
|
|
|
|
{% endhighlight %}
|
|
|
|
|
|
Version pour les personnes qui sont sous yunohost:
|
|
{% highlight bash %}
|
|
#!/bin/bash
|
|
|
|
DIR_SAVE=$1
|
|
|
|
if [ ! -d "$DIR_SAVE" ]; then
|
|
echo "Le repertoire $DIR_SAVE n'existe pas" 1>&2;
|
|
exit 1;
|
|
fi
|
|
|
|
|
|
/usr/bin/yunohost backup create -o $DIR_SAVE
|
|
if [ $? -eq 1 ]; then
|
|
echo "Il y a eu une erreur pendant l'exécution de yunohost backup" 1>&2;
|
|
exit 1;
|
|
fi
|
|
|
|
```
|
|
Il y a biensur des choses à améliorer, mais c'est fonctionnel. On pourrait par exemple avoir plusieurs scripts ou vérifier si le support de sauvergarde a encore la place suffisante. Mais pour mes besoins ça me suffit. Une fonction pour la restauration ça serait pas mal aussi :).
|
|
|