Здравствуй, уважаемый %habrauser%. Около 3 лет назад я написал статью о том Как я создал систему установки принтеров на работе. Не могу не согласится с комментариями и отзывами от прошлой статьи, гласящие о том, что для установки принтеров можно воспользоваться групповыми политиками, но в мире
Почему надо менять то, что и так работает
Идея о том, что систему надо переписать пришла когда в очередной раз я получил жалобу от HelpDesk о том, что система не очень удобная и много возни с добавлением нового принтера. Каждый раз после установки принтера на сервер, необходимо было создавать два файла, где первый файл это VBS скрипт установки с записанным в него адресом нового принтера (Например: \\server01\printer1) и BAT файл который запускает этот VBS скрипт. Этот самый BAT файл загружался в систему установки принтеров, откуда пользователи находили нужный принтер и скачивали его. Для пользователей все просто: скачал -> запустил -> принтер поставился. Но настройка для технического отдела была нудной и долгой и эту проблему необходимо было решить.
Постановка требований
Ниже приведен список требований, которые были выполнены по мере разработки:
- В первую очередь должна открываться Home страница с выбором филиала
- Должна быть возможность поиска филиала по имени на Home странице
- После выбора филиала должна открываться страница со списком принтеров соответствующего филиала
- На странице выбора принтеров соответствующего филиала должна быть возможность поиска принтера по имени
- На странице выбранного принтера должна показываться следующая информация
- Имя принтера
- Изображение принтера
- Отображение знака Online если принтер в сети
- Тип принтера
- Описание принтера
- IP адрес
- Местоположение принтера
- Производитель принтера
- Иконка производтеля принтера (опционально)
- Количество просмотров принтера (опционально)
- Возможность отправлять ссылку на принтер по почте по клику по ссылке mailto типа
- Кнопка «Install» для загрузки установочного скрипта, который генерируется на лету
- Возможность глобального поиска как и принтеров так и филиалов
- Страница с мануалом текст, которого можно будет писать в админке в WYSIWYG редакторе
- Админ панель для управления всеми возможностями системы
- Должна быть минимальная API для получения какой-либо информации
- Возможность изменения данных для подключения к БД, если БД поднят на другом сервере без редактирования исходного кода приложения и пересобирания его в WAR
- Должна быть возможность изменения скрипта установки принтера на любую другую
- Для облегчения задачи первоначальной установки системы администратору, установка БД должна выполняться через страницу /install с сохранением введенных параметров подключения к БД (IP БД сервера с mysql (или localhost), пользователь и пароль от БД)
Выбор движка
Конечно есть большое количество возможных вариантов того, на чем можно было бы написать приложение, но я выбрал Java Server Faces Framework (JSF) так, как хотелось немного пощупать то, что это такое, да и возможность упаковать приложение в готовый WAR файл и деплоить его на Tomcat как и на Linux, так и на Windows подкупила меня.
Разработка
Введение
До того момента, я ни разу не писал на Java веб приложения, поэтому все начиналось с трудом. Имел опыт с PHP и то только умел кодить на своем велосипеде
Разработку я начал вести сперва в NetBeans, но спустя некоторе время перешел на Eclipse потому, что Eclipse показался мне гораздо более удобным и понятным.
Проект был создан по MVC паттерну, где: JSP — это сами страницы с HTML и JSTL разметкой (views); Servlet — контроллер, который обрабатывет POST или GET запросы этой страницы; DAO класс страницы — тянет или сохраняет нужную информацию в БД. Если обобщить, то все страницы в системе состоят из JSP и своего Servlet-а, но могут не иметь своего DAO класса, если они статичны, а функции для API состоят только из Servlet-а и DAO класса. В качестве стилистики дизайна я использовал Bootstrap 4.
Структура БД
Структура БД достаточно проста и приведена ниже на картинке:
Имеются 5 таблиц для хранения информации:
- branches — предназначен для хранения филиалов
- printers — здесь хранятся принтеры
- printerstype — типы принтеров
- users — администраторы
- systemsettings — настройки системы, где столбец «parameter» имя настройки и «value» его значение
Трудности
Первая
С первой трудностью с которой я столкнулся, это было то, как сделать так, чтобы страницу возможно было бы открыть только по его относительному пути (например /home), а не открывая JSP файл (например Home.jsp) непосредственно. Если открывать файл JSP напрямую, то вызов Servlet-а этой страницы не происходил, соответственно и нужная информация не тянулась на страницу. Решение оказалось простым. Достаточно нужно было в начало каждой JSP страницы поставить проверку того, если станицу открыли не по относительному пути, то перенаправить его туда, а в самом Servlet-е возвращать содержимое JSP файла страницы. Некоторые функции перенаправления я писал сначала не верно и страница уходила в бесконечный loop, с чем я спустя 3 дня танцев с бубном и гадания на кофейной гуще справился.
Вторая
Вторая трудность заключалась в том, как реализовать генерирование файла скрипта установки принтера на лету и чтобы для каждего принтера оно было индивидуальным, и чтобы его можно было редактировать. Решение данной проблемы пришло спустя некоторое время и оказалось следующим. Очевидно, что текст самого скрипта необходимо хранить в БД. Для этих целей создал таблицу systemsettings, добавил туда строку, где в столбец «parameter» вписал «installscript», а в «value» сам VBS скрипт из прошлой статьи. А что если у нас завтра будет не VBS, а PowerShell скрипт или любой другой? Поэтому в таблицу systemsettings добавил еще одну строку, где в столбец «parameter» вписал «installscriptextension», а в «value» значение «vbs». Далее создал Servlet download, который принимает GET значение переменной целочисленного типа printerid и выглядит следущим образом:
if(request.getParameter("printerid") != null)
{
// Получаем ID принтера из GET запроса
Integer printerid = 0;
try
{
printerid = Integer.parseInt(request.getParameter("printerid"));
}
catch (NumberFormatException e)
{
request.getRequestDispatcher("/home").forward(request, response);
}
// Получаем наш класс принтера со всеми его параметрами из БД
Printer printer = DownloadScriptDao.GetPrinter(printerid);
if(printer != null)
{
String scriptname = "none";
String script= "none";
String scriptextension = "txt";
// Получаем наш скрипт из БД
script = DownloadScriptDao.GetInstallScript();
// Получаем расширение скрипта из БД
scriptextension = DownloadScriptDao.GetInstallScriptExtension();
// Получаем имя принтера
scriptname = printer.GetName().trim();
// Если обнаруживаем в скрипте %PRINTER_NAME%, то заменяем его значение Имени принтера полученное из БД
script = script.replace("%PRINTER_NAME%", printer.GetName());
// Если обнаруживаем в скрипте %PRINTER_DESCRIPTION%, то заменяем его значение Описания принтера полученное из БД
script = script.replace("%PRINTER_DESCRIPTION%", printer.GetDescription());
// Если обнаруживаем в скрипте %PRINTER_SHARE_NAME%, то заменяем его значение Общего серверного адреса принтера полученное из БД
script = script.replace("%PRINTER_SHARE_NAME%", printer.GetServerShareName());
// Если обнаруживаем в скрипте %PRINTER_ID%, то заменяем его значение на ID принтера из БД
script = script.replace("%PRINTER_ID%", printer.GetId().toString());
// Если обнаруживаем в скрипте %PRINTER_BRANCH_ID%, то заменяем его значение на ID филиала принтера из БД
script = script.replace("%PRINTER_BRANCH_ID%", printer.GetBranchId().toString());
// Если обнаруживаем в скрипте %PRINTER_BRANCH_ID%, то заменяем его значение на IP принтера из БД
script = script.replace("%PRINTER_IP%", printer.GetIp());
// Если обнаруживаем в скрипте %PRINTER_BRANCH_ID%, то заменяем его значение на Имени производителя принтера из БД
script = script.replace("%PRINTER_VENDOR%", printer.GetVendor());
// Если обнаруживаем в скрипте %PRINTER_TYPE%, то заменяем его значение на Тип принтера из БД
script = script.replace("%PRINTER_TYPE%", printer.GetPrinterTypeId().toString());
// Если обнаруживаем в скрипте %PRINTER_CUSTOM_FIELD1%, то заменяем его значение на Свой параметр принтера из БД
script = script.replace("%PRINTER_CUSTOM_FIELD1%", printer.GetCustomField1());
// Даем браузеру понять что сейчас будет возвращен файл а не страница
response.setContentType("application/octet-stream");
// Даем браузеру понять что имя файла будет значение имени принтера и его расширение
response.setHeader("Content-Disposition", "attachment;filename=" + scriptname + "." + scriptextension);
// Задаем содержимое скачиваемого файла (скрипта) после всех изменений
StringBuffer sb = new StringBuffer(script);
InputStream in = new ByteArrayInputStream(sb.toString().getBytes("UTF-8"));
ServletOutputStream out = response.getOutputStream();
byte[] outputByte = new byte[1];
while(in.read(outputByte, 0, 1) != -1)
{
out.write(outputByte, 0, 1);
}
in.close();
out.flush();
out.close();
}
}
Хотелось бы отметить про PRINTER_CUSTOM_FIELD1. Поле создано для случаев, если кому-то будет необходимо хранить какое-то свое значение для скриптовых целей, поэтому оно нигде в системе не отображается. Его можно задать при создании принтера или менять через админку.
Третья
И наконец пробема которую все не удавалось решить — это возможность установки БД при первоначальной настройке прямо с браузера с последующим сохранением данных подключения для работы системы в конфигурационный файл. Сперва я написал огромное полотно Java кода с SQL запросами, которое нормально не работало, но затем обнаружил гораздо более удобный вариант. Есть библиотека с классом ScriptRunner, которая позволяет читать и выполнять SQL файл. С помощью неё создание бд с нужными таблицами умещаются всего лишь в пару строк. Пример указан ниже:
// Ваша функция которая служит для подключения к БД, которую вы должны инициализировать
Connection someconnection;
// Чтение файла из папки WEB-INF\classes\YOURDUMPFILE.sql
ScriptRunner runner = new ScriptRunner(someconnection, false, false);
ClassLoader loader = Thread.currentThread().getContextClassLoader();
InputStream stream = loader.getResourceAsStream("YOURDUMPFILE.sql");
InputStreamReader reader = new InputStreamReader(stream);
runner.runScript(reader);
reader.close();
conn.close();
После импорта БД, в конфигурационный файл расположенный в WEB-INF\classes\config.properties записываются данные подключения БД. Каждый раз при обращении к БД система читает этот файл. Конечно чтобы ограничить вход на ссылку /install после установки всем, в config.properties записывается значение «db.configured=yes». Если значение yes, то открыть ссылку /install невозможно.
Отображение знака «online» если принтер в сети
Функцию отображения принтера онлайн я пытался сделать с помощью JavaScript WebSocket, но это у меня не получилось. Поэтому решение было другим. После загурзки страницы посылается AJAX запрос на API PrintDesk CheckPrinterIsOnline и возвращается результат. На основе результата отображается или не отображается знак «Онлайн». Ниже представлен код Java, который это делает:
InetAddress inet = InetAddress.getByName(printerip);
if(inet.isReachable(500))
result = "online";
else
result = "offline";
Недостаток этого метода в том, что пингует принтер не сам пользователь, а сервер и может быть сеть устроена так, что доступа с сервера на принтер может и не быть (или вообще отключен ICMP), хотя принтер в онлайне в этот момент. Конечно хотелось бы мне устранить этот недостаток и вообще иметь возможность как-то даже получать информацию о том, что происходит во время запроса в принтере, но пока не знаю как это реализовать.
Заключение
В заключение могу сказать, что во время разработки я многому научился и надеюсь, что мои труды кому-то окажутся полезными. На долгосрочную перспективу планирую расширять и улучшать функционал с сохранением удобства и НЕ перегруженности системы. Спасибо за внимание!
Бонус
Скриншоты
Ссылка на GitHub
Ссылка на релиз PrintDesk
Ссылка на предыдущую статью
malefix
К сожалению, первая статься прошла мимо меня, к ней много вопросов, начиная от идиотского диалога с пользователем и не менее идиотского progressbar'а при установке, заканчивая выбранным способом подключения принтеров.
Поэтому пройдусь только по второй статье.
У вас скрипты для разных принтеров отличаются? Не итоговые, с заменёнными переменными, которые получает юзер, а те, которые лежат в базе. Если для каждого принтера — один и тот же скрипт из первой статьи, то зачем его хранить в БД, да ещё и расширение отдельно?
Прописать генерацию в кодеВынести его в шаблон и наслаждаться лёгкостью доработок.На скриншоте vbs вводят руками, а если опечатка?
Кстати, у пользователей браузер не ругается на скачивание/запуск BAT-файла? Во всех браузерах работает или у вас просто политиками принят IE?
У вас же все принтеры подключены к принтсерверу и расшарены. Печать всё равно пойдёт через сервер, если с сервера нет доступа, то и печатать не будет, разве нет?
Можно попробовать соединиться с 80 портом принтера, это web-интерфейс, если пингов нет, а web-интерфейс открылся — значит принтер онлайн.
В любом случае проект хоть и костыльный, но полезный для вас. Заставил изучить (или хотя бы не забыть) PHP/JAVA/SQL и прочие составляющие. В HelpDesk'е всё это никак не развивается.
AkshinM Автор
Нет, не отличается. Скрипт один и тот же для всех принтеров.
А если вам необходимо будет делать не VBS скрипт а свой PowerShell? В таком случае вы поменяете расширение генерируемого скрипта на ps1.
Ну вот это и есть шаблон один на всех.
Ну напишите скрипт где угодно, протестируйте, потом copy paste туда, где сможете вдобавок переменные вставить, значения которых будут тянутся из БД для каждого принтера и меняться нужным значением в итоговом скачиваемом файле, любого расширения, которое вы укажете. Чем не template?
Ни один браузер не ругается. Все прекрасно скачивается.
А не обязательно система должна крутиться на принтер сервере. У кого-то уже может быть развернуто на каком-то другом тестовом сервере (может даже на Linux-e) tomcat и он решил не парится и сразу задеплоить туда, или вообще в свой доккер контейнер запихнуть. И скорее всего, что это изолированная другая сеть и оттуда нет доступов до принтеров. Ну а кто-то решил все на принтер сервере поставить, тогда ваше утверждение верно.
Веб интерфейс принтера может быть отключен, конечно, нигде я не видел такое, но все может быть. Или может даже быть настроен файрвол, который блокирует 80 порт на принтер влан. Пинг мне кажется более надежным.
malefix
Извиняюсь, сначала подумал, что для каждого принтера свой скрипт в базе лежит. Изучил более внимательно скриншоты и схему БД, был неправ.
Но тогда, с другой стороны, у вас сейчас ОДИН шаблон для всех принтеров?
Пока у пользователей Win-only машины и все принтеры расшарены на принтсервере — всё ОК.
Как только появится рабочее место с Linux/MacOS — там заработает?
Если надо будет подключить принтера НЕ через принт-сервер, одним скриптом уже сложно будет обойтись. Можно, но в нём будет куча ветвлений для разных вендоров/способов подключения.
Если принт-сервер и PrintDesk на разных серверах, тогда можно через WMI/PS удалённо пинговать принтера с принт-сервера. Наличие пинга от юзера до принтера бесполезно, если вы печатаете через принт-сервер. Для безпринтсерверных случаев придётся переделывать, чтобы пинговалось именно с пользовательской машины.
В любом случае, нужна сетевая доступность именно с того ПК/сервера, через который идёт печать.
А вы пробовали ВНЕ вашей инфраструктуры? Может у вас админы просто всё разрешили? Браузеры обычно не любят, когда скачивают bat/vbs/msg/exe/xml и т.д.
я ж не спорю:
Пинг тоже может блокироваться. Web-интерфейс — просто вторая вещь, которую можно быстро и просто проверить (даже вручную, через браузер или telnet).
Может и 80 порт блокироваться, суровые безопасники могут вообще оставить только минимальный набор портов, необходимый для печати (вроде 139 или 9100)
А ведь есть ещё snmp…
А если с сетевиками дружить, то они ещё и данные непосредственно с коммутаторов могут подогнать…
Возвращаясь к значку online для принтера — из JS в браузере можно попробовать отправлять запрос какой-нибудь странички веб-интерфейса — тогда будет «пинг» с машины пользователя. Но, возможно, ещё с кешированием придётся побороться.
P.s. пока писал, вспомнил вот этот комментарий и понял, что именно мне не нравится в статье — всё самое важное пропущено: началось с описания ТЗ (хотя, наверняка, оно подгонялось под остальную часть статьи), потом небольшое (ну действительно, маленькое) отступление о том, что автор впервые пишет на JAVA, как он выбрал IDE, описать БД, потом описать пару кейсов, добавить кусок скрипта «шобнаодинэкранневлезало», состоящего из почти идентичных строк и в конец накидать скриншотов готовой системы (хорошо хоть под кат). Где-то после описания второй проблемы система внезапно оказалась готова к тиражированию, отчего возникла третья проблема (чистая установка с созданием БД).
У вас правда не было других трудностей, кроме 3-4 перечисленных?
Первая проблема вообще оффтопик, вторая хоть как-то связана с темой, а третья вроде должна показать, что PrintDesk «готов к работе» не только в инфрастуктуре автора… четвертая — решена только для одного случая (PrintDesk развернут на принт-сервере) из… скольки?
На деле же, получается, решали надуманную проблему не самым оптимальным способом.
Почему проблема надуманная? Вы просто переложили работу на плечи пользователя. Вместо своей работы он должен заниматься настройкой ПК. Я ещё могу понять, если речь о BYOD или гипер-мобильных сотрудниках с ноутбуками, но в статье об этом не сказано.
Админы AD у вас не настраивают GPO (но сервачок 2012 R2 таки выделили), техподдержка не настраивает принтеры, нахрена они все тогда нужны? Кто-то ковыряется в носу, кто-то (как вы) пилит свои велосипеды, прокачивает скилы (что похвально, но не всегда соответствует целям бизнеса). А работу всё равно выполняет пользователь. Причём, судя по предыдущей статье — неграмотный пользователь.
Почему нельзя при первом подключении ПК (или переезде) на рабочем месте ОДИН РАЗ установить драйвера всех ближайших принтеров (если не запрещено подключаться напрямую к принтеру, без принт-сервера)?
Почему не кинуть скрипт в автозагрузку, который будет подключать все принтера филиала при логоне (если кол-во принтеров в филиале не чрезмерное; чужие можно отсеивать по маске сети)?
Если уж заставили делать свою работу пользователей — чем ДЛЯ ПОЛЬЗОВАТЕЛЕЙ ваше решение лучше просто списка расшаренных принтеров в нативном интерфейсе ОС (если ОС не древняя — набрать в ПУСКе «принтер», выбрать «Добавить принтер», клик на принтере и выбрать «Добавить/Подключиться»)? Если не хочется видеть принтера других филиалов, можно на файловой шаре создать для филиалов папки и в них накидать установочные ярлыки (примерно как писали тут). Это УЖЕ работает, поэтому выше я написал, что «проблема» решена не оптимальным способом (хотя автор частенько упоминает минимализм).
Из реально полезного — наклейки на сами принтеры.
AkshinM Автор
Я думал об этом и решение оставил на будущие версии. Пока только так.
Так я и есть админ. Мы ничего не разрешали. Даже наоборот. Разрешили только то, что необходимо, а все остальное под запретом.
Знаю, но тем не менее и вне, и внутри с очень жесткими ограничениями все работает.
Вроде остальное все было ОК. Обычное рутинное создание страниц, с содержанием и функционалом.
Не соглашусь с этим. Не запускается Servlet (напрямую открыли JSP файл), не работает вывод нужных данных из бд, а значит куча жалоб «твоя система не работает!»
Ну, а кто мешает ставить ее на много разных принт серверов, если они есть? Поставил, настроил и все. Правда смысла в этом тоже не вижу. Одной установки достаточно, как мне кажется.
Записал как фичу для реализации в будущем, чтобы можно было выбрать КАК проверять: пингом или проверкой веб адреса
А кто сказал, что он настройкой компьютера занимается. Пользователю дается полностью готовый комп в том числе с подключенным принтером. Если ему надо больше принтеров, то он может сам подключить, что ему нужно, а при возникновении проблемы конечно же обратиться в техподдержку. Так если не сказано, это не значит, что этого нет. Система хоть как-то снижает нагрузку на техподдержку. Пользователю не приходится лишний раз куда-то звонить, кого-то теребить, писать заявку. Зачем ему эта возня, если он сам методом «скачал запустил» запросто поставил себе то, что ему нужно. Профит двухсторонний.
Данные утверждения в корне ошибочны и не соответсвуют действительности, и в прочем я виноват, что у вас сложилось такое впечатление. Я вообще не считаю, что админ чуть-что должен создавать GPO шки. Конечно я понимаю, что предназначение GPO как раз таки для облегчения задач админу, но плодить сотни GPO и «чуть что запихнем все в GPO» я не считаю правильным.
А с каких пор пользователи бывают грамотнымы (в IT), по карайней мере подавляющее большинство? И это везде одинаково. К тому же они и не обязаны быть гиками. Они умеют делать свою работу, что от них и требует начальство. А скачать запустить что-то они все смогут, ведь дома же все на своих компьютерах делают, разве нет? Так чем отличается это «скачать и запустить», от домашнего «скачать запустить»? Возникнет проблема — позвонит в техподдержку. Делов-то…
Так все это и так делается, о чем я написал выше. Возможны другие случаи когда нужен оказыватся другой принтер.
А вы представьте это все дело в организации размером больше 2к человек, с большой текучкой сотрудников, ротациями и т.д и т.п… с сотнями принтеров....(GPO не предлагать)
Так это тоже все есть. Но опыт показал, что пользователю открыть и найти нужный принтер с браузера с удобным поиском и разделением «по полочкам» гораздо легче, чем в куче из сотен принтеров. Технически — разницы нет. Чисто писхологический фактор.
malefix
Спасибо за ответы!
В статьях, действительно, отсутствовали детали — что хотя бы один принтер у пользователя уже настроен, а PrintDesk нужен для подключения дополнительного; что
Про «оставить на потом» Linux/MacOS — уже сейчас, без модификации архитектуры ничего не заработает, поскольку скрипт всего один. Вроде в прошлой версии у вас упоминалась возможность запускать три разных скрипта
Про первую проблему — «как сделать так, чтобы страницу возможно было бы открыть только по его относительному пути (например /home), а не открывая JSP файл (например Home.jsp) непосредственно» — я назвал её оффтопиком из-за того, что это тонкости JAVA, не связанные с темой администрирования принтеров. Как если бы вы в первой статье начали рассуждать об одинарных vs двойных кавычках в PHP. Вроде и нужно… но зачем?
Тут есть и моя вина, не совсем удачно сформулировал. Слова «из скольки?» относятся к тексту перед скобками, полностью звучит так «четвертая — решена только для одного случая из… скольки?» Как минимум, случаев ещё два:
— PrintDesk установлен НЕ на принт-сервер
— принт-сервер вообще не используется
оба решаемые, конечно
Ну да, конечно, лучше написать своё решение, чем использовать то, что уже есть в AD ))
Но, учитывая, что описываемое решение нужно только для доп.принтеров (можно сказать, «хотелок пользователя»), то уже неважно.
С 2k человек не представляю. Зато представляю c 50 пользователями, с 5000 и >10000.
Чем меньше организация — тем больше может оказаться загонов у пользователей. В больших организациях всего перечисленного не было:
Принтер сломался — заменили на новый, поставив ему ip старого. Если вендор тот же — даже драйвера можно не менять.
Сломался и нечем заменить — для этого при первичной настройке/переезде добавляем все принтера в кабинете/на этаже/в филиале (по ситуации), дав им соответствующие имена. Когда один вышел из строя (даже просто замятие), пользователи просто печатают на следующий из списка.
Поменяли принтер местами — ну ip-адреса тоже поменяли, пользователь ничего не заметит.
За счёт того, что вместо принтсервера подключаемся напрямую к принтеру, ротация персонала вообще не колышет — у любого нового сотрудника уже будут подключены принтеры.
Аномалии бывают только в случаях «особенных принтеров» (например, все принтера — монохромные A4, но есть один цветной и один, умеющий печатать A3; или все просто принтера — но есть один МФУ, но там другие решения)
ToDo: поизучайте web-интерфейс своих принтеров. Нормальные сетевые принтеры умеют сохранять/показывать свои имя/местоположение, это можно считывать программно (не уверен, как там насчёт кроссвендорства(?) ), зачастую там же можно посмотреть уровень тонера/ошибку, чтобы «предвидеть» необходимость замены картриджа.