How to automate the creation of virtual machines? We tell in detail

Published on October 04, 2018

How to automate the creation of virtual machines? We tell in detail

  • Tutorial
Creating a new virtual machine is a time-consuming routine. And the more infrastructure and organization, the more procedures associated with this process. We automated this process using PowerShell.

Welcome under the cat, if you are interested.




Programmers do not like to do double work, system administrators, too.

Below is an example of the automation of one of our customers.

We wanted to make sure that any engineer or project manager could create a new virtual machine with minimal effort and in the shortest possible time. Our customer has an ITSM system, in this example it is ServiceNow, we have created the corresponding web-form in the service catalog. To “order” a new machine, the manager needs to fill in the fields and confirm the “order”, after that the process chain is started, and at the output we get a ready-to-use machine.

So let's consider what the manager needs to define in order to create a new virtual machine:



VM Description: description of the virtual machine
Here we need some explanations. PowerShell 5.1 is being actively used in our solution, so for now Windows-only, in the future we will try to add support for Unix-machines and switch to PowerShell Core.

OS , operating system. There are no particular obstacles to using Windows 2008 (R2), but we are using 2012R2 or 2016.

VM Size , virtual machine size. For each, this can be defined differently, in this example, Small 1CPU-4Gb Ram, Medium 2CPU-8Gb, Large 4-16.

VM Storage, Disk 0 (C: \) has a fixed size that you cannot change, only the Fast / Slow storage selector is available. “Fast” - this can be a Storage Tier with SSD, and “Slow” is storage on “normal” HDDs (of course - SAN). Disk1 (Disk2 and on) also has a selector for the type of Storage, as well as fields for entering the desired size in gigabytes, Letter for the partition and cluster size (which is important for SQL Server).

Trust , we determine that the machine must be Domain-joined or not, with access from the Public Network or not.

Type , type of machine. Almost every car can be defined as front-end or back-end applications or other in all other cases. Based on the selected type, we will be able to further determine the most appropriate subnet for the machine.

Environment, in the customer's infrastructure there are two data centers: Primary (Production) and Secondary (Dev / test), DC are interconnected by a fast communication channel and provide fault tolerance. By convention, all virtual machines in Primary DC have an IP address starting at 10.230, and in Secondary DC at 10.231.

(SLA) Service Level Agreement , this parameter affects the quality of service of this machine.

Applications . We added the ability to install and configure SQL Server. You must select edition, instance name and collation. It is also possible to set up a Web Server role and more.

Now we need to determine how to store the selected values. We decided that the most convenient format is a JSON file. As I said earlier, ITSM ServiceNow is used in the customer’s environment; the manager, after he has selected all the necessary values, clicks the “order” button and after that ServiceNow sends all the parameters to our PowerShell script (to the back-end ServiceNow), which will create a JSON file. It looks like this:

.\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 


In the body of the CreateConfiguration .ps1 script:

#создаем PowerShell-объект
$config = [ordered]@{} 
#И заполняем его входными параметрами. 
$config.SecurityZone=$SecurityZone


At the end, export our object to a JSON file:

$ServerConfig = New-Object –TypeName PSObject  $config
ConvertTo-Json -InputObject $ServerConfig -Depth 100 | Out-File "C:\Configs\TargetNodes\Build\$($Hostname.ToLower()).json" -Force 


Approximate sample of configuration:

{
    "Hostname":  "dsctest552",
    "SecurityZone":  "trusted",
    "Domain":  "testdomain",
    "Requestor":  "evgeniy.vpro",
    "VM":  {
               "Size":  "Medium",
               "Environment":  "Prod",
               "SLA":  "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":  "E:\\MSSQL",
                                             "UserDB":  "E:\\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
}


You may have noticed that the web form was missing the name of the virtual machine and the IP address. We get these values ​​automatically as follows:

Machine name , ITSM ServiceNow has a special section: CMDB (Configuration Management Data Base), this database stores all records about existing virtual machines, their status, support team, and so on. We have created about 200 backup records with the status Allocated. To get the name for the virtual machine, we make a REST request to the CMDB and get the first “free” entry and change its status from Allocated to Pending install.

IP address and VLAN, we deployed IPAM on our network - this is a built-in feature in Windows Server 2016 that allows you to manage IP addresses on your network. It is not necessary to use all the capabilities of IPAM (DHCP, DNS, AD), but to use it only as a database of IP addresses with a potential extension of functionality. The script that creates the JSON file makes a request to IPAM for the first free IP address in the subnet. And the VLAN subnet (x / 24 subnet) is determined based on the selected SLA, Environment, Trust, and Type values.
The configuration file is ready, all fields are in place, you can create a machine. The question is "how to store credentials for all our scripts?". We use the CredentialManager package . This package works with the built-in Windows Credential Manager API for storing passwords. Example of creating a password:


New-StoredCredential -Target "ESXi" -UserName "testdomain.eu\vmwareadm" -Password "veryultraP@ssw00rd." -Type Generic -Persist LocalMachine


The password will be readable within this machine and account.

$ESXiAdmin = Get-StoredCredential -Type Generic -Target ESXi


We have a server that stores all configurations with GIT, now we can reliably track all changes in configurations: who, what, where, and when.

The scheduled task is configured on this server: check the configuration folder and write all changes to the Windows Event Log.

After 15 minutes, the scheduled task will write to the Windows EventLog that a new configuration file has been detected.

It's time to check out this configuration. First of all, we need to make sure that the file has the correct formatting:

$Configuration=(Get-Content -Raw $File | Out-String | ConvertFrom-Json) 


If everything is good, it's time to start building the machine and run the BuildVM.ps1 script.

In BuildVM.ps1, we verify that the configuration file has a description of all the characteristics of the virtual machine: size, env, sla, type, storage, ram, network.

Be sure to check if there is a machine with the same name in the infrastructure (CheckVM.ps1).
Connect via VMWare PowerShell CLI to our vSphere:

$VmWareAdmin = Get-StoredCredential -Type Generic -Target ESXi
Connect-VIServer -Server "vSphereSrv" -Credential $VmWareAdmin | Out-Null 


Check if there is a machine with the same name in the infrastructure

$VM=Get-VM $server -ErrorAction SilentlyContinue


And disconnect:

Disconnect-VIServer * -Force -Confirm:$false 


Make sure the machine is also not available via WinRM

$ping=Test-NetConnection -ComputerName $Configuration.Hostname -CommonTCPPort WINRM -InformationLevel Quiet -ErrorAction SilentlyContinue 


If $ VM and $ ping are empty, then you can create a new machine. (We handle situations when the machine is already created in ESXi manually or this machine is in another data center.)

A few words about the car. This is a prepared virtual machine image that was finalized by sysprep and converted to a template in our vSphere. The image has a local administrator with a known password, this account does not crash after sysprep, which will allow us to access each machine from this template, and later we will be able to replace this password for security reasons.


Create a virtual machine


Find the corresponding SLR cluster:

$Cluster=Get-Cluster -Name $Configuration.VM.SLA 


Check that we have enough space on the 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
} 


And enough memory:

$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
} 


We take our template

$VMTemplate = Get-Template -Name 'Win2016_Std_x64_Template' 


And create a new virtual machine

New-VM -Name $Configuration.Hostname.ToUpper() -VMHost $VMHost -ResourcePool $ResourcePool -Datastore $Datastore -Template $VMTemplate -Location "AutoDeployed VMs" 


It is important to connect the network interface to a subnet with DHCP enabled.

We start the virtual machine

Start-VM $VM


And save the description of the machine, so that you can then define the machine at the VMWare level.

Set-Annotation -Entity $VM -CustomAttribute "Change request" -Value $Configuration.Request -Confirm:$false
Set-VM $VM -Notes $Configuration.Description -Confirm:$false


The machine has started and now we can find out the received MAC address:

$vMAC = (($VM | Get-NetworkAdapter | Select-Object -Property "MacAddress").MacAddress).Replace(':','')


Save this value to our JSON file

$Configuration.Network.MAC=$VMAC
ConvertTo-Json -InputObject $Configuration -Depth 100 | Out-File "C:\Configs\TargetNodes\Build\$Hostname.json" -Force


Here it's time to commit to our Git, that the machine is created and has its own unique MAC.

The machine starts to initialize (after sysprep), tune the hardware and initial configuration.

Let's wait for our WinRM machine to be available with the EstablishConnection.ps1 script.

First, find out what IP the machine received from 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++
        }
    }
}


Now let's wait for the machine to be available via 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
        }
    }
} 


Machine ready to drive.

Desired State Configuration


To configure the desired configuration, we use the PowerShell part - DSC (Desired State Configuration). The network has a configured DSC Pull Server: dscpull.testdomain.eu.
Below is the configuration of our DSC Pull Server. Good article on setting up 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
        }
    } 


It is available at: https://dscpull.testdomain.eu:8080.

His Endpoint: https://dscpull.testdomain.eu:8080/PSDSCPullserver.svc

All PowerShell 5.1 servers must be installed on all client pull servers
:

$PSVersionTable.PSVersion.Major –lt 5


Install 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" 


A PKI server is also deployed in our network. This condition is for secure encryption of credentials stored in DSC mof files (Mof files are the “language” in which the Pull Server and its clients communicate). When a client tries to register with the Pull Server, you must specify the Thumbprint certificate and later the Pull Server will use this certificate to encrypt passwords. Below we look at how this works.

We import Root CA to our new machine:

  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 


For further work, we need a pair of RSA-keys. We will generate a self-signed certificate and will temporarily work with it.

Now we can register on the 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


Send the first configuration to our machine

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 


The server is automatically renamed and rebooted. Now we can perform the 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,DC=testdomain,DC=eu"
        }
        GroupSet LocalAdmins
        {
            GroupName = @( 'Administrators')
            Ensure = 'Present'
            MembersToInclude = @( 'testdomain-eu\dscstaging' )
        }
    }
}
$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 


Here’s what our mof file looks like:

instance of MSFT_Credential as $MSFT_Credential1ref
{
Password = "-----BEGIN CMS-----\nMIIBsgYJKoZIhvcNAQcDoIIBozCCAZ8CAQAxggFKMIIBRgIBADAuMBoxGDAWBgNVBAMMD1dJTi1H\nNFFKTFFQME4xNQIQOQN77pxew75HU6l7GPn99TANBgkqhkiG9w0BAQcwAASCAQAlhFf7Zs2gJbJEnc1DEK2yWbKcO+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 encrypted credentials from the service account with Domain Admin rights: testdomain.eu \\ service_DomainJoin_001 with a self-signed certificate. The DSC Client decrypts its credentials with its Private Key and applies all configuration modules with the specified domain credentials. In this case, performs a Domain Join to the specified organization unit.

GroupSet LocalAdmins
        {
            GroupName = @( 'Administrators')
            Ensure = 'Present'
            MembersToInclude = @( testdomain-eu\dscstaging' )
        }


This module adds dscstaging to local administrators for further configuration.

After the reboot, we will be able to log into the machine with domain credentials.

We are waiting for the server to receive a certificate from our PKI (we have configured auto enrollment) and in the future we will work with the certificate issued by our 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  


Now you will register again on the Pull Server with the updated thumbprint.

Everything, the domain-joined machine, and we can use it as it is convenient for us.

Installing SQL Server


The JSON file describes the requirements for MS SQL Server, and we also use DSC to install and configure SQL Server. Here is the configuration:

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\Backup')
					    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'
} 

Where $ MSSQL is defined:
$MSSQL=$Configuration.Applications | where {$_.Application -eq "Microsoft SQL Server 2012"}


$ MSSQL.InstanceName - all this is indicated in our Json file. Applying this configuration will install MS SQL Server with all updates in the Updates folder and restart the server if necessary.

The machine is ready.

Service-Now


There are several APIs available in Service-Now . We use Rest API.
To get a list of machines with Allocated status, use the following query:
instance.service-now.com/cmdb_ci_server_list.do?sysparm_query=install_status=16 ^ u_subtype = ^ ORDERBYname
In PowerShell, it looks like this:
$url="https://instance.service-now.com/api/now/table/cmdb_ci_server?sysparm_query=install_status=16^u_subtype=^ORDERBYname"
$uri= new-object System.Uri("https://instance.service-now.com/")
#берем учетные записи нашей сервисной учетной записи
$credentials = (Get-StoredCredential -Type Generic -Target DSC).GetNetworkCredential()
$credentials = new-object System.Net.NetworkCredential $credentials.UserName, $credentials.SecurePassword
Add-Type -AssemblyName System.Net.Http
$handler = New-Object  System.Net.Http.HttpClientHandler
$handler.CookieContainer = New-Object System.Net.CookieContainer
$handler.UseCookies=$true
$handler.Credentials=$credentials
$HttpClient = New-Object System.Net.Http.HttpClient($handler)
$HttpClient.BaseAddress= $uri
$Header = New-Object System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")
$HttpClient.DefaultRequestHeaders.Accept.Clear()
$HttpClient.DefaultRequestHeaders.Accept.Add($Header);
$response=$HttpClient.GetAsync($url)
$respStream=$response.Result.Content.ReadAsStringAsync()
$Servers = $respStream.Result | ConvertFrom-Json
#получаем объекты нашего Configuration Items каталога
$ServersCI=$Servers.result

The first array object is the hostname we need.
If the machine is ready, you can change the status of the machine in Service-Now, for this the UpdateCI.ps1 script:
param(
    $CI,
    [ValidateSet("Allocated","In use","Pending install")]
    $NewStatus='In use'     
)
$url="https://instance.service-now.com/api/now/table/cmdb_ci_server?sysparm_query=name=$CI"
$uri= new-object System.Uri("https://instance.service-now.com/")
$credentials = (Get-StoredCredential -Type Generic -Target DSC).GetNetworkCredential()
$credentials = new-object System.Net.NetworkCredential $credentials.UserName, $credentials.SecurePassword
Add-Type -AssemblyName System.Net.Http
$handler = New-Object  System.Net.Http.HttpClientHandler
$handler.CookieContainer = New-Object System.Net.CookieContainer
$handler.UseCookies=$true
$handler.Credentials=$credentials
$HttpClient = New-Object System.Net.Http.HttpClient($handler)
$HttpClient.BaseAddress= $uri
$Header = New-Object System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")
$HttpClient.DefaultRequestHeaders.Accept.Clear()
$HttpClient.DefaultRequestHeaders.Accept.Add($Header);
$response=$HttpClient.GetAsync($url)
$respStream=$response.Result.Content.ReadAsStringAsync()
$Servers = $respStream.Result | ConvertFrom-Json
$ServerCI=$Servers.result[0]
$update=@{}
if($NewStatus -eq "In use"){
    $update.install_status=1
}
if($NewStatus -eq "Pending install"){
    $update.install_status=4
}
$stringcontent =  New-Object System.Net.Http.StringContent((ConvertTo-Json -InputObject $update -Depth 100),[System.Text.Encoding]::UTF8, "application/json");
$result=$HttpClient.PutAsync("https://instance.service-now.com/api/now/table/cmdb_ci_server/$($ServerCI.sys_id)", $stringcontent)

RET API GET requests are used to get the table and records, to change the PUT / POST request records, in the body of which the fields to be changed.

We have created a convenient tool with a graphical tool similar to Azure Portal, which allows you to manage on-premises infrastructure as convenient as possible for us and our customer.
PS 12/24/2018. It seems all outdated? Time to use Azure DevOps. In the next article I will tell you how to do all this with the help of the Azure DevOps pipelines.