🏠: Viacheslav

Праздничные дни в Powershell

Сегодня у меня сработало оповещение о том, что базы Консультанта старые — скрипт смотрит на дату в файле [Путь к каталогу Консультанта]\RECEIVE\LAST_REC.TXT, и если эта дата раньше вчерашней и сегодня не понедельник, отправляет уведомление по почте.

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

Можно! В данном случае я нашёл сайт xmlcalendar.ru, где выкладывают производственный календарь в формате XML. Чтобы выгрузка с него была понятнее, я сделал некоторые преобразования. Вот скрипт:

# Скачивание XML
curl http://xmlcalendar.ru/data/ru/2020/calendar.xml -OutFile d:\temp\cal.xml
# Импорт
[xml]$cal = gc d:\temp\cal.xml -Encoding utf8
# Обработка
foreach ($day in $cal.calendar.days.day) {
$day.d = (($cal.calendar.year + '.' + $day.d) -as [datetime]).tostring("dd.MM.yyyy")

if ($day.t -eq 1) {$day.t = "Вых"}
elseif ($day.t -eq 2) {$day.t = "Сокр"}
elseif ($day.t -eq 3) {$day.t = "Раб"}

if ($day.h -match "\d") {$day.h = ($cal.calendar.holidays.holiday |? id -eq $day.h).title}
}
# Экспорт в CSV
$cal.calendar.days.day |Export-Csv "d:\temp\cal$($cal.calendar.year).csv" -Encoding utf8 -Delimiter ';' -NoTypeInformation

Результат:

d          t    h                                                                     
-          -    -                                                                     
01.01.2020 Вых  Новогодние каникулы (в ред. Федерального закона от 23.04.2012 № 35-ФЗ)
02.01.2020 Вых  Новогодние каникулы (в ред. Федерального закона от 23.04.2012 № 35-ФЗ)
03.01.2020 Вых  Новогодние каникулы (в ред. Федерального закона от 23.04.2012 № 35-ФЗ)
04.01.2020 Вых  Новогодние каникулы (в ред. Федерального закона от 23.04.2012 № 35-ФЗ)
05.01.2020 Вых  Новогодние каникулы (в ред. Федерального закона от 23.04.2012 № 35-ФЗ)
06.01.2020 Вых  Новогодние каникулы (в ред. Федерального закона от 23.04.2012 № 35-ФЗ)
07.01.2020 Вых  Рождество Христово                                                    
08.01.2020 Вых  Новогодние каникулы (в ред. Федерального закона от 23.04.2012 № 35-ФЗ)
23.02.2020 Вых  День защитника Отечества                                              
24.02.2020 Вых                                                                        
08.03.2020 Вых  Международный женский день                                            
09.03.2020 Вых                                                                        
30.04.2020 Сокр                                                                       
01.05.2020 Вых  Праздник Весны и Труда                                                
04.05.2020 Вых                                                                        
05.05.2020 Вых                                                                        
08.05.2020 Сокр                                                                       
09.05.2020 Вых  День Победы                                                           
11.05.2020 Вых                                                                        
11.06.2020 Сокр                                                                       
12.06.2020 Вых  День России                                                           
03.11.2020 Сокр                                                                       
04.11.2020 Вых  День народного единства                                               
31.12.2020 Сокр

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

Удобная альтернатива — https://www.isdayoff.ru

Судьба прадеда

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

Мой прадед по отцовской линии — Фёдор Степанович Булынкин — ушёл на фронт в сентябре 1941-го и не вернулся.

Предположительно, 1920-е годы.
Оттиск в левом нижнем углу — «Фотография Б. [И]. Клементьева г. Коломна с. Боброво»

За неимением информации, считалось, что он пропал без вести. Я запросил фамилию, имя и отчество на сайте «Память народа», который замечателен тем, что там выкладывают качественные копии реальных документов времён войны. Действительно, Фёдор Степанович нашёлся, но информация была так же скудна и подтверждала то, что и думали раньше:

bulynkinfs-missing-1.jpg bulynkinfs-missing-2-scaled.jpg bulynkinfs-missing-3.jpg
Фрагмент донесения № 69719, Коломенский ГОВК Моск. обл., 10.08.1946

Тем не менее, это уже, как-никак, реальный документ, с верным домашним адресом и именем жены — моей прабабушки. Но дата всё равно неясна — почему-то и день, и месяц написаны римскими цифрами — II\II-42г., то есть, непонятно, 2-е февраля или 11-е? А может, ноября? И почему в карточке на сайте написано «Дата выбытия 06.1942»?

Второй момент — это послевоенное донесение, августа 1946-го, то есть, подсчёт постфактум военкоматом. Наконец, нет никакой информации о том, в каком подразделении Фёдор Степанович воевал и хотя бы примерное место его гибели. «По сообщению товарищей, погиб» — кто были эти товарищи и где они теперь? Ответ очевиден до зубного скрипа, но я задавал его себе несколько дней.

Где-то на третий день я решил посмотреть на другие результаты, выдаваемые поиском, ведь он имеет некоторую вариативность и выдаёт не только точно совпадающие ответы на запрос, но и немного отличающиеся значения. Кто ещё был похож на прадеда именем и судьбой? Вот полный тёзка, но из Краснодарского края, вот, по-видимому, он же, отмеченный в военно-пересыльном пункте, вот другие Фёдоры Степановичи, но фамилии немного другие, и они вроде не погибали… А вот какой-то Булычкин, почти того же года рождения, и тоже погиб в сорок втором…

Да он призван в том же военкомате! И домашний адрес тот же!

bulynkinfs-killed-in-action-1.jpg bulynkinfs-killed-in-action-2.jpg bulynkinfs-killed-in-action-37-scaled.jpg bulynkinfs-killed-in-action-44-scaled.jpg
Фрагмент донесения о безвозвратных потерях № 4058, упр. 358 сд, 30.03.1942

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

Служил он в 358-й стрелковой дивизии, входившей в состав 4-й ударной армии, а погиб в бою 10 февраля 1942 года в районе деревни Дубровка Демидовского района Смоленской области, недалеко от границы с Белоруссией.

4-я ударная армия в феврале 1942 г. участвовала в Велижской операции. Достаточно подробно о ней пишет командарм А. И. Ерёменко в 11-й главе своей книги «В начале войны»:

Передышки войска 4-й ударной армии, однако, не получили. С утра 22 января армия по приказу Ставки была передана в Калининский фронт. Командующий Калининским фронтом генерал-полковник И. С. Конев потребовал от 4-й ударной армии продолжать энергичное наступление, выйти в глубокий тыл и перерезать коммуникации вражеской группы армий «Центр», не дать войскам этой группы отойти на тыловые оборонительные рубежи и во взаимодействии с другими армиями Калининского и Западного фронтов создать условия для их уничтожения. Для выполнения этих задач 4-я ударная армия должна была развивать наступление в направлении Торопец — Велиж — Рудня и выйти в район Рудни к 29 января. Таким образом, Торопецкая операция без какой-либо паузы переросла в новую — Велижскую операцию.
Дивизии получили следующие задачи: […] 358-й стрелковой дивизии двигаться во втором эшелоне за 332-й стрелковой дивизией и перейти в район Горовахи […]

358-я стрелковая дивизия, двигавшаяся за бригадами в направлении Западная Двина, Ильино, по приказу командования армии выслала 24 января 1191-и стрелковый полк с задачей овладеть ст. Земцы и разобрать железную дорогу. В дальнейшем полк должен был прикрывать левый фланг армии и, продвигаясь на юг, выйти в район Демидова.
28 января 358-я стрелковая дивизия подошла к Крестам и сменила здесь части 48-й и 39-й стрелковых бригад, за исключением 1-го батальона 39-й бригады, который перехватывал дорогу из Крестов на Велиж. Дивизия начала готовиться к атаке Крестов.
Командование гарнизона Крестов поняло угрозу опасности и запросило помощи. В ночь на 29 января 3-й батальон 257-го пехотного полка, усиленный артиллерией, был направлен из Велижа на помощь гарнизону Крестов.
Отбросив роту 1-го батальона 39-й бригады, занимавшую Починок, вражеский батальон сумел прорваться к Крестам. Однако 1-му батальону 39-й стрелковой бригады удалось снова выйти в район Починка и закрыть выход из Крестов на юг. Этой же ночью 1187-й стрелковый полк 358-й стрелковой дивизии двумя батальонами подошел к Крестам с юго-запада и запада. Одновременно 1-й батальон 1189-го стрелкового полка этой же дивизии подошел к Крестам с севера. Это завершило окружение противника в Крестах. С утра одновременной атакой всех трех батальонов 1187-го и одного батальона 1189-го стрелковых полков Кресты были полностью очищены от противника.

3 февраля 332-я стрелковая дивизия, развивая наступление, завязала бой за окружение Демидова, но взять его не смогла из-за отсутствия артиллерийских снарядов.
Задержка наших войск на рубеже Демидова позволила противнику перебросить сюда из района Рудни 330-ю пехотную дивизию, которая вступила в бон с нашей 332-й дивизией.
358-я стрелковая дивизия, действовавшая по обеспечению левого фланга, 2 февраля вышла на рубеж Понизовье, Титовщина.
Таким образом, в начале февраля 4-я ударная армия вынуждена была раздробить свои силы по трем направлениям и вести затяжные бои с подтянутыми противником свежими частями.
Соседние армии, отставшие от нас более чем на 100 км, не только не могли помочь нам, но и сами нуждались в помощи.

И наконец, вероятно, тот самый последний бой 10 февраля: «358сд во взаимодействии с 39[стр] вела бой с частями 555пп, усиленного 5 танками.»

 Из журнала боевых действий 4-й ударной армии, стр. 110

Похоронен Фёдор Степанович в братской могиле на опушке соснового бора посёлка Пржевальское на берегу озера Сапшо, под той самой фамилией, написанной с ошибкой. В альбоме он записан под № 330. Ему было столько же лет, сколько сейчас мне.

bulynkinfs-grave-2-scaled.jpg bulynkinfs-grave-4-scaled.jpg

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

Подкручиваем HTTPS

Больше трёх лет назад я делал подобную заметку, настало время дальнейшего пересмотра настроек. Я мало смыслю в безопасности, но зато люблю, когда онлайн-проверки светятся зелёным. За прошедшее время появился протокол HTTP/2, который я включил в конце 2017-го, и TLS 1.3, а версии TLS 1.0 и 1.1 уже признаются устаревшими с марта этого года, и отправляются на заслуженную пенсию составлять компанию уже находящемуся там SSL.

Итак, чтобы выключить все протоколы, кроме самых новых и надёжных, нужно добавить в файл конфигурации Апача, например, /etc/apache2/sites-available/default-ssl.conf (у меня другой файл, который создаёт Let’s Encrypt), следующее:

# Set Forward Secrecy
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLHonorCipherOrder on
SSLCipherSuite HIGH:!aNULL:!MD5:!3DES

# Strict transport security
<IfModule mod_headers.c>
 Header always set Strict-Transport-Security "max-age=15768000; includeSubDomains"
 Header always set Referrer-Policy "no-referrer-when-downgrade"
</IfModule>

С TLS 1.3, по ощущениям, действительно работает быстрее — ведь для установки защищённого соединения ему нужно меньше согласований. SSLHonorCipherOrder on — это включение расстановки приоритета алгоритмов шифрования самим сервером, что рекомендуется.

Также, я включил в Апаче поддержку OCSP Stapling — вроде бы полезная вещь — и, забавы ради, HSTS preloading, но это уже совсем необязательно.

Результат обстоятельного теста на Ssllabs после настройки:

Список других полезных тестов:

Что сделал - 5

Скрипт поиска несмонтированных виртуальных дисков Hyper-V

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

Алгоритм работы: берутся все виртуальные машины всех хостов, для каждой выводится список дисков (.vhd и .vhdx), если там вместо диска снапшот, то ищется родительский диск (там другое свойство), и результаты заносятся в один массив. Затем ищутся файлы с теми же расширениями на локальных дисках серверов, результаты заносятся в другой массив. Потом массивы сравниваются — отбрасываются значения первого массива, т. е., смонтированные диски, а потом сортируются по уникальным значениям — дело в том, что если хосты принадлежат кластеру и подключены к одной дисковой полке, то и результаты у них будут практически одинаковые. Ну, а дальше из этого делается красивая табличка, подсчитывается общий итог, генерируются команды на удаление и всё высылается на почту. Выглядит это примерно так:

# Команды на удаление
del "D:\VM\disk_c.vhdx" -Force -Confirm:$false
del "L:\vm\vmws-sccm\sccm2.vhdx" -Force -Confirm:$false
del "L:\vm\vmws-dns\Virtual Hard Disks\vmws-dns.vhdx" -Force -Confirm:$false
del "L:\back\vmls-web\vmls-web_Disk0.vhdx" -Force -Confirm:$false
del "L:\vm\vmws-sccm\sccm_c.vhdx" -Force -Confirm:$false

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

$hosts = "hv01","hv02","hv03","hv04","hv05"
$allvhds = Invoke-Command -computername $hosts -command {
dir ((gcim Win32_LogicalDisk -filter "drivetype=3").DeviceID -replace "$","\") -Include *.vhd,*.vhdx -Recurse -ErrorAction SilentlyContinue
}

Здесь инициатор просто даёт команду всем удалённым хостам, а потом просто ждёт, когда они завершат работу, и результаты падают в массив по мере их поступления. Работает во столько раз быстрее, сколько хостов участвует. Там есть нюансы — стандартно может работать до 32 потоков одновременно, но это настраивается, да и редко нужно.

Результат работы скрипта — нашлось 49 бесхозных дисков общим объёмом немногим менее 4 терабайт.

Скрипт создания папки на файловом ресурсе

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

Конвертация виртуальных дисков VMWare (.vmdk) в Hyper-V (.vhdx)

Последний месяц плотно занимался перетаскиванием виртуальных машин со старой VMWare ESXi 5.5 на модный молодёжный Hyper-V 2016. Есть прекрасная программа под названием Microsoft Virtual Machine Converter, которая хорошо выполняет эту функцию. Но иногда виртуальные машины были уже выгружены в виде набора файлов, и нужно было просто переконвертировать диск. Оказалось, что этот конвертер имеет в своём составе модуль для Powershell, и всё заверте…

# импортировать модуль
Import-Module 'C:\Program Files\Microsoft Virtual Machine Converter\MvmcCmdlet.psd1'
# конвертировать
ConvertTo-MvmcVirtualHardDisk -SourceLiteralPath C:\temp\disk.vmdk -DestinationLiteralPath 'C:\temp' -VhdType DynamicHardDisk -VhdFormat Vhdx

И последняя на сегодня забавная история.

Прошивка дисковой полки HP P2000 G3 FC

Великая и ужасная компания Хьюлет-Паккард выпускает (или выпускала) широко известные в узких кругах дисковые полки P2000. После того, как я перенёс все виртуальные машины со старой VMWare, эта полка освободилась, и там нужно было в одном контроллере заменить сбойную флеш-карту и всячески её обновить. С обновлением возникли проблемы, потому что без наличия сервисного контракта Хьюлет-Паккард скачать свежую прошивку со своего сайта не даёт.

Начал поиск в интернетах — и обнаружилось, что эта полка — перемаркированная DotHill AssuredSAN 3000 series, и компания Дотхилл, её выпускавшая, впоследствии была поглощена компанией Сигейт, у которой эти прошивки свободно доступны на сайте. Я скачал прошивку с сайта Сигейта, и она без проблем установилась и работает.

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

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

Обучение компьютера русскому

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

Уважаемый пользователь Информационных Систем!

Проблема хорошо описана в статье «Здравствуйте, с вами говорит робот», и мне хотелось очеловечить эти формальные уведомления, например, чтобы первой строкой было обращение по имени и отчеству с соответствующим изменением окончания слова «уважаемый» в зависимости от пола адресата.

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

Первым делом нужно понять, как отделять мужские ФИО от женских. Просматривая списки, я обнаружил, что ключом к разгадке являются отчества — все мужские отчества заканчиваются на «ич», а все женские — на «на». Сначала я думал, что одинаковы три последних буквы, но, например, у мужчин есть Ильич и Кузьмич, а у женщин Ильинична и Кузьминична, так что остановился на двух.

Так-то лучше:

# Выборка
if ($user.givenname -match "на$") {$body1 = "Уважаемая $($user.givenname)!"}
elseif ($user.givenname -match "ич$") {$body1 = "Уважаемый $($user.givenname)!"}
# А случаи бывают разные
elseif (!($user.givenname)) {$body1 = "Уважаемый $($user.name)!"}
else {$body1 = "Уважаемый пользователь информационных систем АО «Мир шерстяных носков»!"}
# Результат
$body1

Уважаемый Игорь Валерианович!
Уважаемый Михаил Сергеевич!
Уважаемый Игорь Алексеевич!
Уважаемая Олеся Андреевна!
Уважаемая Анна Васильевна!
Уважаемая Светлана Николаевна!
Уважаемый Алексей Владимирович!
Уважаемый Сергей Владимирович!
Уважаемая Любовь Владимировна!
Уважаемая Полина Евгеньевна!

Следующий этап — зависимость окончания слова «день» от их количества. Надо составить фразу «Действие вашего пароля истекает через N дней». Путём нехитрых размышлений получается, что вроде бы все числа, заканчивающиеся на 1 — это «день», на 2,3 и 4 — «дня», а все остальные — «дней». Проверяем:

if ($user.daystoexp -match "1$") {$in = "через $($user.daystoexp) день"}
elseif ($user.daystoexp -match "[234]$") {$in = "через $($user.daystoexp) дня"}
else {$in = "через $($user.daystoexp) дней"}
# Результат
"Действие вашего пароля истекает $in."

Действие вашего пароля истекает через 24 дня.
Действие вашего пароля истекает через 17 дней.
Действие вашего пароля истекает через 14 дня. # !
Действие вашего пароля истекает через 2 дня.
Действие вашего пароля истекает через 49 дней.
Действие вашего пароля истекает через 23 дня.
Действие вашего пароля истекает через 55 дней.
Действие вашего пароля истекает через 12 дня. # !
Действие вашего пароля истекает через 1 день.
Действие вашего пароля истекает через 11 день. # !

Русский язык велик и могуч, его с кондачка не возьмёшь! Исключение — числа с одиннадцати до четырнадцати: они все должны заканчиваться словом «дней», причём, в любой сотне — «через 11, 112, 1513, 614 дней». Исправляем:

if ($user.daystoexp -match "(?<!1)1$") {$in = "через $($user.daystoexp) день"}
elseif ($user.daystoexp -match "(?<!1)[234]$") {$in = "через $($user.daystoexp) дня"}
else {$in = "через $($user.daystoexp) дней"}
# Результат
"Действие вашего пароля истекает $in."

Действие вашего пароля истекает через 24 дня.
Действие вашего пароля истекает через 17 дней.
Действие вашего пароля истекает через 14 дней.
Действие вашего пароля истекает через 2 дня.
Действие вашего пароля истекает через 49 дней.
Действие вашего пароля истекает через 23 дня.
Действие вашего пароля истекает через 55 дней.
Действие вашего пароля истекает через 12 дней.
Действие вашего пароля истекает через 1 день.
Действие вашего пароля истекает через 11 дней.

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

Напоследок расскажу, что, оказывается, дата истечения срока действия пароля вычисляется самим контроллером домена и хранится в атрибуте msDS-UserPasswordExpiryTimeComputed, причём, в нечитаемом формате FileTime (количество 100-наносекундных интервалов с 1 января 1601 г.). Чтобы преобразовать это значение в «нормальный» формат при запросе, нужно написать что-то вроде

Get-ADUser username -Properties msDS-UserPasswordExpiryTimeComputed |
select name,samaccountname,@{n='passexpdate';e={[datetime]::FromFileTime($_.'msDS-UserPasswordExpiryTimeComputed')}}

У нас в домене нет Fine-Grained Password Policies (FGPP), так что при их наличии значение этого параметра надо проверять.