Администраторам Linux: это статья о “Puppet” для Windows, и уже есть бета-версия DSC для Linux.
Для тех, кто в теме: не будет ничего о новинках PowerShell 5.0, только о том, что доступно из “коробки” Windows Server 2012 R2.
Преамбула
В 2013 году с выходом Windows Server 2012 R2 компания Microsoft сообщила о появлении Powershell Desired State Configuration (DSC).
К этому моменту я более или менее представлял, что делают подобные системы для Linux (например, уже упомянутый Puppet). Поэтому, предложенные возможности мне показались недостаточными для полной автоматической настройки системы. И только недавние сообщения о готовящемся Powershell 5.0 и о новых возможностях DSC побудили меня снова обратить внимание на эту технологию.
Чтобы разобраться, я выдумал задачку попроще:
- Пусть, есть некий клиент, который хочет самостоятельно установить разработанное нами ASP.NET приложение на своем сервере. Кроме IIS, нам нужен MS SQL Server, а также требуется сделать некоторые настройки операционной системы и установить какие-нибудь важные утилиты.
Можно ли вместо инструкции по установке и настройке выдать некий конфигурационный скрипт, который сделает все что требуется на только что установленном Windows Server 2012 R2?
Для лучшего понимания этой статьи, наверное, предварительно стоит прочесть описание в блоге Microsoft — http://habrahabr.ru/company/microsoft/blog/253497/.
Исходное положение
Первоначально предполагалось, что где-то у хостинг-провайдера был заказан сервер. На нем установлен Windows Server 2012 R2 и нам только что пришло оповещение с паролем администратора и ip-адресом сервера.
А мы набираем одну единственную команду, например:
makemagic -server new.example.com
и спустя какое-то время получаем готовую к употреблению систему.
К сожалению, пока это невозможно. Но у меня есть хорошая новость — это будет уже в следующей версии Windows Server 2016. Пока я пишу эту статью, описанная ниже конфигурация (естественно, без установки обновлений) накатывается на только установленный Technical Preview 3.
Сервер
Если у вас есть образ Windows Server 2012 R2 с интегрированными обновлениями, который вы можете использовать — смело пропускайте этот раздел.
В нынешней версии (2012 R2) — проблема в цепочке обновлений:
- Одна из первых задач, которую я попробовал сделать — проверить/настроить часовой пояс. Для этого нужно установить последнее обновление часовых поясов.
- Это обновление не устанавливается, так как требует большое обновление KB2919355.
- Которое, в свою очередь хочет чтобы было обновление KB2975061 — это в моем случае.
Ни одно из этих обновлений недоступно для установки через Windows Update на только что установленной системе.
Поэтому есть два варианта: 1) установить все обновления через Windows Update, но это будет долго (этот процесс вполне можно выполнить позднее), или 2) поставить только пару самых необходимых.
Во втором случае делаем так: подключаемся по RDP на сервер, запускаем консоль Powershell с повышенными привилегиями и скачиваем нужные нам обновления по прямой ссылке и устанавливаем их:
Invoke-WebRequest -Uri http://download.microsoft.com/download/3/9/7/3971FEA1-C483-409E-BF13-219F8A6E907E/Windows8.1-KB2975061-x64.msu -OutFile .\Downloads\Windows8.1-KB2975061-x64.msu
.\Downloads\Windows8.1-KB2975061-x64.msu /quiet /norestart
Invoke-WebRequest -Uri http://download.microsoft.com/download/2/5/6/256CCCFB-5341-4A8D-A277-8A81B21A1E35/Windows8.1-KB2919355-x64.msu -OutFile .\Downloads\Windows8.1-KB2919355-x64.msu
.\Downloads\Windows8.1-KB2919355-x64.msu /quiet /promtrestart
После перезагрузки наш сервер будет готов к приему конфигурации.
Еще раз напоминаю, что Windows Server 2016 TP3 сразу готов к экспериментам с DSC.
Компьютер администратора
Важно: Так некоторые из вас могли пропустить предыдущий раздел: Для тестирования этого примера конфигурации любым доступным способом подключите на сервере образ MS SQL Server 2014, я выбрал Express Edition и подключил как R:.
Должен огорчить тех, кто уже обновился до Windows 10 — есть нюансы, которые не позволят использовать созданные в этой системе конфигурации. Тоже самое относится к тем, кто поставил Powershell 5.0.
Создание и применение конфигураций выполнялось на Windows 8.1. Версия Powershell:
PS C:\Users\nelsh> $PSVersionTable
Name Value
---- -----
PSVersion 4.0
WSManStackVersion 3.0
SerializationVersion 1.1.0.1
CLRVersion 4.0.30319.34209
BuildVersion 6.3.9600.17400
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0}
PSRemotingProtocolVersion 2.2
Но и этого еще недостаточно. На компьютере администратора необходимо включить Powershell Remoting (на сервере, после установки двух обновлений, PSRemoting уже включен). Это выполняется в консоли Powershell с повышенными привилегиями:
Enable-PSRemoting -Force
Кроме того, нужно разрешить (!) с компьютера администратора подключаться к другим компьютерам.
Set-Item WSMan:\localhost\Client\TrustedHosts -Value *
Для удобства, я также добавил в файл
hosts
строку c ip-адресом созданного сервера.<ip-address> cs1.example.com
Исходный код примера доступен на Github: https://github.com/nelsh/DSC-WS2012R2.
Первая конфигурация
Это скриншот первой версии файла DSC-W2012R2.ps1 (в репозитории, для удобства, он лежит под именем DSC-W2012R2-First.ps1).
- 1-21 строки — собственно сама конфигурация, названная DSCW2012R2.
- В ней на 3-7 строке мы сообщаем, что будет всего один параметр — массив имен серверов.
- На 8 строке мы подключаем необходимый модуль PowerShell
- 10-20 строки — список используемых ресурсов в конфигурации. В нашем случае — только один ресурс “Script”, о нем чуть ниже.
- 23-25 строки — так как одним из шагов будет добавление пользователя, то нам потребуется создать для него пароль. Чтобы не разбираться с шифрованием — разрешим хранить пароли в конфигурации в открытом виде.
- 27 строка — создание конфигурации. В результате выполнения этого скрипта у нас появится файл DSCW2012R2\cs1.example.com.mof — что-то типа скомпилированной конфигурации.
- 29-31 строки — запускает применение конфигурации к серверу с именем cs1.example.com. Предварительно появиться стандартное окошко для запроса имени и пароля для доступа к серверу.
- Начиная с 33 строки — пример для одновременной настройки пары серверов.
Возвращаемся к ресурсам — Powershell DSC имеет 12 встроенных ресурсов. Большинство (а точнее 11 из 12) из них просты и понятны. Но для полноценной настройки системы их явно недостаточно. Microsoft предлагает самостоятельно создавать необходимые ресурсы. Но, честно признаюсь, даже сейчас у меня нет особого желания разбираться с этим.
Однако, при первом знакомстве я не обратил внимание на ресурс Script. Раcсмотрим пример подробнее:
Script First {
TestScript = { if ( "Test script content" ) { $true } else { $false } }
SetScript = { "Set script content" }
GetScript = { return @{ Result = "Result for GetScript"
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
Это самый простой вариант, который ничего не делает. Как он работает:
- Для применения конфигурации мы вызываем команду Start-DSCConfiguration (строка 29).
- Когда в процессе обработки списка ресурсов скрипт доходит до ресурса “Script First”, то вначале вызывает код из переменной TestScript. В нашем примере он всегда возвращет $true.
- А вот если бы он вернул $false, тогда вызвался бы код из переменной SetScript.
В этих переменных может быть любой код на Powershell — масштабы зависят только от наших фантазий. В идеальном варианте, код в TestScipt должен проверять правильность всех настроек, которые выполняются в коде SetScript.
Попробуем запустить нашу первую конфигурацию (по старой привычке я запускаю из Far Manager):
powershell.exe -ExecutionPolicy RemoteSigned .\DSC-WS2012R2.ps1
Завершено без ошибок.
Чтобы не объяснять как запустить проверку с компьютера администратора, я зашел на наш сервер и выполнил пару команд.
Первая:
Test-DSCConfiguration
— проверяет конфигурацию. Обратите внимание на последнее сообщение (после желтого текста) — True. Т.е. конфигурация проверена и ошибок не обнаружено.Следующая команда:
Get-DSCConfiguration
— сообщает подробности о текущей конфигурации. Достаточно сверить код нашего ресурса “Script First” с этим скриншотом, чтобы понять что и откуда берется.В этот самый момент я понял, что, пожалуй, все получится.
Итак. Переходим к…
Продвинутая конфигурация
Начинаем добавлять реальные задачи в конфигурацию узлов.
Первое, в чем я хотел бы быть уверен — имя компьютера и основной dns-суффикс. Если в нашей DNS-зоне этот компьютер будет называться cs1.example.com, то имя — cs1, а dns-суффикс — example.com
Начнем с имени. Вначале я написал такой код:
$shortName = $Server.Split(".")[0].ToLower()
Script ComputerName {
SetScript = { Rename-Computer -NewName $shortName }
GetScript = { return @{ Result = $env:computerName
GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim() } }
TestScript = { $env:computerName.ToLower() -eq $shortName }
}
Но он не работает. SetScript, GetScript и TestScript ничего не знают о переменных вне своей зоны видимости. Передать можно только используя форматирование строк. Вот так:
$shortName = $Server.Split(".")[0].ToLower()
Script ComputerName {
SetScript = ({
Rename-Computer -NewName "{0}"
} -f @($shortName))
GetScript = { return @{ Result = $env:computerName
GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim()
}
}
TestScript = ({
$env:computerName.ToLower() -eq "{0}"
} -f @($shortName))
}
С проверкой dns-суффикса все оказалось проще — это параметр в реестре, поэтому используем стандартный ресурс Registry:
Registry PrimaryDomainSuffix {
Key = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\"
ValueName = "NV Domain"
Ensure = "Present"
ValueData = "example.com"
ValueType = "String"
}
Чтобы во время экспериментов обновления не ставились автоматически — настроим Windows Update. Только получать уведомления о доступных обновлениях:
Script WindowsUpdateSettings {
SetScript = {
$WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings
$WUSettings.NotificationLevel=2
$WUSettings.IncludeRecommendedUpdates=$true
$WUSettings.Save()
}
GetScript = { return @{ Result = ''
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
TestScript = {
$WUSettings = (New-Object -com "Microsoft.Update.AutoUpdate").Settings; $WUSettings.NotificationLevel -eq 2 -and $WUSettings.IncludeRecommendedUpdates -eq $true
}
}
Получаем пару важных обновления и все-таки настраиваем часовой пояс
Следующий важный параметр — временная зона. И зачем только я предположил, что московское время подходит не всем? Видимо, для этого:
Итак, первая проблема: похоже, встроенных средств установки отдельного обновление через Windows Update не существует. К счастью, есть небольшая утилита для решения этой задачи.
Создаем каталог для утилиты:
$abcUpdatePath = "C:\UTILS\ABC-Update"
$abcUpdateZip = Join-Path $abcUpdatePath "ABC-Update.zip"
File AbcUpdateDir {
Ensure = "present"
DestinationPath = $abcUpdatePath
Type = "Directory"
}
Скачиваем:
Script AbcUpdateDownload {
DependsOn = "[File]AbcUpdateDir"
SetScript = ({
Invoke-WebRequest -Uri http://abc-deploy.com/Files/ABC-Update.zip -OutFile {0}
} -f @($abcUpdateZip))
GetScript = {
return @{ Result = $TestScript
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
TestScript = ({
Test-Path {0}
} -f @($abcUpdateZip))
}
Распаковываем, используя встроенный ресурс Archive:
Archive AbcUpdateUnpack {
Ensure = "Present"
DependsOn = "[Script]AbcUpdateDownload"
Path = $abcUpdateZip
Destination = $abcUpdatePath
}
Запускаем, и, кроме обновления часовых поясов, сразу же обновим .NET Framework до версии 4.5.2:
Script AbcUpdateNet452Install {
DependsOn = "[Archive]AbcUpdateUnpack"
SetScript = { C:\UTILS\ABC-Update\ABC-Update.exe /a:install /k:2934520 }
GetScript = { return @{ Result = if ( Get-HotFix -Id KB2934520 -ErrorAction SilentlyContinue ) { "KB2934520: Installed" } else { "KB2934520: Not Found" }
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
TestScript = { if ( Get-HotFix -Id KB2934520 -ErrorAction SilentlyContinue ) { $true } else { $false } }
}
Script AbcUpdateTimeZoneInstall {
DependsOn = "[Archive]AbcUpdateUnpack"
SetScript = { C:\UTILS\ABC-Update\ABC-Update.exe /a:install /k:3013410 }
GetScript = { return @{ Result = if ( Get-HotFix -Id KB3013410 -ErrorAction SilentlyContinue ) { "KB3013410: Installed" } else { "KB3013410: Not Found" }
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
TestScript = { if ( Get-HotFix -Id KB3013410 -ErrorAction SilentlyContinue ) { $true } else { $false } }
}
Наконец-то мы добрались до часовых поясов. Методом научного тыка я выбрал часовой пояс около озера Байкал.
Я нашел такой вариант: изменить часовой пояс можно только с помощью утилиты командной строки tzutil.exe, а проверить только с помощью Powershell. Но этот случай особенный — при установке используется одно значение «North Asia East Standard Time», а проверяется совершенно другое «Russia TZ 7 Standard Time»:
Script TimeZoneSettings {
SetScript = { tzutil.exe /s "North Asia East Standard Time" }
GetScript = { return @{ Result = [System.TimeZone]::CurrentTimeZone.StandardName
GetScript = $GetScript.Trim(); SetScript = $SetScript.Trim(); TestScript = $TestScript.Trim()
}
}
TestScript = { [System.TimeZone]::CurrentTimeZone.StandardName -eq "Russia TZ 7 Standard Time" }
}
Похоже, такая беда со всеми часовыми поясами России.
Компоненты Windows
С ними все очень просто и очень большое количество примеров в интернете. Вполне может сложиться впечатление, что администраторы Windows занимаются только установкой и удалением компонентов. Только два первых ресурса WindowsFeature из конфигурации:
WindowsFeature offFSSMB1 {
Ensure = "Absent"
Name = "FS-SMB1"
}
WindowsFeature WebAspNet45 {
Ensure = "Present"
Name = "Web-Asp-Net45"
IncludeAllSubFeature = $True
}
В первом случае компонент удаляется, во втором ставится вместе со всеми зависимостями.
Установка пакетов
На примере Far Manager. Во-первых, пакет нужно скачать уже известным нам способом:
Script FarDownLoad {
SetScript = { Invoke-WebRequest -Uri http://www.farmanager.com/files/Far30b4400.x64.20150709.msi -OutFile C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi }
GetScript = { return @{ Result = Test-Path C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
TestScript = { Test-Path C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi }
}
В параметрах ресурса Package есть «ProductId». И вроде есть даже программа, которая анализирует msi-файл и сообщает этот самый «ProductId». Я пошел напролом: сразу же попробовал применить конфигурацию без этого параметра и тексте ошибки обнаружил «ProductId», а также правильный «Name». Описание ресурса получилось следующее:
Package FarInstall {
Ensure = "Present"
DependsOn = "[Script]FarDownLoad"
Name = "Far Manager 3 x64"
ProductId = 'E5512F32-B7C1-48E3-B6AF-E5F962F99ED6'
Path = "C:\Users\Public\Downloads\Far30b4400.x64.20150709.msi"
Arguments = ''
LogPath = "C:\Users\Public\Downloads\FarInstall.log"
}
Пользователи и права
По постановке задачи, сервер находится под управлением заказчика, но тем не менее я допустил, что будет существовать возможность обновления web-приложения с помощью нашего сервера непрерывной интеграции. Мы используем Jenkins CI (кстати, все задачи в нем тоже реализованы на Powershell).
В минимальном варианте нам нужен пользователь Jenkins в группе Users и с правом записи в каталог, где размещается web-приложение. Пусть это будет
c:\web
.Пользователь создается таким образом:
$JenkinsCredential = New-Object System.Management.Automation.PSCredential(`
"Jenkins", ("Pa`$`$w0rd" | ConvertTo-SecureString -asPlainText -Force)`
)
User JenkinsUser {
UserName = "Jenkins"
Ensure = "Present"
Password = $JenkinsCredential
PasswordChangeNotAllowed = $true
PasswordNeverExpires = $true
}
Есть способ использовать в конфигурации зашифрованные пароли, но мы пойдем простым путем. В данном случае будет создан пользователь «Jenkins» с паролем «Pa$$w0rd».
Создание каталога делается уже привычным образом. А вот с назначением прав на каталоги и проверкой пришлось повозится:
$AccessStringTmpl = "NT AUTHORITY\SYSTEM Allow FullControl`nBUILTIN\Administrators Allow FullControl`nBUILTIN\Users Allow ReadAndExecute, Synchronize`nCS1\Jenkins Allow Modify, Synchronize"
File DirDweb {
Ensure = "present"
DestinationPath = "c:\web"
Type = "Directory"
}
Script AclsDweb
{
DependsOn = "[File]DirDweb"
SetScript = {
icacls c:\web /reset /t /q
takeown.exe /f c:\web /r /a /d y
icacls.exe c:\web /inheritance:r
icacls.exe c:\web /grant:r "Administrators:(OI)(CI)(F)" "System:(OI)(CI)(F)" "Users:(OI)(CI)(RX)" "Jenkins:(OI)(CI)(M)" /t /q
}
GetScript = { return @{ Result = (get-acl c:\web).AccessToString
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
TestScript = ({ (get-acl c:\web).AccessToString -eq "{0}"
} -f @($AccessStringTmpl))
}
Проще все назначить права с помощью
icacls.exe
. В данном случае выполняется по порядку:- в первой строке: сброс всех прав и включение наследования от родительского каталога
- во второй: владельцем назначается встроенная группа Administrators
- в третьей: отменяется наследование и удаляются все права
- в четвертой: назначение полных прав для Administrators и SYSTEM, чтение для пользователей и изменение для Jenkins.
Для проверки используется метод
(get-acl c:\web).AccessToString
— полученная строка должна совпадать с переменной $AccessStringTmpl
. Кстати, в примере ошибка — в строке явным образом указано имя сервера “CS1” — а должно подставляться значение $Server.Split(".")[0].ToUpper()
.MS SQL
Я несколько пожалел, что решил не использовать сторонних модулей. Так как уже есть модуль для установки и настройки MS SQL Server. Но у меня есть конфигурационный файл для автоматической установки и я решил попробовать.
Во-первых, нам потребуется еще один компонент Windows — ресурс “WindowsFeature NetFrameworkCore”:
WindowsFeature NetFrameworkCore {
Ensure = "Present"
Name = "Net-Framework-Core"
IncludeAllSubFeature = $True
}
Во-вторых, конфигурационный файл для установщика — ресурс “Script MSSQLConfigDownLoad”:
Script MSSQLConfigDownLoad {
SetScript = { Invoke-WebRequest -Uri https://raw.githubusercontent.com/nelsh/DSC-WS2012R2/master/SQL2014-Setup.ini -OutFile C:\Users\Public\Downloads\SQL2014-Setup.ini }
GetScript = { return @{ Result = Test-Path C:\Users\Public\Downloads\SQL2014-Setup.ini
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
TestScript = { Test-Path C:\Users\Public\Downloads\SQL2014-Setup.ini }
}
В-третьих, вы не забыли подключить образ с дистрибутивом какой-нибудь редакции MS SQL Server 2014?
Script MSSQL {
SetScript = { r:\setup.exe /configurationfile=C:\Users\Public\Downloads\SQL2014-Setup.ini /SAPWD=1q@w3e }
GetScript = { return @{ Result = if ( Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue ) { "Servise MSSQLSERVER is exist" } else { "Servise MSSQLSERVER not found" }
GetScript = $GetScript; SetScript = $SetScript; TestScript = $TestScript
}
}
TestScript = { if ( Get-Service -Name "MSSQLSERVER" -ErrorAction SilentlyContinue ) { $true } else { $false } }
}
Я воспользовался диском с Express Edition, который подключен как R:. В процессе будут установлены Database Engine плюс FullSearch, а также средства администрирования. Проверка в TestScript самая простая — есть сервис MSSQLSERVER или нет.
… И когда по списку процессов на сервере я понял, что установка запустилась — стало понятно, что эксперимент можно считать завершенным.
Дальнейшая настройка может зависеть от ситуации: если требуется срочно-срочно показать рабочее приложение, то дальше каким-то образом получаем и устанавливаем наше приложение.
Если же у нас плановая установка (без авралов), то предварительно можно настроить брандмауэр и установить служебные программы (мониторинг, резервное копирование) — у всех могут быть свои варианты. Из того, что используется у нас, наибольшую трудность может вызвать только AWStats: код в TestScript будет напоминать небольшую программу, но это тоже решаемо.
Поэтому, на этом пункте решил остановится. По-моему, получился удачный пример, который любой может адаптировать к своей ситуации.
Предварительные итоги
На мой взгляд, DSC можно брать на вооружение, не дожидаясь следующей версии Windows Server.
В доменной инфраструктуре полностью заменить групповые политики эта технология не сможет, но у нее есть определенные плюсы:
- Может использоваться как в ручном режиме, так и в автоматическом с сервером конфигураций.
- Может использоваться независимо от наличия Active Directory, а может и вместе с групповыми политиками.
- Каталог с конфигурациями можно положить в систему контроля версий.
- С выходом Powershell 5.0 мы получаем удобный способ использования дополнительных модулей — см. powershellgallery.com. Там уже есть несколько десятков модулей, созданных сообществом. Возможно, среди них уже есть такие, которыми можно заменить скрипты из моего примера.
Прошу учесть, что в примере из этой статьи возможны ошибки, а также, вероятно, есть более эффективные решения.
Последнее важное примечание: насколько я понял, вопрос с необходимой перезагрузкой в процессе настройки даже в новых версиях не имеет решения. Поэтому описанная выше конфигурация применяется за два прохода — причем и на 2012R2 и на 2016 — прерывается на установке компонентов и просит перезагрузку. После чего необходимо снова запустить применение конфигурации.
Полезные ссылки
- Все примеры из этой статьи находятся в проекте на Github-е
https://github.com/nelsh/DSC-WS2012R2 - Windows PowerShell Desired State Configuration Overview
https://technet.microsoft.com/ru-ru/library/dn249912.aspx - Ближайшее будущее Powershell 5.0 и DSC
https://www.powershellgallery.com/
http://blogs.msdn.com/b/powershell/archive/2015/09/15/updated-dsc-resource-kit-available-in-powershell-gallery.aspx
Комментарии (3)
mvs
30.09.2015 08:45Типичный Microsoft-продукт, созданный Microsoft с Microsoft головного мозга: тут через команду PowerShell, тут — через реестр, тут — через консольную команды, а вот тууут — GUI-only.
К автору топика никаких претензий, спасибо, что рассказали про новый инструмент. Но вот пользоваться им желания не возникает. Летом писал playbook'и для настройки Windows при помощи Ansible и как-то оно более удобно, хотя тоже через PowerShell и граблей там тоже эээ… много!
4c74356b41
30.09.2015 09:45Что-то вы жжете. В DSC уже ресурсов 200 наверно.
xTimeZone: Allows setting the system time zone in Windows using PowerShell DSC
xTimeZone
xSqlPs: Allows configuration of SQL Server
xSqlServerInstall
xPSDesiredStateConfiguration:
xPackage
Ну и как обычно сразу налетели наркоманы с критикой МС.
пс. Я слабо представляю где у МС GUI-only…
Sergey-S-Kovalev
Я впечатлён.
Осталось прокурить хорошенько.