В данной статье рассмотрена реализация мониторинга хранилища файлов резервных копий, создаваемых средствами Windows Backup, при помощи скрипта Powershell с целью контроля своевременности резервного копирования и размера формируемых данных. Также статья ставит задачей привести примеры некоторых полезных приёмов программирования на языке Powershell.
Длинное вступление
Хорошо располагать системой управления резервным копированием, которая работает по расписанию и вовремя уведомляет администратора о состоянии дел в его подчас непростом хозяйстве. Хорошо и, на первый взгляд – не слишком накладно: благо, выбор как коммерческих, так и бесплатных систем этого рода сейчас достаточно широк. Однако уровень трудоёмкости как развёртывания, так и поддержания любой такой системы в рабочем состоянии, а также аппаратные затраты на неё предполагают некий разумный минимум сложности самой IT-системы, данные элементов которой предполагается резервировать. Думается что развёртывание, скажем, Bacula для нужд небольшого офиса, число рабочих станций под Windows в котором – около дюжины и есть, положим, два-три сервера, представляет собой непродуктивные затраты времени и материальных ресурсов в контексте конечного продукта предприятия и единственным позитивным результатом такого действия будет практический опыт, получаемый системным администратором. Рассмотрение систем децентрализованного резервного копирования, предлагающих хороший уровень функций и относительно высокую автономность, приводит к коммерческим продуктам типа Veeam Endpoint Backup.
Бесплатные же редакции таких программ, к сожалению, если и могут похвастать какими-либо преимуществами перед встроенным Windows Backup, то эти преимущества актуальны не для всех и не во всех случаях и далеко не всегда очевидны. Да и «родная» поддержка механизма теневых копий, прямо скажем, является его большим плюсом. Словом, иногда возникает ощущение, что все дороги ведут к этому самому Windows Backup. На самом деле, вопрос выбора и организации системы резервного копирования является сугубо индивидуальным для каждого конкретного случая и решается на фоне, в том числе, субъективных факторов. Однако в любом случае хочется создать надёжную и стабильную систему, которая, к тому же, проста в обслуживании. А вот как раз функциями обобщённого мониторинга масштаба организации автономные агенты резервного копирования похвастать и не могут. На этом, думаю, пора завершать это предисловие, дабы не наскучить слишком скоро.
Мизансцена наша такова: есть десяток компьютеров под управлением Windows 7 и Windows 10, а также сервер под управлением Windows Server 2008. Есть также NAS, приобретённый для использования в качестве хранилища резервных копий данных. Стоит задача: без дополнительного бюджета обеспечить резервное копирование несложного, но достаточно надёжного рода. Несложного, в том числе, и потому, что штатного системного администратора в организации нет, и все IT-проблемы решаются администратором-совместителем при посильной помощи некоторых пользователей. Принято решение воспользоваться средствами Windows Backup, который будет сохранять данные в выделенную папку с именем «netbackup» на удаленной машине с сетевым именем «SA-NAS» в соответствии с локальными расписаниями каждого компьютера. Это решение работает, причём – достаточно стабильно, и каждый компьютер регулярно отправляет отчёты о выполнении резервного копирования на email системного администратора (считываются из системных логов скриптом). Но вот вопрос анализа объёмов и «свежести» создаваемых архивов остаётся открытым, поскольку Windows Backup не расположен делиться этой информацией иначе как интерактивно. Системный администратор засучил рукава и начал писать очередной скрипт, для чего пришлось сначала разобрать вопрос структуры данных резервных копий, создаваемых Windows Backup. Итак…
Что создаёт Windows Backup и как это организовано?
Чтобы ответить на этот вопрос придётся, совсем по-одесски, задать встречный вопрос: «А что вы конкретно имеете в виду?» Ибо Windows Backup, как известно, может создавать (а) резервные копии избранных элементов файловой системы компьютера, т.е. файловые архивы (file backup), и (б) образы дисковых томов (image backup). Мы сами выбираем тип резервного копирования для конкретного компьютера при его настройке при помощи соответствующего апплета Панели Управления и, как известно, можем задать как архивирование лишь определённых папок и файлов, так и формирование образов выбранных дисковых томов, а также комбинацию этих двух методов. Не хочется отдаляться от темы статьи, вдаваясь в описание этой операции, которая является тривиальной, так что за подробностями осмелюсь отослать читателя к соответствующей документации и поисковым системам Интернета. Я же приведу данные собственного изучения структур данных Windows Backup, называть которое модным словом «реинжиниринг» как-то нескромно в силу относительной простоты задачи. Ещё замечу, что далее в тексте для каталогов файловой системы буду использовать термин «папка» – для того, чтобы не было путаницы с термином «каталог», который будет употребляться только применительно к файлам каталогов Windows Backup.
Наборы данных, создаваемые Windows Backup в каждом из двух режимов архивации на целевом носителе, будут отличаться структурой.
На первом рисунке показан типичный пример структуры папок файловой системы, создаваемой в целевой сетевой папке «netbackup» при резервном копировании в файловом режиме. В первую очередь, в корневой папке появляется служебный файл «MediaID.bin». Как хорошо видно, на первом уровне иерархии (уровне компьютеров) создаются индивидуальные подпапки для каждого компьютера («PC-001», «PC-002» и т.д.). Эти папки содержат, как минимум, пару служебных файлов: «MediaID.bin» и «Desktop.ini».
В индивидуальной папке каждого компьютера, по мере работы Windows Backup, появляются подпапки второго уровня иерархии (уровень архивов) с именами вида "Backup Set <дата> <время>", каждая из которых содержит один архив: начальную полную резервную копию выбранных данных и последующие, основанные на ней, инкрементные резервные копии. Также, каждая папка уровня архива содержит подпапку с именем «Catalogs», которая вмещает файл с именем «GlobalCatalog.wbcat» (содержит каталог данного архива).
Данный файл очень полезен для анализа структуры данных и её статуса: само его присутствие в папке «Catalogs» говорит о том, что, во-первых, эта папка и соседние расположены внутри уровня архивов (на уровне томов) и, во-вторых, анализ времени последнего его изменения позволяет узнать, когда выполнялась последняя операция резервного копирования, обновлявшая данный архив.
Как начальная полная копия, так и последующие инкрементные копии, располагаются в отдельных подпапках уровня томов с именами вида «Backup Files <дата> <время>». Так же, как и на уровне архивов, каждая папка уровня томов содержит подпапку «Catalogs», но она – в отличие от тёзки уровня архивов – содержит довольно много файлов с расширениями ".wbcat" и ".wbverify", образующими каталог конкретного тома архива. Помимо подпапки «Catalogs», каждая папка этого уровня содержит лишь произвольное количество ZIP-архивов, которые в совокупности вмещают данные конкретного тома. Всё достаточно просто и – что приятно – данные можно, в крайнем случае, извлечь вручную простой распаковкой ZIP-архивов.
Поскольку на настроенный и работающий Windows Backup можно повлиять в той же степени, что и на выпущенный из рогатки камень, решения о прекращении дополнения текущего архива инкрементными томами и инициализации нового принимаются где-то глубоко в недрах кода этой программы и не становятся известны окружающим иначе как в виде появления новой папки на уровне архивов. Автору статьи не удалось выяснить алгоритм принятия программой решений такого рода, но логичной (и обоснованной в контексте имеющегося опыта) выглядит взаимосвязь возможности продолжения цепочки инкрементных томов с успешностью завершения последней операции резервного копирования. Более того, Windows Backup, породив новый архив, напрочь забывает о его предшественнике, молча поручая его судьбу на полное усмотрение администратора системы. Что, разумеется, незамедлительно отражается на размере свободного дискового пространства вплоть до того момента как администратор не удалит папки устаревших архивов тем или иным образом. Вероятно, таким образом обеспечивается множественность резервных копий одного и того же файла (см. закладку «Предыдущие версии» окна свойств файла в Проводнике). Ну что же, каков он есть – Windows Backup – таков он и есть: сложно ожидать отсутствия ограничений от виртуально бесплатного продукта.
Полагая, что в вопрос структуры хранения архивов файлового типа достаточная ясность внесена, перейдём к образам дисковых томов. Структура каталогов в этом случае (см. рисунок ниже) достаточно схожа со структурой каталогов файловых архивов Windows Backup, однако имеются и существенные отличия:
- Первый уровень иерархии папок образован папкой «WindowsImageBackup», которая создается в целевой папке («netbackup») при резервном копировании в дисковом режиме. Подпапки для отдельных компьютеров создаются внутри «WindowsImageBackup» и образуют уровень компьютеров.
- Внутри подпапки каждого компьютера (на уровне архивов) находятся лишь три папки. Две из них имеют статические имена: «Catalogs» и «SPPMetadataCache», а третья – папка архива — имеет имя вида «Backup <дата> <время>».
Каждая папка уровня компьютеров всегда содержит, помимо трёх вышеуказанных подпапок, служебный файл «MediaId» без расширения. Подпапка «Catalog», в свою очередь, всегда содержит два файла: «GlobalCatalog» и «BackupGlobalCatalog».
Папка архива (с именем вида «Backup <дата> <время>») меняет своё имя всякий раз, когда выполняется архивация данного компьютера в режиме образов дисковых томов. Это логично, поскольку каждый раз выполняется полная, а не инкрементальная архивация томов. Остаётся загадкой, однако, почему в этом режиме устаревший архив удаляется автоматически немедленно, а устаревшие архивы файлового режима архивации продолжают лежать на прежнем месте. Так или иначе, эта папка содержит набор файлов VHD (образ виртуального диска) и группу вспомогательных файлов XML, среди которых выделим «BackupSpecs.xml». Он, судя по всему, служит для сохранения параметров конкретного архива, и формируется после фактического создания файлов образов дисковых томов, позволяя, таким образом, судить о времени завершения операции архивирования.
Вот, собственно, и все, что можно считать существенной информацией о структуре хранилища архивов Windows Backup. Рассматривать файлы «Desktop.ini», которые Windows имеет обыкновение формировать в некоторых папках, не вижу смысла, поскольку они никак не задействованы в нашем мониторинге.
Наши алгоритмы
Определимся с информацией, которую хотелось бы получать автоматически. Пусть это будет следующий набор атрибутов:
- список машин, для которых существуют архивы Windows Backup, и их типы;
- дата/время обновления самого актуального архива;
- размер самого актуального архива;
- имя/путь папки самого актуального архива.
Для архивов Windows Backup, создаваемых в файловом режиме, алгоритм сбора информации будет следующим:
- Выполняем последовательный перебор подпапок первого уровня вложенности в целевой папке и выявление тех из них, которые содержат файл «MediaID.bin», фактически являющийся флагом корневой папки архивов отдельной машины (далее, для краткости – КПАМ).
- В каждой выявленной КПАМ выполняем анализ наличия в её подпапках папки «Catalogs», содержащей файл «GlobalCatalog.wbcat».
- Выделяем самый новый среди файлов «GlobalCatalog.wbcat», обнаруженных на предыдущем шаге, и принимаем папку, содержащую его родительскую папку, как хранилище самого актуального архива файлового режима (далее по тексту – САА). Принимаем время последней модификации этого файла как время завершения формирования последнего тома этого архива.
- Суммируем размеры ZIP-файлов в подпапках папки САА и принимаем это значение за размер САА.
- Формируем и возвращаем пользовательское представление накопленной информации: имя (путь), размер и время создания САА для каждой машины из списка КПАМ.
Для архивов режима дисковых томов алгоритм сбора информации будет таким:
- Выполняем последовательный перебор подпапок первого уровня вложенности в целевой папке и выявление тех из них, которые содержат файл «MediaID», фактически являющийся флагом корневой папки архивов отдельной машины (далее, для краткости – КПАМ).
- В каждой КПАМ выполняем анализ наличия в её подпапках файла с именем «BackupSpecs.xml». На всякий случай исходим из возможности существования не одной, а нескольких папок архива.
- Выделяем самый новый среди файлов «BackupSpecs.xml», обнаруженных на предыдущем шаге, и принимаем папку, содержащую его, как хранилище самого актуального архива режима дисковых томов (далее – САА). Принимаем время последней модификации этого файла как время завершения формирования этого архива.
- Суммируем размеры VHD-файлов в подпапках папки САА и принимаем это значение за размер САА.
- Формируем и возвращаем пользовательское представление накопленной информации: имя (путь), размер и время создания САА для каждой машины из списка КПАМ.
Пока всё выглядит достаточно простым для реализации, в т.ч. и в виде скрипта Powershell, так что – приступим.
Скрипт Powershell
В этом разделе мы рассмотрим ключевые элементы скрипта, а за опущенными подробностями реализации предлагаю обратиться к исходному его коду, который прилагается. Сразу замечу, что текстовые элементы пользовательского интерфейса в приведенных ниже примерах кода приведены на русском языке, но в скрипте они реализованы на английском – просто разработан он был для применения в англоязычной среде и я не хотел бы отпочковывать версии кода по языковому критерию, а реализация многоязычного интерфейса неоправданно сложна для такого простого продукта.
Обработка архивов файлового режима
Для начала, получим список КПАМ:
function Get-MachineListFB ($WinBackupRoot) {
$MachineRoots = @{}
Get-ChildItem -Path $WinBackupRoot -Recurse -Depth 1 -File -Filter "MediaID.bin" |
Where-Object {$_.Directory.FullName -ne $WinBackupRoot} |
foreach {
$MachineRoots[$_.Directory.Name]=$_.Directory.FullName
}
return $MachineRoots
}
Функция Get-MachineListFB принимает один параметр: полное имя корневой папки хранилища архивов Windows Backup и, как видим, ничего замысловатого в её коде нет. Командлет Get-ChildItem формирует список объектов-описателей файлов «MediaID.bin», расположенных в пределах одного уровня вложенности по отношению к заданной папке. Этот список конвейером передаётся командлету Where-Object, который пропускает на последующий этап только те из найденных файлов, которые расположены вне корня заданной папки, в её подпапках (как мы помним, корневая папка хранилища архивов Windows Backup также содержит файл с именем «MediaID.bin»). Далее производится наполнение возвращаемого объекта именами и полными путями папок, содержащих найденные файлы.
По поводу использования командлета Get-ChildItem для отбора файлов и папок по заданному критерию хотелось бы сделать замечание. Как видно, критерий имени в командлете выражен в форме параметра "-Filter". При этом, Get-ChildItem имеет также и другой параметр, способный выполнить ту же функцию: "-Include". В чём же разница и как выбрать способ описания критерия отбора? При использовании параметра "-Filter" Powershell выполняет отбор путём задействования соответствующих механизмов провайдера, реализовывающего доступ к конкретному набору данных (в нашем случае – дисковой файловой системе). Как альтернативу, Powershell имеет собственный встроенный механизм отбора, задействуемый параметром "-Include". Разница для нас, как для пользователя, проявляется в:
- Быстродействии: отбор при помощи механизмов провайдера может быть (а в нашем случае – точно является) более быстрым, чем собственный функционал PS;
- Возможностях: при использовании параметра "-Include" совместно с параметром "-Recurse" не работает ограничение глубины иерархии параметром "-Depth" (как минимум – в Powershell версии 5 применительно к дисковой файловой системе – это было установлено экспериментально).
Далее, реализовываем функцию выявления САА:
function Get-LastFileBackupSet ($MachineRoot) {
$objLB = $null
try {
$LastWbcat = Get-ChildItem -Path $MachineRoot -Recurse -Depth 2 `
-File -Filter "GlobalCatalog.wbcat" |
Where-Object {$_.Directory.Name -eq "Catalogs"} |
Sort-Object LastWriteTime |
Select-Object -Last 1 LastWriteTime, Directory, `
@{Name="Machine";Expression={$_.Directory.Parent.Parent.Name.ToUpper()}}, `
@{Name="Path";Expression={$_.Directory.Parent.FullName}}, `
@{Name="Name";Expression={$_.Directory.Parent.Name}}
$ZIPFiles = (
Get-ChildItem $LastWbcat.Directory.Parent.FullName -Recurse -Depth 2 `
-File -Filter "*.zip" |
Select-Object Length |
Measure-Object -Property Length -Sum
)
}
finally
{
if (($ZIPFiles) -and ($LastWbcat)) {
$objLB = @{
Machine = $LastWbcat.Machine
Path = $LastWbcat.Path
Name = $LastWbcat.Name
Updated = $LastWbcat.LastWriteTime
Size = $ZIPFiles.Sum
}
}
}
return $objLB
}
Поясню для новичков в Powershell: обратные одинарные кавычки в концах некоторых строк являются стандартным символом продолжения выражения на следующей строке в языке скриптов PS. Здесь они использованы лишь для улучшения читаемости примеров кода в рамках статьи.
Проясним логику работы функции. Параметр вызова у неё – так же, как и у предыдущей – только один: полное имя корневой папки архивов файлового режима для отдельной машины (эти пути для всех машин, архивы которых обнаружены, мы определяем заранее с помощью функции Get-MachineListFB). Вызов Get-ChildItem возвращает список объектов, содержащих описания найденных файлов с именем «GlobalCatalog.wbcat» в подпапках заданной папки. Это список передаётся на обработку командлета Where-Object, который отбирает среди найденных файлов только такие, у которых имя родительской папки совпадает с заданным («Catalogs»). Следующим этапом нашего конвейера выступает командлет Sort-Object, упорядочивающий остаток списка в порядке возрастания отметки времени последнего изменения файла, а командлет Select-Object отбирает последний элемент упорядоченного списка, сужая набор атрибутов в возвращаемом объекте до LastWriteTime и Directory в целях экономного расходования машинных ресурсов.
На этом всё, что можно сопроводить наречием «просто», заканчивается: далее командлет Select-Object пристыковывает к каждому возвращаемому объекту дополнительные пользовательские атрибуты (оказывается, PS позволяет и такие вещи «на лету» делать). В результате, возвращаемое значение представляет собой объект с атрибутами, составленными из двух «встроенных» свойств Directory и LastWriteTime исходного элемента, а также из определённых нами (в формате хэш-таблицы) динамически вычисляемыми пользовательских свойств Machine, Path и Name. Формат определения пользовательских атрибутов такого рода следующий:
@{Name = "<идентификатор_свойства>"; Expression = {$_.<выражение>}}
Выражение, которое вычисляет значение свойства, может быть как произвольным, не имеющим прямой связи с обрабатываемыми данными (например: Get-Date), так и включающим свойства и методы элемента-итератора $_ – как сделано в коде данной функции. Как видно из примера кода функции, конструкции такого вида просто перечисляются в списке параметров, возвращаемых командлетом Select-Object.
Возврат результата вычислений в виде составного объекта удобен тем, что нет нужды делать индивидуальные вычисления для различных параметров одного и того же набора исходных данных. В частности, только что рассмотренная функция не только сообщает нам имя папки САА, а и полный её путь, отметку времени последнего обновления архива в ней и его размер.
Далее по тексту функции мы выполняем подсчёт суммарного размера ZIP-файлов, составляющих данный архив, выбирая файлы с расширением «ZIP» во всех подпапках папки, находящейся на один уровень выше папки, содержащей родительскую папку файла «GlobalCatalog.wbcat» (т.е. в пределах папки данного архива). Вызов Get-ChildItem возвращает список фалов, который передаётся командлету Select-Object, который извлекает из данных только свойство Length (является синонимом размера для объектов файловой системы), а он, в свою очередь, отдаёт полученную выборку командлету Measure-Object для суммирования размеров.
Конструкция условия
($ZIPFiles) -and ($LastWbcat)
служит для проверки существования переменных $ZIPFiles и $LastWbcat в целях элементарного обнаружения ошибочных ситуаций. Это – стандартный приём в Powershell: поскольку мы не инициализировали заранее эти переменные, то если какая-либо из них осталась неинициализированной к моменту проверки (не имеет допустимого значения) – проверка вернёт отрицательный в логическом смысле результат. Т.е. что-то вроде конструкции If Not IsNull() …
в Visual Basic.Далее – как, наверное, понятно – производится присваивание вычисленных значений полям объекта (типа «хэш-таблица») $objLB, который и будет возвращаться нашей функцией.
Определением параметров САА для конкретной машины собственно и завершается наш вычислительный алгоритм, поскольку теперь об искомом САА известно всё и это «всё» записано в поля объекта, который вернула функция Get-LastFileBackupSet:
- Machine – сетевое имя компьютера,
- Path – полный путь папки САА для данного компьютера,
- Name – имя папки САА (имеет опознаваемое значение и может идентифицировать архив),
- Updated – дата последнего обновления,
- Size – суммарный размер архива.
Осталось реализовать представление получаемой информации. Поскольку потребителем выступает системный администратор, никаких визуально-стилистических изысков изобретать не будем и просто сделаем два представления выходных данных: простой текст и HTML табличного вида. Для каждого формата создадим по две функции: первая будет формировать строку данных для отдельного компьютера, а вторая – собирать из таких строк таблицу с заголовком. На самом деле, в коде скрипта они будут следовать в обратном порядке: исполнительная подсистема Powershell является интерпретатором, так что объявление функции в коде скрипта должно предшествовать её вызову и PS не поддерживает механизма предварительного объявления прототипов функций.
function Get-LastFileBackupSetSummaryTXTRow ($MachineRoot) {
$ret = ""
$Now = Get-Date
$LastBT = Get-LastFileBackupSet ($MachineRoot)
if ($LastBT) {
$ret = "Машина: " + $LastBT.Machine + "; Архив: " + $LastBT.Name + `
"; Обновлялся: " + ($Now - $LastBT.Updated).Days + " дней назад. (" + `
$LastBT.Updated + "); Размер: " + ('{0:0} {1}' -f ($LastBT.Size/1024/1024), "МБ")
}
else{
$ret = "Ошибка получения данных"
}
return $ret
}
function Get-LastFileBackupDatesTXT ($WinBackupRoot) {
$table="Архивы файлового режима:`r`n"
$MachineRoots = Get-MachineListFB ($WinBackupRoot)
foreach ($machine in $MachineRoots.Keys) {
$line = Get-LastFileBackupSetSummaryTXTRow($MachineRoots[$machine])
$table+= -join($line, "`r`n")
}
return $table
}
Функция-сборщик Get-LastFileBackupDatesTXT совершенно тривиальна: она просто поэтапно формирует текстовую строку, содержащую таблицу с заголовком. Строки таблицы генерируются функцией Get-LastFileBackupSetSummaryTXTRow, а «склейка» их производится оператором конкатенации строк "-join". В литературе его называют по-разному: где – оператором, а где – методом, да и форм записи у него – несколько. Здесь мы используем «функциевидную» форму записи, при которой конкатенация делается по умолчанию без разделительного символа. Вторым операндом выступает строковая константа, составленная из Esc-символов конца строки и перевода каретки ("`r" и "`n"), т.е. стандартный для текстовых файлов Windows перенос строки.
Функция Get-LastFileBackupSetSummaryTXTRow получает путь к КПАМ и, определяет САА для соответствующей машины. Поскольку это значение является составным, она извлекает из него отдельные элементы данных и, снабжая их текстовыми метками, собирает из них читабельную строку. Powershell и здесь позволяет нам воспользоваться прелестями объектного характера оперируемых ею значений: разница двух значений объектного типа «дата/время» тоже является объектом и её значение в днях вычисляется простым оператором вычитания без привлечения дополнительных функций. Нужно лишь уточнить, в какой единице измерения мы хотим получить результат, что достигается указанием на свойство Days результируещего объекта – поскольку именно дни нас и интересуют.
Для формирования представления значения размера архива в мегабайтах использована следующая конструкция:
('{0:0} {1}' -f ($LastBT.Size/1024/1024), "МБ")
Это – оператор форматирования и он является аналогом функции форматирования во многих языках программирования. Внутри внешних скобок (они оставлены здесь для удобочитаемости) последовательно расположены: строка форматов, сам оператор ("-f") и список подставляемых значений. Наша строка форматов содержит два спецификатора формата (каждый спецификатор заключён в фигурные скобки): число без дробной части и значение без указания форматирования. Поскольку между спецификаторами форматов стоит пробел – он буде присутствовать и в результирующей строке между значениями. После оператора, соответственно, следуют два значения: выражение, вычисляющее размер в мегабайтах и строка единицы измерения. На выходе мы получаем строковые значения вида «123 МБ», т.е. Powershell при указанном форматировании числа производит приведение числового значения к типу, соответствующему указанному формату.
Если значение САА является пустым – LastFileBackupSetSummaryTXTRow возвращает строку с указанием на ошибочную ситуацию.
Аналогичным образом формируются и гипертекстовые представления информации, отличаясь, по сути, только тем, что в формируемые строки вставляются несложные тэги HTML. Не будем приводить здесь этот код, поскольку ничего радикально нового с точки зрения алгоритмов нашей задачи в нём нет, а ознакомиться с ним можно, изучив исходник скрипта, приложенный к статье. Отметим только, что форматирование HTML-кода включает цветовую маркировку строк с возрастом САА более одного дня – для привлечения внимания администратора.
Обработка архивов режима образов дисковых томов
Список КПАМ мы получаем аналогично предыдущему случаю, с учётом специфики хранения архивов этого режима:
function Get-MachineListIB ($WinBackupRoot) {
$MachineRoots = @{}
$ImageBackupRoot = $WinBackupRoot+"\WindowsImageBackup"
Get-ChildItem -Path $ImageBackupRoot -Recurse -Depth 1 -File -Filter "MediaID" |
foreach {
$MachineRoots[$_.Directory.Name]=$_.Directory.FullName
}
return $MachineRoots
}
Т.е. мы просматриваем подпапку с именем «WindowsImageBackup» в корневой папке хранилища на глубину одного уровня в поисках подпапки. Ищем сигнальные файлы с именем «MediaID», обозначающие собой КПАМ.
Подобно случаю с архивами файлового режима, определяем САА – для чего описываем функцию Get-LastImageBackupSet и формируем пользовательские представления результатов в текстовом и гипертекстовом форматах – с помощью функций Get-LastImageBackupSetSummaryTXTRow, Get-LastImageBackupSetSummaryHTMLRow, Get-LastImageBackupDatesTXT и Get-LastImageBackupDatesHTML. Аналогично рассмотрению случая архивов файлового режима, здесь не показана (см. прилагаемый исходный код) пара функций, генерирующих гипертекстовое представление.
function Get-LastImageBackupSet ($MachineRoot) {
$objLB = $null
try {
$LastBSXML = Get-ChildItem -Path $MachineRoot -Recurse -Depth 1 `
-File -Filter "BackupSpecs.xml" |
Sort-Object LastWriteTime |
Select-Object -Last 1 LastWriteTime, Directory, `
@{Name="Machine";Expression={$_.Directory.Parent.Name.ToUpper()}}, `
@{Name="Path";Expression={$_.Directory.FullName}}, `
@{Name="Name";Expression={$_.Directory.Name}}
$VHDFiles = (
Get-ChildItem $LastBSXML.Directory.FullName -File -Filter "*.vhd" |
Select-Object Length |
Measure-Object -property length -sum -ErrorAction SilentlyContinue)
}
finally
{
if (($VHDFiles) -and ($LastBSXML)) {
$objLB = @{
Machine = $LastBSXML.Machine
Path = $LastBSXML.Path
Name = $LastBSXML.Name
Updated = $LastBSXML.LastWriteTime
Size = $VHDFiles.Sum
}
}
}
return $objLB
}
function Get-LastImageBackupSetSummaryTXTRow ($MachineRoot) {
$ret = ""
$Now = Get-Date
$LastBT = Get-LastImageBackupSet ($MachineRoot)
if ($LastBT) {
$ret = "Машина: " + $LastBT.Machine + "; Архив: " + `
$LastBT.Name + "; Обновлялся: " + ($Now - $LastBT.Updated).Days + `
" дней назад. (" + $LastBT.Updated + "); Размер: " + `
('{0:0} {1}' -f ($LastBT.Size/1024/1024), "МБ")
}
else{
$ret = "Ошибка получения данных."
}
return $ret
}
function Get-LastImageBackupDatesTXT ($WinBackupRoot) {
$table="Образы дисковых томов:`r`n"
$MachineRoots = Get-MachineListIB ($WinBackupRoot)
foreach ($machine in $MachineRoots.Keys) {
$line = Get-LastImageBackupSetSummaryTXTRow($MachineRoots[$machine])
$table+= -join($machine, " => ", $line, "`r`n")
}
return $table
}
На этом завершим разбор кода анализа хранилища и перейдём к общей части скрипта, ответственной за сборку и вывод отчёта в целом.
Сборка и вывод отчёта
Будем исходить из того, скрипт должен быть способен, как мы уже условились, формировать отчёт в формате простого текста и в формате HTML. Кроме того, поставим себе задачу обеспечить возможность вывода отчёта двумя путями: (а) в виде тестового потока на стандартное устройство вывода и (б) отправкой через электронную почту на заданный адрес. При этом наш скрипт должен принимать следующие параметры командной строки:
- root – (строка) путь к корневой папке хранилища Windows Backup.
- txt – (флаг) использовать формат простого текста, а при отсутствии этого параметра – HTML.
- mail – (флаг) отправить отчёт на электронную почту, а при отсутствии этого параметра – вывести содержимое отчёта «как есть» на stdout.
- smtpsrv – (строка) сетевое имя или IP-адрес SMTP-сервера для отправки отчёта через email.
- port – (целое число) номер порта TCP для отправки отчёта через email.
- to – (строка) email-адрес получателя сообщения.
- fm – (строка) email-адрес отправителя сообщения (он же – логин пользователя SMTP-сервера).
- pwd – (строка) пароль пользователя SMTP-сервера (хоть это и не совсем правильно с точки зрения безопасности).
- sub – (строка) тема отправляемого сообщения.
Создадим в начале заголовок скрипта с описание параметров командной строки. Стоит упомянуть, что механизм работы с ними, который в зачаточном виде был реализован в скриптах CMD.EXE, был значительно улучшен в скриптах Windows Script Host, но всё равно был весьма неудобен и требовал множества дополнительных действий для организации системы типизированных параметров и их анализа. В Powershell же он претерпел радикальные трансформации и стал полноценным удобным инструментом – блоком Param, который позволяет реализовать обработку параметров встроенными средствами PS на основании их объявлений. Применим же его в своих целях:
Param(
[Parameter(Mandatory)][string] $Root,
[switch] $txt=$false,
[switch] $mail=$false,
[string] $SmtpSrv,
[int] $Port=25,
[string] $To,
[string] $Fm,
[string] $Pwd,
[string] $Sub
)
Как видно из вышеприведённого фрагмента, асе параметры командной строки описываются одним блоком Param. Внутри его скобок, списком через запятую, перечислены его атрибуты – все наши параметры с указанием типов данных и обязательности. Вообще, простейший вариант описания отдельного параметра состоит из простого идентификатора, начинающегося с символа "$". Следующим по сложности вариантом объявления параметра является уточнение типа данных, которое описывается в виде имени типа данных в квадратных скобках слева от идентификатора параметра. Можно также задать значение по умолчанию, для чего достаточно добавить к идентификатору справа оператор присваивания и значение. Отсутствие явного указания значения по умолчанию приводит к инициализации параметра пустым значением указанного типа.
Тип параметра switch (т.е. «переключатель») работает следующим образом: если скрипт получил этот параметр простым упоминанием в командной строке – ему присваивается истинное значение. Если же в командной строке его не было – будет присвоено ложное значение.
Если нужно указать дополнительные атрибуты параметра – такие, как, например, обязательность – придётся добавить слева ещё один блок в квадратных скобках, который начинается с ключевого слова Parameter, за которым следует список его атрибутов и, возможно, их значений, заключённый в круглые скобки. В частности, обязательность отдельного параметра задаётся его атрибутом Mandatory логического типа. Начиная с версии PS 3.0, явное указание значения True атрибутам параметров не обязательно: если значение опущено – подразумевается значение True.
В нашем случае, к примеру, параметр Root описан как обязательный, имеющий тип string, без значения по умолчанию. Указанная обязательность этого параметра приведёт к тому, что если скрипт будет запущен без него, интерпретатор Powershell выведет на консоль приглашение к вводу его значения и не допустит исполнения кода скрипта без так или иначе указанного какого-либо его значения. Отсутствие явно указанных параметров, не объявленных как обязательные, не является препятствием к началу исполнения скрипта.
Нужно сказать, что применение блока Param в языке скриптов Powershell не ограничивается только параметрами командной строки – его можно использовать и применительно к параметрам функций. Более верным будет, пожалуй, даже обратное утверждение: он применим не только к параметрам функций, а и к параметрам командной строки скрипта, т.к. тело скрипта является аналогом функции верхнего уровня иерархии программного кода (как, например, функция main() в С). С его помощью мы могли бы переопределить нашу функцию Get-LastImageBackupDatesTXT следующим образом:
function Get-LastImageBackupDatesTXT
{
Param(
[Parameter(Mandatory)][string] $WinBackupRoot
)
$table="Образы дисковых томов:`r`n"
$MachineRoots = Get-MachineListIB ($WinBackupRoot)
foreach ($machine in $MachineRoots.Keys) {
$line = Get-LastImageBackupSetSummaryTXTRow($MachineRoots[$machine])
$table+= -join($machine, " => ", $line, "`r`n")
}
return $table
}
В случае вызова функции без обязательного параметра на экране также появится приглашение к вводу, полезность которого, правда, спорна в данном случае. Однако, возможности задания значений по умолчанию и типизации параметров несомненно полезны в некоторых случаях.
В довершение этого отступления на тему формата определения параметров скриптов и функций Powershell следует упомянуть о том, что описания параметров скрипта являются частью системы их самодокументирования. Они автоматически объединяются с информацией, указанной автором в разделах ".PARAMETER" текста Comment-Based Help скрипта, и выводятся в составе сгенерированной справки к скрипту командой «Get-Help <имя_скрипта>».
Перейдём к коду сборки полного текста отчёта. Исходя из сформулированной нами выше окончательной постановки задачи, очевидной кажется необходимость реализации трёх функций, которые будут вызываться непосредственно из тела скрипта: сборки текстовой версии отчёта, сборки гипертекстовой версии отчёта и отправки текста отчёта через электронную почту:
function Get-LastWinBackupDatesHTML ($WinBackupRoot) {
$Now = Get-Date
$table='<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'+"`n"
$table+='<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Резервные копии в "'+
`$WinBackupRoot+'"</title></head><body>'+"`n"
$table+='<h3>Резервные копии в "'+$WinBackupRoot+'"'+"</h3>`n"
$table+='<h3>Дата формирования отчёта: '+$Now+"</h3>`n"
$table+= Get-LastFileBackupDatesHTML ($WinBackupRoot)
$table+= Get-LastImageBackupDatesHTML ($WinBackupRoot)
$table+= "</body>`n</html>"
return $table
}
function Get-LastWinBackupDatesTXT ($WinBackupRoot) {
$Now = Get-Date
$table='Резервные копии в "'+$WinBackupRoot+'"'+"`r`n"
$table+='Дата формирования отчёта: '+$Now+"`r`n`r`n"
$table+= Get-LastFileBackupDatesTXT ($WinBackupRoot)
$table+= "`r`n"
$table+= Get-LastImageBackupDatesTXT ($WinBackupRoot)
return $table
}
function Send-Email-Report ($SendInfo, $TXTonly=$false){
$Credential = New-Object -TypeName "System.Management.Automation.PSCredential" `
-ArgumentList $SendInfo.Username, $SendInfo.SecurePassword
if ($TXTonly) {
Send-MailMessage -To $SendInfo.To -From $SendInfo.From `
-Subject $SendInfo.Subject -Body $SendInfo.MsgBody `
-SmtpServer $SendInfo.SmtpServer -Credential $Credential `
-Port $SendInfo.SmtpPort -Encoding UTF8
}
else
{
Send-MailMessage -To $SendInfo.To -From $SendInfo.From `
-Subject $SendInfo.Subject -BodyAsHtml $SendInfo.MsgBody `
-SmtpServer $SendInfo.SmtpServer -Credential $Credential `
-Port $SendInfo.SmtpPort
}
}
Отдельно, безотносительно к Powershell, поясним, почему для разделения строк в простом тексте мы использовали CR/LF, а в HTML – только LF. Это сделано для обеспечения нормального отображения HTML-сообщений в MS Outlook. Ну, а если будем смотреть отчёт в обычном браузере – разницы никакой не будет.
Обе функции окончательной компоновки очень просты и оперируют статически определёнными текстовыми фрагментами, которые комбинируются с текстовыми блоками, полученными от вызываемых подпрограмм, образуя целостный отчёт.
Функция Send-Email-Report служит «обёрткой» командлету Send-MailMessage и отображает систему параметров, которой оперирует наш скрипт в «систему координат» параметров этого командлета. Следует отметить необходимость использования экземпляра класса .NET с именем «System.Management.Automation.PSCredential» для передачи идентификатора и пароля пользователя командлету Send-MailMessage, поскольку последний не принимает к использованию обычные строковые значения для этих параметров. Использованный класс реализовывает безопасный (с точки зрения системы) контейнер (переменная $Credential) для хранения этих значений, который передаётся командлету вместо классической пары «логин/пароль». Как уже упоминалось выше, передача пароля в открытом виде скрипту, строго говоря, не является безопасной и более безопасным вариантом его предоставления являлось бы применение непосредственного ввода пароля пользователем в ходе исполнения скрипта с применением специального командлета Get-Credential для отображения системного графического диалога ввода таких данных. Но, поскольку мы создаём скрипт для пакетного, а не интерактивного запуска – приходится идти на такие вот отступления от догм…
Закончим же с функцией отправки сообщения. Параметров у неё только два, поскольку те из них, которые непосредственно отвечают за сообщение, инкапсулированы в параметр $SendInfo, являющийся хэш-таблицей. Отдельно от него передаётся только необязательный параметр-флаг $TXTonly, сообщающий о необходимости отправки сообщения в формате простого текста. Если он при вызове будет опущен или будет иметь ложное значение – сообщение будет отправлено с гипертекстовом формате.
Нам осталось сформировать только код тела скрипта, которое будет использовать все выше приведённые функции. Предположение, что его размер и сложность будут обратно пропорциональны количеству этих функций, будет совершенно справедливым – не зря же мы столько усилий потратили на проработку алгоритмов и соответствующую декомпозицию функционала.
if ($txt) {
$Ret = Get-LastWinBackupDatesTXT ($Root)
}
else {
$Ret = Get-LastWinBackupDatesHTML ($Root)
}
if ($mail) {
if (($SmtpSrv) -and ($Port) -and ($To) -and ($Fm) -and ($Pwd) -and ($Sub) -and ($Ret))
{
$objArgs = @{
SmtpServer = $SmtpSrv
SmtpPort = $Port
To = $To.Split(',')
From = $Fm
Username = $Fm
SecurePassword = ConvertTo-SecureString -String $Pwd -AsPlainText -Force
Subject = $Sub
MsgBody = $Ret
}
Send-Email-Report ($objArgs, $txt)
}
else {
Write-Host $Ret
}
}
else {
Write-Host $Ret
}
Всё, действительно, получилось весьма просто и, я бы сказал, схематично: в зависимости от того, был ли передан скрипту параметр командной строки $txt, вызывается одна из двух существующих функций Get-LastWinBackupDatesXXX. Результат выполнения (собственно, текст сформированного отчёта) сохраняется в переменной $Ret. Если параметры командной строки скрипта предписывают отправку его электронной почтой – вызываем функцию Send-Email-Report, сформировав и предав ей хеш-таблицу, полностью описывающую сообщение и способ его отправки. Если об электронной почте речи не шло, а также в случае явных недочётов в описаниях параметров отправки – просто выводим на стандартное устройство вывода (консоль) текст отчёта.
Осталось привести примеры создаваемых отчётов:
Гипертекстовый формат:
Резервные копии в "\\sa-nas\netbackup"
Дата формирования отчёта: 10.01.2017 09:00:01
Архивы файлового режима:
Машина |
Самый актуальный архив |
Возраст |
Обновлялся |
Размер |
SA-CHENG |
Backup Set 2017-01-10 040000 |
0 дней |
01/10/2017 04:16:23 |
6446 MB |
SA-DOCTOR |
Backup Set 2017-01-10 080002 |
0 дней |
01/10/2017 08:10:09 |
2431 МБ |
SA-BAR |
Backup Set 2016-12-28 033001 |
0 дней |
01/10/2017 02:36:35 |
248028 МБ |
SA-RECEPTION |
Backup Set 2017-01-10 060737 |
0 дней |
01/10/2017 06:16:29 |
1264 МБ |
SA-ECR |
Backup Set 2017-01-08 220005 |
0 дней |
01/09/2017 22:03:22 |
2863 МБ |
SA-HOTELMGR |
Backup Set 2017-01-08 020003 |
0 дней |
01/09/2017 22:11:02 |
26788 МБ |
SA-ITO |
Backup Set 2017-01-06 230005 |
0 дней |
01/09/2017 23:08:48 |
17486 МБ |
SA-CHEF |
Backup Set 2017-01-05 230002 |
0 дней |
01/09/2017 23:02:11 |
3791 МБ |
SA-MASTER |
Backup Set 2017-01-09 102958 |
0 дней |
01/09/2017 23:12:51 |
8441 МБ |
SA-CONTROLLER |
Backup Set 2017-01-09 000007 |
0 дней |
01/10/2017 00:17:04 |
9788 МБ |
Образы дисковых томов:
Машина |
Самый актуальный архив |
Возраст |
Обновлялся |
Размер |
SA-ITO |
Backup 2017-01-10 020013 |
0 дней |
01/09/2017 23:12:44 |
47534 МБ |
POSSERVER |
Backup 2017-01-10 020006 |
0 дней |
01/09/2017 23:07:36 |
23138 МБ |
Простой текст:
Резервные копии в "\\sa-nas\netbackup"
Дата формирования отчёта: 10.01.2017 09:00:01
Архивы файлового режима:
Машина: SA-CHENG; Имя архива: Backup Set 2017-01-10 040000; Обновлялся: 0 дней назад. (01/10/2017 04:16:23); Размер: 6446 МБ
Машина: SA-DOCTOR; Имя архива: Backup Set 2017-01-10 080002; Обновлялся: 0 дней назад. (01/10/2017 08:10:09); Размер: 2431 МБ
Машина: SA-BAR; Имя архива: Backup Set 2016-12-28 033001; Обновлялся: 0 дней назад. (01/10/2017 02:36:35); Размер: 248028 МБ
Машина: SA-RECEPTION; Имя архива: Backup Set 2017-01-10 060737; Обновлялся: 0 дней назад. (01/10/2017 06:16:29); Размер: 1264 МБ
Машина: SA-ECR; Имя архива: Backup Set 2017-01-08 220005; Обновлялся: 0 дней назад. (01/09/2017 22:03:22); Размер: 2863 МБ
Машина: SA-HOTELMGR; Имя архива: Backup Set 2017-01-08 020003; Обновлялся: 0 дней назад. (01/09/2017 22:11:02); Размер: 26788 МБ
Машина: SA-ITO; Имя архива: Backup Set 2017-01-06 230005; Обновлялся: 0 дней назад. (01/09/2017 23:08:48); Размер: 17486 МБ
Машина: SA-CHEF; Имя архива: Backup Set 2017-01-05 230002; Обновлялся: 0 дней назад. (01/09/2017 23:02:11); Размер: 3791 МБ
Машина: SA-MASTER; Имя архива: Backup Set 2017-01-09 102958; Обновлялся: 0 дней назад. (01/09/2017 23:12:51); Размер: 8441 МБ
Машина: SA-CONTROLLER; Имя архива: Backup Set 2017-01-09 000007; Обновлялся: 0 дней назад. (01/10/2017 00:17:04); Размер: 9788 МБ
Образы дисковых томов:
Машина: SA-ITO; Имя архива: Backup 2017-01-10 020013; Обновлялся: 0 дней назад. (01/09/2017 23:12:44); Размер: 47534 МБ
Машина: POSSERVER; Имя архива: Backup 2017-01-10 020006; Обновлялся: 0 дней назад. (01/09/2017 23:07:36); Размер: 23138 МБ
Заключение
Описанный здесь скрипт реально используется и исправно докладывает о состоянии дел в хранилище резервных копий, будучи запущен с помощью системного планировщика заданий. Конечно, он несовершенен и легко можно предложить разнообразные улучшения – такие, как, например, контроль остатка дискового пространства или объёма, занимаемого устаревшими архивами. Также широкий простор для творчества предоставляет вопрос обработки ошибочных ситуаций, который в нашем скрипте практически обойдён вниманием. Но, так или иначе, этот вариант вполне работоспособен и не потребовал слишком значительных затрат времени на разработку. Собственно, как мне кажется, в этом и состоит смысл скриптов, разрабатываемых на уровне администратора системы: эффективно решать задачи минимальными затратами ресурсов, пускай и не на уровне коммерческого продукта.
Загрузить архив с исходным кодом скрипта можно отсюда.
В следующей статье, являющейся продолжением настоящей, пойдёт речь о расширении функциональности этого скрипта технологией фильтров-add-on'ов для поддержки им наборов данных, создаваемых иными, произвольными, системами резервного копирования, в частности – NT Backup и ghettoVCB (для VMware ESXi).
Поделиться с друзьями
Sergey-S-Kovalev
Статья требует качественного вдуманного переваривания, но приятно видеть столь подробный разбор. Спасибо! :)
IgCat
Спасибо за отзыв)