Copias de seguridad

Estrategia

Se desea que el servidor web mantenga copias de seguridad diarias de los archivos del sistema y de los archivos de los usuarios.

En primer lugar se debe definir una estrategia para realizar las copias. Debemos tener en cuenta dos factores.

  • El espacio es limitado.
  • Las copias se realizarán solo de algunos directorios

Dado que el espacio es limitado se descarta el uso de copias diarias completas. Se hará una copia completa cada lunes de la semana y para los días siguientes se evaluan dos opciones:

  • Copias incrementales. Se guardará una copia de los cambios del martes

respecto al lunes, una copia de los del miércoles respecto al martes, etc.

Las copias incrementales tienen como principal ventaja que el espacio almacenado será mínimo y las copias se harán rápido. Sin embargo, la restauración será costosa.

  • Copias diferenciales. Cada día se hará una copia de los cambios respecto al lunes.

En este caso el espacio ocupado será mayor aunque la restauración será más rápida ya que solo habrá que restaurar la copia del lunes y la correspondiente a ese día.

Finalmente optamos por las copias incrementales ya que aprovecharán mejor el espacio.

Dado que se desea realizar copias de distintos directorios la opción elegida para hacerlo será mediante Tar y los scripts necesarios para automatizar las copias incrementales. Se descarta dump, la otra alternativa propuesta por estar indicada para realizar copias de sistemas de ficheros completos.

Realizar Backup

Se implementará un módulo como en ocasiones anteriores, esta vez será Cactus::Backups.

En el tendremos las funciones necesarias para hacer una copia, listar las copias existentes en un directorio y restaurar una de ellas.

tar_backup será la subrutina que nos permita realizar una copia de seguridad. Recibe como argumentos el directorio del que se hace la copia, el directorio donde se guarda y el nivel de la copia. Se permiten niveles entre 0 y 9 pero se podría utilizar el mismo código para tener cualquier número de copias.

Se almacenarán las copias en disco de forma estructurada. Se tendrá que almacenar para cada copia un archivo .tar.gz y otro .snar que incluye información sobre la copia incremental. Estos archivos se encontrarán en un subdirectorio de la anterior copia de nivel inferior.

Un ejemplo en el que se hagan copias sucesivas con los siguientes t1(0), t2(1), t3(2), t4(1), t5(0), t6(1) tiene el siguiente aspecto:

        /t1
            /t2
                /t3
            /t4
        /t5
            /t6
=====================
Nivel:   0    1    2

En caso de que se haga una copia de un nivel y no existan copias de los niveles anteriores se guardarán de la siguiente forma. Supongamos que se hacen las copias t1(0), t2(0), t3(3). Los directorios serían:

        /t1
        /t2
            /t3:3
                 /t3:3
                      /t3
==========================
Nivel:   0    1    2    3

De este modo se facilitará la restauración de copias.

El código de tar_backup es el que se muestra a continuación.

sub tar_backup($$$)
{

    my($input_dir, $back_dir, $level) = @_;

    if ( $level < 0 || $level > 9)
    {
        Cactus::Utils::backuplog("Backup","$input_dir", "", "", ,$level, "Error: Invalid Level");

        die("Invalid Level");
    }

    unless(-e $input_dir)
    {
        Cactus::Utils::backuplog("Backup","$input_dir", "", "", $level,  "Error, $input_dir does not exist");
        die("$input_dir does not exist");
    }

    unless(-e $back_dir)
    {
        File::Path::mkpath($back_dir);
    }

En primer lugar se comprueban los argumentos y la existencia de los directorios.

my $back_date = POSIX::strftime("%Y-%m-%d_%H-%M-%S", localtime());
my $last_tar = "";

Recuperamos la fecha con la función localtime() y la guardamos en un formato legible cont POSIX::strftime.

my $act_level = 0;
my $cont = 1;

while($act_level < $level)
{
    my $dir;
    my @dirs;
    my @fields;

Comienzan los bucles en los que se encuentra la carpeta en la que debe almacenarse la copia.

opendir (DIR, "$back_dir");
while ($dir = readdir(DIR))
{
    #if it's a folder
    if(!(-f $dir) && $dir =~ /^\d{4}-\d{2}-\d{2}\_\d{2}-\d{2}-\d{2}(:(\d)*){0,1}$/  )
    {
        @fields = split(':', $dir);
        if(@fields == 1 || $fields[1] < $level)
        {
            push (@dirs, $dir);
        }
    }
}
@dirs = sort {$a cmp $b} @dirs;

Cada vez que encontremos una carpeta con el formato de fecha de una copia de seguridad la guardamos. Estas serán las copias de este nivel y las ordenaremos para encontrar la última.

    if(@dirs == 0)
    {
        File::Path::mkpath("$back_dir/$back_date:$level");
        $back_dir = "$back_dir/$back_date:$level";
    }
    else
    {
        my $last = $dirs[-1];
        $back_dir = "$back_dir/$last";
    }

    my @snars = glob("$back_dir/*.snar");
    if(@snars > 0) #==1
    {
        $last_tar = $snars[0];
    }


    $act_level += 1;

}

Se repite este proceso hasta el nivel deseado y se crea una carpeta con el identificador :nivel en caso de que no existan copias para los niveles anteriores.

$back_dir = "$back_dir/$back_date";

if(-e $back_dir)
{
    Cactus::Utils::backuplog("Backup", "$input_dir", $back_dir, $back_date, $level,"Error, a backup for that time exists");
    die("There is one backup with date $back_date, wait at least 1 second");
}
File::Path::mkpath($back_dir);


if($last_tar ne "")
{
copy( $last_tar ,  "$back_dir/$back_date.snar"  );
}

Una vez estamos en esta carpeta copiamos el archivo .snar de la copia anterior, ya que se utilizará para discernir entre los datos antiguos y los nuevos.

    my @args = ("tar", "--listed-incremental", "$back_dir/$back_date.snar", "-czpPf" , "$back_dir/$back_date.tar.gz", $input_dir );

    system(@args) ==0
        or die ("tar failed: @args\n");

    Cactus::Utils::backuplog("Backup", "$input_dir", $back_dir, $back_date, $level, "OK");

}

Finalmente se hace la copia mediante la orden Tar llamada con system(). Su uso en esta ocasión es necesario, ya que aunque existe un módulo perl para interactuar con Tar en CPAN (Archive::Tar) no soporta actualmente el uso del parámetro –listed-incremental para generar y utilizar los archivos .snar.

Listar Backups

Para interactuar con nuestro sistema de copias de forma sencilla tambien se añade una función para obtener las copias almacenadas en un directorio.

Se implementa tar_list que recibe como argumento el directorio y utiliza la función explore_dir que explora de forma recursiva el directorio devolviendo la lista de copias.

sub tar_list($)
{
    (my $back_dir) = @_;
    my @backups;
    if(-e $back_dir)
    {

        @backups = explore_dir($back_dir);
    }
    else
    {
        return @backups;
    }
    return @backups


}

En lugar de explore_dir podría utilizarse el módulo File::Find, sin embargo se mantiene la versión escrita por explorar solo los directorios necesarios. Es decir, sólo un directorio por nivel, el de la fecha anterior más próxima al buscado.

sub explore_dir($)
{


my ($dir) = @_;
my $f;
my @backups;
my @read;
opendir (DIR, $dir) or die $!;
while ($f = readdir(DIR))
{
    push(@read, $f);
}
closedir(DIR);

@read = sort {$a cmp $b} @read;

foreach $f (@read)
{
    if(-f "$dir/$f" &&  $f =~ /^\d{4}-\d{2}-\d{2}\_\d{2}-\d{2}-\d{2}.snar$/ )
    {

        my @fields = split('\.', $f);
        my $date = $fields[0];
        push(@backups, $date);

    }

    if(-e "$dir/$f" &&  $f =~ /^\d{4}-\d{2}-\d{2}\_\d{2}-\d{2}-\d{2}(:(\d)*){0,1}$/ )
    {

        my @r = explore_dir("$dir/$f");
        push(@backups, @r);
    }
}

return @backups;

}

Restaurar una copia

Para restaurar una copia habrá que encontrar todas las copias anteriores y restaurarlas una a una. Se explorara el arbol de ficheros de forma similar a la anterior almacenando las rutas de los ficheros que se restaurarán, y, en caso de que estén todos, se hará en orden la recuperación de cada uno.

Se podría hacer igualmente con find ya que las copias anteriores son aquellas que se encuentran en los directorios superiores a la que se desea restaurar, pero se mantiene por los mismos motivos que en el caso anterior.

sub tar_restore($$$)
{
    my ($from, $date, $ip) = @_;

    unless(-e $from)
    {
        return 0;
    }

    my $back_dir = $from;
    my $cont = 1;
    my @tar_files;

    while($cont == 1 )
    {

        my $back_prev_dir;
        my @back_prev_dirs;
        my $dir;

        opendir (DIR, "$back_dir")  or return -1;

        while ($dir = readdir(DIR))
        {
            if($dir =~  /^\d{4}-\d{2}-\d{2}\_\d{2}-\d{2}-\d{2}(:(\d)*){0,1}$/ )
            {
                push (@back_prev_dirs, $dir);
            }
        }
        closedir(DIR);

        @back_prev_dirs = sort {$a cmp $b} @back_prev_dirs;

        my $i = 0;
        my $n_back_prev_dirs = @back_prev_dirs;

        #case of $date is older than first backup
        if(($n_back_prev_dirs == 0 ||
            ($date lt $back_prev_dirs[$i] && !($back_prev_dirs[$i] =~ /^$date(:(\d)*){0,1}$/)))
            )
        {
            return 0;
        }

        while($i+1 < $n_back_prev_dirs && $date gt $back_prev_dirs[$i])
        {
            $i = $i + 1;
        }


        #if it is the first backup
        if($date ge $back_prev_dirs[$i] || $back_prev_dirs[$i] =~ $date )
        {
            $back_prev_dir = $back_prev_dirs[$i];
            if($date =~ $back_prev_dir)
            {
                $cont = 0;
            }
        }
        else
        {
            $back_prev_dir = $back_prev_dirs[$i-1]
        }

        my @fields = split(":", $back_prev_dir);
        my $back_prev_date = $fields[0];

        $back_dir = "$back_dir/$back_prev_dir";

        my $tar_file = "$back_dir/$back_prev_date.tar.gz";
        my $snar_file = "$back_dir/$back_prev_date.snar";


        if($back_prev_dir =~/^\d{4}-\d{2}-\d{2}\_\d{2}-\d{2}-\d{2}$/)
        {
            if( -f $tar_file && -f $snar_file)
            {
                push(@tar_files, $tar_file);
            }
            else
            {
                Cactus::Utils::restorelog("Restore", $from, $date, "Error: There is a file left");
                return 0;
            }
        }
    }

    foreach my $tar (@tar_files)
    {
        my @tar_args = ("tar", "--incremental", "-xzPpf" , $tar );
        system(@tar_args) == 0
            or return 0;
    }

    Cactus::Utils::restorelog("Restore", $from, $date, "OK");

    return 1;
}

Devuelve 1 en caso de éxito en la recuperación o 0 en caso contrario.

Automatización

Como se ha dicho se desean hacer copias diarias de los archivos del sistema y de los pertenecientes a los usuarios. Se creará un script para cada uno de ellos.

Para el sistema es el siguiente (backupSystem.pl).

#!/usr/bin/perl -w

use strict;
use warnings;
use diagnostics;
use POSIX;
use Switch;
use File::Copy;
use File::Path qw(make_path);

use lib "/usr/local/tronco/";
use Cactus::Backups;


my $argc = @ARGV;


if ($argc != 1 || ! ($ARGV[0] =~ /^\d+$/) || $ARGV[0] >9)
{
    print "\nUsage: backupSystem LEVEL\n";
    exit;
}
my $level = $ARGV[0];

my @backup_paths = ("/etc",  "/root");


unless(-d "/backups")
{
    mkdir "/backups";
}

foreach my $dir (@backup_paths)
{

    if(-d $dir)
    {
        unless(-d "/backups$dir")
        {
            mkdir "/backups$dir";
        }
        Cactus::Backups::tar_backup("$dir", "/backups$dir", $level);
    }
}

Es posible añadir en esta linea los archivos deseados.

my @backup_paths = ("/etc",  "/root");

En nuestro caso será el directorio /root y /etc. Se podrían almacenar otros archivos como los programas instalados en /usr o incluso los logs de /var/log. Como las copias serán diarias y el espacio en el laboratorio es limitado únicamente lo hacemos para los anteriores.

El script hace uso de nuestro módulo Cactus::Backups y comprueba que todos los directorios existan, si el directorio de la copia no existiera lo crearía. Este directorio será /backups, emulando que es un disco externo.

Para los usuarios el script es similar. El cambio es que se iterará por las carpetas en el directorio /home haciendo una copia de aquellas en las que exista la carpeta web. En este caso se llama backupUsers.pl

unless(-d "/backups/users")
{
    mkdir "/backups/users";
}

while ($userdir = readdir(DIR)) {

    next unless(-d "$home/$userdir" );
    next if($userdir  eq "." or $userdir eq "..");
    if(-d "$home/$userdir/web/")
    {
        Cactus::Backups::tar_backup("$home/$userdir/web", "/backups/users/$userdir/web", $level);
    }
}

La idea de hacer copias únicamente de la carpeta web viene dada por la intención de dar un uso práctico a estas copias ya que se realizan sobre el mismo sistema de ficheros.

Toda la información de los usuarios estará contenida en la ruta /home/$usuario/web como se vio en la sección de las páginas personales ya que será la única forma que tengan de subir archivos al sistema.

Teniendo esto en cuenta, se dará la posibilidad a los usuarios de restaurar la copia de seguridad de su página personal. Esto se muestra más adelante en esta sección.

Adicionalmente a estos scripts se incluye un script para listar las copias de seguridad directamente desde consola (listBackups.pl). Si después de hacer algunas copias de seguridad pedimos que nos muestre las del usuario que hemos venido usando nos mostrará lo siguiente.

#/usr/local/tronco/ ./listBackups.pl /backups/users/hormigon/web/
    2013-05-23_19-13-13
    2013-05-23_19-13-15
    2013-05-23_19-13-16

Ahora tenemos nuestros scripts definidos y la estrategia elegida, por lo que solo queda hacer uso de cron para que las copias se realicen automáticamente. Utilizaremos la orden crontab -e para añadir los trabajos, que serán uno diario a las 23:00 horas para los usuarios, siendo cada dia de un nivel superior al anterior (incrementales) y uno diario para la copia del sistema a las 23:30 (actuando del mismo modo).

# crontab -e

### Copia de seguridad de las carpetas personales de los usuarios
0 23 * * 0 /usr/local/tronco/BackupUsers.pl 0
0 23 * * 1 /usr/local/tronco/BackupUsers.pl 1
0 23 * * 2 /usr/local/tronco/BackupUsers.pl 2
0 23 * * 3 /usr/local/tronco/BackupUsers.pl 3
0 23 * * 4 /usr/local/tronco/BackupUsers.pl 4
0 23 * * 5 /usr/local/tronco/BackupUsers.pl 5
0 23 * * 6 /usr/local/tronco/BackupUsers.pl 6
#############################################

### Copia de los archivos del sistema
30 23 * * 0 /usr/local/tronco/BackupSystem.pl 0
30 23 * * 1 /usr/local/tronco/BackupSystem.pl 1
30 23 * * 2 /usr/local/tronco/BackupSystem.pl 2
30 23 * * 3 /usr/local/tronco/BackupSystem.pl 3
30 23 * * 4 /usr/local/tronco/BackupSystem.pl 4
30 23 * * 5 /usr/local/tronco/BackupSystem.pl 5
30 23 * * 6 /usr/local/tronco/BackupSystem.pl 6
#############################################

Cron ya tendrá estos cambios en cuenta y realizará las copias (no es necesario reiniciarlo).

Restauración de página web

Como se ha dicho, para darle una utilidad práctica inmediata a las copias, los usuarios podrán restaurar sus páginas web.

Cuando crean una página web aparecerá el siguiente enlace.

../_images/backuplink.png

Si accedemos nos mostrará la lista de copias que tiene el usuario disponibles.

../_images/firstbackup.png

Estas corresponden a las que hemos visto anteriormente. Para probarlo vamos a añadir una página web para este usuario con un contenido personalizado mediante sftp.

Vemos que la página aparece correctamente.

../_images/preback.png

A continuación generamos una copia de seguridad de los usuarios y volvemos a la página con la lista de backups, en la que aparece el último realizado.

../_images/listanuevos.png

Ahora, por desgracia, perderemos nuestros datos, borrando todos los archivos del usuario.

Si volvemos a la página personal vemos que ya no es posible acceder.

../_images/noweb.png

Restauramos la copia.

../_images/restaurado.png

Y comprobamos que la página está de nuevo disponible.

../_images/preback.png

Contenidos

Tema anterior

Alojamiento de espacio personal

Próximo tema

Monitorización

Esta página