Блог

  • «Дайте интернет, но чтобы безопасно и что бы было вчера готово!»
    В любой компании наступает момент, когда безопасность говорит: «Хватит». Хватит пускать пользователей в интернет с их рабочих станций, где лежит бухгалтерия, документы и доступы к базам.
    Задача прилетела мега-классическая: нужно обеспечить доступ к внешним веб-ресурсам для 200 сотрудников, но изолировать этот процесс от внутренней сети. Вариантов было немного:

    • Поставить каждому второй ПК (дорого, супер-тупо, стол как и бюджет, не резиновый).
    • VDI (VDI на 200 персон ради браузера? Оверкилл).
    • Обскурные решения которыми заполнены просторы интернетов.
    • Terminal Server (RDS) + RemoteApp. Наш выбор.

    Почему «по талонам»? Потому что Chrome — это уже давно не браузер. Это операционная система, которая преиодически показывает сайты. И если дать ей волю на терминальном сервере, она съест всю оперативку, закусит процессором и попросит добавки.

    Архитектура: Строим «загон» для Хрома. Мы не стали изобретать велосипед и развернули классическую ферму на базе Windows Server 2022.
    Connection Broker: Мозг операции, балансирует нагрузку.
    Session Hosts: Рабочие лошадки, где, собственно, и крутится Chrome.
    СХД: Профили пользователей (об этом ниже).

    Спойлер

    @Тут я хотел бы вставить скриншот из диспетчера серверов со списком хостов но ИБ строго сказала «А-та-та так делать!».@

    Битва за ресурсы: Chrome vs RAM Главный враг терминального сервера — это современный веб. Один открытый YouTube в 4K может положить сессию целого отдела. Что мы сделали, чтобы сервер не умер в первый же день:
    GPO для Chrome — наше всё. Мы использовали официальные ADMX-шаблоны от Google.
    Отключение аппаратного ускорения. В виртуалке без vGPU оно только мешает, создавая лишнюю нагрузку на CPU.
    Блокировщик рекламы (uBlock Origin). Это не вопрос комфорта, это вопрос выживания. Баннеры и трекеры жрут трафик и ресурсы процессора. Мы внедрили расширение принудительно через политики.
    Управление вкладками. Расширения типа The Great Suspender помогают выгружать неактивные вкладки из памяти.
    Боль с кэшем. Хранить профили локально на хостах в ферме нельзя — юзера каждый раз может кинуть на новый сервер. Roaming Profiles — медленно. Мы остановились FSLogix.

    Тут же первая проблема: Кэш Хрома растет бесконечно. За месяц профиль может раздуться до 5–10 ГБ.
    Быстрофикс который стал постоянным решением: Настроили политику очистки кэша при выходе или перенаправили папку кэша в temp, который чистится. «Интернет по талонам» не предполагает хранения истории мемов за пять лет.

    Безопасность: RemoteApp создает иллюзию, что приложение работает локально. Но мы-то знаем, что юзверь находится внутри сервера. Чтобы любопытные пользователи не начали изучать файловую систему сервера через «Сохранить как…»:

    Скрытие дисков: Через GPO скрыли диски C: и D: сервера.
    AppLocker: Разрешен запуск только chrome.exe. Даже если они умудрятся скачать «super_game.exe», запустить они его не смогут.
    Буфер обмена: Ограничили копирование файлов между сессией и локальным ПК, чтобы исключить утечку документов.

    Факапы и выводы Конечно, без приключений не обошлось.

    В первый день забыли настроить таймауты, и «висячие» сессии съели все лицензии. Настроили — радуемся.

    Кто-то нашел сайт, который майнит крипту в браузере, и один хост ушел в 100% CPU. настроили аллертинг и заодно обновили black-list.

    Итог: Ферма живет, 200 пользователей ходят в интернет, внутренняя сеть в безопасности. Нагрузка на техподдержку снизилась: если у юзера «глючит интернет», мы просто сбрасываем его сессию, а не чистим всякое на его компьютере. Хотя все же мне кажется что наиболее лучшим решением мог быть дополнительный слой защиты в виде разнообразные NGFW, HP Wolf Security, RBI и иже с ними, но это уже совсем другая история…

  • Недавно ко мне подошёл младший коллега и спросил:

    «Почему у некоторых виртуальных машин в имени приписка _rest или _restored

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

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


    На пример:

    Однажды мы потеряли SRV-DC01 — контроллер домена, который на тот момент был PDC. Причиной стала банальная проблема — datastore переполнился, потому что старые снепшоты не удалялись корректно. В итоге машина «упала», и пришлось срочно поднимать восстановленную копию. Мы добавили суффикс _rest, чтобы сразу видеть, что это новая инстанция, и не путать с оригиналом в документации и системах мониторинга.

    Другой случай — SRV-CRM24_restored. Сервер с CRM-системой упал после неудачного обновления. В тот раз решение внедряли в спешке, и обкатку в тестовой среде посчитали «лишней тратой времени». Клиенты начали жаловаться через 10 минут после начала сбоя. DR-план сработал, но осадок остался. Суффикс — напоминание о спешке, которой могло бы не быть.

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


    Да, менять имя виртуальной машины в vCenter — не лучшая практика. Это может нарушить скрипты, автоматизацию, документацию. Но в таких случаях это осознанное решение. Это не про эстетику, а про историю инцидентов, которые нельзя забывать. Эти «шрамы» служат не только напоминанием мне, но и важным сигналом для других коллег: здесь когда-то было больно.


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

  • Создание бекапа — это только половина дела. Гораздо важнее быть уверенным, что в момент аварии данные можно будет восстановить быстро и без ошибок. В моей практике уже были случаи когда бекапы оказались «непригодными», и это привело к не очень приятным последствиям. Именно для этого Veeam Backup & Replication имеет технологию SureBackup, которая автоматически проверяет работоспособность виртуальных машин в бекапах, при этом делая это безопасно, изолировано и с огромной кучей автоматизации.

    И как же работает SureBackup?

    SureBackup выполняет автоматизированное тестирование бекапов, запуская виртуальные машины в изолированной среде (без влияния на рабочую инфраструктуру). Процесс включает:

    1. Виртуальная лаборатория – изолированная сеть, где разворачиваются ВМ из бэкапа. Причем это выглядит как ВМ которая выступает в качестве прокси для сетевых взаимодействий со своими тестовыми VLAN-ми.
    2. Проверка загрузки ОС – SureBackup убеждается, что машина запускается без ошибок.
    3. Проверка сервисов и приложений – с помощью скриптов или ping-тестов можно удостовериться, что критичные службы (например, SQL, Active Directory) работают корректно и что самое интересное, в зависимости от скриптов pre или post job модно получить огромный комплекс данных вплоть до самых тонких метрик.

    Практическое применение SureBackup

    1. Настройка виртуальной лаборатории

    Перед использованием SureBackup необходимо создать Virtual Lab в Veeam:

    • Указать ESXi-хосты для развертывания тестовых ВМ.
    • Настроить изолированную сеть (например, через vSwitch).
    • Добавить прокси-приложение, если требуется проверка внутренних сервисов (например, веб-сайтов).

    2. Создание задания SureBackup

    • Выбрать бекапы для тестирования.
    • Задать правила проверки (например, «ждать успешного ответа от службы RDP»).
    • Настроить расписание (рекомендуется запускать тесты еженедельно).

    3. Автоматизация отчетов

    Veeam отправляет уведомления о результатах тестов. Если ВМ не загружается, администратор получит алерт до того, как произойдет реальный инцидент.

    Вывод: почему SureBackup — это маст-хэв?

    • Исключает «фантомные» бекапы, которые невозможно восстановить.
    • Экономит время – вместо ручного тестирования всё проверяется автоматически.
    • Соответствует стандартам (например, фин.регуляторы, ISO 27001 и CIS-20 требуют регулярной проверки резервных копий).

    Учитесь на чужих ошибках: Не ограничивайтесь проверкой только «важных» ВМ. Лучше тестировать всё если на то есть возможность, потому что даже файловый сервер может оказаться критичным при аварии а за сломанный бекап виноватым будет кто?

    SureBackup — это не просто инструмент, а страховка от возможной катастрофы. Настройте его один раз, и вы будете чуть менее тревожны за инфру.

    Целостных бекапов и спокойных снов ребята!

  • Недавно гиперконвергентные кластеры (ГКС) стали популярны для построения IT-инфраструктуры. Причина в том, что их легко развернуть и можно сэкономить на старте. Но по моему опыту, такой подход не идеален. Хочу объяснить, почему я выбрал классический кластер с внешней системой хранения данных (СХД).

    Первое – цена. Сначала ГКС кажется выгодным, особенно для малого и среднего бизнеса с небольшим бюджетом. Развернуть просто, и не нужно покупать отдельную СХД, что экономит время и деньги. Но эта экономия быстро исчезает, когда нужно увеличить кластер, особенно хранилище. Например, чтобы добавить узел, придётся покупать не только диски, но и модули с ресурсами, памятью и лицензиями. Это дорого.

    Второй важный момент – производительность гиперконвергентных систем. Сначала ГКС работает быстро, потому что вычислительные мощности и хранилище объединены. Но когда нагрузка растёт, особенно если ресурсы используются для виртуализации и хранения, производительность падает. У меня, когда одновременно выполнялись задачи резервного копирования и миграции виртуальных машин, появились задержки и система работала медленно. С отдельной СХД такого не было.

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

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

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

    Эта статья – моё личное мнение.

  • Приветствую. После того как я полностью автоматизировал AD в организации, появилась острая необходимость в безопасной передаче пароля конечному пользователю. По регламенту организации сотрудники тех поддержки не должны видеть пароль но при этом должны как то его сбросить и передать. Решение было быстрым и тривиальным. Написал простенький инструмент. конвертировал его в EXE и отдал сотрудникам. Сразу же сообщу. В коде много обращений к API внутренних сервисов организации, у вас же это может сильно отличатся. например функции отправки СМС и получения инфы о пользователе завязаны на веб-серверах, у вас это может иметь другую реализацию. На худой конец, вы всегда можете закомментировать не нужные строки и пользоваться кор-функционалом с отправкой данных только на e-mail.

    Тыкни
    <#
    Итак. Все обращения к базам данных HR у нас осуществляются по API. У вас же это может выполнятся разными способами. 
    #>
    $global:headers = @{
        "content-type"  = "application/json"
        "Authorization" = "Basic SOMEA123123123123123TOKEN"
    }
    # Запрос к API базы HR  
    function Invoke-APIRequest 
    {
        param (
            [Parameter(Mandatory)]
            [string]$Uri,
            [Parameter()]
            [string]$Method = 'POST',
            [Parameter()]
            [string]$Body
        )
        return Invoke-RestMethod -Uri $Uri -Method $Method -Headers $global:headers -Body $Body
    }
    # Получение списка сотрудников
    function Get-Employees 
    {
    $body = @"
    {
            "get_child_departments":"True"
    }
    "@
    return $(Invoke-APIRequest -Uri 'https://hr.domain.site/api/employees' -Body $body)
    }
    # Получение всех Департаментов банка
    function Get-Departments 
    {
    $body = @"
    {
            "get_child_departments":"True"
    }
    "@
        return Invoke-APIRequest -Uri 'https://hr.domain.site/api/gate/department' -Body $body
    }
    # Очевидно
    function get-userinf
    {
    $ipregex = "^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"
    [string]$date = get-date -Format "yyyy.MM.dd hh-mm"
    $ipaddress = Get-NetIPAddress | ? IPAddress -Match  $ipregex | select IPAddress
    $return = @{
    "Username" = $($env:USERNAME)
    "ComputerName" = $($env:COMPUTERNAME)
    "IPAdress" = $ipaddress.IPAddress
    "ExecutionDate" = $($date)
    }
    
    return ConvertTo-Json $return
    }
    # Очевидно
    function Check-User 
    {
        param (
            [Parameter(Mandatory)]
            [string]$samaccountname = "ttestesteronyan"
        )
    
        try {
            # получаем пользователя
            $user = Get-ADUser -Filter * -Properties GivenName,Surname,Name,Department,Title,Mobile,wWWHomePage,EmployeeID,EmployeeNumber,whenCreated |`
                  ? {if(($_.EmployeeID -eq $samaccountname) -or ($_.Name -eq $samaccountname) -or ($_.employeeNumber -eq $samaccountname) -or ($_.SamaccountName -eq $samaccountname)){$_}}
            $HRuser = (get-Employees).data `
                    | ? {if(($_.employee_id -eq $samaccountname) -or ($_.Name -eq $samaccountname) -or ($_.system_id -eq $samaccountname) -or ($_.name -eq $user.name)){$_}} `
                    | select firstname, lastname, Name, department_id, Employee_ID, email, phone_number ,System_ID 
    
            # собираем данные
            $output = @()
            if($user){
            $output += "* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *"
            $output += "Информация о пользователе $Username :"
            $output += "====================================="
            $output += "Логин: $($user.samaccountname)"
            $output += "Статус учетки пользователя: $(if($user.enabled){"включена"}else{"выключена"})"
            $output += "Имя: $($user.GivenName)"
            $output += "Фамилия: $($user.Surname)"
            $output += "ФИО: $($user.Name)"
            $output += "Департамент: $($user.Department)"
            $output += "Должность: $($user.Title)"
            $output += "Мобильный телефон: $($user.Mobile)"
            $output += "Личная почта: $($user.wWWHomePage)"
            $output += "Номер сотрудника: $($user.EmployeeID)"
            $output += "ID сотрудника: $($user.EmployeeNumber)"
            $output += "Дата создания: $($user.whenCreated)"
            $output += "====================================="
            }
            else{
            $output = @()
            }
            if($HRuser){
            $output += "Информация о пользователе $samaccountname в базе HR:"
            $output += "====================================="
            $output += "Имя: $($HRuser.firstname)"
            $output += "Фамилия: $($HRuser.lastname)"
            $output += "ФИО: $($HRuser.Name)"
            $output += "Департамент: $(((Get-Departments).data  | ? id -eq $HRuser.department_id).name)"
            $output += "Должность: $($HRuser.post)"
            $output += "Мобильный телефон: $($HRuser.phone_number)"
            $output += "Личная почта: $($HRuser.email)"
            $output += "Номер сотрудника: $($HRuser.Employee_ID)"
            $output += "ID сотрудника: $($HRuser.System_ID)"
            $output += "====================================="
            }
    
            if(!$user -and !$HRuser){
            return "ПОльзователь не найден ни в АД ни в базе кадров."
            }
            elseif($user -and !$HRuser){
            $output +=  "Пользователь не найден в базе кадров но есть в AD."
            return ($output -join "`r`n")        
            }
            elseif(!$user -and $HRuser){
            $output +=  "Пользователь не найден в AD но найден в базе кадров."
            return ($output -join "`r`n")        
            }
            else{
            return ($output -join "`r`n")        
            }
        }
        catch {
            return "Ошибка при получении информации о пользователе $Username : $_"
        }
    }
    # Очевидно
    function Write-Log 
    {
        param (
            [Parameter(ValueFromPipeline = $true)]
            [string]$Message,
            [Parameter(Mandatory = $true)]
            [string]$LogFilePath
        )
        process {
            $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
            $logEntry = "$($timestamp): $Message"
            Add-Content -Path $LogFilePath -Value $logEntry
        }
    }
    # Генерация пароля
    function Get-RandomPassword 
    {
        Param (
            [Parameter(Mandatory)]
            [int] $length
        )
        
        $CharSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.ToCharArray()
        $SpecialCharSet = '!@'.ToCharArray()
        $rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
        $bytes = New-Object byte[]($length)
        $rng.GetBytes($bytes)
        $result = New-Object char[]($length)
        
        # Генерация пароля без спецсимволов
        for ($i = 0 ; $i -lt $length ; $i++) {
            $result[$i] = $CharSet[$bytes[$i] % $CharSet.Length]
        }
        
        # Вставка специального символа в случайную позицию
        $randomIndex = Get-Random -Minimum 0 -Maximum $length
        $specialChar = $SpecialCharSet[$(Get-Random -Minimum 0 -Maximum $SpecialCharSet.Length)]
        $result[$randomIndex] = $specialChar
        
        return -join $result
    }
    # отправка смс по номеру телефона и мыло туда же
    function ConvertTo-Lat 
    {
        param ([string]$russian)
        $alphabet = @{
            "а" = "a"; "б" = "b"; "в" = "v"; "г" = "g"; "д" = "d";
            "е" = "e"; "ё" = "yo"; "ж" = "j"; "з" = "z"; "и" = "i";
            "й" = "i"; "к" = "k"; "л" = "l"; "м" = "m"; "н" = "n";
            "о" = "o"; "п" = "p"; "р" = "r"; "с" = "s"; "т" = "t";
            "у" = "u"; "ф" = "f"; "х" = "h"; "ц" = "c"; "ч" = "ch";
            "ш" = "sh"; "щ" = "sch"; "ь" = ""; "ъ" = ""; "ы" = "y";
            "э" = "e"; "ю" = "yu"; "я" = "ya"; " " = " ";
        }
        $russian_chars = $russian.ToCharArray()
        $russian_in_lat = ""
        foreach ($char in $russian_chars) {
            $stringChar = $char.ToString()
            $lowerChar = $stringChar.ToLower()
            if ($alphabet.ContainsKey($lowerChar)) {
                $translatedChar = $alphabet[$lowerChar]
                if ($stringChar -ne $lowerChar) {
                    $translatedChar = $translatedChar[0].ToUpper() + $translatedChar.Substring(1)
                }
                $russian_in_lat += $translatedChar
            } else {
                $russian_in_lat += $char
            }
        }
        return $russian_in_lat
    }
    # получение токена авторизации для смс-шлюза
    function Get-Token 
    {
    try{
    $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
    $headers.Add("Content-Type", "application/json")
    $body = @"
    {
        `"username`": `"someUser`",
        `"password`": `"PrettyStronGPa$$w0rd`"
    }
    "@
    $response = Invoke-RestMethod 'https://smsgtw.domain.site/api/auth/token' -Method 'POST' -Headers $headers -Body $body
    return $response
    
    }
    catch{
    return $Error[0].Exception.Message
    }
    }
    # отправка смс по номеру телефона
    function Send-SMS 
    {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
    $AuthToken,
    $phone_number,
    $purpose,
    $message
    )
    
    $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
    $headers.Add("Content-Type", "application/json")
    $headers.Add("Authorization", "Bearer $AuthToken")
    
    $body = @"
    {
        `"purpose`": `"$purpose`",
        `"phone_number`": `"$phone_number`",
        `"message`": `"$message`",
    }
    "@
    
    $response = Invoke-RestMethod 'https://smsgtw.domain.site/api/messages' -Method 'POST' -Headers $headers -Body $body 
    $response.data.when_sent | ConvertTo-Json
    
    }
    # отправка смс по номеру телефона и мыло туда же
    function Send-Notification 
    {
        [CmdletBinding(SupportsShouldProcess = $true)]
        param(
            [string]   $Email,
            [string]   $PhoneNum,
            [string]   $EmailBody,
            [string]   $SmsBody
        )
        $p = ConvertTo-SecureString -String "PrettyStronGPa$$w0rd" -AsPlainText -Force 
        $u = "robot@domain.site"
        $smtpCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $u, $p
    
        $hasEmail = -not [string]::IsNullOrWhiteSpace($Email)
        $hasPhone = -not [string]::IsNullOrWhiteSpace($PhoneNum)
    
        if (-not $hasEmail -and -not $hasPhone) {
            return "Ошибка: не указан ни адрес электронной почты, ни номер телефона."
        }
        $combinedBody = $EmailBody, $SmsBody -join " | "
    
        $sentEmail = $false
        $sentSms   = $false
    
        try {
            if ($hasEmail -and $hasPhone) {
                Send-MailMessage -To $Email `
                                 -From $u `
                                 -Subject "Уведомление" `
                                 -Body $EmailBody `
                                 -SmtpServer "smtp.domain.site" `
                                 -Credential $smtpCredential `
                                 -UseSsl `
                                 -Encoding UTF8
    
                $sentEmail = $true
                Send-SMS         -AuthToken (Get-Token).access_token `
                                 -phone_number $PhoneNum `
                                 -purpose "Send User's data" `
                                 -message "$SmsBody"
                $sentSms = $true
            }
            elseif ($hasEmail) {
                Send-MailMessage -To $Email `
                                 -From $u `
                                 -Subject "Уведомление" `
                                 -Body $combinedBody `
                                 -SmtpServer "smtp.domain.site" `
                                 -Credential $smtpCredential `
                                 -UseSsl `
                                 -Encoding UTF8
                $sentEmail = $true
            }
            else {
                Send-SMS         -AuthToken (Get-Token).access_token `
                                 -phone_number $PhoneNum `
                                 -purpose "Send User's data" `
                                 -message "$((ConvertTo-Lat $combinedBody).replace("_"," "))"
                $sentSms = $true
            }
    
            return [pscustomobject]@{
                DateTime  = Get-Date
                EmailSent = $sentEmail
                SmsSent   = $sentSms
            }
        }
        catch {
            return $_.Exception
        }
    }
    # Смена пароля пльзователя
    function Reset-ADUserPassword
    {
    param($samaccountname)
    try{
        $chekADuser = Get-ADUser -Filter * -Properties GivenName,Surname,Name,Department,Title,MobilePhone,HomePage,EmployeeID,EmployeeNumber,whenCreated |`
                    ? {if(($_.EmployeeID -eq $samaccountname) -or ($_.Name -eq $samaccountname) -or ($_.employeeNumber -eq $samaccountname) -or ($_.SamaccountName -eq $samaccountname)){$_}}
            if(!$chekADuser){
                $result = "$(get-date): Не найден пользователь"
            }
            else{
                if(![string]::IsNullOrWhiteSpace($chekADuser.HomePage) -and ![string]::IsNullOrWhiteSpace($chekADuser.MobilePhone)){
                 $passsword = Get-RandomPassword -length 10
                     try{
                         Set-ADAccountPassword -Identity $chekADuser.DistinguishedName  `
                                               -NewPassword (ConvertTo-SecureString -String $passsword -AsPlainText -Force) `
                                               -Confirm:$false 
                         Set-ADUser $chekADuser.DistinguishedName -ChangePasswordAtLogon $true -Confirm:$false
                         Send-Notification -Email $chekADuser.HomePage `
                                           -PhoneNum $chekADuser.MobilePhone.Replace('+',"") `
                                           -EmailBody "Пароль пользователя $($chekADuser.samaccountname) был сброшен и отправлен на мобильный телефон."`
                                           -SmsBody "One time password: $passsword" 
                     $result = "$(get-date): Cмена пароля у пользователя $($chekADuser.samaccountname) прошла успешно, пароль отправлен и на E-mail и на мобильный телефон."
                        }
                     catch{
                         $result = "$(get-date): Произошла ошибка при сменен пароля у пользователя $($chekADuser.samaccountname): $($Error[0].exception.message)"
                     }
                }
                elseif(![string]::IsNullOrWhiteSpace($chekADuser.HomePage) -and [string]::IsNullOrWhiteSpace($chekADuser.MobilePhone)){
                # Это если нет телефона
                $passsword = Get-RandomPassword -length 10
                     try{
                         Set-ADAccountPassword -Identity $chekADuser.DistinguishedName  `
                                               -NewPassword (ConvertTo-SecureString -String $passsword -AsPlainText -Force) `
                                               -Confirm:$false 
                         Set-ADUser $chekADuser.DistinguishedName -ChangePasswordAtLogon $true -Confirm:$false
                         Send-Notification -Email $chekADuser.HomePage `
                                           -PhoneNum $chekADuser.MobilePhone `
                                           -EmailBody "Пароль пользователя $($chekADuser.samaccountname) был сброшен." `
                                           -SmsBody "One time password: $passsword"
                         $result = "$(get-date): Cмена пароля у пользователя $($chekADuser.samaccountname) прошла успешно, пароль отправлен на E-mail, МОБИЛЬНЫЙ НЕ УКАЗАН!!!."
                        }
                     catch{
                         $result = "$(get-date): Произошла ошибка при сменен пароля у пользователя $($chekADuser.samaccountname): $($Error[0].exception.message)"
                     }
                }
                elseif([string]::IsNullOrWhiteSpace($chekADuser.HomePage) -and ![string]::IsNullOrWhiteSpace($chekADuser.MobilePhone)){
                # Это если нет Email
                $passsword = Get-RandomPassword -length 10
                     try{
                         Set-ADAccountPassword -Identity $chekADuser.DistinguishedName  `
                                               -NewPassword (ConvertTo-SecureString -String $passsword -AsPlainText -Force) `
                                               -Confirm:$false 
                         Set-ADUser $chekADuser.DistinguishedName -ChangePasswordAtLogon $true -Confirm:$false
                         Send-Notification -Email $chekADuser.HomePage `
                                           -PhoneNum $chekADuser.MobilePhone.Replace('+',"") `
                                           -EmailBody "Hi!"`
                                           -SmsBody "$($chekADuser.samaccountname) | one time password: $passsword" 
                        $result = "$(get-date): Cмена пароля у пользователя $($chekADuser.samaccountname) прошла успешно, пароль отправлен на мобильный телефон, ЭЛЕКТРОННАЯ ПОЧТА НЕ УКАЗАНА!!!."
                        }
                     catch{
                        $result = "$(get-date): Произошла ошибка при сменен пароля у пользователя $($chekADuser.samaccountname): $($Error[0].exception.message)"
                     }
                    
                }
                elseif([string]::IsNullOrWhiteSpace($chekADuser.HomePage) -and [string]::IsNullOrWhiteSpace($chekADuser.MobilePhone)){
                # Это если нет Email и телефона
                      $result = "$(get-date): Cмена пароля у пользователя $($chekADuser.samaccountname) отменена, мобильный телефон и электронная почта не указаны в AD и в базе HR!!!."  
                }
                else{
                # Любая другая херня
                    $result = "$(get-date): Произошла ошибка при сменен пароля у пользователя необходимо уточнение данных по этому пользователю."
                }
            }
            }
        catch [System.Management.Automation.CommandNotFoundException]{
        $result = "$(get-date): У вас не установлены средства управления AD! Вы точно привелигированный пользователь?? На всякий случай я отправил ваши данные в ДИБ!!!"
        $whois = get-userinf
        Send-Notification -Email alert@domain.site `
                                      -PhoneNum $null `
                                      -EmailBody "Обнаружена попытка сменить пароль пользователя $samaccountname на компьютере с отсутвующими инструментами администрирования AD!!! Возьмите 'на карандаш'!" `
                                      -SmsBody $whois
        }
    return ($result  + "`r`n")
    }
    
    #
    
    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -AssemblyName System.Drawing
    # Форма
    $form = New-Object System.Windows.Forms.Form
    $form.Text = "Сброс пароля пользователя"
    $form.Size = New-Object System.Drawing.Size(700,400)
    $form.StartPosition = "CenterScreen"
    $form.FormBorderStyle = 'FixedDialog'
    $form.MaximizeBox = $false
    
    # Метка
    $label = New-Object System.Windows.Forms.Label
    $label.Text = "Введите логин, или ФИО, или ID пользователя"
    $label.Location = New-Object System.Drawing.Point(20,20)
    $label.AutoSize = $true
    $form.Controls.Add($label)
    
    # Поле ввода
    $textBox = New-Object System.Windows.Forms.TextBox
    $textBox.Location = New-Object System.Drawing.Point(20,50)
    $textBox.Size = New-Object System.Drawing.Size(300,20)
    $form.Controls.Add($textBox)
    
    # Кнопка «Сменить пароль!»
    $resetButton = New-Object System.Windows.Forms.Button
    $resetButton.Text = "Сменить пароль!"
    $resetButton.Location = New-Object System.Drawing.Point(510,45)
    $resetButton.Size = New-Object System.Drawing.Size(120,30)
    $form.Controls.Add($resetButton)
    
    # Кнопка «Проверить пользователя»
    $checkButton = New-Object System.Windows.Forms.Button
    $checkButton.Text = "Проверить пользователя"
    $checkButton.Location = New-Object System.Drawing.Point(340,45)
    $checkButton.Size = New-Object System.Drawing.Size(160,30)
    $form.Controls.Add($checkButton)
    
    # Текстовое поле для логов
    $logBox = New-Object System.Windows.Forms.TextBox
    $logBox.Location = New-Object System.Drawing.Point(20,100)
    $logBox.Size = New-Object System.Drawing.Size(640,220)
    $logBox.Multiline = $true
    $logBox.ScrollBars = "Vertical"
    $logBox.ReadOnly = $true
    $form.Controls.Add($logBox)
    
    # Прогрессбар ниже logBox, не налезает
    $progressBar = New-Object System.Windows.Forms.ProgressBar
    $progressBar.Location = New-Object System.Drawing.Point(20,340)  # чуть ниже logBox
    $progressBar.Size = New-Object System.Drawing.Size(640,20)       # по ширине logBox
    $progressBar.Style = 'Marquee'
    $progressBar.Visible = $false
    $form.Controls.Add($progressBar)
    
    # Действие кнопки
    $resetButton.Add_Click({
        $username = $textBox.Text
        if ([string]::IsNullOrWhiteSpace($username)) {
            $logBox.AppendText("$(get-date):* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *`r`n")
            $logBox.AppendText("$(get-date):Введите логин пользователя!`r`n")
            return
        }
    
        $logBox.AppendText("$(get-date): Сброс пароля для пользователя $username...`r`n")
        try {
            # Вызов твоей функции
            $logBox.AppendText("$(get-date):* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *`r`n")
            $result = Reset-ADUserPassword -samaccountname $username
            $logBox.AppendText($result + "`r`n")
        }
        catch {
            $logBox.AppendText("$(get-date):* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *`r`n")
            $logBox.AppendText("$(get-date): Ошибка: $_`r`n")
        }
    })
    
    # Действие кнопки проверки пользователя
    $checkButton.Add_Click({
        $progressBar.Visible = $true
        $form.Refresh()
        $username = $textBox.Text
        if ([string]::IsNullOrWhiteSpace($username)) {
            $logBox.AppendText("$(get-date):* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *`r`n")
            $logBox.AppendText("$(get-date): Введите логин пользователя!`r`n")
            $progressBar.Visible = $false
            return
        }
        $logBox.AppendText("$(get-date): Проверка пользователя $username...`r`n")
        try {
            $result = Check-User -samaccountname $username
            $logBox.AppendText("$(get-date):* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *`r`n")
            $logBox.AppendText($result + "`r`n")
        }
        catch {
            $logBox.AppendText("$(get-date):* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *`r`n")
            $logBox.AppendText("Ошибка: $_`r`n")
        }
        $progressBar.Visible = $false
    })
    
    # Показ формы
    $form.Add_Shown({$form.Activate()})
    [void]$form.ShowDialog()
    
    

  • Недавно у меня была работа по массовой миграции виртуальных машин между кластерами. Я столкнулся с проблемой при миграции в виде ошибок и варнингов на тему того что в дисководе виртуальной машины имеется некоторые Datastore ISO файлы. Это конечно же недоработка которая была не учтена когда инфра разворачивалась с помощью Terraform. Однако проблему нужно было решать, и решил я ее радикально. Прочесать все машины на предмет подключенных ISO и «извлечь». Под катом тело скрипта.

    Тыкни
    Connect-VIServer -Server vcenter.site -Credential (Get-Credential administrator@vsphere.local)
    
    $servers = get-vm * | select Folder, Name, VMHost
    #Машины обрабатываются по папке в VCenter но это можно удалить и пройтись по всем машинам в VCenter если закомментить оператора IF
    $foldername = "LinuxInfra"
    
    Foreach($serv in $servers){
    if($serv.folder.name -eq $foldername){
    $vm = Get-VM "$($serv.Name)"
    $cd = Get-CDDrive -VM $vm
    $spec = New-Object VMware.Vim.VirtualMachineConfigSpec
    $cdSpec = New-Object VMware.Vim.VirtualDeviceConfigSpec
    $cdSpec.Operation = [VMware.Vim.VirtualDeviceConfigSpecOperation]::Edit
    $cdSpec.Device = $cd.ExtensionData
    $cdSpec.Device.Backing = New-Object VMware.Vim.VirtualCdromRemoteAtapiBackingInfo
    $cdSpec.Device.Backing.DeviceName = $null
    $cdSpec.Device.Connectable.Connected = $false
    $cdSpec.Device.Connectable.StartConnected = $false
    $cdSpec.Device.Connectable.AllowGuestControl = $true
    $spec.DeviceChange += $cdSpec
    $task = $vm.ExtensionData.ReconfigVM_Task($spec)
    Write-Host "Ожидание завершения операции..."
    while ($task.Info.State -eq "running" -or $task.Info.State -eq "queued") {
        Start-Sleep -Seconds 1
        $task = Get-View -Id $task.MoRef
    }
    Write-Host "ISO диск отключен от $($serv.Name)"
    }
    } 
    
  • Мне довелось создать реплику виртуальных машин средствами Veeam B&R. Забавно то что используя GUI это делается за пару кликов, однако задача состояла в том что бы создать реплики на более чем 15 машин при этом каждая машина должна быть отдельной джобой. Что бы не терять большое количество времени, я решил написать небольшой скрипт на Powershell который сделает это за меня. Я постарался максимально унифицировать этот скриптик, но думаю можно будет сделать это еще эффективнее. Тело скрипта под катом.

    Тыкни
    function New-ReplicaJob {
        param (
            [string]$JobName, 
            [string]$TargetServerIP, 
            [string]$VMName, 
            [string]$TargetDatastore,
            [string]$SourceIP, 
            [string]$SourceMask, 
            [string]$ReIpTargetIP, 
            [string]$ReIpTargetMask,
            [string]$ReIpGateway, 
            [string]$ReIpDNS, 
            [string]$SourceNetwork, 
            [string]$TargetNetwork,
            [string]$SourceProxy, 
            [string]$TargetProxy, 
            [string]$Repository = "DefaultRepo"
        )
    
        $Description = "$JobName replication job"
        $RestorePointsToKeep = 7
    
        $TargetServer = Get-VBRServer -Name    $TargetServerIP
        $VMObject = Find-VBRViEntity -Name $VMName
        $Datastore = Find-VBRViDatastore -Name $TargetDatastore -Server $TargetServerIP
        $ReIpRule = New-VBRViReplicaReIpRule -SourceIp $SourceIP -SourceMask $SourceMask -TargetIp $ReIpTargetIP -TargetMask $ReIpTargetMask -TargetGateway $ReIpGateway -DNS $ReIpDNS
    
        $SrcNetwork = Get-VBRViServerNetworkInfo -Server $VMObject.VmHostName | Where-Object { $_.Name -eq $SourceNetwork }
        $TgtNetwork = Get-VBRViServerNetworkInfo -Server $TargetServer.Name | Where-Object { $_.Name -eq $TargetNetwork }
    
        $SrcProxy = Get-VBRViProxy -Name $SourceProxy
        $TgtProxy = Get-VBRViProxy -Name $TargetProxy
    
        Add-VBRViReplicaJob -Name $JobName `
                            -Server $TargetServer `
                            -Entity $VMObject `
                            -Datastore $Datastore `
                            -Suffix "_replica" `
                            -BackupRepository $Repository `
                            -Description $Description `
                            -EnableNetworkMapping `
                            -SourceNetwork $SrcNetwork `
                            -TargetNetwork $TgtNetwork `
                            -SourceProxy $SrcProxy `
                            -TargetProxy $TgtProxy `
                            -RestorePointsToKeep $RestorePointsToKeep `
                            -ReIpRule $ReIpRule `
                            -HighPriority `
                            -Force
    }
    
    Connect-VIServer "$(Read-Host 'Enter vCenter hostname or IP')" -Credential (Get-Credential)
    Connect-VBRServer -Server "$(Read-Host 'Enter Veeam hostname or IP')" -Credential (Get-Credential)
    
    $VMList = @(
    "$(Read-Host 'Enter VM names to replicate, separated by comma (no spaces)')".Split(",") | Where-Object { $_ -ne '' }
    )
    
    foreach ($VM in $VMList) {
        $GuestIP = (Get-VM $VM).Guest.IPAddress[0]
        $GuestNet = (Get-VM $VM).Guest.Nics.NetworkName[0]
        $LastOctet = $GuestIP.Split(".")[-1]
        $NewTargetIP = "192.168.100.$LastOctet"
        $NewGateway = "192.168.100.1"
    
        New-ReplicaJob -JobName $VM `
                       -TargetServerIP "192.168.0.10" `
                       -VMName $VM `
                       -TargetDatastore "TargetDatastore" `
                       -SourceIP $GuestIP `
                       -SourceMask "255.255.255.0" `
                       -ReIpTargetIP $NewTargetIP `
                       -ReIpTargetMask "255.255.255.0" `
                       -ReIpGateway $NewGateway `
                       -ReIpDNS "192.168.100.10" `
                       -SourceNetwork $GuestNet `
                       -TargetNetwork "TargetNetworkName" `
                       -SourceProxy "SourceProxyName" `
                       -TargetProxy "TargetProxyName" `
                       -Repository "DefaultRepo"
    }
    
  • Недавно я столкнулся с острой необходимостью прямой работы с IE на Windows 11. Однако, я столкнулся с тем что просто режим совместимости в EDGE совершенно недостаточно. Немного поразмышляв, я пришел к выводу что ничто так не симулирует стабильную работу IE как сам IE. На просторах Интернета я нашел довольно забавный и рабочий метод открытия IE несмотря на все заявления мелкомягких о том что IE выпилен. Ниже пример скрипта на Powershell. В моем решении я скомпилировал нижеследующий скрипт в EXE файл с помощью Invoke-PS2EXE и использую в своих целях.

    Тыкни
    $vbsFilePath = "$env:APPDATA\ie.vbs"
    $vbsContent = @'
    Set objIE = CreateObject("InternetExplorer.Application")
    objIE.Navigate "https://любой.произвольный.сайт"
    objIE.Visible = 1
    '@
    Set-Content -Path $vbsFilePath -Value $vbsContent
    Start-Process "wscript.exe" -ArgumentList $vbsFilePath
    Start-Sleep -Seconds 10
    Remove-Item $vbsFilePath
    
  • Установка NUPKG файлов в оффлайн режиме при помощи powershell и GUI на Windows Form.
    1 — Окно для выбора файла установки
    2 — Кнопка установить и кнопка закрыть
    3 — Окно лога для возврата статуса установки или ошибки.
    4 — Окно для выбора CodeSign сертификата если в этом есть необходимость

    Тыкни
    <code>Add-Type -AssemblyName System.Windows.Forms  
    Add-Type -AssemblyName System.Drawing 
    
    Function Install-NUPKG {
    param(
    [parameter(Mandatory=$true)]$nupkgFile,
    $Certificate
    )
    try{
        $RenameFrom = $nupkgfile
        $RenameTo = $($nupkgfile.Replace('.nupkg','.zip'))
        Copy-Item $RenameFrom -Destination $RenameTo -Force
        $DestToExpnd = $RenameTo.replace('.zip','')
        Expand-Archive $RenameTo -DestinationPath $DestToExpnd -Force
        Start-Sleep -Seconds 5
        $name = (Get-ChildItem -Path $DestToExpnd `
        |? {($_.name -Like "*.psm1") -or ($_.name -Like "*.psd1")} `
        | select -First 1).Name.split('.')[0]
        $parentpath = ((Get-Item $DestToExpnd).PSParentPath).replace('Microsoft.PowerShell.Core\FileSystem::','')
        $newfoldname = $($parentpath + "\" + $name)
        Rename-Item $DestToExpnd -NewName $newfoldname
        $psmodulefolders = $ENV:PSModulePath.Split(";")[2]
        Copy-Item $newfoldname -Destination $psmodulefolders -Recurse -Force 
        Start-Sleep -Seconds 2
        Remove-Item $newfoldname -Force -Recurse 
        Remove-Item $RenameTo -Force -Recurse
        
        if($Certificate){
        try{
            Get-ChildItem -Path "$psmodulefolders\$name" -Recurse -Attributes !Directory `
            | % {Set-AuthenticodeSignature -Certificate $Certificate -FilePath $_.FullName -ErrorAction SilentlyContinue `
            | ? Status -eq Valid}
            Return Write-Output "$name is installed and signed with cert $($CheckBox.Text.Split('\') `
            | select -Last 1)"
            }
        catch [System.Management.Automation.ParameterBindingException]{
            $opencert = Get-PfxCertificate $Certificate
            Get-ChildItem -Path "$psmodulefolders\$name" -Recurse -Attributes !Directory `
            | % {Set-AuthenticodeSignature -Certificate $Certificate -FilePath $_.FullName -ErrorAction SilentlyContinue `
            | ? Status -eq Valid}
            Return Write-Output "$name is installed and signed with cert $($CheckBox.Text.Split('\') `
            | select -Last 1)"
            }
    
        }
        
        else{
            Return Write-Output "$name is installed"
        }
    }
    Catch{
        Return Write-Output $Error[0].Exception
    }
    }
    
    Function Get-FileName{
     $initialDirectory =  'C:'
     [System.Reflection.Assembly]::LoadWithPartialName(“System.windows.forms”) `
     | Out-Null
    
     $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
     $OpenFileDialog.initialDirectory = $initialDirectory
     $OpenFileDialog.filter = “All files (*.*)| *.*”
     $OpenFileDialog.ShowDialog() | Out-Null
     $OpenFileDialog.filename
    } 
    
    $window_form = New-Object System.Windows.Forms.Form
    $window_form.Text ='Устновка кастомных модулей NUPKG'
    $window_form.Width = 500
    $window_form.Height = 220
    $window_form.AutoSize = $true
    $window_form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
    ####
    $FormLabel1 = New-Object System.Windows.Forms.Label
    $FormLabel1.Text = "Выберите файл для установки:"
    $FormLabel1.Location = New-Object System.Drawing.Point(0,5)
    $FormLabel1.AutoSize = $true
    $window_form.Controls.Add($FormLabel1)
    $FormLabel3 = New-Object System.Windows.Forms.Label
    
    $FormLabel3.Text = "Результат: "
    $FormLabel3.Location = New-Object System.Drawing.Point(0,100)
    $FormLabel3.AutoSize = $true
    $window_form.Controls.Add($FormLabel3)
    
    $CheckBox = New-Object System.Windows.Forms.CheckBox
    $CheckBox.Text = 'Подписать модуль сертификатом'
    $CheckBox.AutoSize = $true
    $CheckBox.Checked = $False
    $CheckBox.Location  = New-Object System.Drawing.Point(5,55)
    $checkbox.Add_CheckStateChanged({
        If ($Checkbox.Checked) {
            $Certificate = Get-FileName
            $TextBox2.Appendtext("Выбран сертификат: " + $Certificate  + [Environment]::NewLine)
            $CheckBox.Text = $Certificate
        } 
        Else {
            $TextBox2.Appendtext("Выбор отменен" + [Environment]::NewLine)
            $CheckBox.Text = 'Подписать модуль сертификатом'
        }
    })
    $window_form.Controls.Add($CheckBox)
    
    ###
    $TextBox1 = New-Object System.Windows.Forms.TextBox
    $TextBox1.Size = New-Object System.Drawing.Size(390,30)
    $TextBox1.Location  = New-Object System.Drawing.Point(5,25)
    $TextBox1.Text = "Путь к файлу..."
    $window_form.Controls.Add($TextBox1)
    $window_form.Add_Shown({$TextBox1.Select()})
    
    $TextBox2 = New-Object System.Windows.Forms.RichTextBox 
    $TextBox2.ReadOnly = $true 
    $TextBox2.Multiline = $true  
    $TextBox2.Width = 390  
    $TextBox2.Height = 150
    $TextBox2.Multiline = $true
    $TextBox2.Scrollbars = "Vertical"
    $TextBox2.Refresh()
    $TextBox2.ScrollToCaret()
    $TextBox2.ToString()
    $TextBox2.text = ("------------------------------------------------------------" + [Environment]::NewLine)
    $TextBox2.Location = New-Object System.Drawing.Point(5,120)
    $window_form.Controls.Add($TextBox2)
    $window_form.Add_Shown({$TextBox2.Select()})
    ###
    
    $FormButton1 = New-Object System.Windows.Forms.Button
    $FormButton1.Location = New-Object System.Drawing.Size(400,25)
    $FormButton1.Size = New-Object System.Drawing.Size(100,25)
    $FormButton1.Text = "Выбрать"
    $FormButton1.Add_Click({$TextBox1.Text = Get-FileName})
    $window_form.Controls.Add($FormButton1)
    
    $FormButton = New-Object System.Windows.Forms.Button
    $FormButton.Location = New-Object System.Drawing.Size(400,175)
    $FormButton.Size = New-Object System.Drawing.Size(100,25)
    $FormButton.Text = "Поехоли"
    $window_form.Controls.Add($FormButton)
    $FormButton.Add_Click({
    if($CheckBox.Checked -eq $true){
        $info = Install-NUPKG -nupkgFile $TextBox1.Text -Certificate $CheckBox.Text `
        | Out-String
        $TextBox2.Appendtext("$info`r`n")
        $TextBox2.Appendtext("------------------------------------------------------------" + [Environment]::NewLine)
    }
    else{
        $info = Install-NUPKG -nupkgFile $TextBox1.Text `
        | Out-String
        $TextBox2.Appendtext("$info`r`n")
        $TextBox2.Appendtext("------------------------------------------------------------" + [Environment]::NewLine)
    }
    }
    )
    
    $FormButton2 = New-Object System.Windows.Forms.Button
    $FormButton2.Location = New-Object System.Drawing.Size(400,200)
    $FormButton2.Size = New-Object System.Drawing.Size(100,25)
    $FormButton2.Text = "Закрыть"
    $FormButton2.Add_Click({[void]$window_form.Close()})
    $window_form.Controls.Add($FormButton2)
    ###
    [void]$window_form.ShowDialog()
    </code>
    

  • Что же, добро пожаловать. Здесь собраны мои мысли, скрипты, идеи, иногда просто бред. Кстати, тут еще и мое резюме. В остальном же путник, желаю тебе всего наилучшего. Надеюсь что хоть одна моя мысль будет тебе полезна.