Для аудита сервера печати можно использовать разные средства, как платные и бесплатные. Бесплатных, удовлетворяющих моим требованиям, я так и не нашёл. Мои требование просты: кто, когда, сколько и на каком принтере. Лишнего мне не надо. Но в любом случае, дополнительное программное обеспечение необходимо устанавливать на сервере, а в некоторых случаях и агенты на клиентские компьютеры. Как следствие - дополнительные процессы, и нагрузка. Именно по этому, я решил найти вариант штатными средствами.
Я никак не рашелся серьёзно занятся этим делом пока однажды на одном торренте (не скажу в каком разделе) был скачен журнал "Системный администратор. Июнь. 2012". Там и была статья посвящённая аудиту печати на Windows Server 2008 R2. Однако при выполнении начальных действий по статье не привели к результату. Я специально выделил R2. Т.к. в этой ОС логи печати включаются не так как описано в статье и собираются не в журнале SYSTEM. Однако, возможно автор писал статью с сервера без SP1. Т.к. у меня нет под рукой 2008 или 2008 R2 без SP1 - проверить не смогу. Но, спасибо автору - направление указал. Кому интересна статья вот ссылка.
Уже изначально я решил что буду записывать логи в базу даных, а потом красиво показывать через веб-интерфейс. У меня есть виртуальные машины на Debian (Cacti, Nagios) по этому ещё одна малюсенькая база данных там не помешает. Для записи из PowerShell в MySQL нам понадобится коннектор MySQL Connector Net. Но позже, я попробую реализовать тоже самое для MS SQL Express. Начнём по порядку. По умолчанию логи печати отключены. Для того, чтобы их включить необходимо раскрыть "Просмотр Журналов":
В свойствах журнала Operational включается логирование почты. На рисунке у меня уже включено логирование. Так как данные будут записываться в базу данных mysql, то дополнительной настройки для журнала не потребуется. Но если вам нужно, в свойствах вы можете указать размер журнала, действия в случае достижения максимального размера. Так же, возможно, потребуется настройка принтера. Но для перед этим нужно несколько раз распечатать документ с количеством копий больше чем одна. Если в событии 805 при количестве копий больше чем одина, всё равно будет "1", то необходимо обновить драйвера принтера, а, если не поможет, отключить рендеринг на стороне клиента. К сожалению, для некоторых приложений может не помочь. У меня количество копий не работало для Word 2010, но при этом работало для Excel 2010 из тогоже дистрибутива.
Для статистики нам нужно событие с номером 307 и событие 805. Про 805 я узнал спустя почти год после написания данной статьи. Узнал случайно, когда на форуме Oszone спросили про аудит печати скриптом с MSDN. Оказалось, что параметр $Pages = $event.Event.UserData.DocumentPrinted.Param8 содержит количество страниц только для одной копии. А количество копий как раз можно узнать из лога 805. Логи из журнала обрабатываются с помощью PowerShell v3. С журнале СА рекомендуется использовать команду: get-eventlog. Однако, она умеет анализировать только Windows Logs и не работает напрямую с ID лога:
PS C:\Users\admin> Get-EventLog -List Max(K) Retain OverflowAction Entries Log ------ ------ -------------- ------- --- 512 7 OverwriteOlder 334 Active Directory Web Services 20 480 0 OverwriteAsNeeded 16 831 Application 15 168 0 OverwriteAsNeeded 1 075 DFS Replication 512 0 OverwriteAsNeeded 2 621 Directory Service 16 384 0 OverwriteAsNeeded 513 DNS Server 8 192 0 OverwriteAsNeeded 0 Doctor Web 20 480 0 OverwriteAsNeeded 0 HardwareEvents 512 7 OverwriteOlder 0 Internet Explorer 20 480 0 OverwriteAsNeeded 0 Key Management Service Security 20 480 0 OverwriteAsNeeded 54 581 System 15 360 0 OverwriteAsNeeded 9 Windows PowerShell
Для анализа других логов используется Get-WinEvent, который имеет ключ -ComputerName для подключения к удалённому компьютеру. Но я буду собирать логи с локального компьютера. Для начала назначим права для пользователя, который будет подключаться с сервера печати с IP адресом XXX.XXX.XXX.XXX. Пароль не должен содержать знак $. Выполнить на сервере MySQL:
mysql -h localhost -u root -p
Enter password:
mysql> CREATE USER 'root'@'XXX.XXX.XXX.XXX' IDENTIFIED BY 'asdASD';
Query OK, 0 rows affected (0.00 sec)
mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'XXX.XXX.XXX.XXX' WITH GRANT OPTION;
Query OK, 0 rows affected (0.00 sec)
Разрешаем выполнение скриптов PS1 на сервере печати:
Set-ExecutionPolicy RemoteSigned
После этого создаём базу данных из PowerShell, где yyy.yyy.yyy.yyy IP адрес mysql сервера:
Add-Type -Path "C:\Program Files (x86)\MySQL\MySQL Connector Net 6.9.9\Assemblies\v4.5\MySql.Data.dll"
$connectionString = "server=yyy.yyy.yyy.yyy;uid=root;pwd=asdASD;"
$connection = New-Object MySql.Data.MySqlClient.MySqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()
$sql = New-Object MySql.Data.MySqlClient.MySqlCommand
$sql.Connection = $connection
$sql.CommandText = "CREATE DATABASE PRINT CHARACTER SET utf8 COLLATE utf8_general_ci;"
$sql.ExecuteNonQuery()
$sql.CommandText = "CREATE TABLE PRINT.LOGS (
`id` INT(11) NOT NULL,
`Day` DATE NOT NULL,
`Time` TIME NOT NULL,
`User` VARCHAR(30) NOT NULL,
`Printer` VARCHAR(21) NOT NULL,
`Port` VARCHAR(15) NOT NULL,
`Document` VARCHAR(255) NOT NULL,
`Pages` INT NOT NULL,
`Copy` INT NOT NULL,
`Size` INT NOT NULL);"
$sql.ExecuteNonQuery()
$sql.CommandText = "SHOW COLUMNS FROM LOGS FROM PRINT;"
$Table=New-Object Data.DataTable
$Table.Load($sql.ExecuteReader())
$Table | ft
$connection.Close()
Вывод:
PS C:\> C:\scripts\mysql1.ps1 1 0 Field Type Null Key Default Extra ----- ---- ---- --- ------- ----- id int(11) NO Day datetime NO Time datetime NO User varchar(30) NO Printer varchar(20) NO Port varchar(15) NO Document varchar(255) NO Pages int(11) NO Copy int(11) NO Size int(11) NO
Далее, я приведу готовый скрипт для анализа логов и разберу его части. 9,458 заданий с 4-х принтеров за год были обработаны им за 7 минут на сервере c Intel E31230 3,2 GHz.
Add-Type -Path "C:\Program Files (x86)\MySQL\MySQL Connector Net 6.9.9\Assemblies\v4.5\MySql.Data.dll"
$connectionString = "server=doc;uid=root;pwd=asdasd;database=allprint;"
$connection = New-Object MySql.Data.MySqlClient.MySqlConnection
$connection.ConnectionString = $connectionString
$connection.Open()
$sql = New-Object MySql.Data.MySqlClient.MySqlCommand
$sql.Connection = $connection
$today = get-date -DisplayHint date -UFormat %Y-%m-%d
Get-WinEvent -FilterHashTable @{LogName="Microsoft-Windows-PrintService/Operational";starttime="$today";id=307} | Foreach {
$event = [xml]$_.ToXml()
if($event)
{
$Time = Get-Date $_.TimeCreated -UFormat "%Y-%m-%d %H:%M:%S"
$Job = $event.Event.UserData.DocumentPrinted.Param1
$Document = $event.Event.UserData.DocumentPrinted.Param2.ToString().Replace("\","\\")
$User = $event.Event.UserData.DocumentPrinted.Param3
$Port = $event.Event.UserData.DocumentPrinted.Param6
$Printer = $event.Event.UserData.DocumentPrinted.Param5
$Size = $event.Event.UserData.DocumentPrinted.Param7
$Pages = $event.Event.UserData.DocumentPrinted.Param8
$sql.CommandText = "INSERT INTO alllog (User,Printer,Port,Time,Document,Pages,Size,Job) VALUES ('$User','$Printer','$Port','$Time','$Document','$Pages','$Size','$Job')"
$sql.ExecuteNonQuery()
}
}
Get-WinEvent -FilterHashTable @{LogName="Microsoft-Windows-PrintService/Operational";starttime="$today";id=805} | Foreach {
$event = [xml]$_.ToXml()
if($event)
{
$Time = Get-Date $_.TimeCreated -UFormat "%Y-%m-%d %H:%M:%S"
$Copy = $event.Event.UserData.RenderJobDiag.Copies
$sql.CommandText = "UPDATE alllog set Copy=$Copy WHERE Time='$Time'"
$sql.ExecuteNonQuery()
}
}
$connection.Close()
А теперь разбор скрипта. Некоторые поля я не смогу объяснить...
Add-Type -Path добавляет путь к библиотеке коннектора к MySQL. Необходим при старте каждой сессии. Путь зависит от версии коннетора. Будьте внимательны.
$connectionString = здесь определяем имя сервера с MySQL, пользователя и его пароль для подключения к базе. Базу тут же указываем.
$connection =
$connection.ConnectionString = вот эти две строчки я не знаю для чего :)
$connection.Open() наверное открывает сессию...
$sql = New-Object MySql.Data.MySqlClient.MySqlCommand
$sql.Connection = $connection вот эти две строчки я не знаю для чего :)
$today = get-date -DisplayHint date -UFormat %d.%m.%y присваиваем переменной $today значение текущего дня. Другие даты нас не будут интересовать,так как скрипт запускается каждую ночь и прошлые данные уже в базе.
Get-WinEvent команда анализирует логи расположенные по определённому пути, начиная со времени текущего дня (если вам нужно загрузить все логи, то starttime="$today"; можно убрать), только логи с ID 307 или ID 805 и передаёт вывод команде Foreach, которая будет работать циклически.
$Time, $Job, $Document - это та информация, которая нам нужна. Вы можете назвать эти переменные своими именами.
$event.Event.UserData.DocumentPrinted.Param6 эти все строчки опишет одна картинка:
$_.TimeCreated найдёте, если раскроете +System
$Time = Get-Date $_.TimeCreated -UFormat "%Y-%m-%d %H:%M:%S" меняем формат выводимого времени потому, что mysql не воспринимает формат выводимый $_.TimeCreated. Только если таблица имеет тип varchar. Но это не удобно при работе с php. К счастью, get-date работает не только с системным временем.
$sql.CommandText = выполнение команды MySQL
"INSERT INTO alllog (User,Printer,Port,Time,Document,Pages,Size,Job) VALUES ('$User','$Printer','$Port','$Time','$Document','$Pages','$Size','$Job')" команда для ввода (но не сам ввод) в таблицу alllog соотвествующих данных.
"UPDATE alllog set Copy=$Copy WHERE Time='$Time'" команда для обновления базы. Записывает значение Copy (количество копий) в поле, у которого имеющееся время Time совпадает со временем из события 805 $Time. Вначале, я хотел делать соответствие с полем Job, но оказалось, что Job сбрасывается в ноль, после достижения значения 255. В этом случае, UPDATE перезаписал бы все значения, чьи задания имели одинаковое значение.
$sql.ExecuteNonQuery() выполняет предыдущую команду, т.е. вводит информацию в базу.
$connection.Close() закрывает сессию.
Далее простой скрипт на php. Позже будет более красивый. Этот просто выводит суммарную информацию:
<?php
// Подключаемся к серверу MySQL
$hostname = '192.168.10.17';
$username = 'root';
$password = 'asdasd';
$db = mysql_connect($hostname, $username, $password)
or die('connect to database failed');
// Назначаем кодировку базы
mysql_set_charset('utf8');
// выбираем базу
mysql_select_db('allprint')
or die('db not found');
// Запрашиваем нужную информацию и результат назначаем переменной $result
$query = 'SELECT User,Printer,SUM(Pages) FROM `alllog` GROUP BY User,Printer';
$result = mysql_query($query) or die(mysql_error() ."<br/>". $query);
// Строим таблицу (информация только по пользователям, принтерам, количеству страниц)
$table = "<table border=0 width=25% align=center>\n";
while ($row = mysql_fetch_assoc($result))
{
$table .= "<tr>\n";
$table .= "<td>".$row['User']."</td>\n";
$table .= "<td>".$row['Printer']."</td>\n";
$table .= "<td>".$row['SUM(Pages)']."</td>\n";
$table .= "</tr>\n";
}
$table .= "</table>\n";
echo $table;
mysql_close($db);
?>
Жирным я выделил строчку, которая и определяет выборку содержимого. Вся соль в ней, так как код таблицы можно легко найти в интернете. В приведённом коде выборка идёт за все периоды. А вот строчка, которая показывает за вчерашний день (т.к. за сегодняшний данные загрузятся ночью):
$query = 'SELECT User,Printer,SUM(Pages) FROM `alllog` WHERE Time LIKE "'.date('m', strtotime("now -1 day")).'/'.date('d', strtotime("now -1 day")).'/'.date('Y', strtotime("now -1 day")).'%" GROUP BY User,Printer';
Если убрать значения strtotime("now -1 day") , то получим данные за сегодняшний день. Всё готово. Помещаем этот скрипт на ваш веб-сервер и получаем простую информацию в вашем браузере о том кто, какой принтер и сколько. Скоро будет - когда :) всё-таки я не программист php.
Так же благодарю miksoft за помощь с UPDATE - всё гениальное просто!
Первая редакция (без ID 805) - Июль 2012
Вторая редакция (c ID 805) - Июнь 2013