Содержание
Samba очень часто используется для организации файл-сервера для Windows-машин. А в Windows есть такой хитрый механизм, который позволяет сохранять и просматривать предыдущие версии файлов. Для того, чтобы наглядно на него посмотреть, зайдите в свойства любого файла на расшаренном диске и обратите внимание на вкладку «Предыдущие версии».
Samba позволяет вам организовать хранение предыдущих версий файла так, чтобы они были доступны механизму учёта предыдущих версий в Windows. Для этого предназначен специальный модуль vfs object
с именем shadow_copy
. Для активации этого механизма со стороны Samba просто добавьте в блок, описывающий нужную вам шару, строчку
vfs objects = shadow_copy
Например, вот как сделано у меня:
[users] comment = Users shared folders path = /var/data/users/ admin users = "@DOMAIN\Администраторы домена" read only = No create mask = 0600 directory mask = 0700 map acl inherit = Yes hide unreadable = Yes locking = No vfs objects = shadow_copy
Теперь необходимо собственно организовать механизм сохранения предыдущих копий и их учёта в Samba. Для этого нам понадобятся следующие вещи:
Во-первых, нам потребуется LVM, и для обслуживание нужного нам ресурса Samba нужно будет выделить отдельный логический том.
Сохранение архивных копий мы будем осуществлять с помощью механизма снапшотов LVM (подробнее об этом чуть ниже), а из-за специфики модуля
shadow_copy
необходимо, чтобы расшаренный каталог находился в корне диска (На самом деле я не проверял верность последнего утверждения на практике, просто сделал именно так, поскольку где-то встречал предупреждение, что иначе работать ничего не будет).Я создал отдельный том
/dev/data/users
, который монтируется в/var/data/users
, т.е. Samba-ресурс[users]
у меня находится в корне логического тома LVM.Во-вторых нам потребуется создавать средствами LVM снапшоты с указанного выше тома и монтировать их потом в специально названные каталоги.
Работает это примерно так: вы создаёте в нужный момент снапшот с логического тома, а затем монтируете его в каталог с именем вида
@GMT-2010.01.01-12.00.00
в корень Samba-ресурса. Дату и время в имени каталога естественно нужно указывать такой же, что и время создания снапшота. Каталоги такого вида не будут отображаться в Windows, а как раз с помощью модуляshadow_copy
будут восприниматься как архивные копии.
Естественно, управление всем этим должно осуществляться с помощью скриптов, однако сначала давайте рассмотрим все этапы и необходимые команды подробно.
Настройка LVM и создание основного логического тома и Samba-ресурса
Сначала вам необходимо разобраться в принципах работы LVM. Я не буду подробно описывать эту систему, так что рекомендую вам ознакомиться с ней самостоятельно.
Я создал группу томов с именем data
, в которую добавил логический том users
размером 100Gb с файловой системой ext3
. Т.е. мой логический том имеет основное имя в системе /dev/mapper/data-users
или же более удобоваримое /dev/data/users
:
# ls -al /dev/data/users lrwxrwxrwx 1 root root 22 2010-05-19 16:30 /dev/data/users -> /dev/mapper/data-users
Этот том монтируется в пустой каталог /var/data/users
, вот собственно запись из /etc/fstab
:
/dev/mapper/data-users /var/data/users ext3 defaults,nosuid,noexec,acl 0 2
Как настроен Samba-ресурс я уже писал, на всякий случай вот секция файла /etc/samba/smb.conf
, за него отвечающая:
[users] comment = Users shared folders path = /var/data/users/ # Пользователи с правами редактирования галочек ;) admin users = "@DOMAIN\Администраторы домена" hide unreadable = yes read only = no # Маски - только пользователь по умолчанию должен иметь доступ create mask = 0600 directory mask = 0700 # блокировки - иногда бывают грабли без этого пункта locking = no # наследуемые права map acl inherit = yes # Поддержка архивных версий файлов vfs objects = shadow_copy
Работа с LVM-снапшотами
Для начала на всякий случай немного расскажу, что такое снапшот. Снапшот - это моментальная копия диска на момент создания этого самого снапшота. Т.е. снапшоты позволяют заморозить некое состояние диска в какой-то момент, а потом использовать это замороженное состояние с любыми целями. В нашем случае, мне кажется, цель использования снапшотов понятна. Мы снимаем в момент Х снапшот всего диска users
, далее его монтируем в каталог /var/data/users/@GMT-X
и после этого в Windows на вкладке предыдущих версий мы увидим версию файла на момент Х. Кстати, если файл с момента Х не изменится, то на вкладке предыдущих версий ничего отображаться не будет, учтите это.
В LVM механизм снапшотов устроен достаточно хитро. Основные моменты:
- Снапшот не должен быть такого же размера, что и исходный диск, а может быть значительно меньше.
- В момент создания снапшот полностью пустой. То есть никакие данные при создании снапшота никуда не копируются.
- После того, как снапшот создан, при записи данных на исходный диск оригинальные данные сохраняются на снапшот. Т.е. всё на самом деле просто. Если что-то пишется на оригинал, что на снапшоте сохраняются оригинальные данные. А если что-то на оригинале не изменяется, то и смысла писать это на снапшот нет, можно напрямую считать с оригинала. Весь этот механизм обслуживается LVM и внешне снапшот выглядит как диск точно такого-же размера, как и оригинал, с полной ФС и всеми файлами на момент создания снапшота. Если вам сложно это всё понять - попробуйте изобразить на бумажке процесс записи данных на диск, для которого создан снапшот. Учитывая, что конечные файлы и ФС - это самый верхний уровень абстракции, под которым в нашем случае находится механизм снапшотов, механизм логических томов и групп томов LVM и только потом реальные физические винчестеры вашего компьютера.
Ещё разок: поскольку снапшот не хранит все данные оригинала, а только разницу с оригиналом с момента создания снапшота, то размер снапшота может быть сильно меньше оригинала. В случае, если снапшот переполнится, т.е. если суммарный размер отличий от оригинала превысит объём снапшота, то он автоматически деактивируется. Кстати, размер снапшота в любой момент можно увеличить.
Итак, снапшот создаётся командой lvcreate
с ключом -s
. Например, следующая команда создаст снапшот с именем 2010.01.01-12.00.00
для логического тома /dev/data/users
размером 10Gb:
lvcreate -s -L 10G -n 2010.01.01-12.00.00 /dev/data/users
То, что снапшот создался, можно проверить десятком способов. Попробуйте, например, следующие команды:
# lvscan # lvdisplay /dev/data/users # lvdisplay /dev/data/2010.01.01-12.00.00 # ls -al /dev/data
Собственно осталось создать директорию с именем @GMT-2010.01.01-12.00.00
в каталоге /var/data/users
и примонтировать туда наш снапшот:
# mkdir /var/data/users/\@GMT-2010.01.01-12.00.00 # mount -r /dev/data/2010.01.01-12.00.00 /var/data/users/\@GMT-2010.01.01-12.00.00
Я монтирую с опцией -r
(read only), поскольку LVM позволяет писать на снапшоты, но нам это не нужно. Поэтому лучше сразу себя обезапасить от неожиданностей.
Вот и всё. Если вместо 2010.01.01-12.00.00
использовать текущее время, то после изменения какого-либо файла на расшаренном ресурсе в Windows на вкладке предыдущих версий появится версия вашего файла, датированная временем создания снапшота.
Автоматизация процесса
Естественно, вся прелесть этого механизма в том, что можно автоматически создавать архивные копии. Но для этого придётся написать несколько скриптов. Я написал все необходимые функции в одном Perl-файле, который потом использовал как подключаемую библиотеку для скриптов управления. Вот этот файл:
#!/usr/bin/perl -w # Библиотека функций работы со снапшотами. # Author: Nevorotin Vadim aka Malamut # Лицензия: GPLv3 use 5.010; # Проверка элемента на вхождение в массив (а-ля оператор in) sub isIn { @_ > 0 or die "isIn - OOPS!\n"; my $element = shift @_; my @arr = @_; foreach (@arr) { if ($_ eq $element) { return 1 } } return 0; } # Возвращает текущее время в формате yyyy.mm.dd-hh.mm.ss sub getDate { my @time = localtime; return ($time[5] + 1900) . '.' . sprintf("%02d",$time[4] + 1) . '.' . sprintf("%02d",$time[3]) . "-" . sprintf("%02d",$time[2]) . '.' . sprintf("%02d",$time[1]) . '.' . sprintf("%02d",$time[0]); } # Функция ищет все примонтированные снапшоты из заданной Volume Group в заданную директорию sub getMounted { @_ == 2 or die "getMounted - OOPS!\n"; my ($vg,$path) = @_; my @snapshots = (); foreach (`mount | grep /dev/mapper/$vg`) { if (/$vg-(\d{4}\.\d{2}\.\d{2})--(\d{2}\.\d{2}.\d{2})\s+\S+\s+$path\/\@GMT-\1-\2\s+/) { push @snapshots, "$1-$2"; } } return @snapshots; } # Функция ищет все снапшоты для указанного тома из указанной VG. Возвращяет хеш снапшот-состояние sub getActive { @_ == 2 or die "getActive - OOPS!\n"; my ($lv,$vg) = @_; my %snapshots = (); my $flag = 0; foreach (`lvdisplay /dev/$vg/$lv 2>&1`) { if (/LV\ssnapshot\sstatus/) { $flag = 1 } elsif ($flag) { if (m#^\s+/dev/\S+?/(\S+)\s+\[(\S+)]#) { $snapshots{$1} = lc $2; } else { $flag = 0 } } } return %snapshots; } # Функция ищет все каталоги для снапшотов в заданной директории sub getListed { @_ == 1 or die "getListed - OOPS!\n"; my $path = shift @_; my @dirs = (); my @content = glob "$path/\@GMT*"; foreach (@content) { if (-d $_ and /GMT-(\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}.\d{2})/) { push @dirs, $1; } } return @dirs; } # Создаёт снапшот с текущим временем для тома LV в группе VG и монтирует # в подкаталог path с именем @GMT-sn_name sub createSnapshot { @_ == 4 or die "createSnapshot - OOPS!\n"; my ($lv, $vg, $path, $sn_size) = @_; my $sn_name = getDate; # Создаём директорию под снапшот mkdir "$path/\@GMT-$sn_name", 0777 or die "I can't create a directory for snapshot $sn_name! ($!)\n"; # Создаём снапшот if (system "lvcreate -L ${sn_size}G -s -n $sn_name /dev/$vg/$lv 1>/dev/null") { rmdir "$path/\@GMT-$sn_name" or warn "Very big error: I can't remove a directory for snapshot :("; die "I can't create a snapshot $sn_name!\n"; } # Монтируем if (system "mount -o ro,acl,user_xattr /dev/$vg/$sn_name $path/\@GMT-$sn_name") { !system "lvremove -f /dev/$vg/$sn_name 1>/dev/null" or warn "Very big error: I can't remove a snapshot :("; rmdir "$path/\@GMT-$sn_name" or warn "Very big error: I can't remove a directory for snapshot :("; die "I can't mount a new snapshot $sn_name to directory!"; } } # Удаляет снапшот из группы томов VG с именем snName, а так же пытается удалить каталог для снапшота в директории # с адресом path и если снапшот примонтирован, то и тот каталог, куда примонтирован sub removeSnapshot { @_ == 3 or die "removeSnapshot - OOPS!\n"; my ($sn_name, $vg, $path) = @_; # Проверяем смонтирован ли, и если да - то куда my $mpath = $path; chomp(my $ms = `mount | grep $sn_name`); if ($ms) { ($mpath) = $ms =~ /^\S+\s+\S+\s+(\S+)/; !system "umount -lf /dev/$vg/$sn_name" or die "I can't umount $sn_name!\n"; rmdir $mpath or die "I can't remove directory $mpath!\n"; } # Удаляем директорию для снапшота if (-e "$path/\@GMT-$sn_name") { rmdir "$path/\@GMT-$sn_name" or die "I can't remove directory $path/\@GMT-$sn_name!\n"; } # Удаляем снапшот !system "lvremove -f /dev/$vg/$sn_name 1>/dev/null 2>/dev/null" or die "I can't remove a snapshot $sn_name!\n"; } # Проверяет размер снапшота и при необходимости и возможности увеличивает его sub checkSize { @_ == 4 or die "checkSize - OOPS!\n"; my ($sn_name, $vg, $sn_limit, $sn_add) = @_; my $size = 0; foreach (`lvdisplay /dev/$vg/$sn_name 2>&1`) { if (/Allocated\s+to\s+snapshot\s+(\S+)%/i) { $size = $1 } } if ( $size > $sn_limit ) { !system "lvextend -L +${sn_add}G /dev/$vg/$sn_name 1>/dev/null 2>/dev/null" or warn "I can't extend snapshot $sn_name!\n"; } } # Функция ротации снапшотов. Для заданного тома LV в заданной VG пытается поддерживать ровно COUNT снапшотов. # При вызове всегда создаёт новый снапшот, при этом если надо - удаляет самый старый. # Проверяет также текущие снапшоты, удаляет INACTIVE и расширяет те, которым необходимо расширение. # sn_limit - в процентах (0..100), sn_size и sn_add - в гигабайтах # snapshotsRotate($lv, $vg, $path, $count, $sn_size, $sn_limit, $sn_add) sub snapshotsRotate { @_ == 7 or die "snapshotsRotate - OOPS!\n"; my ($lv, $vg, $path, $count, $sn_size, $sn_limit, $sn_add) = @_; my %snapshots = getActive($lv,$vg); # Удаляем неактивные снапшоты в принципе и снапшоты с неизвестными именами из списка foreach (keys %snapshots) { if (! $snapshots{$_} =~ /active/i) { removeSnapshot($_, $vg, $path); delete $snapshots{$_}; } if (! /^\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}.\d{2}$/) { delete $snapshots{$_}; } } # Все оставшиеся снапшоты пишем в отсортированный список @snapshots = sort keys %snapshots; # Если нужно - удаляем самые старые, чтобы в итоге осталось $count-1 штук foreach ( 0..(@snapshots-$count) ) { removeSnapshot($snapshots[$_], $vg, $path); } splice @snapshots, 0, @snapshots-$count+1 if @snapshots-$count+1 > 0; # Теперь проверяем, не надо ли чего увеличить в размерах foreach (@snapshots) { checkSize($_, $vg, $sn_limit, $sn_add); } # А теперь создаём новый снапшотик createSnapshot($lv, $vg, $path, $sn_size); } # Пытаемся примонтировать все снапшоты для указанного тома в указанной группе в их целевые каталоги в path sub snapshotsRemount { @_ == 3 or die "snapshotsRemount - OOPS!\n"; my ($lv, $vg, $path) = @_; my %snapshots = getActive($lv,$vg); # Удаляем неактивные снапшоты в принципе и снапшоты с неизвестными именами из списка foreach (keys %snapshots) { if (! $snapshots{$_} =~ /active/i) { removeSnapshot($_, $vg, $path); delete $snapshots{$_}; } if (! /^\d{4}\.\d{2}\.\d{2}-\d{2}\.\d{2}.\d{2}$/) { delete $snapshots{$_}; } } my @mounted = getMounted($vg,$path); my @listed = getListed($path); # Монтируем все снапшоты в предназначенные для них директории foreach my $sn_name (keys %snapshots) { unless (isIn($sn_name, @listed)) { mkdir "$path/\@GMT-$sn_name", 0777 or die "I can't create a directory for snapshot $sn_name! ($!)\n"; } unless (isIn($sn_name, @mounted)) { if (system "mount -o ro,acl,user_xattr /dev/$vg/$sn_name $path/\@GMT-$sn_name") { rmdir "$path/\@GMT-$sn_name" or warn "Very big error: I can't remove a directory for snapshot $sn_name!:(\n"; die "I can't mount a snapshot $sn_name to it's directory!\n"; } } } # Удаляем директории, для которых нету снапшотов foreach (@listed) { unless (isIn($_, keys %snapshots)) { rmdir "$path/\@GMT-$_" or die "Error: I can't remove an unused directory $_!:(\n"; } } } # Удаляет все снапшоты для заданного тома sub removeAllSnapshots { @_ == 3 or die "removeAllSnapshots - OOPS!\n"; my ($lv, $vg, $path) = @_; my %snapshots = getActive($lv, $vg); # Удаляем все снапшоты foreach (keys %snapshots) { removeSnapshot($_, $vg, $path); } } # pm же! 1;
А вот файл управления снапшотами для упоминавшегося выше тома /var/data/users
:
#!/usr/bin/perl -w # Скрипт управления ротацией снапшотов. # Author: Nevorotin Vadim aka Malamut # Лицензия: GPLv3 use 5.010; use Getopt::Long; # Для разбора опций # Библиотека с необходимыми функциями require "/etc/samba/snapshots/libsnapshot.pm"; ######################################## # Параметры тома для ротации снапшотов # ######################################## # Группа томов $vg = 'data'; # Логический том $lv = 'users'; # Точка монтирования $path = '/var/data/users'; # Количество поддерживаемых снапшотов $count = 5; # Начальный размер снапшота, Gb $sn_size = 5; # Предел заполнения до ресайза, % $sn_limit = 80; # Шаг увеличения снапшота при переполнении, Gb $sn_add = 3; ######################################### $clear = 0; $rotate = 0; $remount = 0; Getopt::Long::Configure ("bundling"); # Конфигурирование getopt дабы воспринимать склейку коротких аргументов GetOptions( "clear|c" => \$clear, # Удалить все снапшоты "rotate|r" => \$rotate, # Провести ротацию "remount|m" => \$remount, # Перемонтировать имеющиеся снапшоты "help|h" => \$help); # Помощь же if (@ARGV or $help) { die "Usage: snapshots.pl [--clear|--rotate|--remount]\n\t-c = --clear\n\t-r = --rotate\n\t-m = --remount\n"; } elsif ($clear) { removeAllSnapshots($lv, $vg, $path); } elsif ($rotate) { snapshotsRotate($lv, $vg, $path, $count, $sn_size, $sn_limit, $sn_add); } elsif ($remount) { snapshotsRemount($lv, $vg, $path); } else { die "Usage: snapshots.pl [--clear|--rotate|--remount]\n\t-c = --clear\n\t-r = --rotate\n\t-m = --remount\n"; }
Его необходимо запускать по cron с опцией -r (ротация), я делаю это раз в сутки в 00:12.
Также надо не забыть после рестарта сервера перемонтировать все снапшоты, для этого нужно запустить скрипт с параметром -m.Сделать это можно из /etc/rc.local
. Если же вы решите удалить все снапшоты, то вызовите скрипт с параметром -c.
Естественно, вам нужно будет поменять все параметры, присвоив им нужные значения. Кроме того, для каждого тома необходимо будет создать свою копию скрипта с нужными параметрами.