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. Для этого нам понадобятся следующие вещи:

  1. Во-первых, нам потребуется LVM, и для обслуживание нужного нам ресурса Samba нужно будет выделить отдельный логический том.

    Сохранение архивных копий мы будем осуществлять с помощью механизма снапшотов LVM (подробнее об этом чуть ниже), а из-за специфики модуля shadow_copy необходимо, чтобы расшаренный каталог находился в корне диска (На самом деле я не проверял верность последнего утверждения на практике, просто сделал именно так, поскольку где-то встречал предупреждение, что иначе работать ничего не будет).

    Я создал отдельный том /dev/data/users, который монтируется в /var/data/users, т.е. Samba-ресурс [users] у меня находится в корне логического тома LVM.

  2. Во-вторых нам потребуется создавать средствами 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 механизм снапшотов устроен достаточно хитро. Основные моменты:

  1. Снапшот не должен быть такого же размера, что и исходный диск, а может быть значительно меньше.
  2. В момент создания снапшот полностью пустой. То есть никакие данные при создании снапшота никуда не копируются.
  3. После того, как снапшот создан, при записи данных на исходный диск оригинальные данные сохраняются на снапшот. Т.е. всё на самом деле просто. Если что-то пишется на оригинал, что на снапшоте сохраняются оригинальные данные. А если что-то на оригинале не изменяется, то и смысла писать это на снапшот нет, можно напрямую считать с оригинала. Весь этот механизм обслуживается LVM и внешне снапшот выглядит как диск точно такого-же размера, как и оригинал, с полной ФС и всеми файлами на момент создания снапшота. Если вам сложно это всё понять - попробуйте изобразить на бумажке процесс записи данных на диск, для которого создан снапшот. Учитывая, что конечные файлы и ФС - это самый верхний уровень абстракции, под которым в нашем случае находится механизм снапшотов, механизм логических томов и групп томов LVM и только потом реальные физические винчестеры вашего компьютера.

Ещё разок: поскольку снапшот не хранит все данные оригинала, а только разницу с оригиналом с момента создания снапшота, то размер снапшота может быть сильно меньше оригинала. В случае, если снапшот переполнится, т.е. если суммарный размер отличий от оригинала превысит объём снапшота, то он автоматически деактивируется. Кстати, размер снапшота в любой момент можно увеличить.

Поскольку если для диска существует снапшот, то запись данных на этот диск будет приводить к физической одновременной записи данных и на снапшот. Т.е. реально количество физических дисковых операций возрастёт чуть ли не в 2 раза, что может существенно снизить скорость записи на оригинал. Если же снапшотов много - то скорость записи может упасть в разы. Учитывайте это при работе со снапшотами.

Итак, снапшот создаётся командой 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.

Естественно, вам нужно будет поменять все параметры, присвоив им нужные значения. Кроме того, для каждого тома необходимо будет создать свою копию скрипта с нужными параметрами.

Ссылки