From b89bd7ab5a013c8ec4df082dc7aa4b1fbf2902c1 Mon Sep 17 00:00:00 2001 From: Sam McGeown Date: Fri, 8 Sep 2017 17:40:08 +0100 Subject: [PATCH] Added HA vCenter Deploy script Added HA vCenter Deploy script, configuration JSON file and note with link to my blog post detailing how to use it --- Scripts/ha-vcenter-deploy-template.json | 58 ++++ Scripts/ha-vcenter-deploy.md | 4 + Scripts/ha-vcenter-deploy.ps1 | 381 ++++++++++++++++++++++++ 3 files changed, 443 insertions(+) create mode 100644 Scripts/ha-vcenter-deploy-template.json create mode 100644 Scripts/ha-vcenter-deploy.md create mode 100644 Scripts/ha-vcenter-deploy.ps1 diff --git a/Scripts/ha-vcenter-deploy-template.json b/Scripts/ha-vcenter-deploy-template.json new file mode 100644 index 0000000..3ac0079 --- /dev/null +++ b/Scripts/ha-vcenter-deploy-template.json @@ -0,0 +1,58 @@ +{ + "__version": "0.1", + "__comments": "Configuration for ha-vcenter-deploy.ps1 - www.definit.co.uk", + "target": { + "server": "vcsa.definit.local", + "user": "administrator@vsphere.local", + "password": "VMware1!", + "datacenter": "Lab", + "cluster": "Workload", + "datastore": "vsanDatastore", + "folder": "Nested Labs/HA-vCenter", + "portgroup": "HA-vCenter-Management", + "ha-portgroup": "HA-vCenter-Heartbeat", + "network": { + "netmask": "255.255.255.0", + "gateway": "10.0.11.1", + "prefix": "24", + "dns": "192.168.1.20", + "domain": "definit.local", + "ntp": "192.168.1.1" + } + }, + "sources": { + "VCSAInstaller": "e:\\Pod-Deploy\\vSphere\\VMware-VCSA-all-6.5.0-4944578" + }, + "active": { + "deploymentSize": "small", + "name": "ha-vc-active", + "ip": "10.0.11.10", + "ha-ip": "172.16.1.1", + "hostname": "ha-vc.definit.local", + "rootPassword": "VMware1!", + "sso": { + "domain": "vsphere.local", + "site": "Default-First-Site", + "password": "VMware1!" + }, + "datacenter": "HA-vCenter-Datacenter", + "cluster": "HA-vCenter-Cluster-1", + "distributedSwitch": "HA-vCenter-VDS", + "portgroup": "HA-vCenter-PortGroup" + }, + "cluster": { + "passive-ip": "172.16.1.2", + "passive-name": "ha-vc-passive", + "witness-ip": "172.16.1.3", + "witness-name": "ha-vc-witness", + "ha-mask": "255.255.255.248" + }, + "general": { + "syslog": "192.168.1.26", + "ssh": true, + "log": "ha-vcenter-deploy.log" + }, + "license": { + "vcenter": "7H23H-11111-22222-33333-90ZQN" + } +} \ No newline at end of file diff --git a/Scripts/ha-vcenter-deploy.md b/Scripts/ha-vcenter-deploy.md new file mode 100644 index 0000000..91271ca --- /dev/null +++ b/Scripts/ha-vcenter-deploy.md @@ -0,0 +1,4 @@ +# HA-vCenter-Deploy +PowerShell script to deploy a highly available vCenter Server + +See https://www.definit.co.uk/2017/06/powershell-deploying-vcenter-high-availability-in-advanced-mode/ for details diff --git a/Scripts/ha-vcenter-deploy.ps1 b/Scripts/ha-vcenter-deploy.ps1 new file mode 100644 index 0000000..7a9984a --- /dev/null +++ b/Scripts/ha-vcenter-deploy.ps1 @@ -0,0 +1,381 @@ +param( + [Parameter(Mandatory=$true)] [String]$configFile, + [switch]$deployActive, + [switch]$licenseVCSA, + [switch]$addSecondaryNic, + [switch]$prepareVCHA, + [switch]$clonePassiveVM, + [switch]$cloneWitnessVM, + [switch]$configureVCHA, + [switch]$resizeWitness, + [switch]$createDRSRule +) + +if($psboundparameters.count -eq 1) { + # Only the configFile is passed, set all steps to true + $deployActive = $true + $licenseVCSA = $true + $addSecondaryNic = $true + $prepareVCHA = $true + $clonePassiveVM = $true + $cloneWitnessVM = $true + $configureVCHA = $true + $resizeWitness = $true + $createDRSRule = $true +} + +# Import the PowerCLI and DNS modules +Get-Module -ListAvailable VMware*,DnsServer | Import-Module +if ( !(Get-Module -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) ) { + throw "PowerCLI must be installed" +} +# Written by Sam McGeown @sammcgeown - www.definit.co.uk +# Hat tips and thanks go to... +# William Lam http://www.virtuallyghetto.com/2016/11/vghetto-automated-vsphere-lab-deployment-for-vsphere-6-0u2-vsphere-6-5.html +# http://www.virtuallyghetto.com/2017/01/exploring-new-vcsa-vami-api-wpowercli-part-1.html + +# Get the folder location +$ScriptLocation = Split-Path -Parent $PSCommandPath + +# Import the JSON Config File +$podConfig = (get-content $($configFile) -Raw) | ConvertFrom-Json + +# Path to VCSA Install Sources +$VCSAInstaller = "$($podConfig.sources.VCSAInstaller)" + +# Log File +$verboseLogFile = $podConfig.general.log + +$StartTime = Get-Date + +Function Write-Log { + param( + [Parameter(Mandatory=$true)] + [String]$message, + [switch]$Warning, + [switch]$Info + ) + $timeStamp = Get-Date -Format "dd-MM-yyyy hh:mm:ss" + Write-Host -NoNewline -ForegroundColor White "[$timestamp]" + if($Warning){ + Write-Host -ForegroundColor Yellow " WARNING: $message" + } elseif($Info) { + Write-Host -ForegroundColor White " $message" + }else { + Write-Host -ForegroundColor Green " $message" + } + $logMessage = "[$timeStamp] $message" | Out-File -Append -LiteralPath $verboseLogFile +} + +function Get-VCSAConnection { + param( + [string]$vcsaName, + [string]$vcsaUser, + [string]$vcsaPassword + ) + $existingConnection = $global:DefaultVIServers | where-object -Property Name -eq -Value $vcsaName + if($existingConnection -ne $null) { + return $existingConnection; + } else { + $connection = Connect-VIServer -Server $vcsaName -User $vcsaUser -Password $vcsaPassword -WarningAction SilentlyContinue; + if($connection -ne $null) { + return $connection; + } else { + throw "Unable to connect to $($vcsaName)..." + } + } +} + +function Close-VCSAConnection { + param( + [string]$vcsaName + ) + if($vcsaName.Length -le 0) { + if($Global:DefaultVIServers -ne $null) { + Disconnect-VIServer -Server $Global:DefaultVIServers -Confirm:$false -ErrorAction SilentlyContinue + } + } else { + $existingConnection = $global:DefaultVIServers | where-object -Property Name -eq -Value $vcsaName + if($existingConnection -ne $null) { + Disconnect-VIServer -Server $existingConnection -Confirm:$false; + } else { + Write-Warning -Message "Could not find an existing connection named $($vcsaName)" + } + } +} + +function Get-PodFolder { + param( + $vcsaConnection, + [string]$folderPath + ) + $folderArray = $folderPath.split("/") + $parentFolder = Get-Folder -Server $vcsaConnection -Name vm + foreach($folder in $folderArray) { + $folderExists = Get-Folder -Server $vcsaConnection | Where-Object -Property Name -eq -Value $folder + if($folderExists -ne $null) { + $parentFolder = $folderExists + } else { + $parentFolder = New-Folder -Name $folder -Location $parentFolder + } + } + return $parentFolder +} + + +Close-VCSAConnection + +if($deployActive) { + Write-Log "#### Deploying Active VCSA ####" + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + $pCluster = Get-Cluster -Name $podConfig.target.cluster -Server $pVCSA + $pDatastore = Get-Datastore -Name $podConfig.target.datastore -Server $pVCSA + $pPortGroup = Get-VDPortgroup -Name $podConfig.target.portgroup -Server $pVCSA + $pFolder = Get-PodFolder -vcsaConnection $pVCSA -folderPath $podConfig.target.folder + + Write-Log "Disabling DRS on $($podConfig.target.cluster)" + $pCluster | Set-Cluster -DrsEnabled:$true -DrsAutomationLevel:PartiallyAutomated -Confirm:$false | Out-File -Append -LiteralPath $verboseLogFile + + + Write-Log "Creating DNS Record" + Add-DnsServerResourceRecordA -Name $podConfig.active.name -ZoneName $podConfig.target.network.domain -AllowUpdateAny -IPv4Address $podConfig.active.ip -ComputerName "192.168.1.20" -CreatePtr -ErrorAction SilentlyContinue + + Write-Log "Deploying VCSA" + $config = (Get-Content -Raw "$($VCSAInstaller)\vcsa-cli-installer\templates\install\embedded_vCSA_on_VC.json") | convertfrom-json + $config.'new.vcsa'.vc.hostname = $podConfig.target.server + $config.'new.vcsa'.vc.username = $podConfig.target.user + $config.'new.vcsa'.vc.password = $podConfig.target.password + $config.'new.vcsa'.vc.datacenter = @($podConfig.target.datacenter) + $config.'new.vcsa'.vc.datastore = $podConfig.target.datastore + $config.'new.vcsa'.vc.target = @($podConfig.target.cluster) + $config.'new.vcsa'.vc.'deployment.network' = $podConfig.target.portgroup + $config.'new.vcsa'.os.'ssh.enable' = $podConfig.general.ssh + $config.'new.vcsa'.os.password = $podConfig.active.rootPassword + $config.'new.vcsa'.appliance.'thin.disk.mode' = $true + $config.'new.vcsa'.appliance.'deployment.option' = $podConfig.active.deploymentSize + $config.'new.vcsa'.appliance.name = $podConfig.active.name + $config.'new.vcsa'.network.'system.name' = $podConfig.active.hostname + $config.'new.vcsa'.network.'ip.family' = "ipv4" + $config.'new.vcsa'.network.mode = "static" + $config.'new.vcsa'.network.ip = $podConfig.active.ip + $config.'new.vcsa'.network.'dns.servers'[0] = $podConfig.target.network.dns + $config.'new.vcsa'.network.prefix = $podConfig.target.network.prefix + $config.'new.vcsa'.network.gateway = $podConfig.target.network.gateway + $config.'new.vcsa'.sso.password = $podConfig.active.sso.password + $config.'new.vcsa'.sso.'domain-name' = $podConfig.active.sso.domain + $config.'new.vcsa'.sso.'site-name' = $podConfig.active.sso.site + + Write-Log "Creating VCSA JSON Configuration file for deployment" + + $config | ConvertTo-Json | Set-Content -Path "$($ENV:Temp)\active.json" + if((Get-VM | Where-Object -Property Name -eq -Value $podConfig.active.name) -eq $null) { + Write-Log "Deploying OVF, this may take a while..." + Invoke-Expression "$($VCSAInstaller)\vcsa-cli-installer\win32\vcsa-deploy.exe install --no-esx-ssl-verify --accept-eula --acknowledge-ceip $($ENV:Temp)\active.json"| Out-File -Append -LiteralPath $verboseLogFile + $vcsaDeployOutput | Out-File -Append -LiteralPath $verboseLogFile + Write-Log "Moving $($podConfig.active.name) to $($podConfig.target.folder)" + if((Get-VM | where {$_.name -eq $podConfig.active.name}) -eq $null) { + throw "Could not find VCSA VM. The script was unable to find the deployed VCSA" + } + Get-VM -Name $podConfig.active.name | Move-VM -Destination $pFolder | Out-File -Append -LiteralPath $verboseLogFile + } else { + Write-Log "VCSA exists, skipping" -Warning + } + Close-VCSAConnection +} + + +if($licenseVCSA) { + Write-Log "#### Configuring VCSA ####" + Write-Log "Getting connection to the new VCSA" + $nVCSA = Get-VCSAConnection -vcsaName $podConfig.active.ip -vcsaUser "administrator@$($podConfig.active.sso.domain)" -vcsaPassword $podConfig.active.sso.password + + Write-Log "Installing vCenter License" + $serviceInstance = Get-View ServiceInstance -Server $nVCSA + $licenseManagerRef=$serviceInstance.Content.LicenseManager + $licenseManager=Get-View $licenseManagerRef + $licenseManager.AddLicense($podConfig.license.vcenter,$null) | Out-File -Append -LiteralPath $verboseLogFile + $licenseAssignmentManager = Get-View $licenseManager.LicenseAssignmentManager + Write-Log "Assigning vCenter Server License" + try { + $licenseAssignmentManager.UpdateAssignedLicense($nVCSA.InstanceUuid, $podConfig.license.vcenter, $null) | Out-File -Append -LiteralPath $verboseLogFile + } + catch { + $ErrorMessage = $_.Exception.Message + Write-Log $ErrorMessage -Warning + } + Close-VCSAConnection -vcsaName $podConfig.active.ip +} + +if($addSecondaryNic) { + Write-Log "#### Adding HA Network Adapter ####" + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + + if((Get-VM -Server $pVCSA -Name $podConfig.active.name | Get-NetworkAdapter).count -le 1) { + Write-Log "Adding HA interface" + Get-VM -Server $pVCSA -Name $podConfig.active.name | New-NetworkAdapter -Portgroup (Get-VDPortgroup -Name $podConfig.target."ha-portgroup") -Type Vmxnet3 -StartConnected | Out-File -Append -LiteralPath $verboseLogFile + } + Close-VCSAConnection + + $nVCSA = Get-VCSAConnection -vcsaName $podConfig.active.ip -vcsaUser "administrator@$($podConfig.active.sso.domain)" -vcsaPassword $podConfig.active.sso.password + + Write-Log "Configuring HA interface" + $CisServer = Connect-CisServer -Server $podConfig.active.ip -User "administrator@$($podConfig.active.sso.domain)" -Password $podConfig.active.sso.password + + $ipv4API = (Get-CisService -Name 'com.vmware.appliance.techpreview.networking.ipv4') + $specList = $ipv4API.Help.set.config.CreateExample() + $createSpec = [pscustomobject] @{ + address = $podConfig.active."ha-ip"; + default_gateway = ""; + interface_name = "nic1"; + mode = "is_static"; + prefix = "29"; + } + $specList += $createSpec + $ipv4API.set($specList) + Close-VCSAConnection +} + +if($prepareVCHA) { + Write-Log "#### Preparing vCenter HA mode ####" + + $nVCSA = Get-VCSAConnection -vcsaName $podConfig.active.ip -vcsaUser "administrator@$($podConfig.active.sso.domain)" -vcsaPassword $podConfig.active.sso.password + + Write-Log "Preparing vCenter HA" + $ClusterConfig = Get-View failoverClusterConfigurator + + $PassiveIpSpec = New-Object VMware.Vim.CustomizationFixedIp + $PassiveIpSpec.IpAddress = $podConfig.cluster."passive-ip" + + $PassiveNetwork = New-object VMware.Vim.CustomizationIPSettings + $PassiveNetwork.Ip = $PassiveIpSpec + $PassiveNetwork.SubnetMask = $podConfig.cluster."ha-mask" + + $PassiveNetworkSpec = New-Object Vmware.Vim.PassiveNodeNetworkSpec + $PassiveNetworkSpec.IpSettings = $PassiveNetwork + + $WitnessIpSpec = New-Object VMware.Vim.CustomizationFixedIp + $WitnessIpSpec.IpAddress = $podConfig.cluster."witness-ip" + + $WitnessNetwork = New-object VMware.Vim.CustomizationIPSettings + $WitnessNetwork.Ip = $WitnessIpSpec + $WitnessNetwork.SubnetMask = $podConfig.cluster."ha-mask" + + $WitnessNetworkSpec = New-Object VMware.Vim.NodeNetworkSpec + $WitnessNetworkSpec.IpSettings = $WitnessNetwork + + $ClusterNetworkSpec = New-Object VMware.Vim.VchaClusterNetworkSpec + $ClusterNetworkSpec.WitnessNetworkSpec = $WitnessNetworkSpec + $ClusterNetworkSpec.PassiveNetworkSpec = $PassiveNetworkSpec + + $PrepareTask = $ClusterConfig.prepareVcha_task($ClusterNetworkSpec) + + Close-VCSAConnection +} + +if($clonePassiveVM) { + Write-Log "#### Cloning VCSA for Passive Node ####" + + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + $pVMHost = Get-Random (Get-VMhost -Location $podConfig.target.cluster) + $pFolder = Get-PodFolder -vcsaConnection $pVCSA -folderPath $podConfig.target.folder + + $activeVM = Get-VM -Name $podConfig.active.name + $CloneSpecName = "vCHA_ClonePassive" + + Write-Log "Creating customization spec" + # Clean up any old spec + Get-OSCustomizationSpec -Name $CloneSpecName -ErrorAction SilentlyContinue | Remove-OSCustomizationSpec -Confirm:$false -ErrorAction SilentlyContinue | Out-File -Append -LiteralPath $verboseLogFile + New-OSCustomizationSpec -Name $CloneSpecName -OSType Linux -Domain $podConfig.target.network.domain -NamingScheme fixed -DnsSuffix $podConfig.target.network.domain -NamingPrefix $podConfig.active.hostname -DnsServer $podConfig.target.network.dns -Type NonPersistent | Out-File -Append -LiteralPath $verboseLogFile + Get-OSCustomizationNicMapping -OSCustomizationSpec $CloneSpecName | Set-OSCustomizationNicMapping -IpMode UseStaticIP -IpAddress $podConfig.active.ip -SubnetMask $podConfig.target.network.netmask -DefaultGateway $podConfig.target.network.gateway | Out-File -Append -LiteralPath $verboseLogFile + New-OSCustomizationNicMapping -OSCustomizationSpec $CloneSpecName -IpMode UseStaticIP -IpAddress $podConfig.cluster."passive-ip" -SubnetMask $podConfig.cluster."ha-mask" -DefaultGateway $podConfig.target.network.gateway | Out-File -Append -LiteralPath $verboseLogFile + + Write-Log "Cloning Active VCSA to Passive VCSA" + $passiveVM = New-VM -Name $podConfig.cluster."passive-name" -VM $activeVM -OSCustomizationSpec $CloneSpecName -VMhost $pVMHost -Server $pVCSA -Location $pFolder | Start-VM | Out-File -Append -LiteralPath $verboseLogFile + + # Ensure the network adapters are connected + $passiveVM | Get-NetworkAdapter | Set-NetworkAdapter -Connected:$true -Confirm:$false + + Write-Log "Waiting for VMware Tools" + $passiveVM | Wait-Tools + + Close-VCSAConnection +} + +if($cloneWitnessVM) { + Write-Log "#### Cloning VCSA for Witness Node ####" + + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + $pVMHost = Get-Random (Get-VMhost -Location $podConfig.target.cluster) + $pFolder = Get-PodFolder -vcsaConnection $pVCSA -folderPath $podConfig.target.folder + + $activeVM = Get-VM -Name $podConfig.active.name + $CloneSpecName = "vCHA_CloneWitness" + + Write-Log "Creating customization spec" + # Clean up any old spec + Get-OSCustomizationSpec -Name $CloneSpecName -ErrorAction SilentlyContinue | Remove-OSCustomizationSpec -Confirm:$false -ErrorAction SilentlyContinue | Out-File -Append -LiteralPath $verboseLogFile + New-OSCustomizationSpec -Name $CloneSpecName -OSType Linux -Domain $podConfig.target.network.domain -NamingScheme fixed -DnsSuffix $podConfig.target.network.domain -NamingPrefix $podConfig.active.hostname -DnsServer $podConfig.target.network.dns -Type NonPersistent | Out-File -Append -LiteralPath $verboseLogFile + New-OSCustomizationNicMapping -OSCustomizationSpec $CloneSpecName -IpMode UseStaticIP -IpAddress $podConfig.cluster."witness-ip" -SubnetMask $podConfig.cluster."ha-mask" -DefaultGateway $podConfig.target.network.gateway | Out-File -Append -LiteralPath $verboseLogFile + + Write-Log "Cloning Active VCSA to Witness VCSA" + $witnessVM = New-VM -Name $podConfig.cluster."witness-name" -VM $activeVM -OSCustomizationSpec $CloneSpecName -VMhost $pVMHost -Server $pVCSA -Location $pFolder | Start-VM | Out-File -Append -LiteralPath $verboseLogFile + + # Ensure the network adapters are connected + $witnessVM | Get-NetworkAdapter | Set-NetworkAdapter -Connected:$true -Confirm:$false + + Write-Log "Waiting for VMware Tools" + $witnessVM | Wait-Tools + + Close-VCSAConnection +} + +if($configureVCHA) { + Write-Log "#### Configuring vCenter HA mode ####" + + $nVCSA = Get-VCSAConnection -vcsaName $podConfig.active.ip -vcsaUser "administrator@$($podConfig.active.sso.domain)" -vcsaPassword $podConfig.active.sso.password + + $ClusterConfig = Get-View failoverClusterConfigurator + $ClusterConfigSpec = New-Object VMware.Vim.VchaClusterConfigSpec + $ClusterConfigSpec.PassiveIp = $podConfig.cluster."passive-ip" + $ClusterConfigSpec.WitnessIp = $podConfig.cluster."witness-ip" + $ConfigureTask = $ClusterConfig.configureVcha_task($ClusterConfigSpec) + Write-Log "Waiting for cluster configuration task" + Start-Sleep -Seconds 30 + + Close-VCSAConnection -vcsaName $podConfig.active.ip +} + +if($resizeWitness) { + Write-Log "#### Resizing Witness Node ####" + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + + $witnessVM = Get-VM -Name $podConfig.cluster."witness-name" + Write-Log "Waiting for Witness node to shut down" + $witnessVM | Stop-VMGuest -Confirm:$false | Out-File -Append -LiteralPath $verboseLogFile + do { + Start-Sleep -Seconds 3 + $witnessVM = Get-VM -Name $podConfig.cluster."witness-name" + } until($witnessVM.PowerState -eq "Poweredoff") + Write-Log "Setting CPU and Memory" + $witnessVM | Set-VM -MemoryGB 1 -NumCpu 1 -Confirm:$false | Out-File -Append -LiteralPath $verboseLogFile + Write-Log "Starting Witness VM" + $witnessVM | Start-VM | Out-File -Append -LiteralPath $verboseLogFile + Close-VCSAConnection +} + +if($createDRSRule) { + Write-Log "#### Creating DRS Rule ####" + $pVCSA = Get-VCSAConnection -vcsaName $podConfig.target.server -vcsaUser $podConfig.target.user -vcsaPassword $podConfig.target.password + $pCluster = Get-Cluster $podConfig.target.cluster + $vCHA = Get-VM -Name $podConfig.active.name,$podConfig.cluster."passive-name",$podConfig.cluster."witness-name" + New-DRSRule -Name "vCenter HA" -Cluster $pCluster -VM $vCHA -KeepTogether $false | Out-File -Append -LiteralPath $verboseLogFile + Write-Log "Enabling DRS on $($podConfig.target.cluster)" + $pCluster | Set-Cluster -DrsEnabled:$true -DrsAutomationLevel:FullyAutomated -Confirm:$false | Out-File -Append -LiteralPath $verboseLogFile + Close-VCSAConnection +} + + +$EndTime = Get-Date +$duration = [math]::Round((New-TimeSpan -Start $StartTime -End $EndTime).TotalMinutes,2) + +Write-Log "Pod Deployment Completed in $($duration) minutes" \ No newline at end of file