Работая в компании IT-аутсорса в качестве руководителя 3 линии поддержки, задумался, как автоматизировать подключение сотрудников по RDP, через VPN к серверам десятков клиентов.
Таблички с адресами, паролями и прочими настройками серверов, конечно, хорошо, но поиск клиента и вбивание адресов с аккаунтами занимает довольно существенное время.
Держать все подключения к VPN в Windows не самая лучшая идея, да и при переустановке оного, создавать VPNы тоже не доставляет удовольствие.
Плюс к тому, в большинстве случаев, требуется установить VPN подключение к клиенту без использования шлюза. дабы не гонять весь интернет-трафик через клиента.
Задача, к тому же, осложняется тем, что у некоторых клиентов pptp, у кого-то l2tp, у некоторых несколько подсетей, туннели и т.п.
В результате, для начала был написан скрипты на Powershell для каждого клиента, но позже, узнав, что в Powershell можно использовать Winforms они переродились в некое приложение, написанное с помощью того же Powershell.
До написания этого скрипта-приложения программированием не занимался вообще, разве что лет 20 назад что-то пописывал на VBS в MS Excel и MS Access, поэтому не гарантирую красивость кода и принимаю критику от опытных программистов, как можно было бы сделать красивее.
В Powershell, начиная с Windows 8 и, конечно в Windows 10, появилась прекрасная возможность создавать VPN подключения командой Add-VpnConnection и указывать какие маршруты использовать с этими соединениями командой Add-VpnConnectionRoute, для использования VPN без шлюза.
На основании этих команд и создано данное приложение. Но, обо всем по порядку.
? Хранить чувствительные данные (имена, пароли, коды доступа) в публичных облаках небезопасно, выбирайте защищенные места хранения таких данных.
В данном коде таблица в Google Disk представлена для примера, не содержит никаких реальных данный и предназначена для быстрой проверки работоспособности скрипта без раскрытия каких-либо данных автора, которые он не хотел бы разглашать.
Для начала, создаем в удобном вам и безопасном хранилище (в примере в Google Disk) таблицу с именованными столбцами:
Number; Name; VPNname; ServerAddress; RemoteNetwork; VPNLogin; VPNPass; VPNType; l2tpPsk; RDPcomp; RDPuser; RDPpass; DefaultGateway; PortWinbox; WinboxLogin; WinboxPwd; Link; Inform
Основное требование к таблице - свободное конвертирование в формат CSV, либо изначальное хранение формате CSV (знак разделения указывается в переменной comma)
VPNname – произвольное имя для VPN соединения
ServerAddress – адрес VPN сервера
RemoteNetwork – адреса подсети или подсетей клиента, разделенные «;»
VPNLogin; VPNPass – учетная запись VPN
VPNType -тип VPN (пока используется pptp или l2tp)
l2tpPsk – PSK для l2tp, в случае pptp оставляем пустым
RDPcomp – адрес сервера RPD
RDPuser; RDPpass – учетная запись RPD
DefaultGateway принимает значение TRUE или FALSE и указывает на то, использовать ли «Шлюз по умолчанию» для этого соединения. В 90% случаев = FALSE
PortWinbox; WinboxLogin; WinboxPwd – порт, логин и пароль для Winbox, поскольку у нас большинство клиентов использует Mikrotik)
Link – ссылка на расширенную информацию о компании, например, на диске Google, или в любом другом месте, будет выводиться в информационном поле для быстрого доступа к нужной информации
Inform – примечание
Пример таблицы доступен по ссылке
Number | Name | VPNname | ServerAddress | RemoteNetwork | VPNLogin | VPNPass | VPNType | l2tpPsk | RDPcomp | RDPuser | RDPpass | DefaultGateway | PortWinbox | WinboxLogin | WinboxPwd | Link | Inform |
1 | Тест1 | Test1 | a.b.c.d | 192.168.10.0/24: 10.10.0.0/24 | vpnuser | passWord | pptp | none | 192.168.10.1 | user | passWord | TRUE | 8291 | Admin | Admin | тест | |
2 | Тест2 | Test2 | e.f.j.k | 192.168.2.0/24 | vpnuser | passWord | l2tp | KdoSDtdP | 192.168.2.1 | user | passWord | FALSE | 8291 | Admin | Admin |
Скриншот работающего приложения с затертыми данными:
Далее следует листинг приложения с комментариями и пояснениями. Если интересно, но непонятно, задавайте вопросы, постараюсь прокомментировать
function Get-Clients #Функция принимает строку адреса файла в Google Drive и возвращает в виде массива данных о клиентах
{
param
(
[string]$google_url = ""
)
[string]$xlsFile = $google_url
$csvFile = "$env:temp\clients.csv"
$Comma = ','
Invoke-WebRequest $xlsFile -OutFile $csvFile
$clients = Import-Csv -Delimiter $Comma -Path "$env:temp\clients.csv"
Remove-Item -Path $csvFile
return $clients
}
function Main {
<#
Функция, срабатываемая при запуске скрипта
#>
Param ([String]$Commandline)
#Иннициализируем переменные и присваиваем начальные значения. Здесь же, указываем путь к таблице с клиентами
$Global:Clients = $null
$Global:Current
$Global:CurrentRDPcomp
$Global:google_file = "https://docs.google.com/spreadsheets/d/1O-W1YCM4x3o5W1w6XahCJZpkTWs8cREXVF69gs1dD0U/export?format=csv" # Таблица скачивается сразу в виде csv-файла
$Global:Clients = Get-Clients ($Global:google_file) # Присваиваем значения из таблицы массиву
#Скачиваем Winbox64 во временную папку
$download_url = "https://download.mikrotik.com/winbox/3.27/winbox64.exe"
$Global:local_path = "$env:temp\winbox64.exe"
If ((Test-Path $Global:local_path) -ne $true)
{
$WebClient = New-Object System.Net.WebClient
$WebClient.DownloadFile($download_url, $Global:local_path)
}
#Разрываем все текущие VPN соединения (на всякий случай)
foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" })
{
Rasdial $item.Name /disconnect
}
#Удаляем все, ранее созданные программой временные соединения, если вдруг не удалились при некорректном закрытии приложения
get-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force
#Запускаем приложение
Show-MainForm_psf
}
#Собственно, само приложение
function Show-MainForm_psf
{
[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
[void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
#Создаем форму и объекты формы
[System.Windows.Forms.Application]::EnableVisualStyles()
$formКлиентыАльбус = New-Object 'System.Windows.Forms.Form'
$statusbar1 = New-Object 'System.Windows.Forms.StatusBar'
$groupboxTools = New-Object 'System.Windows.Forms.GroupBox'
$buttonPing = New-Object 'System.Windows.Forms.Button'
$buttonВыход = New-Object 'System.Windows.Forms.Button'
$buttonWindox = New-Object 'System.Windows.Forms.Button'
$buttonПеречитатьДанные = New-Object 'System.Windows.Forms.Button'
$buttonPingAll = New-Object 'System.Windows.Forms.Button'
$groupboxRDP = New-Object 'System.Windows.Forms.GroupBox'
$comboboxRDP = New-Object 'System.Windows.Forms.ComboBox'
$textboxRDPLogin = New-Object 'System.Windows.Forms.TextBox'
$textboxRdpPwd = New-Object 'System.Windows.Forms.TextBox'
$buttonПодключитьRDP = New-Object 'System.Windows.Forms.Button'
$groupboxVPN = New-Object 'System.Windows.Forms.GroupBox'
$buttonПодключитьVPN = New-Object 'System.Windows.Forms.Button'
$buttonОтключитьVPN = New-Object 'System.Windows.Forms.Button'
$checkboxШлюзПоумолчанию = New-Object 'System.Windows.Forms.CheckBox'
$richtextboxinfo = New-Object 'System.Windows.Forms.RichTextBox'
$listbox_clients = New-Object 'System.Windows.Forms.ListBox'
$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
#----------------------------------------------
# Обработчики событий
#----------------------------------------------
$formКлиентыАльбус_Load = {
#При загрузке формы очистить поле информации и заполнить поле с клиентами (их названиями)
$richtextboxinfo.Clear()
$Global:Clients | ForEach-Object {
[void]$listbox_clients.Items.Add($_.Name)
} # В листбокс добавляем всех наших клиентов по именам и массива при загрузке формы
}
$listbox_clients_SelectedIndexChanged = {
#Прочитать из массива информацию о клиенте при выборе его в поле listbox_clients (массив, как мы помним считан из файла с диска Google)
$statusbar1.Text = 'Выбран клиент: ' + $listbox_clients.SelectedItem.ToString() # Пишем клиента в статусбар
$Global:Current = $Global:Clients.Where({ $_.Name -eq $listbox_clients.SelectedItem.ToString() })
If ($Current.PortWinbox -ne 0) # Если порт Winbox указан, то у клиента Mikrotik, включаем соответствующую кнопку
{
$buttonWindox.Enabled = $true
$buttonWindox.Text = "Winbox"
}
$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только их
switch ($Global:Current.VPNType) #В зависимости от типа VPN пишем на кнопке "Подключить pptp VPN" или "Подключить l2tp VPN", если у клиента нет VPN, то пишем "Здесь нет VPN"
{
"pptp" {
$buttonПодключитьVPN.Enabled = $true
$buttonПодключитьVPN.Text = "Подключить pptp VPN"
}
"l2tp" {
$buttonПодключитьVPN.Enabled = $true
$buttonПодключитьVPN.Text = "Подключить l2tp VPN"
}
DEFAULT
{
$buttonПодключитьVPN.Enabled = $false
$buttonПодключитьVPN.Text = "Здесь нет VPN"
}
}
switch ($Global:Current.DefaultGateway) #Смотрим в массиве, используется ли у клиента "Шлюз по-умолчанию" и заполняем соответствующий чекбокс
{
"FALSE"
{ $checkboxШлюзПоумолчанию.Checked = $false }
"Нет"
{ $checkboxШлюзПоумолчанию.Checked = $false }
"TRUE"
{ $checkboxШлюзПоумолчанию.Checked = $true }
"Да"
{ $checkboxШлюзПоумолчанию.Checked = $true }
DEFAULT
{ $checkboxШлюзПоумолчанию.Checked = $false }
}
$VPNStatus = (ipconfig | Select-String $VPNname -Quiet) #Проверяем, не установлено ли уже это VPN соединение?
If ($VPNStatus) #Если установлено, то разблокируем кнопку "Подключить RDP"
{
$buttonПодключитьRDP.Enabled = $true
}
else
{
$buttonПодключитьRDP.Enabled = $false
}
$richtextboxinfo.Clear() #Очищаем информационное поле
# И заполняем информацией о клиенте из массива
$richtextboxinfo.SelectionColor = 'Black'
$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLine + `
"Имя VPN: " + $Global:Current.VPNname + [System.Environment]::NewLine + `
"Тип VPN: " + $Global:Current.VPNType + [System.Environment]::NewLine + `
"Адрес сервера: " + $Global:Current.ServerAddress + [System.Environment]::NewLine + `
"Подсеть клиента: " + $Global:Current.RemoteNetwork + [System.Environment]::NewLine + `
"Адрес сервера RDP: " + $Global:Current.RDPcomp + [System.Environment]::NewLine + [System.Environment]::NewLine + `
"DefaultGateway: " + $Global:Current.DefaultGateway + [System.Environment]::NewLine + [System.Environment]::NewLine + `
"Примечание: " + [System.Environment]::NewLine + $Global:Current.Inform + [System.Environment]::NewLine + `
"Connection '" + $VPNname + "' status is " + $buttonПодключитьRDP.Enabled + [System.Environment]::NewLine
$richtextboxinfo.AppendText($Global:Current.Link)
$RDPServers = $Global:Current.RDPcomp.Split(';') -replace '\s', '' #Считываем и разбираем RDP серверы клиента из строки с разделителем в массив
#Добавляем из в выпадающее поле выбора сервера
$comboboxRDP.Items.Clear()
$comboboxRDP.Text = $RDPServers[0]
foreach ($RDPServer in $RDPServers)
{
$comboboxRDP.Items.Add($RDPServer)
}
#Заполняем поля имени и пароля RDP по умолчанию из таблицы о клиенте (при желании, их можно поменять в окне программы)
$textboxRdpPwd.Text = $Global:Current.RDPpass
$textboxRdpLogin.Text = $Global:Current.RDPuser
} # Форма заполнена, при смене выбранного клиента произойдет перезаполнение полей в соответствии с выбранным клиентом
$buttonWindox_Click = {
#Обработка нажатия кнопки Winbox
If ($Global:Current.PortWinbox -ne 0) #Если порт Winbox заполнен, то открываем скачанный ранее Winbox, подставляем туда имя и пароль к нему и запускаем
{
$runwinbox = "$env:temp\winbox64.exe"
$ServerPort = $Global:Current.ServerAddress + ":" + $Global:Current.PortWinbox
$ServerLogin = " """ + $Global:Current.WinboxLogin + """"
$ServerPass = " """ + $Global:Current.WinboxPwd + """"
$Arg = "$ServerPort $ServerLogin $ServerPass "
Start-Process -filePath $runwinbox -ArgumentList $Arg
}
}
$buttonПодключитьVPN_Click = {
#Обработка нажатия кнопки ПодключитьVPN
$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только их
$richtextboxinfo.Clear() #Очищаем информационное поля для вывода туда информации о процессе подключения
$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLine
foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }) #Разрываем все установленные соединения
{
$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLine
Rasdial $item.Name /disconnect
}
Remove-VpnConnection $VPNname -Force #Удаляем соединение, если ранее оно было создано
$RemoteNetworks = $Global:Current.RemoteNetwork.Split(';') -replace '\s', '' #Считываем и разбираем по строкам в массив список подсетей клиента разделенный ;
switch ($Global:Current.VPNType) #В зависимости от типа VPNа создаем pptp или l2tp соединение
{
"pptp" {
$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем pptp подключение " + $VPNname + [System.Environment]::NewLine
If ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты
{
$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPN
foreach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN
{
$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)
Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru
}
}
else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны
{
$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -Force -RememberCredential -PassThru)
}
}
"l2tp" {
$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем l2tp подключение " + $Global:Current.VPNname + [System.Environment]::NewLine
If ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты
{
$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPN
foreach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN
{
$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)
Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru
}
}
else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны
{
$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -Force -RememberCredential -PassThru)
}
}
}
$richtextboxinfo.AppendText("Устанавливаем " + $Global:Current.VPNType + " подключение к " + $VPNname + [System.Environment]::NewLine)
$Errcon = Rasdial $VPNname $Global:Current.VPNLogin $Global:Current.VPNPass #Устанавливаем созданное VPN подключение и выводим информацию в поле
$richtextboxinfo.Text = $richtextboxinfo.Text + [System.Environment]::NewLine + $Errcon + [System.Environment]::NewLine
If ((ipconfig | Select-String $VPNname -Quiet)) #Проверяем успешность соединения и, если все удачно, разблокируем кнопку RDP и кнопку "Отключить VPN"
{
$buttonПодключитьRDP.Enabled = $true
$buttonОтключитьVPN.Visible = $true
$buttonОтключитьVPN.Enabled = $true
$statusbar1.Text = $Global:Current.Name + ' подключен'
}
}
$formКлиентыАльбус_FormClosing = [System.Windows.Forms.FormClosingEventHandler]{
#При закрытии формы подчищаем за собой. Разрываем и удаляем все созданные соединения.
foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" })
{
$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLine
Rasdial $item.Name /disconnect
}
$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLine
get-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force
#Удаляем информацию о RPD-серверах из реестра
$Global:Clients | ForEach-Object {
$term = "TERMSRV/" + $_.RDPcomp
cmdkey /delete:$term
}
}
$buttonПодключитьRDP_Click = {
#Обработка кнопки ПодключитьRDP
$RDPcomp = $comboboxRDP.Text
$RDPuser = $textboxRDPLogin.Text
$RDPpass = $textboxRdpPwd.Text
cmdkey /generic:"TERMSRV/$RDPcomp" /user:"$RDPuser" /pass:"$RDPpass"
mstsc /v:$RDPcomp
}
$buttonОтключитьVPN_Click = {
#При отключении VPN подчищаем за собой и оповещаем о процессе в поле информации
foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" })
{
$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLine
Rasdial $item.Name /disconnect
}
$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLine
get-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force
$buttonОтключитьVPN.Visible = $false
$buttonПодключитьRDP.Enabled = $false
$statusbar1.Text = $Global:Current.Name + ' отключен'
}
$buttonPingAll_Click={
#Пингуем всех клиентов и оповещаем о результатах
$I=0
$richtextboxinfo.Clear()
$richtextboxinfo.SelectionColor = 'Black'
$clientscount = $Global:Clients.count
$Global:Clients | ForEach-Object {
if ((test-connection -Count 1 -computer $_.ServerAddress -quiet) -eq $True)
{
$richtextboxinfo.SelectionColor = 'Green'
$richtextboxinfo.AppendText($_.Name +' ('+ $_.ServerAddress +') доступен' + [System.Environment]::NewLine)
}
else
{
$richtextboxinfo.SelectionColor = 'Red'
$richtextboxinfo.AppendText($_.Name + ' (' + $_.ServerAddress + ') недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)
}
$richtextboxinfo.ScrollToCaret()
$I = $I + 1
Write-Progress -Activity "Ping in Progress" -Status "$i clients of $clientscount pinged" -PercentComplete ($i/$clientscount*100)
}
$richtextboxinfo.SelectionColor = 'Black'
Write-Progress -Activity "Ping in Progress" -Status "Ready" -Completed
}
$buttonПеречитатьДанные_Click={
#Перечитываем данные из таблицы Google
$Global:Clients = Get-Clients ($Global:google_file)
$listbox_clients.Items.Clear()
$Global:Clients | ForEach-Object {
[void]$listbox_clients.Items.Add($_.Name)
}
}
$buttonВыход_Click = {
#Выход
$formКлиентыАльбус.Close()
}
$richtextboxinfo_LinkClicked=[System.Windows.Forms.LinkClickedEventHandler]{
#Обработка нажатия на ссылку в окне информации
Start-Process $_.LinkText.ToString()
}
$buttonPing_Click={
#Пингуем ip текущего клиента и выводим результат в поле информации
if ((test-connection -Count 1 -computer $Global:Current.ServerAddress -quiet) -eq $True)
{
$richtextboxinfo.AppendText([System.Environment]::NewLine)
$richtextboxinfo.SelectionColor = 'Green'
$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ') доступен' + [System.Environment]::NewLine)
}
else
{
$richtextboxinfo.AppendText([System.Environment]::NewLine)
$richtextboxinfo.SelectionColor = 'Red'
$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ') недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)
}
}
#----------------------------------------------
#Описание объектов формы
#----------------------------------------------
#
# formКлиентыАльбус
#
$formКлиентыАльбус.Controls.Add($statusbar1)
$formКлиентыАльбус.Controls.Add($groupboxTools)
$formКлиентыАльбус.Controls.Add($groupboxRDP)
$formКлиентыАльбус.Controls.Add($groupboxVPN)
$formКлиентыАльбус.Controls.Add($richtextboxinfo)
$formКлиентыАльбус.Controls.Add($listbox_clients)
$formКлиентыАльбус.AutoScaleDimensions = '6, 13'
$formКлиентыАльбус.AutoScaleMode = 'Font'
$formКлиентыАльбус.AutoSize = $True
$formКлиентыАльбус.ClientSize = '763, 446'
$formКлиентыАльбус.FormBorderStyle = 'FixedSingle'
$formКлиентыАльбус.MaximizeBox = $False
$formКлиентыАльбус.Name = 'formКлиентыАльбус'
$formКлиентыАльбус.SizeGripStyle = 'Hide'
$formКлиентыАльбус.StartPosition = 'CenterScreen'
$formКлиентыАльбус.Text = 'Клиенты Альбус'
$formКлиентыАльбус.add_FormClosing($formКлиентыАльбус_FormClosing)
$formКлиентыАльбус.add_Load($formКлиентыАльбус_Load)
#
# statusbar1
#
$statusbar1.Location = '0, 424'
$statusbar1.Name = 'statusbar1'
$statusbar1.Size = '763, 22'
$statusbar1.TabIndex = 17
#
# groupboxTools
#
$groupboxTools.Controls.Add($buttonPing)
$groupboxTools.Controls.Add($buttonВыход)
$groupboxTools.Controls.Add($buttonWindox)
$groupboxTools.Controls.Add($buttonПеречитатьДанные)
$groupboxTools.Controls.Add($buttonPingAll)
$groupboxTools.Location = '308, 258'
$groupboxTools.Name = 'groupboxTools'
$groupboxTools.Size = '147, 163'
$groupboxTools.TabIndex = 10
$groupboxTools.TabStop = $False
$groupboxTools.Text = 'Tools'
$groupboxTools.UseCompatibleTextRendering = $True
#
# buttonPing
#
$buttonPing.Location = '7, 44'
$buttonPing.Name = 'buttonPing'
$buttonPing.Size = '133, 23'
$buttonPing.TabIndex = 12
$buttonPing.Text = 'Ping'
$buttonPing.UseCompatibleTextRendering = $True
$buttonPing.UseVisualStyleBackColor = $True
$buttonPing.add_Click($buttonPing_Click)
#
# buttonВыход
#
$buttonВыход.Location = '7, 125'
$buttonВыход.Name = 'buttonВыход'
$buttonВыход.Size = '133, 23'
$buttonВыход.TabIndex = 15
$buttonВыход.Text = 'Выход'
$buttonВыход.UseCompatibleTextRendering = $True
$buttonВыход.UseVisualStyleBackColor = $True
$buttonВыход.add_Click($buttonВыход_Click)
#
# buttonWindox
#
$buttonWindox.Enabled = $False
$buttonWindox.Location = '7, 17'
$buttonWindox.Name = 'buttonWindox'
$buttonWindox.Size = '133, 23'
$buttonWindox.TabIndex = 11
$buttonWindox.Text = 'Windox'
$buttonWindox.UseCompatibleTextRendering = $True
$buttonWindox.UseVisualStyleBackColor = $True
$buttonWindox.add_Click($buttonWindox_Click)
#
# buttonПеречитатьДанные
#
$buttonПеречитатьДанные.Location = '7, 98'
$buttonПеречитатьДанные.Name = 'buttonПеречитатьДанные'
$buttonПеречитатьДанные.Size = '133, 23'
$buttonПеречитатьДанные.TabIndex = 14
$buttonПеречитатьДанные.Text = 'Перечитать данные'
$buttonПеречитатьДанные.UseCompatibleTextRendering = $True
$buttonПеречитатьДанные.UseVisualStyleBackColor = $True
$buttonПеречитатьДанные.add_Click($buttonПеречитатьДанные_Click)
#
# buttonPingAll
#
$buttonPingAll.Location = '7, 71'
$buttonPingAll.Name = 'buttonPingAll'
$buttonPingAll.Size = '133, 23'
$buttonPingAll.TabIndex = 13
$buttonPingAll.Text = 'Ping All'
$buttonPingAll.UseCompatibleTextRendering = $True
$buttonPingAll.UseVisualStyleBackColor = $True
$buttonPingAll.add_Click($buttonPingAll_Click)
#
# groupboxRDP
#
$groupboxRDP.Controls.Add($comboboxRDP)
$groupboxRDP.Controls.Add($textboxRDPLogin)
$groupboxRDP.Controls.Add($textboxRdpPwd)
$groupboxRDP.Controls.Add($buttonПодключитьRDP)
$groupboxRDP.Location = '308, 128'
$groupboxRDP.Name = 'groupboxRDP'
$groupboxRDP.Size = '147, 126'
$groupboxRDP.TabIndex = 5
$groupboxRDP.TabStop = $False
$groupboxRDP.Text = 'RDP'
$groupboxRDP.UseCompatibleTextRendering = $True
#
# comboboxRDP
#
$comboboxRDP.FormattingEnabled = $True
$comboboxRDP.Location = '7, 17'
$comboboxRDP.Name = 'comboboxRDP'
$comboboxRDP.Size = '133, 21'
$comboboxRDP.TabIndex = 6
$comboboxRDP.Text = 'IP RDP сервера'
#
# textboxRDPLogin
#
$textboxRDPLogin.Location = '7, 44'
$textboxRDPLogin.Name = 'textboxRDPLogin'
$textboxRDPLogin.Size = '133, 20'
$textboxRDPLogin.TabIndex = 7
$textboxRDPLogin.Text = 'RDP-login'
#
# textboxRdpPwd
#
$textboxRdpPwd.Location = '7, 69'
$textboxRdpPwd.Name = 'textboxRdpPwd'
$textboxRdpPwd.PasswordChar = '*'
$textboxRdpPwd.Size = '133, 20'
$textboxRdpPwd.TabIndex = 8
$textboxRdpPwd.Text = 'RDP-Password'
#
# buttonПодключитьRDP
#
$buttonПодключитьRDP.Enabled = $False
$buttonПодключитьRDP.Location = '7, 94'
$buttonПодключитьRDP.Name = 'buttonПодключитьRDP'
$buttonПодключитьRDP.Size = '133, 20'
$buttonПодключитьRDP.TabIndex = 9
$buttonПодключитьRDP.Text = 'Подключить RDP'
$buttonПодключитьRDP.UseCompatibleTextRendering = $True
$buttonПодключитьRDP.UseVisualStyleBackColor = $True
$buttonПодключитьRDP.add_Click($buttonПодключитьRDP_Click)
#
# groupboxVPN
#
$groupboxVPN.Controls.Add($buttonПодключитьVPN)
$groupboxVPN.Controls.Add($buttonОтключитьVPN)
$groupboxVPN.Controls.Add($checkboxШлюзПоумолчанию)
$groupboxVPN.Location = '308, 27'
$groupboxVPN.Name = 'groupboxVPN'
$groupboxVPN.Size = '147, 98'
$groupboxVPN.TabIndex = 1
$groupboxVPN.TabStop = $False
$groupboxVPN.Text = 'VPN'
$groupboxVPN.UseCompatibleTextRendering = $True
#
# buttonПодключитьVPN
#
$buttonПодключитьVPN.Enabled = $False
$buttonПодключитьVPN.Location = '7, 45'
$buttonПодключитьVPN.Name = 'buttonПодключитьVPN'
$buttonПодключитьVPN.Size = '133, 20'
$buttonПодключитьVPN.TabIndex = 3
$buttonПодключитьVPN.Text = 'Подключить VPN'
$buttonПодключитьVPN.UseCompatibleTextRendering = $True
$buttonПодключитьVPN.UseVisualStyleBackColor = $True
$buttonПодключитьVPN.add_Click($buttonПодключитьVPN_Click)
#
# buttonОтключитьVPN
#
$buttonОтключитьVPN.Enabled = $False
$buttonОтключитьVPN.Location = '7, 67'
$buttonОтключитьVPN.Name = 'buttonОтключитьVPN'
$buttonОтключитьVPN.Size = '133, 20'
$buttonОтключитьVPN.TabIndex = 4
$buttonОтключитьVPN.Text = 'Отключить VPN'
$buttonОтключитьVPN.UseCompatibleTextRendering = $True
$buttonОтключитьVPN.UseVisualStyleBackColor = $True
$buttonОтключитьVPN.Visible = $False
$buttonОтключитьVPN.add_Click($buttonОтключитьVPN_Click)
#
# checkboxШлюзПоумолчанию
#
$checkboxШлюзПоумолчанию.Location = '7, 19'
$checkboxШлюзПоумолчанию.Name = 'checkboxШлюзПоумолчанию'
$checkboxШлюзПоумолчанию.Size = '133, 24'
$checkboxШлюзПоумолчанию.TabIndex = 2
$checkboxШлюзПоумолчанию.Text = 'Шлюз по-умолчанию'
$checkboxШлюзПоумолчанию.TextAlign = 'MiddleRight'
$checkboxШлюзПоумолчанию.UseCompatibleTextRendering = $True
$checkboxШлюзПоумолчанию.UseVisualStyleBackColor = $True
#
# richtextboxinfo
#
$richtextboxinfo.Cursor = 'Default'
$richtextboxinfo.ForeColor = 'WindowText'
$richtextboxinfo.HideSelection = $False
$richtextboxinfo.Location = '461, 27'
$richtextboxinfo.Name = 'richtextboxinfo'
$richtextboxinfo.ReadOnly = $True
$richtextboxinfo.ScrollBars = 'ForcedVertical'
$richtextboxinfo.ShowSelectionMargin = $True
$richtextboxinfo.Size = '290, 394'
$richtextboxinfo.TabIndex = 16
$richtextboxinfo.Text = ''
$richtextboxinfo.add_LinkClicked($richtextboxinfo_LinkClicked)
#
# listbox_clients
#
$listbox_clients.FormattingEnabled = $True
$listbox_clients.Location = '12, 27'
$listbox_clients.Name = 'listbox_clients'
$listbox_clients.Size = '290, 394'
$listbox_clients.TabIndex = 0
$listbox_clients.add_SelectedIndexChanged($listbox_clients_SelectedIndexChanged)
#Сохраняем состояние формы
$InitialFormWindowState = $formКлиентыАльбус.WindowState
#Восстанавливаем состояние при загрузке
$formКлиентыАльбус.add_Load($Form_StateCorrection_Load)
#Очищаем элементы формы при закрытии
$formКлиентыАльбус.add_FormClosed($Form_Cleanup_FormClosed)
#Сохраняем значения элементов при закрытии
$formКлиентыАльбус.add_Closing($Form_StoreValues_Closing)
#Показать форму
return $formКлиентыАльбус.ShowDialog()
}
#Запуск приложения!
Main ($CommandLine)
Скрипт можно запускать как скрипт ps1 или скомпилировать в exe через ps2exe и использовать как полноценное приложение
UPD: скрипт может работать непосредственно в Powershell, достаточно запустить PS от имени Администратора, скопировать и вставить скрипт в окно PS
amarao
Powershell — настоящий язык программирования, и он обладает потрясающим свойством — стоит только хоть чуть-чуть задуматься о происхождении магических заклинаний с SO, как тебя в полный рост макают в .net. Вместо того, чтобы думать про админские задачи, обнаруживаешь себя в мире
волшебныхуродливых классов, интерфейсов и монстросити, которая на тебя смотрит со дна .net.При том, что есть много админских инструментов, которые уводят в недра языка программирования (тот же питон), только .net с C# обладает настолько ярко выраженной ненавистью к красивому и простому.
mayorovp
Э-э-э, а что такого уродливого в классах .NET?
amarao
То же, что и в java.
(рандомный пример из интернетов).
Я считаю, что оно антигуманно. Сравните с эквивалентом на питоне:
Все вот эти
class
иstatic void
могли быть придуманы только тем, кому насрать на людей, пишущих скрипты.Sing
Так у вас не эквивалент, вот эквивалент на C#
Ну так они и не обязательны. Можно писать без всего этого.amarao
Конечно, можно без этого. Тем паче, что это C#. А сам powershell ещё более ужасающий.
Из рандомного SO ответа про работу с pipe'ом.
Я по своему опыту знаю, то если какой-то инструмент имеет вырвиглазный синтаксис с самого начала, то и вся остальная работа с ним такая же — через ненависть и боль.
mvv-rus
Ну что вам на это сказать? Я не знаю, почему ответ был написан так сложно. Может, автор решал какую-то специфичную задачу, требующую указать нестандартные параметры, передаваемые в CreateProcess. Или — чтобы скрипт можно было запустить внутри другого конвейера(pipe) — вложенные конвейеры в Powershell не поддерживаются. А может, он просто не знал как следует Powershell, потому что Powershell не был его основным рабочим языком.
Потому что в Powershell для того, чтобы просто запустить внешнюю команду (исполняемый файл) и перенаправить его стандартный вывод в конвейер (pipe) не нужно делать вообще ничего: достаточно просто набрать имя файла и нажать Enter.
Конвейер в Powershell отличается тем, что по нему передаются не байты, а объекты. Но вывод внешней команды автоматически преобразуется в последовательность объектов Powershell типа String (реализован как тип System.String в .NET). Можно эту последовательность присвоить переменной Powershell: она будет преобразована в массив String, можно — передать на вход другой команды Powershell — например вот эта связка команд отсортирует вывод внешней команды netstat (это — чисто для примера, практического смысла это не имеет):
Можно для дальнейшей обработки сделать разбор строк и преобразовать их в объекты. Например, эта связка команд преобразует все строки выдачи, показывающие соединение, в объект со свойствами LocalIP, LocalPort, RemoteIP, RemotePort, State (и отбросит все строки, не являющиеся соединениями):
(пояснение:? — это псевдоним команды Where-Object, % — Foreach-Object, мне так писать сподручнее, но незнающего это может сбить с толку).
Да, регулярка выглядит весьма не наглядно (и мне пришлось потратить минут 15 на ее написание и отладку), но после разбора вывода с ее помощью мы получаем объекты, с которыми дальше работать может быть сильно проще, чем с сырыми строками.
Результат этой последовательности можно присвоить переменной (получится массив объектов) или передать дальше по конвейеру для дальнейшей обработки, в том числе — способом, не слишком просто достижимым стандартными командами обработки строк (например, отбросить все объекты локальных соединений, у которых LocalIP=RemoteIP).
imintsev Автор
Спасибо, приму на вооружение
rrust
вот что меня восхищает в PowerShell, так это обмен не потоком как в архаичных системах, а объектами со всеми их свойствами. Можно и свои свойства и свои объекты динамически заводить.
Может реализация не самая удачная, да и выглядит как обертка над .net, но тем не менее это гигантский шаг вперед со времен дос и юникс
kovserg
Даже если это и выглядит как плюс это скорее минус. В обычных архаичных системах вам ничто не мешает кидаться json-ом (например). При этом вы на любом этапе можете вклиниться в поток данных сохранить его в файл, или прочитать из файла, или преобразовать, при этом работая с одним потоком данных. Даже можете его посети послать или через UART. А тут куча дополнительных сущностный, причем весьма не тривиальных, которые еще могут и эволюционировать. Но главное это убогий синтаксис, тот же perl, gradle и даже pyton на порядки приятнее. Да лучше php написать чем на ps и работать будет на большем количестве платформ.
mvv-rus
Так ведь вам ничто не мешает в современном PS полученный откуда-то JSON прогнать через ConvertFrom-Json и получить на выходе объект соответствующей структуры. Или, наоборот — сконвертировать объект в JSON с помощью ConvertTo-Json (или в один из множества других форматов с помощью команд с глаголом ConvertTo) либо просто перегнать объекты в поток символов банальным вызовом метода ToString().
А насчет ситаксиса… Я вот, к примеру, очень мало знаю perl, и скрипты на нем обычно выглядят для меня «перловой кашей», но я понимаю, что это не дает мне никаких оснований объявлять perl плохим, негодным языком — просто этим инструментом я не владею. Почему в случае с PS не происходит то же самое?
Недостаток «архаичных систем» по сравнению с PS, что в них заметно сложнее манипулировать проходящим по конвейеру потоком символов в случае, если этот поток имеет нетривиальную структуру: ему надо устраивать грамматический разбор перед применением, чтобы структурировать. А по конвейеру PS уже идет структурированный поток объектов, имеющих свойства, к которым можно легко и просто обратиться по их имени, чтобы использовать для фильтрации или обработки.
mvv-rus
А эквивалент на Poweshell — он еще проще:
Первая команда читает файл целиком в одну строку и кладет ее в конвейер (можно это проверить, передав ее результат в Measure-Object: свойство Count результата будет равно 1), вторая — построчно (в конвейер передаются по одной строки, содержащиеся в файле, проверить можно опять-таки с помощью Measure-Count).
Ну а выдача содержимого конвейера на экран после выполнения команды — это действие Powershell по умолчанию.
amarao
Ну давайте меряться.
cat "filename"
mvv-rus
Ну, давайте :-):
cat "filename"
У Get-Content есть и такой псевдоним. И псевдоним type (родом из MS DOS) — тоже еcть.
PS С Get-ChildItem/ls/dir ситуация аналогичная.
amarao
Ага. Только пользы от него никакой, потому что ни /proc, ни /sys, ни /dev.
mvv-rus
Ну, если посмотреть, что показывает Get-PSDrive, то в PS много разных «дисков» в пространстве имен есть, помимо файловой системы: HKLM+HKCU (это реестр), Env, WSMan. А еcли дополнительные компоненты ОС поставить да модули подключить, то может и кое-что дополнительное появиться. Например — AD: (т. е. — каталог Active Directory, DS или LDS). Или «диск» с конфигурацией службы Information Rights Management (это — одно из основных средств задания этой самой конфигурации).
А вообще-то, для полного доступа к конфигурации локальной системы в Windows gwmi (т.е. Get-WmiObject) есть. И Set-WMIInstance.
PS Да, методы администрирования Windows и Linux — они таки отличаются.
PPS На этом соревнование предлагаю считать законченным и, если хотите — признать вас победителем: я на приз в любой дисциплине соревнований типа Windows vs Linux традиционно не претендую. Но, надеюсь, я вас убедил, что Powerhell — он не такой страшный как вам казалось раньше?
amarao
Он не страшный. Он уродливый. Я этот поинт с самого начала вёл. Есть языки, которые пишутся с представлением о красоте, а есть языки, в которых "как-то получилось и сойдёт".
mvv-rus
«Красота — в глазах смотрящего»((с), AFAIK — перевод с английского фразы О.Уайлда(но это неточно)).
То есть понятие красоты — оно сугубо субъективно.
На этом обсуждение Powershell с вами предлагаю закончить.
Надеюсь, вам никогда не придется администрировать MS Exchange: там Powershell без альтернатив.
amarao
Не придётся. Я покинул экосистему windows из-за этой "красоты", в том числе.
heejew
Ну я бы отметил, что java и c# и не были изначально скриптовыми языками, чем является python изначально. Да, по моим ощущениям они конечно пытаются и в скриптовые языки войти в последнее время многими упрощениями, но это все равно не то.
Это как сравнивать теплое с мягким, натягивать сову на глобус, ну и другие аналогии придумайте. Очень странно предъявы кидать языкам, которые и не были именно для этого предназначены
amarao
Я кидаю предъявы не языку, а человеку, который пытается эту монстросити притащить в администрирование и считает, что так и надо.
ровно так же мы можем обсуждать код для автоматического провиза виртуалок в CI, написанный на boost'е в C++. Неподобающий инструмент, провоцирующий невыносимые условия работы.
imintsev Автор
А чем Powershell монструозен? Да, считаю, что все, что упрощает мне жизнь в администрировании, имеет право на жизнь. Никого пользоваться не заставляю
amarao
Я ж показал чем. Ужасный синтаксис, постоянные отсылки объектную модель .net.
imintsev Автор
Принял. Но какая альтернатива скриптового администрирования Windows?
Как, например, скриптом создать подключение к VPN не через Powershell?
А от .NET тут только Win.forms, который лишь объединяет несколько скриптов в один с выбором действий через «окошко».
amarao
Я последний раз видел винды всерьёз в 2008. В принципе, тот кусок, который мне надо было автоматизировать, мы сделали на winrm, но он оставил примерно такое же ощущение брезгливости и уныния.
Я не знаю ответа "как хорошо и красиво администрировать Windows", и это было одним из ключевых моих мотивов для перехода на Linux. Тогда Ансибла ещё не было. Наверное, сейчас я бы попытался использовать Ансибл.
mvv-rus
Если использовать Powershell исключительно как средство доступа к сборкам .NET, то так оно и будет.
Иногда это неизбежно: например, для одновременного доступа к содержимому п/я и к средствам администрирования Exchange нет другого выхода, кроме как использовать Powershell (доступ к администрированию Exchаnge, альтернатив тут нет) и EWS Manged API из .NET (в средствах администрирования Exhange крайне ограничены возможности доступа к содержимому п/я).
Но чаще всего альтернатива есть.