Добро пожаловать под кат, если вам это интересно.

Программисты не любят делать двойную работу, сисадмины тоже.
Ниже пример автоматизации одного из наших заказчиков.
Мы хотели сделать так, чтобы любой инженер или project-менеджер смог создать новую виртуальную машину с минимальными усилиями и за минимальный срок. У нашего заказчика есть ITSM-система, в данном примере это ServiceNow, мы создали соответствующую web-форму в сервисном каталоге. Для «заказа» новой машины менеджеру необходимо заполнить поля и подтвердить «заказ», после этого запускается цепочка процессов, и на выходе получаем готовую к использованию машину.
Итак, давайте рассмотрим, что нужно определить менеджеру, чтобы создать новую виртуальную машину:

VM Description: описание виртуальной машины
Тут нужны некоторые пояснения. В нашем решении активно используется PowerShell 5.1, поэтому пока Windows-only, в будущем мы постараемся добавить поддержку Unix-машин и перейдем на PowerShell Core.
OS, операционная система. Никаких особых препятствий использовать Windows 2008 (R2) нет, но мы используем 2012R2 или 2016.
VM Size, размер виртуальной машины. У каждого это может быть определено по-своему, в данном примере Small 1CPU-4Gb Ram, Medium 2CPU-8Gb, Large 4-16.
VM Storage, Disk 0 (C:\) имеет фиксированный размер, который вы не можете изменить, доступен только селектор Fast/Slow storage. «Fast» — это может быть Storage Tier с SSD, а «Slow» — это storage на «обычных» HDD (конечно — SAN). Disk1 (Disk2 и далее) также имеют селектор выбора типа Storage, а также поля для ввода желаемого размера в гигабайтах, Letter для раздела и размер кластера (что важно для SQL Server).
Trust, определяем, что машина должна быть Domain-joined или нет, с доступом из Public Network или нет.
Type, тип машины. Почти каждую машину можно определить, как front-end или back-end приложения или же other во всех оставшихся случаях. На основе выбранного типа мы сможем в дальнейшем определить наиболее подходящую подсеть для машины.
Environment, в инфраструктуре заказчика есть два дата центра: Primary (Production) и Secondary (Dev/test), DC связаны между собой быстром каналом связи и обеспечивают отказоустойчивость. По договоренности все виртуальные машины в Primary DC имеют IP-адрес, начинающийся на 10.230, а в Secondary DC — на 10.231.
(SLA) Service Level Agreement, этот параметр влияет на качество обслуживания данной машины.
Приложения. Мы добавили возможность установки и настройки SQL Server. Необходимо выбрать издание, instance name и collation. Также возможно настроить и Web Server роль и многое другое.
Теперь нам нужно определить, как хранить выбранные значения. Мы решили, что наиболее удобный формат — JSON-файл. Как я говорил ранее, в среде заказчика используется ITSM ServiceNow; менеджер, после того как выбрал все необходимы значения, нажимает кнопку «order» и после этого ServiceNow передает все параметры нашему PowerShell-скрипту (на back-end ServiceNow), который и создаст JSON-файл. Выглядит это примерно так:
.\CreateConfiguration.ps1 -SecurityZone trusted -VMDescription "VM for CRM System" -Requestor "evgeniy.vpro" -OSVersion 2k16 -OSEdition Standard -BuildNewVM -VMEnvironment Prod -VMServiceLevel GOLD -VMSize Medium -Disk0Tier Fast -Disk1Size 50 -Disk1Tier Eco -Disk1Letter D -MSSQLServer -MSSQLInstanceName "Instance1" -SQLCollation Latin1_General_CI_AS -SQLEdition Standard -Disk2Size 35 -Disk3Size 65
В теле CreateConfiguration .ps1 скрипта:
#создаем PowerShell-объект $config = [ordered]@{} #И заполняем его входными параметрами. $config.SecurityZone=$SecurityZone
В конце экспортируем наш объект в JSON-файл:
$ServerConfig = New-Object –TypeName PSObject $config ConvertTo-Json -InputObject $ServerConfig -Depth 100 | Out-File "C:\Configs\TargetNodes\Build\$($Hostname.ToLower()).json" -Force
Примерный образец конфигурации:
{ "Hostname": "dsctest552", "SecurityZone": "trusted", "Domain": "testdomain", "Requestor": "evgeniy.vpro", "VM": { "Size": "Medium", "Environment": "Prod", "SLR": "GOLD", "DbEngine": "MSSQL", "RAM": 8, "Storage": [ { "Id": 0, "Tier": "Fast", "Size": "100", "Allocation": 4, "Letter": "C" }, { "Id": 1, "Tier": "Eco", "Size": 50, "Label": "Data", "Allocation": 64, "Letter": "D" }, { "Id": 2, "Tier": "Fast", "Size": 35, "Label": "Data", "Allocation": 64, "Letter": "E" }, { "Id": 3, "Tier": "Fast", "Size": 65, "Label": "Data", "Allocation": 64, "Letter": "F" } ] }, "Network": { "MAC": "", "IP": "10.230.168.50", "Gateway": "10.230.168.1", "VLAN": “VLAN168” }, "OS": { "Version": "2k16", "Edition": "Standard", "Administrators": [ "LocaAdmin", "testdomain\\ Security-LocalAdmins" ] }, "OU": "OU=Servers,OU=Staging,DC=testdomain", "Applications": [ { "Application": "Microsoft SQL Server 2016", "InstanceName": "vd", "Collation": "Latin1_General_CI_AS", "Edition": "Standard", "Features": "SQLENGINE", "Folders": { "DataRoot": "F:\\MSSQL", "UserDB": "F:\\MSSQL\\MSSQL11.vd\\MSSQL\\Data", "UserLog": "E:\\MSSQL\\MSSQL11.vd\\MSSQL\\Log", "TempDB": "D:\\MSSQL\\MSSQL11.vd\\MSSQL\\TempDB", "TempDBLog": "D:\\MSSQL\\MSSQL11.vd\\MSSQL\\TempDB", "Backup": "E:\\MSSQL\\MSSQL11.vd\\MSSQL\\Backup" }, "MaxMemory": 2147483647 } ], "Description": "VM for CRM", "Certificate": { "File": null, "Thumbprint": null }, "Version": 0 }
Вы могли заметить, что в веб-форме отсутствовало имя виртуальной машины и IP-адрес. Мы получаем эти значения автоматически следующим образом:
Имя машины, в ITSM ServiceNow есть специальный раздел: CMDB (Configuration Management Data Base), в этой базе хранятся все записи о существующих виртуальных машинах, их статус, команда поддержки и прочее. Мы создали порядка 200 резервных записей со статусом Allocated. Чтобы получить имя для виртуальной машины мы делаем REST-запрос к CMDB и получаем первую «свободную» запись и меняем её статус с Allocated на Pending install.
IP адрес и VLAN, мы развернули IPAM в нашей сети — это встроенная feature в Windows Server 2016, которая позволяет управлять IP-адресами в вашей сети. Вовсе не обязательно использовать все возможности IPAM (DHCP, DNS, AD), а использовать её только как базу данных IP-адресов с потенциальным расширением функционала. Скрипт, который создает JSON файл, делает запрос к IPAM на предмет первого свободного IP адреса в подсети. А подсеть VLAN (х/24 подсеть) определяется на основе выбранных значений SLA, Environment, Trust и Type.
Файл-конфигурация готов, все поля на месте, можно создавать машину. Встает вопрос «как хранить учетные данные для всех наших скриптов?». Мы используем пакет CredentialManager. Этот пакет работает со встроенным Windows Credential Manager API для хранения паролей. Пример создания пароля:
New-StoredCredential -Target "ESXi" -UserName "testdomain.eu\vmwareadm" -Password "veryultraP@ssw00rd." -Type Generic -Persist LocalMachine
Пароль будет доступен для чтения в пределах данной машины и учетной записи.
$ESXiAdmin = Get-StoredCredential -Type Generic -Target ESXi
У нас есть сервер, на котором хранятся все конфигурации c GIT, теперь мы можем надежно отслеживать все изменения в конфигурациях: кто, что, где и когда.
На этом сервере настроен scheduled task: проверять папку c конфигурациями и писать в Windows Event Log обо всех изменениях.
Через 15 минут scheduled task напишет в Windows EventLog, что обнаружен новый файл-конфигурация.
Пришло время проверить эту конфигурацию. В первую очередь нам нужно убедиться, что файл имеет корректное форматирование:
$Configuration=(Get-Content -Raw $File | Out-String | ConvertFrom-Json)
Если все хорошо, пора приступать к созданию машины и запусить BuildVM.ps1 скрипт.
В BuildVM.ps1 мы проверяем, что файл-конфигурация имеет описание всех характеристик виртуальной машины: size, env, sla, type, storage, ram, network.
Обязательно проверим, есть ли в инфраструктуре машина с таким же именем (CheckVM.ps1).
Подключаемся через VMWare PowerShell CLI к нашей vSphere:
$VmWareAdmin = Get-StoredCredential -Type Generic -Target ESXi Connect-VIServer -Server "UKHQSV50001" -Credential $VmWareAdmin | Out-Null
Проверяем, есть ли машина с таким же именем в инфраструктуре
$VM=Get-VM $server -ErrorAction SilentlyContinue
И отключаемся:
Disconnect-VIServer * -Force -Confirm:$false
Убедимся, что машина также не доступна по WinRM
$ping=Test-NetConnection -ComputerName $Configuration.Hostname -CommonTCPPort WINRM -InformationLevel Quiet -ErrorAction SilentlyContinue
Если в $VM и $ping пусто, то можно создавать новую машину. (Мы обрабатываем ситуации, когда машина уже создана в ESXi вручную или же эта машина в другом дата-центре.)
Пару слов о машине. Это подготовленный образ виртуальный машины, который был финализирован sysprep и сконвертирован в template в нашем vSphere. В образе сохранен локальный администратор с известным нам паролем, эта учетная запись «не слетает» после sysprep, что позволит нам получить доступ к каждой машине из этого темплейта, а позже мы сможем заменить этот пароль в целях безопасности.
Создание виртуальной машины
Найдем соответствующий SLR-кластер:
$Cluster=Get-Cluster -Name $Configuration.VM.SLR
Проверим, что у нас достаточно места на Datastore:
$DatastoreCluster = Get-DatastoreCluster |Where-Object {$_.Name -like $Datastore1Name} $Datastore1 = Get-Datastore -Location $DatastoreCluster |sort -Property "FreeSpaceGB" |select -Last 1 IF ($Datastore1.FreeSpaceGB -le "200"){ Write-Host -foreground red "STOP: Not enough datastore capacity for DISK" $vdisk.Id Break }
И достаточно памяти:
$VMHost = Get-VMHost -Location $Cluster |sort -Property "MemoryUsageGB" |select -First 1 IF ($VMHost.MemoryUsageGB -le "20"){ Write-Host -foreground red "STOP: No enough ESXi host capacity" Break }
Берем наш темплейт
$VMTemplate = Get-Template -Name 'Win2016_Std_x64_Template'
И создаем новую виртуальную машину
New-VM -Name $Configuration.Hostname.ToUpper() -VMHost $VMHost -ResourcePool $ResourcePool -Datastore $Datastore -Template $VMTemplate -Location "AutoDeployed VMs"
Важно подключить сетевой интерфейс к подсети с включенным DHCP.
Запускаем виртуальную машину
Start-VM $VM
И сохраняем описание машины, чтобы потом можно было определить машину на уровне VMWare.
Set-Annotation -Entity $VM -CustomAttribute "Change request" -Value $Configuration.Request -Confirm:$false Set-VM $VM -Notes $Configuration.Description -Confirm:$false
Машина запустилась и теперь мы можем узнать полученный MAC-адрес:
$vMAC = (($VM | Get-NetworkAdapter | Select-Object -Property "MacAddress").MacAddress).Replace(':','')
Сохраним это значение в наш JSON-файл
$Configuration.Network.MAC=$VMAC ConvertTo-Json -InputObject $Configuration -Depth 100 | Out-File "C:\Configs\TargetNodes\Build\$Hostname.json" -Force
Здесь самое время сделать commit в наш Git, что машина создана и имеет свой уникальный MAC.
Машина начинает инициализироваться (после sysprep), настраивать оборудование и начальную конфигурацию.
Давайте дождемся, когда будет доступна наша машина по WinRM c скриптом EstablishConnection.ps1.
Сначала узнаем какой IP машина получила от DHCP:
#Здесь $MAC = $vMAC while($isOnline -ne $true){ if((Get-DhcpServerv4Lease -ClientId $MAC -ScopeId $StagingDHCPScope -ComputerName $DHCPServer -ErrorAction Ignore).IPAddress.IPAddressToString){ $tempIP=(Get-DhcpServerv4Lease -ClientId $MAC -ScopeId $StagingDHCPScope -ComputerName $DHCPServer).IPAddress.IPAddressToString break } else{ if($isOnline -ne $true){ Write-Host "`r$i`t" -NoNewline $i++ } } }
А теперь дождемся, когда машина будет доступна по WinRM:
$LocalAdmin = Get-StoredCredential -Type Generic -Target LocalAdmin $i=0 $isOnline=$false while($isOnline -ne $true){ if(Invoke-Command -ComputerName $tempIP -ScriptBlock{ Get-ItemProperty -Path "Registry::\HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing" } -Credential $LocalAdmin -ErrorAction SilentlyContinue){ $isOnline=$true break } else{ if($isOnline -ne $true){ Write-Host "`r$i" -NoNewline $i++ Start-Sleep -Seconds 1 } } }
Машина готова к управлению.
Desired State Configuration
Для настройки желаемой конфигурации мы используем часть PowerShell — DSC (Desired State Configuration). В сети есть настроенный DSC Pull Server: dscpull.testdomain.eu.
Ниже конфигурация нашего DSC Pull Server. Хорошая статья по настройке DSC Pull.
Node $NodeName { WindowsFeature DSCServiceFeature { Ensure = "Present" Name = "DSC-Service" } xDscWebService PSDSCPullServer { Ensure = "Present" EndpointName = "PSDSCPullServer" Port = 8080 PhysicalPath = "$env:SystemDrive\inetpub\PSDSCPullServer" CertificateThumbPrint = $certificateThumbPrint ModulePath = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Modules" ConfigurationPath = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Configuration" State = "Started" DependsOn = "[WindowsFeature]DSCServiceFeature" RegistrationKeyPath = "$env:PROGRAMFILES\WindowsPowerShell\DscService" AcceptSelfSignedCertificates = $true UseSecurityBestPractices = $true } File RegistrationKeyFile { Ensure = 'Present' Type = 'File' DestinationPath = "$env:ProgramFiles\WindowsPowerShell\DscService\RegistrationKeys.txt" Contents = $RegistrationKey } }
Он доступен по адресу: https://dscpull.testdomain.eu:8080
Его Endpoint: https://dscpull.testdomain.eu:8080/PSDSCPullserver.svc
На всех клиентах pull сервера должен быть установлен PowerShell 5.1
Если установлен не PowerShell 5.1:
$PSVersionTable.PSVersion.Major –lt 5
установить PowerShell 5.1:
Write-Host "Download PowerShell 5.1" Invoke-Command -ComputerName $Node -ScriptBlock { [System.Net.ServicePointManager]::SecurityProtocol=[System.Net.SecurityProtocolType]::Tls12;Invoke-WebRequest -Uri "https://dscpull.testdomain.eu:8080/Files/Updates/WMF.msu" -OutFile C:\TEMP\WMF.MSU } Write-Host "Extract PowerShell 5.1" Invoke-Command -ComputerName $Node -ScriptBlock {Start-Process -FilePath 'wusa.exe' -ArgumentList "C:\temp\WMF.msu /extract:C:\temp\" -Wait -PassThru } Write-Host "Apply PowerShell 5.1" Invoke-Command -ComputerName $Node -ScriptBlock {Start-Process -FilePath 'dism.exe' -ArgumentList "/online /add-package /PackagePath:C:\temp\WindowsBlue-KB3191564-x64.cab /Quiet" -Wait -PassThru } Write-Host "PowerShell 5.1 has been installed"
В нашей сети также развернут PKI-сервер. Это условие для безопасного шифрование учетных данных сохранённых в DSC mof файлах (Mof файлы — это «язык» на котором общаются Pull Server и его клиенты). Когда клиент пытается зарегистрироваться на Pull Server, необходимо указать Thumprint сертификата и в дальнейшем Pull Server будет использовать этот сертификат для шифрования паролей. Ниже мы рассмотрим, как это работает.
Импортируем Root CA нашей новой машине:
Invoke-Command -ComputerName $server -ScriptBlock{ $PKI="-----BEGIN CERTIFICATE----- MIIF2TCCA8GgAwIBAgIQSPIjcff9rotNdxbg3+ygqDANBgkqhkiG9w0BAQUFADAe **************************************************************** znafMvVx0B4tGEz2PFss/FviGdC3RohBHG0rF5jO50J4nS/3cGGm+HGdn1w/tZd0 a0FWpn9VCOSmXM2It+tSW1f4nZVt6T2kr1ZlTxkDhT7HMSGsrX/XJswzCkDGe3dE qrVVjNUkhVTaeeBWdujB5J6mcx7YkNsAUhODiS9Cf7FnYnxLFA72M0pijI48P5F0 ShM9HWAAUIrLkv13ug== -----END CERTIFICATE-----" $PKI | Out-File RootCA.cer Import-Certificate RootCA.cer -CertStoreLocation Cert:\LocalMachine\Root | select Thumbprint | Out-Null } -Credential $LocalAdmin | Out-Null
Для дальнейшей работы нам нужна пара RSA-ключей. Сгенерируем самоподписанный сертификат и временно будем работать с ним:
param( $server, $LocalAdmin ) Write-Verbose "Try to generate certificate" $generated=Invoke-Command -ComputerName $server -ScriptBlock{ function New-SelfSignedCertificateEx { [OutputType('[System.Security.Cryptography.X509Certificates.X509Certificate2]')] [CmdletBinding(DefaultParameterSetName = '__store')] param ( [Parameter(Mandatory = $true, Position = 0)] [string]$Subject, [Parameter(Position = 1)] [datetime]$NotBefore = [DateTime]::Now.AddDays(-1), [Parameter(Position = 2)] [datetime]$NotAfter = $NotBefore.AddDays(365), [string]$SerialNumber, [Alias('CSP')] [string]$ProviderName = "Microsoft Enhanced Cryptographic Provider v1.0", [string]$AlgorithmName = "RSA", [int]$KeyLength = 2048, [validateSet("Exchange","Signature")] [string]$KeySpec = "Exchange", [Alias('EKU')] [Security.Cryptography.Oid[]]$EnhancedKeyUsage, [Alias('KU')] [Security.Cryptography.X509Certificates.X509KeyUsageFlags]$KeyUsage, [Alias('SAN')] [String[]]$SubjectAlternativeName, [bool]$IsCA, [int]$PathLength = -1, [Security.Cryptography.X509Certificates.X509ExtensionCollection]$CustomExtension, [ValidateSet('MD5','SHA1','SHA256','SHA384','SHA512')] [string]$SignatureAlgorithm = "SHA1", [string]$FriendlyName, [Parameter(ParameterSetName = '__store')] [Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation = "CurrentUser", [Parameter(Mandatory = $true, ParameterSetName = '__file')] [Alias('OutFile','OutPath','Out')] [IO.FileInfo]$Path, [Parameter(Mandatory = $true, ParameterSetName = '__file')] [Security.SecureString]$Password, [switch]$AllowSMIME, [switch]$Exportable ) $ErrorActionPreference = "Stop" if ([Environment]::OSVersion.Version.Major -lt 6) { $NotSupported = New-Object NotSupportedException -ArgumentList "Windows XP and Windows Server 2003 are not supported!" throw $NotSupported } $ExtensionsToAdd = @() #region constants # contexts New-Variable -Name UserContext -Value 0x1 -Option Constant New-Variable -Name MachineContext -Value 0x2 -Option Constant # encoding New-Variable -Name Base64Header -Value 0x0 -Option Constant New-Variable -Name Base64 -Value 0x1 -Option Constant New-Variable -Name Binary -Value 0x3 -Option Constant New-Variable -Name Base64RequestHeader -Value 0x4 -Option Constant # SANs New-Variable -Name OtherName -Value 0x1 -Option Constant New-Variable -Name RFC822Name -Value 0x2 -Option Constant New-Variable -Name DNSName -Value 0x3 -Option Constant New-Variable -Name DirectoryName -Value 0x5 -Option Constant New-Variable -Name URL -Value 0x7 -Option Constant New-Variable -Name IPAddress -Value 0x8 -Option Constant New-Variable -Name RegisteredID -Value 0x9 -Option Constant New-Variable -Name Guid -Value 0xa -Option Constant New-Variable -Name UPN -Value 0xb -Option Constant # installation options New-Variable -Name AllowNone -Value 0x0 -Option Constant New-Variable -Name AllowNoOutstandingRequest -Value 0x1 -Option Constant New-Variable -Name AllowUntrustedCertificate -Value 0x2 -Option Constant New-Variable -Name AllowUntrustedRoot -Value 0x4 -Option Constant # PFX export options New-Variable -Name PFXExportEEOnly -Value 0x0 -Option Constant New-Variable -Name PFXExportChainNoRoot -Value 0x1 -Option Constant New-Variable -Name PFXExportChainWithRoot -Value 0x2 -Option Constant #endregion #region Subject processing # http://msdn.microsoft.com/en-us/library/aa377051(VS.85).aspx $SubjectDN = New-Object -ComObject X509Enrollment.CX500DistinguishedName $SubjectDN.Encode($Subject, 0x0) #endregion #region Extensions #region Enhanced Key Usages processing if ($EnhancedKeyUsage) { $OIDs = New-Object -ComObject X509Enrollment.CObjectIDs $EnhancedKeyUsage | ForEach-Object { $OID = New-Object -ComObject X509Enrollment.CObjectID $OID.InitializeFromValue($_.Value) # http://msdn.microsoft.com/en-us/library/aa376785(VS.85).aspx $OIDs.Add($OID) } # http://msdn.microsoft.com/en-us/library/aa378132(VS.85).aspx $EKU = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage $EKU.InitializeEncode($OIDs) $ExtensionsToAdd += "EKU" } #endregion #region Key Usages processing if ($KeyUsage -ne $null) { $KU = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage $KU.InitializeEncode([int]$KeyUsage) $KU.Critical = $true $ExtensionsToAdd += "KU" } #endregion #region Basic Constraints processing if ($PSBoundParameters.Keys.Contains("IsCA")) { # http://msdn.microsoft.com/en-us/library/aa378108(v=vs.85).aspx $BasicConstraints = New-Object -ComObject X509Enrollment.CX509ExtensionBasicConstraints if (!$IsCA) {$PathLength = -1} $BasicConstraints.InitializeEncode($IsCA,$PathLength) $BasicConstraints.Critical = $IsCA $ExtensionsToAdd += "BasicConstraints" } #endregion #region SAN processing if ($SubjectAlternativeName) { $SAN = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames $Names = New-Object -ComObject X509Enrollment.CAlternativeNames foreach ($altname in $SubjectAlternativeName) { $Name = New-Object -ComObject X509Enrollment.CAlternativeName if ($altname.Contains("@")) { $Name.InitializeFromString($RFC822Name,$altname) } else { try { $Bytes = [Net.IPAddress]::Parse($altname).GetAddressBytes() $Name.InitializeFromRawData($IPAddress,$Base64,[Convert]::ToBase64String($Bytes)) } catch { try { $Bytes = [Guid]::Parse($altname).ToByteArray() $Name.InitializeFromRawData($Guid,$Base64,[Convert]::ToBase64String($Bytes)) } catch { try { $Bytes = ([Security.Cryptography.X509Certificates.X500DistinguishedName]$altname).RawData $Name.InitializeFromRawData($DirectoryName,$Base64,[Convert]::ToBase64String($Bytes)) } catch {$Name.InitializeFromString($DNSName,$altname)} } } } $Names.Add($Name) } $SAN.InitializeEncode($Names) $ExtensionsToAdd += "SAN" } #endregion #region Custom Extensions if ($CustomExtension) { $count = 0 foreach ($ext in $CustomExtension) { # http://msdn.microsoft.com/en-us/library/aa378077(v=vs.85).aspx $Extension = New-Object -ComObject X509Enrollment.CX509Extension $EOID = New-Object -ComObject X509Enrollment.CObjectId $EOID.InitializeFromValue($ext.Oid.Value) $EValue = [Convert]::ToBase64String($ext.RawData) $Extension.Initialize($EOID,$Base64,$EValue) $Extension.Critical = $ext.Critical New-Variable -Name ("ext" + $count) -Value $Extension $ExtensionsToAdd += ("ext" + $count) $count++ } } #endregion #endregion #region Private Key # http://msdn.microsoft.com/en-us/library/aa378921(VS.85).aspx $PrivateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey $PrivateKey.ProviderName = $ProviderName $AlgID = New-Object -ComObject X509Enrollment.CObjectId $AlgID.InitializeFromValue(([Security.Cryptography.Oid]$AlgorithmName).Value) $PrivateKey.Algorithm = $AlgID # http://msdn.microsoft.com/en-us/library/aa379409(VS.85).aspx $PrivateKey.KeySpec = switch ($KeySpec) {"Exchange" {1}; "Signature" {2}} $PrivateKey.Length = $KeyLength # key will be stored in current user certificate store switch ($PSCmdlet.ParameterSetName) { '__store' { $PrivateKey.MachineContext = if ($StoreLocation -eq "LocalMachine") {$true} else {$false} } '__file' { $PrivateKey.MachineContext = $false } } $PrivateKey.ExportPolicy = if ($Exportable) {1} else {0} $PrivateKey.Create() #endregion # http://msdn.microsoft.com/en-us/library/aa377124(VS.85).aspx $Cert = New-Object -ComObject X509Enrollment.CX509CertificateRequestCertificate if ($PrivateKey.MachineContext) { $Cert.InitializeFromPrivateKey($MachineContext,$PrivateKey,"") } else { $Cert.InitializeFromPrivateKey($UserContext,$PrivateKey,"") } $Cert.Subject = $SubjectDN $Cert.Issuer = $Cert.Subject $Cert.NotBefore = $NotBefore $Cert.NotAfter = $NotAfter foreach ($item in $ExtensionsToAdd) {$Cert.X509Extensions.Add((Get-Variable -Name $item -ValueOnly))} if (![string]::IsNullOrEmpty($SerialNumber)) { if ($SerialNumber -match "[^0-9a-fA-F]") {throw "Invalid serial number specified."} if ($SerialNumber.Length % 2) {$SerialNumber = "0" + $SerialNumber} $Bytes = $SerialNumber -split "(.{2})" | Where-Object {$_} | ForEach-Object{[Convert]::ToByte($_,16)} $ByteString = [Convert]::ToBase64String($Bytes) $Cert.SerialNumber.InvokeSet($ByteString,1) } if ($AllowSMIME) {$Cert.SmimeCapabilities = $true} $SigOID = New-Object -ComObject X509Enrollment.CObjectId $SigOID.InitializeFromValue(([Security.Cryptography.Oid]$SignatureAlgorithm).Value) $Cert.SignatureInformation.HashAlgorithm = $SigOID # completing certificate request template building $Cert.Encode() # interface: http://msdn.microsoft.com/en-us/library/aa377809(VS.85).aspx $Request = New-Object -ComObject X509Enrollment.CX509enrollment $Request.InitializeFromRequest($Cert) $Request.CertificateFriendlyName = $FriendlyName $endCert = $Request.CreateRequest($Base64) $Request.InstallResponse($AllowUntrustedCertificate,$endCert,$Base64,"") switch ($PSCmdlet.ParameterSetName) { '__file' { $PFXString = $Request.CreatePFX( [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)), $PFXExportEEOnly, $Base64 ) Set-Content -Path $Path -Value ([Convert]::FromBase64String($PFXString)) -Encoding Byte } } [Byte[]]$CertBytes = [Convert]::FromBase64String($endCert) New-Object Security.Cryptography.X509Certificates.X509Certificate2 @(,$CertBytes) } New-SelfsignedCertificateEx ` -Subject "CN=${ENV:ComputerName}" ` -EKU 'Document Encryption' ` -KeyUsage 'KeyEncipherment, DataEncipherment' ` -SAN ${ENV:ComputerName} ` -FriendlyName 'DSC Credential Encryption certificate' ` -Exportable ` -StoreLocation 'LocalMachine' ` -KeyLength 2048 ` -ProviderName 'Microsoft Enhanced Cryptographic Provider v1.0' ` -AlgorithmName 'RSA' ` -SignatureAlgorithm 'SHA256' } -ErrorAction Ignore -Credential $LocalAdmin Write-Host "SelfSigned Certificate has been generated" if($generated){ return $true } else{ return $false }
Теперь мы можем зарегистрироваться на Pull Server:
$DscHostFQDN = [System.Net.Dns]::GetHostEntry([string]$env:computername).HostName $DscPullServerURL = "https://$($DscHostFQDN):8080/PSDSCPullserver.svc" $DscWebConfigChildPath = '\inetpub\psdscpullserver\web.config' $DscWebConfigPath = Join-Path -Path $env:SystemDrive -ChildPath $DscWebConfigChildPath $DscWebConfigXML = [xml](Get-Content $DscWebConfigPath) $DscRegKeyName = 'RegistrationKeys.txt' $DscRegKeyXMLNode = "//appSettings/add[@key = 'RegistrationKeyPath']" $DscRegKeyParentPath = ($DscWebConfigXML.SelectNodes($DscRegKeyXMLNode)).value $DscRegKeyPath = Join-Path -Path $DscRegKeyParentPath -ChildPath $DscRegKeyName $DscRegKey = Get-Content $DscRegKeyPath [DSCLocalConfigurationManager()] configuration RegisterOnPull { Node $Node { Settings { ConfigurationModeFrequencyMins = 1440 CertificateID = $Thumbprint RefreshMode ='Pull' RefreshFrequencyMins = 1440 RebootNodeIfNeeded = $true ConfigurationMode ='ApplyAndAutoCorrect' AllowModuleOverwrite = $true DebugMode = 'None' StatusRetentionTimeInDays = 1 } ConfigurationRepositoryWeb $([string]$env:computername) { ServerURL = $DscPullServerURL RegistrationKey = $DscRegKey CertificateID = $Thumbprint ConfigurationNames = @("$hostx") } } } RegisterOnPull -OutputPath $MetaConfigsStorage Set-DscLocalConfigurationManager -ComputerName $Node -Path $MetaConfigsStorage -Verbose -Force -Credential $LocalAdmin
Отправим первую конфигурацию нашей машине
Configuration Rename { param ( [Parameter()] [System.String[]] $Node, $hostname ) Import-DscResource -ModuleName xComputerManagement Import-DscResource –ModuleName PSDesiredStateConfiguration Node $Node { xComputer JoinDomain { Name = $hostname } } } Rename -Node $Node -OutputPath $DscConfigPath -hostname $hostname New-DscChecksum $DscConfigPath -Force Invoke-Command -ComputerName $Node -ScriptBlock{Update-DscConfiguration -Verbose -Wait } -Credential $LocalAdmin -Verbose
Сервер автоматически переименуется и перезагрузится. Теперь мы можем выполнить Join Domain.
Configuration JoinAD { param ( [Parameter()] [System.String[]] $Node, [Parameter(Mandatory = $true)] [ValidateNotNullorEmpty()] [System.Management.Automation.PSCredential] $DomainAdmin, $hostname, $domain ) Import-DscResource -ModuleName xComputerManagement Import-DscResource –ModuleName PSDesiredStateConfiguration Node $Node { xComputer JoinDomain { Name = $hostname DomainName = $domain Credential = $DomainAdmin JoinOU = "OU=Servers,OU=Staging,OU=Provider,DC=testdomain,DC=eu" } GroupSet LocalAdmins { GroupName = @( 'Administrators') Ensure = 'Present' MembersToInclude = @( 'testdomain-eu\srv_dscstaging_001' ) } } } $cd = @{ AllNodes = @( @{ NodeName = $Node PSDscAllowPlainTextPassword = $false PSDscAllowDomainUser=$true Certificatefile = $CertFile Thumbprint = $Certificate.ToString() } ) } JoinAD -Node $Node -OutputPath $DscConfigPath -DomainAdmin $DomainAdmin -hostname $hostname -ConfigurationData $cd -domain $domain New-DscChecksum $DscConfigPath -Force Invoke-Command -ComputerName $Node -ScriptBlock{Update-DscConfiguration -Verbose -Wait } -Credential $LocalAdmin -Verbose
Вот как выглядит наш mof-файл:
instance of MSFT_Credential as $MSFT_Credential1ref { Password = "-----BEGIN CMS-----\nMIIBsgYJKoZIhvcNAQcDoIIBozCCAZ8CAQAxggFKMIIBRgIBADAuMBoxGDAWBgNVBAMMD1dJTi1H\nNFFKTFFQME4xNQIQOQN77pxew75HU6l7GPn99TANBgkqhkiG9w0BAQcwAASCAQAlhFf7Zs2gJsrw\ngvQ0OGDRsVQMr5jZHIa9bAAcl3+V+5dLaN1GA/Jl06YrLJpnulyuivIJWU34SNTkeRCfxpzPwACV\n2RJHdYIqpFApIxTmSh5zhilC515aDukGchCrFsHayNQsr8vAjIALkRvvtECHgIOREaiwdF2WsKUU\nkbeSDAE2FDx6HBZDxrMG8OCxeiNMgLKeB4rwbmx7ZUiABu5OIcTtHOvMaXp4vNWX5jXStsdQ/Ylt\njPNt2FE6CAnMabC256wnXJIBQpTdqqmc2qmzlz/hpSEUMDbJEnc1DEK2yWbKcO+BEyD2cr6vKHdn\nQ9TrjvbysEOvYjT15o6MccwkMEwGCSqGSIb3DQEHATAdBglghkgBZQMEASoEEEdKJT+GX4IkPezR\nwYncyQiAIAFKxwJocH4ufRsq9L2Ipkp+VQCx2ljlwif6ac4X/PqG\n-----END CMS-----"; UserName = "testdomain.eu\\service_DomainJoin_001"; }; instance of MSFT_xComputer as $MSFT_xComputer1ref { ResourceID = "[xComputer]JoinDomain"; Credential = $MSFT_Credential1ref; DomainName = "testdomain.eu"; SourceInfo = "C:\\Program Files\\WindowsPowerShell\\Scripts\\JoinAD.ps1::34::9::xComputer"; Name = "dsctest51"; JoinOU = "OU=Servers,OU=Staging,DC=testdomain,DC=eu"; ModuleName = "xComputerManagement"; ModuleVersion = "4.1.0.0"; ConfigurationName = "JoinAD"; };
DSC зашифровал учетные данные от сервисной учетки с правами Domain Admin: testdomain.eu\\service_DomainJoin_001 самоподписанным сертификатом. DSC Client своим Private Key расшифровывает учетные данные и применяет все модули конфигурации c указанными доменным учетными данными. В данном случае выполняет Domain Join в указанную organization unit.
GroupSet LocalAdmins { GroupName = @( 'Administrators') Ensure = 'Present' MembersToInclude = @( testdomain-eu\srv_dscstaging_001' ) }
Этот модуль добавляет srv_dscstaging_001 в локальные администраторы для дальнейшей настройки.
После перезагрузки, мы сможем зайти на машину с доменными учетными данными.
Ждем, когда сервер получит сертификат от нашего PKI (у нас настроен auto enrollment) и в дальнейшем будем работать с сертификатом, выпущенным нашим PKI.
$vmcert=Invoke-Command -ComputerName $server -ScriptBlock{ return Get-ChildItem -Path cert:\LocalMachine\My | where {$_.EnhancedKeyUsageList.FriendlyName -eq "Document Encryption"-and $_.Issuer -eq "CN=TestDomain Issuing CA, DC=testdomain, DC=eu"} } -ErrorAction Ignore
Теперь снова зарегистрируемся на Pull Server с обновленным thumbprint.
Всё, машина domain-joined, и мы можем использовать её так, как нам удобно.
Установка SQL Server
В JSON- файле описаны требования по MS SQL Server, для установки и настройки SQL Server мы также используем DSC. Вот как выглядит конфигурация:
Configuration $Node{ WindowsFeature "NetFramework35"{ Name = "NET-Framework-Core" Ensure = "Present" Source = "\\$DscHostFQDN\Files\Updates" } WindowsFeature "NetFramework45"{ Name = "NET-Framework-45-Core" Ensure= "Present" } SqlSetup "MSSQL2012NamedInstance"{ InstanceName = $MSSQL.InstanceName Features = $MSSQL.Features ProductKey = $ProductKey SQLCollation = $MSSQL.Collation SQLSysAdminAccounts = @('testdomain-EU\SQLAdmins',' testdomain-EU\SRV_Backup_001') InstallSharedDir = "C:\Program Files\Microsoft SQL Server" InstallSharedWOWDir = "C:\Program Files (x86)\Microsoft SQL Server" InstallSQLDataDir = $MSSQL.DataRoot SQLUserDBDir = $MSSQL.UserDBDir SQLUserDBLogDir = $MSSQL.UserLogDir SQLTempDBDir = $MSSQL.TempDBDir SQLTempDBLogDir = $MSSQL.TempDBLogDir SQLBackupDir = $MSSQL.BackupDir SourcePath = $SQLSource SAPwd = $SA SecurityMode = 'SQL' UpdateSource = ".\Updates" Action = "Install" ForceReboot = $True SQLSvcAccount = $SqlServiceCredential AgtSvcAccount = $SqlServiceCredential ISSvcAccount = $SqlServiceCredential BrowserSvcStartupType = "Automatic" DependsOn = '[WindowsFeature]NetFramework35', '[WindowsFeature]NetFramework45' }
$MSSQL.InstanceName – всё это указано в нашем Json файле. Применение данной конфигурации выполнит установку и перезагрузку MS SQL Server cо всеми обновлениями в папке Updates.
Машина готова.
Мы создали удобный инструмент с графическим инструментом подобный Azure Portal, который позволяет управлять on-premises инфраструктурой максимально удобно нам и нашему заказчику.
ссылка на оригинал статьи https://habr.com/post/425129/
Добавить комментарий