🏠: работа

Что сделал - 10

Продолжаем бахвалиться, юбилейный выпуск. Сегодня в программе сплошное построение велосипедов, что логично — скоро открытие велосезона.

Авторесет виртуальной машины при недоступности HTTP или если она не отвечает на пинг

Есть виртуальная машина на Ubuntu 8.04, перевезённая мной на Hyper-V с железки несколько месяцев назад, где крутится веб-сервис, управляемый CMS отечественного производства RBC Contents 4.0. В каталоге веб-сервера находится более миллиона файлов. Операционная система старая, несовместимая с виртуальной средой, поэтому сетевые адаптеры там пришлось ставить не современные синтетические, а старые эмулируемые (100 Мбит). Мало того, что версия системы управления контентом античных времён — она уже больше 10 лет вообще не существует в природе, как и компания, её выпускавшая. Инструкций тоже нет, в интернете нашлись два документа по три странички, писанных на коленке и совершенно бесполезных. В общем, классический «чёрный ящик», который невозможно тронуть, чтобы он, чего доброго, не развалился, но, вместе с тем, необходимо поддерживать его работоспособность.

Ящик время от времени по неизвестным причинам либо перестаёт отвечать по сети, либо вешается намертво; особенно радует, когда он это делает в три часа ночи. В журналах пусто. Так как всегда решением проблемы является нажатие кнопки сброса, я написал скрипт, который в нерабочее время каждые 10 минут проверяет доступность ящика по сети и порта HTTP. Если что-то не отвечает, скрипт ищет машину в кластере и жмёт ей на ресет, после чего пишет мне письмо, почему он это сделал.

$name = "webserver"
$ip = "192.168.1.10"

if (!(Test-Connection $ip -Count 4 -Quiet -OutVariable ping) -or `
!(Test-NetConnection $ip -Port 80 -OutVariable http).TcpTestSucceeded) {
$vm = Get-Clustergroup |? {$_.grouptype -eq 'VirtualMachine' -and $_.name -eq "$name"} |get-vm

  if ($vm -and $vm.State -eq "Running") {

  $vm |Restart-VM -force

  $body = "<p>Причины:</p>"
  $body += "<ul>"
    if (!$ping) {$body += "<li>Нет сетевого соединения</li>"}
    if (!$http.TcpTestSucceeded) {$body += "<li>Нет ответа по HTTP</li>"}
  $body += "</ul>"

  Send-MailMessage -SmtpServer mail.domain.ru -From support@domain.ru -Subject "Hyper-V - сервер $name был принудительно перезагружен" `
  -To admin@domain.ru -Body $body -BodyAsHtml -Encoding utf8
  }
}

Всё же, виртуализация в любом случае имеет преимущества, потому что ящик примерно так же вис и в свою бытность на железке, а до кнопки ресета там просто так дотянуться было нельзя.

Нужно, конечно, сделать поизящнее — привязать срабатывание скрипта к Zabbix, это дело ближайшего будущего.

Группы рассылки на базе групп подразделений компании

Обратились в просьбой сделать группу рассылки на один из отделов компании. Чтобы не возвращаться больше к этой теме в будущем, решили сделать такие группы сразу для всех отделов (кроме руководства, конечно).

Сначала я сделал скриптом динамические группы рассылки, т. е., в рассылку входят члены группы того или иного отдела. Но выяснилось, что так не работает, потому что структура отделов иерархическая, а динамическая группа рассылки не умеет смотреть в группу домена рекурсивно. Ведь тот или иной сотрудник входит в несколько разных вложенных друг в друга отделов, и должен входить во все группы рассылки этих отделов.

Пришлось создать обычные группы рассылки, а членство в группах спрашивать у домена.

function Get-ADGroupMemberEnabledAndMail {
param(
[parameter(mandatory=$true)]
$group
)
Get-ADGroupMember "$group" -Recursive |? objectclass -eq 'user' |% {
    Get-ADUser -filter "name -eq '$($_.name)' -and enabled -eq 'True'" -Properties mail
    }
}

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

По результатам работы скрипт шлёт отчёт, причём, я распробовал всякие html-символы, чтобы сделать его более читаемым и лаконичным. Примерный вид:

Отдел по развитию (группа рассылки)
Управление по повышению (группа рассылки)
Бумажкин Иван Ильич Дирекция по улучшению (группа рассылки)
Бюрократов Тихон Тарасович Группа по превозмоганию (группа рассылки)

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

Обнаружение лишних объектов в корне DFS

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

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

А как вообще отделить хлам от легитимных папок? Оказывается, папки в DFS имеют атрибут d----l (reparse point), то есть, это даже и не папки, а ссылки. Поэтому, вычислить хлам можно так:

dir `
"\\server1\c$\DFS_roots\*\*",
"\\server2\c$\корни_DFS\*\*",
"\\server3\c$\DFS_roots\*\*" |
? mode -ne 'd----l'

Скрипт, конечно, данные сам не удаляет (мало ли что может туда попасть?), а шлёт письмо, где перечислены потенциально лишние объекты, обёрнутые в команды для их удаления.

Автодобавление разрешения на почтовые ящики для группы

Это нужно, чтобы группе определённых сотрудников иметь возможность заходить через почтовый веб-интерфейс на другой ящик, вместо того, чтобы каждый раз подключать его руками и тянуть все эти гигабайты добра на свой комп.

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

В результате поиска я пришёл к выводу, что ни первого, ни второго сделать не удастся — привилегий будет слишком много, если давать их выше ящика, а доступ на чтение для веб-интерфейса невозможен. Допускаю, что решение есть, но какое-то уж слишком замороченное, да и в моём случае это было непринципиально.

Не стал писать нового скрипта, дополнил старые — в один, который контролирует видимость в глобальной адресной книге, добавил сам функционал раздачи прав на ящики, где они ещё не были розданы, а сводный отчёт по Exchange дополнил данными по явно заданным разрешениям на ящики, раз уж всё равно ковырял эту тему.

$accessNotGranted = $allUserMbxes |? {!((Get-MailboxPermission $_).user -match "$fullControlGroup")}
if ($accessNotGranted) {
    $accessNotGranted |
    Add-MailboxPermission -User "$fullControlGroup" -AccessRights FullAccess -InheritanceType All
}

Что сделал - 9

Отчёт по VMware ESXi

Разобрался с PowerCLI, наконец.

Изображение без описания

Также, установил веб-морду на ESXi 5.5, это гораздо удобнее отдельного клиента, и она поддерживает управление машинами версии 10.

Перевод внешних прямых DNS-зон на хостинг, а обратных c Windows DNS на BIND9

До этого в конторе было два локальных внешних DNS-сервера, где крутились все зоны. Я предложил перевести DNS на внешний хостинг, чтобы не держать эти сервера у себя, что и было после долгих согласований проделано начальником. Правда, пришлось писать на Powershell конвертер DNS-записей из формата выгрузки Windows DNS в формат, принимаемый Руцентром (там, похоже, тоже крутится BIND), потому что просто передать зоны не получалось. Я не лез в процесс переноса, просто сформировал файлики, чтобы их можно было импортировать на хостинге.

Всё прошло хорошо, но оказалось, что избавиться от локальных внешних серверов DNS полностью всё равно нельзя — у компании в собственности внешний IP-диапазон и нужно обеспечивать обратные зоны (PTR), иначе почта ходить не будет.

Тогда я поднял две виртуалки с Ubuntu Server, поставил туда BIND9, оба сервера сделал подчинёнными (кэширующими) для всех прямых зон — они синхронизировались с Руцентром, а для обратных зон один сервер первичный, а второй забирает эти обратные зоны с первичного. Ресурсов обе эти машинки едят меньше, чем один старый сервер на Windows. Всё чудесно работает, и минус 2 лицензии серверной винды. Внешний хостинг DNS в любом случае полезен — он повышает отказоустойчивость, а стоит очень недорого.

Система распознавания текста (OCR) для файлов PDF

В компании закуплены лицензии FineReader, но на всех желающих не хватает, закупать их — тема трудная и долгая, а работать как-то надо, причём, прямо сейчас. Темы закупки ПО и её методов, когда вместо корпоративной лицензии всё покупается поштучно и от случая к случаю и последствиях такого подхода затрагивать не буду, это не моя область ответственности, хотя смотреть на это иногда бывает больно. Короче говоря, нужен какой-то простой способ распознавания текста в файлах PDF по типу механизма сжатия, сделанного мною ранее. Я уже довольно давно знаю о существовании очень неплохой программы gImageReader, которая является оболочкой к OCR-движку Tesseract, который я и задействовал для решения этой задачи. Сборкой этого движка для Windows занимается Маннгеймская университетская библиотека, за что ей огромное спасибо.

Сам Тессеракт не воспринимает файлов PDF, ему картинки подавай, так что пришлось сначала прогонять файл через GhostScript, который преобразует PDF в набор картинок PNG.

& "$ghostScript" -dBATCH -dNOPAUSE -sDEVICE=pnggray -r300 "-sOutputFile=$($pdf.basename)-%04d.png" "$($pdf.fullname)"

Ну, а дальше картинки скармливаются Тессеракту, он делает из них соответствующее количество текстовых файлов, которые потом лепятся в один и выдаются пользователю.

(dir *.png) -match "$($pdf.basename)" |% {
    & "$tesseract" ".\$($_.name)" "$($_.basename)" -l rus+eng
}
gc ((dir *.txt) -match "$($pdf.basename)") -Encoding UTF8 |Out-File "$path\$($pdf.basename).txt" -Encoding default

Возьмём для теста какой-то договор из интернета:

Исходник - какой-то договор из интернета (фрагмент)

Результат распознавания:

- при производстве земляных и строительных работ соглассвывать
предполагаемые работы с Главным управлением по государственной охране
объектов культурного наследия Тверской области.

2. Срок Договора

2.1. Срок аренды Участка устанавливается с 2 ^\*\_\_ по 10.04.2019 года.
2.2. Договор вступает в силу со дня его государственной регистрации. |

3. Размер и условия внесения арендной платы

3.1. Арендатор ежегодно уплачивает Арендодателю арендную плату.
3.2. Размер арендной платы за Участок определяется в соответствии с
Расчетом арендной платы, являющимся неотъемлемой частью настоящего
Договора (приложение № 2).
3.3. Порядок определения размера арендной платы за пользование земельными
участками, устанавливается органом государственной власти Тверской области.
3.4. Арендная плата вносится следующими частями:
3.4.1. юридическими лицами:
- не позднее 15.04. - 1/4 годовой суммы;
- не позднее 15.07. - 1/4 годовой суммы;
- не позднее 15.10. - 1/2 годовой суммы.
путем перечисления на реквизиты, указываемые Арендодателем в асчете
арендной платы на текущий год. Арендатор обязан ежегодно до внесения
первого арендного платежа в текущем году уточнять у Арендодателя реквизиты,
на которые перечисляется арендная плата. :
В случае заключения Договора аренды после 15 сентября (в первый год
аренды) арендная плата за период до конца года, в том числе сумма,

По-моему, для бесплатного движка это очень круто.

Для ускорения обработки можно применить Powershell 7, где есть параллельный цикл, чтобы выжать из железа всё, на что оно способно.

Движемся дальше.

P. S. Чуть позже доделал скрипт, теперь он, помимо PDF, работает с TIF-TIFF (в т. ч., многостраничными), JPG-JPEG и PNG. Тут проще, потому что Тессеракт сам умеет работать с этими форматами без предварительных преобразований. Также, сделал вариант под Powershell 7 с параллельными циклами, что работает гораздо быстрее на одном и том же компьютере.

Влияние одного параметра на общую производительность системы

Несколько месяцев назад я настроил на работе прокси-сервер squid, он успешно прошёл стадию тестирования и с декабря прошлого года переведён в боевой режим.

Статистика за февраль

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

Нагрузка на 6 виртуальных процессоров вчера

Собственно, когда 6 процессоров не справляются, это вызывает вопрос — а всё ли нормально настроено, потому что тупо забрасывать ресурсами кривую систему глубоко неправильно, тем более, просто так добавлять процессоры к виртуальной машине неразумно — этим ситуацию можно скорее ухудшить, ведь реальные процессоры делятся на количество виртуальных, и чем больше их плодить, тем меньше будет выхлоп от отдельно взятого виртуального процессора. Именно поэтому количество виртуальных процессоров лучше максимально сокращать.

Программа top показала, что сам squid весьма скромен в потреблении ресурсов, а процессор загружен процессами аутентификации kerberos negotiate_kerberos_auth. Оказалось, что в kerberos есть replay cache, который пытается бороться с потенциальной подменой ключей шифрования, но, во-первых, он несовершенен, о чём прямо упомянуто в описании, а во-вторых, сильно тормозит более-менее нагруженную систему.

# man negotiate_kerberos_auth

Kerberos can keep a replay cache to detect the reuse of Kerberos
tickets (usually only possible in a 5 minute window). If squid is under
high load with Negotiate (Kerberos) proxy authentication requests the
replay cache checks can create high CPU load. If the environment does
not require high security the replay cache check can be disabled for
MIT based Kerberos implementations by adding the below to the startup
script or use the -t none option.

Добавляем параметр -t none к команде аутентификации:

auth_param negotiate program /usr/lib/squid/negotiate_kerberos_auth -s HTTP/proxy.domain.ru@DOMAIN.RU -t none

Результат очень радует:

Нагрузка на 4 виртуальных процессора сегодня

Хотел сразу оставить два vCPU, но перестраховался, оставил пока четыре, вечерком сделаю.

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

Путь к улучшению путём осмысления целого или автонастройка прокси-сервера на клиентских машинах

Хорошо спроектированная система обладает следующими признаками:

  • Обладает максимальным охватом всех возможных применений в рамках её предназначения
  • Управляется централизованно и имеет автоматические проверки и процедуры для минимизации влияния т. н. человеческого фактора
  • Имеет лаконичное и хорошо читаемое описание
  • Не требует большого количества исключений и их ручной настройки
  • Рационально использует ресурсы

Может быть, что-то ещё, но направление мысли, думаю, понятно. Применительно, предположим, к настройке подключения к интернету пользователей в организации — если централизованно вроде бы что-то и настроено, но при этом постоянно нужно что-то делать на пользовательских машинах, прописывать маршруты, ставить-снимать галки в свойствах браузера, делать резервирования в DHCP, разрешать прямой выход в интернет в обход прокси-сервера, просить сетевиков написать на шлюзе очередное правило для одной машины, при этом все эти 100500 изменений нигде не записаны и делаются бессистемно от случая к случаю; если в организации существуют дополнительные шлюзы, где всё открыто, потому что «надо работать и некогда разбираться» — это всё наводит на мысли, что система спроектирована как-то неправильно.

К сожалению, мало кто занимается осмыслением какой-либо системы в целом, особенно той, которая уже давно работает — люди адаптивны и привыкают практически ко всему, им лень что-то менять, да и опасно (тут прилагается большой список офисной мудрости, предписывающей сидеть тихо, максимально не брать на себя никакой ответственности и не выказывать инициатив, чтобы иметь долговременный и стабильный кусок хлеба). Как-то работает — и ладно, сейчас ещё одну проволочку прикрутим, а сбоку пластилинчика ещё налепим, и хорошо. Тем более, что всегда так настраивали и работали, в чём проблема-то?

Предлагая что-то улучшить, не стоит ждать поддержки. Никто не увлечётся и не будет содействовать, скорее, наоборот. Тебе всегда предложат всё придумать и сделать самому, а на вопросы по делу и высказываемые сомнения в существующем положении вещей ещё будут обижаться — что же, мы тут годами работаем, а ты намекаешь, что тут всё через одно место? Ты что, самый умный?

Сопротивление изменениям, и особенно автоматизации, связано также и с тем, что люди очень боятся стать ненужными. Условно говоря, раньше руками сто компьютеров за день настраивали с пупочным надрывом, а теперь это делается автоматически в тысячу раз быстрее и точнее, а мы теперь становимся не нужны и нас уволят, так зачем эта автоматизация? Пусть руками, зато будет что написать в список выполненных работ, и получить премию за ударный труд.

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

Выяснилось, что за автонастройку клиентов отвечает файл wpad.dat следующего содержания:

function FindProxyForURL(url, host)
{
    $NewProxy = "PROXY proxy.domain.ru:3128";

    if (isPlainHostName(host)) {return "DIRECT";}   
    if (isInNet(host, "10.1.0.0", "255.255.0.0")) {return "DIRECT";}
    if (dnsDomainIs(host,"localhost")) {return "DIRECT";}
    if (dnsDomainIs(host,"127.0.0.1")) {return "DIRECT";}

    if (dnsDomainIs(host,"domain.ru")) {return "DIRECT";}
    if (dnsDomainIs(host,"support.domain.ru")) {return "DIRECT";}
    if (dnsDomainIs(host,"test-sps.domain.ru")) {return "DIRECT";}
    if (dnsDomainIs(host,"portal.domain.ru")) {return "DIRECT";}
    if (dnsDomainIs(host,"portal.domain.ru")) {return "DIRECT";}

    if (shExpMatch(url, "http:*")
    ||  shExpMatch(url, "https:*")
    ||  shExpMatch(url, "ftp:*")
    ||  shExpMatch(url, "gopher:*"))
    return $NewProxy;

    return $NewProxy;
     }

Не принимая в расчёт общую неструктурированность файла, дубль записи и явную копипасту раздела с url (кто вообще сегодня знает про gopher?), возникает вопрос — а почему просто не пустить напрямую весь локальный .domain.ru? «А потому что есть ресурсы в domain.ru, опубликованные на внешних IP-адресах», — ответили мне. Дело в том, что и внешний, и внутренний домен названы одинаково. Что же делать? Полез в интернет и через некоторое время обнаружил отличное описание файла конфигурации с примером, как раз подходящим в моей ситуации:

function FindProxyForURL(url, host) {
  if (
    (isPlainHostName(host) || dnsDomainIs(host, ".mozilla.org")) &&
    !localHostOrDomainIs(host, "www.mozilla.org") &&
    !localHostOrDoaminIs(host, "merchant.mozilla.org")
  ) {
    return "DIRECT";
  } else {
    return "PROXY w3proxy.mozilla.org:8080; DIRECT";
  }
}

Красивая логика — на одну ступенечку сложнее, и уже такая мощь! Пускаем напрямую имена без указания домена или весь домен, но не конкретные имена в этом же домене. Больше того, можно указывать цепочку прокси-серверов — в данном примере, если не будет доступен w3proxy.mozilla.org, то идти напрямую.

Так, логика ясна. И сразу напрашивается решение эти опубликованные вовне ресурсы локального домена собирать из DNS автоматически — они ведь могут добавляться туда — и на этой основе формировать файл автонастройки. Тут уже приходит на помощь любимый Powershell:

# Records in domain.ru zone
$domainRecs = Get-DnsServerResourceRecord -ZoneName "domain.ru" -computerName "srv-dc3" |select hostname,recordtype,@{n='IPAddr';e={$_.RecordData.ipv4address.IPAddressToString}},@{n='Alias';e={$_.RecordData.hostnamealias -replace "\.domain\.ru\."}}
# External A records
$domainARecsExt = $domainRecs |? {$_.IPAddr -match "^186" -and $_.recordtype -eq "A"}
# CNAMEs which correspond external A records
$domainCnameRecsExt = foreach ($aRec in $domainARecsExt.hostname) {
$domainRecs |? {$_.recordtype -eq "CNAME" -and $_.alias -eq "$aRec"}
}
# all external names list
$domainAllExtRecs = ($domainARecsExt + $domainCnameRecsExt).hostname -replace "$",".domain.ru" |sort

# PAC file
$pac1 = "function FindProxyForURL(url, host) {
  if (
    (isPlainHostName(host) ||
    dnsDomainIs(host, `".domain.ru`") ||
    isInNet(host, `"10.0.0.0`", `"255.0.0.0`") ||    
    isInNet(host, `"127.0.0.0`", `"255.0.0.0`") ||
    localHostOrDomainIs(host, `"localhost`")) `&`&
"

# form the list for PAC
$total = $domainAllExtRecs.count
$C = 1

$pac2 = $domainAllExtRecs |% {
    if ($c -lt $total) {
    "    !localHostOrDomainIs(host, `"$_`") `&`&"
    }
    else {
    "    !localHostOrDomainIs(host, `"$_`")"
    }
$c++
}

$pac3 = "
  ) {
    return `"DIRECT`";
  } else {
    return `"PROXY proxy.domain.ru:3128`";
  }
}
"
# Combine all pieces and dump
$pac1 + ($pac2 -join "`n") + $pac3 |Out-File ~\Documents\proxy.pac -Encoding default

По желанию можно прикрутить сверку и оповещение на почту, если что-то убрали или добавили в DNS (в примере адреса начинаются на 186). Вот итоговый файл:

function FindProxyForURL(url, host) {
  if (
    (isPlainHostName(host) ||
    dnsDomainIs(host, ".domain.ru") ||
    isInNet(host, "10.0.0.0", "255.0.0.0") ||
    isInNet(host, "127.0.0.0", "255.0.0.0") ||
    localHostOrDomainIs(host, "localhost")) &&
    !localHostOrDomainIs(host, "8mar.domain.ru") &&
    !localHostOrDomainIs(host, "wolf.domain.ru") &&
    !localHostOrDomainIs(host, "wolf1.domain.ru") &&
    !localHostOrDomainIs(host, "wolf3.domain.ru") &&
    !localHostOrDomainIs(host, "wolfapi.domain.ru") &&
    !localHostOrDomainIs(host, "docs.domain.ru") &&
    !localHostOrDomainIs(host, "old.domain.ru") &&
    !localHostOrDomainIs(host, "xonix.domain.ru") &&
    !localHostOrDomainIs(host, "services.domain.ru") &&
    !localHostOrDomainIs(host, "test.xonix.domain.ru") &&
    !localHostOrDomainIs(host, "test.services.domain.ru") &&
    !localHostOrDomainIs(host, "web.domain.ru") &&
    !localHostOrDomainIs(host, "www.8mar.domain.ru") &&
    !localHostOrDomainIs(host, "www.docs.domain.ru") &&
    !localHostOrDomainIs(host, "www.domain.ru") &&
    !localHostOrDomainIs(host, "www.xonix.domain.ru") &&
    !localHostOrDomainIs(host, "www.services.domain.ru")
  ) {
    return "DIRECT";
  } else {
    return "PROXY proxy.domain.ru:3128";
  }
}

По-моему, очень красиво и чётко. Получается, в этом файле описываются локальные ресурсы — имена машин, указанные без домена, сеть, сам домен и localhost. Исключения из локального домена и всё остальное идёт через прокси-сервер, где уже есть правила доступа и списки запрещённых и разрешённых портов и ресурсов. Всё настраивается централизованно, и не надо ковыряться в локальных настройках пользовательских машин.

А теперь уже можно заняться другими интересными вещами — отрезать назойливую телеметрию, засоряющую access.log, настраивать кэширование для того, чтобы каждый компьютер не лез в интернет за обновлением Google Chrome, а получал его с прокси-сервера, и так далее.

Что сделал - 8

Ежемесячная автоматическая отсылка статистики правовой системы Консультант+

$t = get-date
$zip = "C:\Scripts\Consultant\Cons-Stat-$($t.ToShortDateString()).zip"
$consGuy = "email@mail.eu"

# compress the statistics folder and test archive (use Compress-Archive if possible)
& 'C:\Program Files\7-Zip\7z.exe' a "$zip" "K:\ADM\STS"
$test = & 'C:\Program Files\7-Zip\7z.exe' t "$zip"

# if archive succesfully created
if ($test -match "Everything is Ok") {
# send an email
$body = "<p>Добрый день!<br>
Высылаем статистику СПС Консультант по состоянию на $($t.ToLongDateString())</p>
<p><i>С уважением,<br>
Отдел ИТ<br>
АО "ЗАО"</i></p>
<p style=`"color:red;`">Это письмо создано автоматически, не отвечайте на него.</p>
"

Send-MailMessage -SmtpServer mail.domain.ru -From "robot@domain.ru" -To "$consGuy" -Cc "bulynkin@domain.ru" -Subject "АО ЗАО - Статистика СПС Консультант" -Body "$body" -Attachments "$zip" -Encoding UTF8 -bodyashtml

# wipe statistics folder
del K:\ADM\STS\* -Force -Confirm:$false
}

# if archive creation failed, send an alert
else {
Send-MailMessage -SmtpServer mail.domain.ru -From "robot@domain.ru" -To "bulynkin@domain.ru" -Subject "АО ЗАО - Статистика СПС Консультант" -Body "Ошибка отсылки статистики!" -Priority High -Encoding UTF8 -bodyashtml
}

Система видеоконференцсвязи Jitsi

Один центральный сервер + 5 серверов Jibri — это компонент для записи, пять штук нужно для пяти одновременных потоков. Дополнительно сделал автонастройку интерфейса через скрипт в кроне, чтобы после каждого обновления не прописывать заново название конторы, логотип и т. п. По уму, конечно, надо не плодить виртуальные машины, чтобы они пыхтели 24/7, а сделать какой-то оркестратор контейнеров, где работает только основной сервер и один Jibri, а при задействовании одного записывающего потока поднимался дополнительный контейнер с Jibri и так далее, т. е., система должна распухать и сдуваться динамически. Но это дело будущего, т. к. я пока не осилил контейнеризацию, а нахожусь в процессе освоения.

Все сервера Jibri пишут в свой локальный каталог. Чтобы не лазать по всем серверам в поисках записи — ведь неизвестно, какой именно будет писать ту или иную конференцию — я сделал автоматическое копирование с них на общую папку на Windows-сервере, и последующее автоудаление: с локальных серверов через 3 дня, а на Windows-сервере — через 2 недели.

Виртуализация старого веб-сервера на Ubuntu 8.04

Заодно разобрался, как обновлять старые системы Ubuntu. Оказалось, что для всех старых систем есть репозитории, а с версии на версию в общем случае можно переползать с помощью образа ISO со следующей версией.

Группы в AD на основании статуса сотрудника

«В декрете», «в отпуске», «в командировке» и т. д., и сделал механизм, по которому пользователи автоматически добавляются-удаляются из этих групп, и в почту присылается отчёт. Пока это ни для чего не нужно, но пригодиться может. Дополнительно, статус и отдел прописываются в описание пользователя для наглядности.

Отчёт по пользователям, подключающимся по VPN

Оказалось, что в Powershell (точнее, в .NET) есть механизм генерации графиков, и помимо самого списка пользователей, в письмо вставляется почасовой график количества подключений, очень красиво.

Критерий выгрузки почтовых ящиков из Exchange

Раньше было так — если сотрудник не заходил в систему больше 180 дней, то ящик выгружался. Теперь поступило указание выгружать ящики только через месяц после увольнения сотрудников. Когда сотрудник пропадает из штатного расписания, генерируется команда на заполнение атрибута comment, от чего можно потом оттолкнуться:

Set-ADUser "CN=Петрова Светлана Викторовна,OU=1_Users,DC=domain,DC=ru" -replace `
@{'comment'="Сотрудник уволен 16.10.2020"}

И, наконец, самое вкусное —

Прокси-сервер на базе Squid с прозрачной аутентификацией через Kerberos, с управлением уровнем доступа через группы в AD + статистика

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

Мой подход состоит в том, чтобы иметь самую минимальную конфигурацию, которая выполняет поставленные задачи; максимально оставлять настройки по умолчанию, чтобы все настройки были по возможности осмысленными, было известно, что они конкретно делают, и документировать их. Иначе получается очередной чёрный ящик, который непонятно как работает, и через пару лет к нему становится страшно прикоснуться, потому что если он повалится, то мало того, что никто не знает, как он устроен и как его чинить и восстанавливать — никто иногда даже не знает, что он конкретно делает. В общем, классика отечественных ИТ, да и не только ИТ. Оказалось, что Kerberos не так страшен и Linux-системы довольно легко вяжутся с доменом Windows, что многие пакеты, описанные в интернет-статьях, не требуется ставить (например, Samba), либо они устарели. Но лично мне это стало понятно после двух недель напряжённой работы по разбору этих авгиевых конюшен текста и бесчисленных проб и ошибок. Не сказать, чтобы я создал идеальную конфигурацию — кое-что перекочевало из старой «до востребования», но это во всяком случае более безопасная и структурированная настройка по сравнению с тем, что было раньше.

Статистику тоже хотелось сделать красиво, генерировать списки пользователей и отделы автоматически. На старом прокси-сервере стоял LightSquid, его я и взял для этой цели. Номера отделов для отчётов по группам я сначала просто задумал генерировать скриптом, а потом подумал, что лучше брать эти номера из того же штатного расписания, и попросил коллегу-программиста 1С добавить в ежедневную выгрузку и номер подразделения. Прописав всем пользователям на этой основе атрибут departmentNumber в AD, я написал следующий скрипт на bash:

#!/bin/bash

realnamecfgUTF8="/var/www/html/stat/realname.cfg.utf8";
realnamecfg="/var/www/html/stat/realname.cfg";
groupcfgUTF8="/var/www/html/stat/group.cfg.utf8";
groupcfg="/var/www/html/stat/group.cfg";
rm -f $realnamecfgUTF8 $realnamecfg $groupcfgUTF8 $groupcfg && touch $realnamecfgUTF8 $realnamecfg $groupcfgUTF8 $groupcfg

kinit HTTP/proxy.domain.ru -k

for i in `ldapsearch -H ldap://vmws-dc1.domain.ru -b dc=domain,dc=ru -LLLQ -o ldif-wrap=no "(&(objectclass=person)(primaryGroupID=513)(!(samaccountname=*mailbox*))(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))" sAMAccountName |grep 'sAMAccountName: ' |sed 's/sAMAccountName: //'`;
do
nameRaw=`ldapsearch -H ldap://vmws-dc1.domain.ru -b dc=domain,dc=ru -LLLQ -o ldif-wrap=no "(sAMAccountName=$i)" name |grep 'name:'`;
depRaw=`ldapsearch -H ldap://vmws-dc1.domain.ru -b dc=domain,dc=ru -LLLQ -o ldif-wrap=no "(sAMAccountName=$i)" department |grep 'department:'`;
depNum=`ldapsearch -H ldap://vmws-dc1.domain.ru -b dc=domain,dc=ru -LLLQ -o ldif-wrap=no "(sAMAccountName=$i)" departmentNumber |grep 'departmentNumber:' |sed "s/departmentNumber: //"`;

if [[ $nameRaw =~ '::' ]]
then
name=`echo $nameRaw |sed "s/name:: //" |base64 -d`
else
name=`echo $nameRaw |sed "s/name: //"`
fi

if [[ $depRaw =~ '::' ]]
then
dep=`echo $depRaw |sed "s/department:: //" |base64 -d`
else
dep=`echo $depRaw |sed "s/department: //"`
fi

loginLow=`echo $i |tr "[:upper:]" "[:lower:]"`
if [[ $depNum =~ [0-9] ]]
then
login=`echo "$loginLow@domain.ru"`
echo -e "$login\t$depNum\t$dep" >> $groupcfgUTF8
else
login=`echo "$loginLow"`
fi

#echo -e "$loginRaw\t$nameRaw\t$depRaw"
#echo -e "$login\t$name\t$dep"
#echo -e "$login\t$name\t$depNum\t$dep"

echo -e "$login\t$name" >> $realnamecfgUTF8

done

iconv -f utf-8 -t cp1251 -o $realnamecfg $realnamecfgUTF8
iconv -f utf-8 -t cp1251 -o $groupcfg $groupcfgUTF8

Перекодировка в cp1251 нужна для корректного отображения в браузере. Примерный вид статистики:

Доступ к статистике нужно было тоже ограничить, и это было сделано на той же самой базе Kerberos и basic-аутентификации + LDAP-авторизация. Для Apache2 существует два модуля аутентификации Kerberos — старый mod_auth_kerb, который описан во всех статьях, и новый mod_auth_gssapi. Конечно, хотелось пользоваться новым. В процессе настройки я немножко не разобрался и задал вопрос не в тему автору модуля, но в результате всё получилось. Теперь доступ к статистике имеют только те, кто входит в определённую группу в AD, и это очень удобно. Осталось только подать нагрузку на этот новый прокси-сервер, но это не вполне от меня зависит.