Bulk deploy Veeam Linux Proxies

In version 10 of Veeam Backup & Replication a lot of Linux love has been put in to the product. To me, one of the most interesting things is the new functionality where you can assign a Linux VM the role of a backup proxy. Historically the proxy functionality has been restricted to Windows OS only. The linux proxy does not come as a prebuilt Veeam appliance, just as with Windows proxies you still need to secure and patch the operating system. The rational behind it is easy to understand, most organizations already have some sort of patch management system in place to leverage for their production workload and you would want to have the same level of patching and security on your proxies as you have for all other workloads. I thought it would interesting to see if you could automate the deployment of proxy servers a bit. Sure enough it is a fairly simple process since Veeam Backup & Replication has a great PowerShell extension which will let you automate almost all of the tasks you can do in the GUI. As far as automating proxy servers go, my colleague Anthony Spiteri has a comprehensive project called “Project Ōtosukēru” based on Terraform which may be of interest to you as well.

As a side note, Veeam Backup & Replication version 10 contains a wide variety of new features and updates, the Veeam Vanguards made a list of their top features.

Mind you, the code below should serve as an indication of what you can do – as an example if you will. However, you use at at your own risk. I’ve put in an option to deploy to a test/lab environment (if you are lucky enough to have a lab environment) so you can start testing it out in a safe environment.

So why not combine this small project with the small Linux operating system VMware provides called PhotonOS? PhotonOS is a stripped down linux operating system we can use. Now you can certainly download an iso image and do a traditional install and set it up just the way you’d like and then convert it to a template in vSphere and make use of it that way. However I thought it would be more interesting to see how much you can automate. So instead I’m just downloading the prebuilt Linux appliance and using that as the source for my proxies.

The script performs 4 different tasks:

  • It starts by importing the settings you specified in the configuration file (config.json)
  • Download required components if they’re not available. Two components are needed: The PhotonOS OVA and a powershell script by William Lam called VMKeystrokes.ps1. VMKeystrokes is needed to change the initial password of the appliance.
  • Let’s you choose where to deploy the Linux appliance, you can pick a single host, all hosts in a specific cluster or all hosts in a specific datacenter. It will also let you choose if you’d like to deploy to a test/lab environment or to a production environment (settings fetched from config.json). The script will try to ping the IP range configured in config.json to verify that they are not being used. It then deploys the appliance to the selected target(s). Once deployed the appliance will be configured (Connecting to the specified network portgroup in vSphere, Set vCPU and vRAM, set Static IP address, firewall ports opened, configuring timezone).
  • The Linux server will then be added to Veeam Backup & Replication as a managed server. The managed server will then have the role Backup Proxy assigned to it, concurrent tasks will be set to whatever amount of vCPU’s you’ve assigned to the VM (from the configuration file).

Config.json explained
“location”: – Where files be downloaded and working directory
“ovasource”: – URL to the OVA
“ovaname”: – Name of the OVA
“ProxyBaseName”: – Base name of Proxy VM to be deployed
“ProxyvCPU”: – vCPUs to assign the Proxy VM
“ProxyvRAM”: – vRAM to assign the Proxy VM
“sshuser”: “root” – Logon account for the Proxy VM
“ovainitialpassword”: – Default password of the OVA is “changeme”
“newsshpassword”: – New password to apply for the root user on the Proxy VM

“Prod”: {
“vbrserver”: – IP adress to Veeam Backup & Replication server
“vcserver”: – IP adress to VMware vCenter Server
“network”: – Portgroup the Proxy VM will be connected to
“startipaddress”: – Start of IP pool the Proxy VMs will use
“endipaddress”: – End of IP pool the Proxy VMs till use
“vbruser”: – User to connect to Veeam Backup & Replication
“vcuser”: – User to connect to VMware vCenter Server
“vcdatacenter”: – Datacenter in vCenter to use
“gateway”: – IP address of gateway for the Proxy VM
“dns”: – IP address of DNS for the Proxy VM
“domains”: – DNS Search Domains for the Proxy VM
“ntpserver”: – NTP Server for the Proxy VM to use

The Proxy VM will be named with ProxyBaseName + IP address of the target host to where the VM is deployed with all dots replaced by dashes, i.e “VeeamProxy_10-10-50-38”.

Save the two files to c:\temp and then you’re good to go!
No more bits needed to be downloaded, the script will download what it’s required. The only thing you need to change is the settings in config.json to reflect your environment.

Download Deploy_Veeam_Linux_Proxy.ps1
Download config.json

The script will add proxies based on IP addresses to Veeam Backup & Replication in this version, but per Veeam recommendation FQDNs should be used.


  1. # Deploy_Veeam_Linux_Proxy.ps1
  2. # Version: 1.0
  3. # Author: Niclas Borgstrom, Veeam Cloud Solution Architect EMEA
  4. #
  5. # Synopsis: 
  6. # This script will download a linux appliance (based on VMware PhotonOS)
  7. # and deploy the appliance to a host, cluster or datacenter of your choice.
  8. # It will also download VMKeystrokes.ps1 by William Lam, VMware in order to 
  9. # do the initial configuration of PhotonOS.
  10. # When the OVA is deployed it will be configured with a static IP address,
  11. # firewall will be opened for Veeam Backup Proxy specific ports, vCPU and
  12. # vRAM will be changed and the VM will be added to Veeam Backup & Replication
  13. # as a managed server and then be assigned the proxy role. Concurrent tasks
  14. # will be set according to the vCPUs assigned to the VM.
  15. #
  16. # You can choose to deploy either to a production- or a test environment, if 
  17. # available. The OVA will only be deployed to powered on hosts.
  18. #
  19. # Disclaimer:
  20. # The script is for demonstration purpose only, run it at your own risk
  21. #
  22. # Credits
  23. # 'My-Logger' function by William Lam, VMware
  24. #
  25. # Prereqs:
  26. # - VMware environment managed by vCenter
  27. # - Veeam Backup & Replication already installed on a server.
  28. # - Internet access to download PhotonOS and VMKeystrokes.ps1 to change the default 
  29. #    password on the PhotonOS appliance.
  30. #
  31. # Limitations:
  32. # - Linux proxy can't be used as a 'Guest Interaction Proxy'
  33. # - Linux proxy can't be used as a 'File Backup Proxy'
  34. # - Linux Proxy only supports Hot-Add/Virtual appliance mode
  35. #
  36. # Changelog:
  37. # v 1.0: Initial release
  39. $global:BaseDirectory = "C:\temp\"
  40. $global:BaseConfig = "config.json"
  41. $verboseLogFile = $global:BaseDirectory + "Veeam_Linux_Proxy_Deployment_" + $(Get-Date -Format "yyyy-MM-dd_HH-mm") + ".log"
  43. Function My-Logger {
  44.     param(
  45.     [Parameter(Mandatory=$true)]
  46.     [String]$message
  47.     )
  49.     $timeStamp = Get-Date -Format "yyyy-MM-dd_HH:mm:ss"
  51.     Write-Host -NoNewline -ForegroundColor White "[$timestamp]"
  52.     Write-Host -ForegroundColor Green " $message"
  53.     $logMessage = "[$timeStamp] $message"
  54.     $logMessage | Out-File -Append -LiteralPath $verboseLogFile
  55. }
  57. # Load and parse the JSON configuration file 
  58. My-Logger "Loading JSON configuration file $BaseDirectory$BaseConfig"
  59. try { 
  60. 	$global:Config = Get-Content "$BaseDirectory$BaseConfig" -RAW -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | ConvertFrom-Json -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
  62. } catch { 
  63. 	My-Logger "[ERROR] The configuration files is missing!"
  64. 	Break
  65. } 
  67. # Check the configuration
  68. if (!($Config)) {
  69.     My-Logger "[ERROR] The configuration file is missing!"
  70.     Break
  71. }
  73. $location = $Config.General.location
  74. $ovasource = $Config.General.ovasource
  75. $ovaname = $Config.General.ovaname
  76. $proxybasename = $Config.General.ProxyBaseName
  77. $proxyvcpu = $Config.General.ProxyvCPU
  78. $proxyvram = $Config.General.ProxyvRAM
  79. $sshuser = $Config.General.sshuser
  80. $ovainitialpassword = $Config.General.ovainitialpassword
  81. $newsshpassword = $Config.General.newsshpassword
  83. # Set temp location
  84. Set-Location $location
  86. try {
  87. 	Add-PSSnapin -Name VeeamPSSnapin -ErrorAction Stop
  88. 	Import-Module -Name VMware.VimAutomation.Core -ErrorAction Stop
  89. } catch {
  90. 	My-Logger "[ERROR] PowerCLI or VeeamPSSnapin not installed."
  91. 	Break
  92. }
  94. # Verify that VMKeystrokes.ps1 is available
  95. My-Logger "Checking VMkeystrokes.ps1..."
  96. Clear-Host
  97. try { 
  98. 	# Dot source VMKeystrokes.ps1 if it's available
  99. 	. .\VMKeystrokes.ps1
  101. } catch {
  102. 	# If VMKeystrokes.ps1 isn't available, ask if it should be downloaded
  103. 	My-Logger "  [WARNING] Unable to find 'VMKeystrokes.ps1' in $location"
  104. 	$downloadvmkeystrokes = if(($downloadvmkeystrokes = Read-Host "`nWould you like to download it (Y/N) or press enter to accept default value [Y]") -eq '') {"Y"} else {$downloadvmkeystrokes}
  105. 	if ($downloadvmkeystrokes.ToLower() -eq "y") {
  106. 		My-Logger "  Downloading 'VMKeystrokes.ps1'"
  107. 		Invoke-WebRequest -Uri "https://raw.githubusercontent.com/lamw/vghetto-scripts/master/powershell/VMKeystrokes.ps1" -OutFile ".\VMKeystrokes.ps1"
  108. 		. .\VMKeystrokes.ps1
  109. 	} else {
  110. 		My-Logger "  [ERROR] Can't continue without VMkeystrokes.ps1, exiting..."
  111. 		Break
  112. 	}
  113. }
  115. $targethosts = $null
  117. # Verify that PhotonOS it available
  118. My-Logger "Checking PhotonOS OVA..."
  119. Clear-Host
  120. if(!(Test-Path ($location + "\" + $ovaname))) {
  121. 	My-Logger "  [WARNING] Unable to find PhotonOS OVA: $($location + "\" + $ovaname)"
  122. 	$downloadova = if(($downloadova = Read-Host "`nWould you like to download it (Y/N) or press enter to accept default value [Y]") -eq '') {"Y"} else {$downloadova}
  123. 	if ($downloadova.ToLower() -eq "y") {
  124. 		My-logger "  Downloading $($location + "\" + $ovaname)"	
  125. 		Invoke-WebRequest -Uri $ovasource -OutFile $($BaseDirectory + "\" + $ovaname)
  126. 	} else {
  127. 		My-Logger "  [ERROR] PhotonOS OVA not available!"
  128. 		Break
  129. 	}
  130. }
  132. # Select where to deploy Linux Proxies
  133. Clear-Host
  134. Write-Output "`nWould you like to deploy to prod or test:`n"
  135. Write-Output "1. Production environment - $($Config.Prod.vcserver)"
  136. Write-Output "2. Test environment - $($Config.Test.vcserver)"
  137. $selectenvironment = if(($selectenvironment = Read-Host "`nSelect item or press enter to accept default value [1]") -eq '') {"1"} else {$selectenvironment}
  139. # Populate strings based on selected deployment type
  140. if ($selectenvironment -eq "1") {
  141. 	My-Logger "Deploying to Production environment $($Config.Prod.vcserver)"
  142. 	$vbrserver = $Config.Prod.vbrserver
  143. 	$vcserver = $Config.Prod.vcserver
  144. 	$network = $Config.Prod.network
  145. 	$startipaddress = $Config.Prod.startipaddress
  146. 	$endipaddress = $Config.Prod.endipaddress
  147. 	$vbruser = $Config.Prod.vbruser
  148. 	$vcuser = $Config.Prod.vcuser
  149. 	$vcdatacenter = $Config.Prod.vcdatacenter
  150. 	$gateway = $Config.Prod.gateway
  151. 	$dns = $Config.Prod.dns
  152. 	$domains = $Config.Prod.domains
  153. 	$ntpserver = $Config.Prod.ntpserver
  154. } elseif ($selectenvironment -eq "2"){
  155. 	My-Logger "Deploying to Test environment $($Config.Test.vcserver)"
  156. 	$vbrserver = $Config.Test.vbrserver
  157. 	$vcserver = $Config.Test.vcserver
  158. 	$network = $Config.Test.network
  159. 	$startipaddress = $Config.Test.startipaddress
  160. 	$endipaddress = $Config.Test.endipaddress
  161. 	$vbruser = $Config.Test.vbruser
  162. 	$vcuser = $Config.Test.vcuser
  163. 	$vcdatacenter = $Config.Test.vcdatacenter
  164. 	$gateway = $Config.Test.gateway
  165. 	$dns = $Config.Test.dns
  166. 	$domains = $Config.Test.domains
  167. 	$ntpserver = $Config.Test.ntpserver
  168. } else {
  169. 	My-Logger "[ERROR] Wrong selection..."
  170. }
  172. # Function to build a dynamic menu for infrastructure components (Datacenter, Cluster or Host)
  173. Function Get-HostList {
  174.     Param (
  175.         [Parameter(Mandatory)]
  176.         [ValidateNotNullOrEmpty()]
  177.         $ListItem,
  179.         [Parameter(Mandatory)]
  180.         [ValidateNotNullOrEmpty()]
  181.         $GetItem
  182.     )
  184.     try {
  185.         $menu = @{}
  186.         For ($i=1;$i -le $ListItem.count; $i++) { 
  187.             Write-Host "$i. $($ListItem[$i-1].Name)"
  188.             $menu.Add($i,($ListItem[$i-1].Name)) 
  189.         }
  190.         [int]$ans = if (($ans = Read-Host "`nSelect item or press enter to accept default value [1]") -eq '') {"1"} else {$ans}
  191.         $Selection = $menu.Item($ans); Invoke-Expression $($GetItem)
  192.     } catch {
  193.         Write-Host "The selection you made is unavailable, please make a valid selection between 1 and $($i-1)..."
  194.     }
  195. }
  197. # Connect to VMware vCenter Server and Veeam Backup & Replication
  198. #Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false
  199. My-Logger "Connecting to VMware vCenter Server $vcserver"
  200. $vicredential = Get-Credential -UserName $vcuser -Message "Enter password for VMware vCenter server: $vcserver"
  201. Connect-VIServer $vcserver -Credential $vicredential -WarningAction SilentlyContinue
  203. My-Logger "Connecting to Veeam Backup & Replication Server $vbrserver"
  204. $vbrcredential = Get-Credential -UserName $vbruser -Message "Enter password for Veeam Backup & Replication server: $vbrserver"
  205. Connect-VBRServer -Server $vbrserver -Credential $vbrcredential -WarningAction SilentlyContinue
  207. # Select if scope of deployment
  208. Clear-Host
  209. Write-Output "`nWould you like to deploy a proxy to:`n"
  210. Write-Output "1. A Single Host"
  211. Write-Output "2. A Cluster (to all hosts in a specific cluster)"
  212. Write-Output "3. A Datacenter (to all hosts in a specific datacenter)"
  213. [int]$option = if(($option = Read-Host "`nSelect item or press enter to accept default value [2]") -eq '') {"2"} else {$option}
  215. if ($option -eq "1") {
  216. 	$MyQuery = (Get-Datacenter $vcdatacenter | Get-VMHost)
  217. 	$MyList = "Get-VMHost `$Selection | Where-Object -FilterScript { `$_.PowerState -eq 'PoweredOn' }"
  218. 	My-Logger "Deploying to Host"
  219. } elseif ($option -eq "2"){
  220. 	$MyQuery = (Get-Datacenter $vcdatacenter | Get-Cluster)
  221. 	$MyList = "Get-Cluster `$Selection | Get-VMHost | Where-Object -FilterScript { `$_.PowerState -eq 'PoweredOn' }"
  222. 	My-Logger "Deploying to Cluster"
  223. } elseif ($option -eq "3"){
  224. 	$MyQuery = (Get-Datacenter)
  225. 	$MyList = "Get-Datacenter `$Selection | Get-Cluster | Get-VMHost | Where-Object -FilterScript { `$_.PowerState -eq 'PoweredOn' }"
  226. 	My-Logger "Deploying to Datacenter"
  227. } else {
  228. 	My-Logger "Wrong selection..."
  229. }
  231. Clear-Host
  232. Write-Output "Select where to deploy Linux Proxies:`n"	
  233. $targethosts = (Get-HostList -ListItem $MyQuery -GetItem $MyList)
  235. # Verify the selected object actually contains powered on hosts
  236. if ($null -eq $targethosts) {
  237. 	My-Logger "No powered on hosts found..."
  238. 	Disconnect-VBRServer
  239. 	Disconnect-VIServer $vcserver -Confirm:$false
  240. 	Break
  241. }
  243. # Check and verify assigned IP range from config.json and verify correct IP format
  244. $deployedproxies = [ordered]@{}
  245. $iplistunverified = @()
  246. $iplistverified = @()
  247. try {
  248.     [IPADDRESS]$startip = $startipaddress
  249.     [IPADDRESS]$endip = $endipaddress
  250.     $iplistunverified += $($startip.GetAddressBytes()[3])..$($endip.GetAddressBytes()[3]) | ForEach-Object {$($startip.GetAddressBytes()[0],$startip.GetAddressBytes()[1],$startip.GetAddressBytes()[2], $_ -join '.')}
  252.     # Verify if IP addresses are available
  253.     My-Logger "Available IP addresses in range $startip to $endip :"
  254.     Clear-Host
  255.     Write-Output "Checking available IP addresses..."
  256.     foreach ($checkedip in $iplistunverified) {
  257.         if (!($(Test-Connection -BufferSize 2 -TTL 5 -ComputerName $checkedip -quiet -count 1))) {
  258.             $iplistverified += $checkedip
  259.             My-Logger "  $checkedip - available"
  260.         } else {
  261.             My-Logger "  $checkedip - in use"
  262.         }
  263.     }
  265.     if ($iplistverified.Count -lt $targethosts.count) {
  266.         My-Logger "[ERROR] Not enough IP addresses assigned, $($targethosts.count) IP addresses are needed. Please update config.json"
  267.         Disconnect-VBRServer
  268.         Disconnect-VIServer $vcserver -Confirm:$false
  269.         Break
  270.     }
  271. } catch {
  272. 	#Clear-Host
  273. 	My-Logger "[ERROR] IP address assignment is incorrect, verify in config.json"
  274. 	Disconnect-VBRServer
  275. 	Disconnect-VIServer $vcserver -Confirm:$false
  276. 	Break
  277. }
  279. # Deploy PhotonOS, change root password and assign IP settings, add VM as Managed Server and assign Proxy Role
  280. $ipincrement = 0
  281. foreach ($targethost in $targethosts) {
  282. 	My-Logger "Processing host: $targethost"
  283. 	$sw = [Diagnostics.Stopwatch]::StartNew()
  285. 	# Verify if a proxy is already present on host or deploy proxy if it's missing
  286. 	if (Get-VMHost $targethost | Get-VM | Where-Object { $_.Name -like $($ProxyBaseName + "_*") }) {
  287. 		My-Logger "  A Veeam Proxy is already deployed to host: $targethost"
  288. 	} else {	
  289. 		$ProxyName = $ProxyBaseName + "_" + ($targethost.name).replace(".","-")
  290. 		My-Logger "  Importing PhotonOS OVA $ProxyName"
  291. 		$vm = Import-VApp -Source $ovaname -Name $ProxyName -VMHost $targethost -DiskStorageFormat thin
  292. 		# "VM Netork" will be used to attach NIC, must be present on host
  293. 		My-Logger "  Assigning resources to $ProxyName"
  294. 		$vm | Set-VM -NumCpu $proxyvcpu -MemoryGB $proxyvram -Confirm:$false
  295. 		$vm | Get-NetworkAdapter -Name "Network adapter 1" | Set-NetworkAdapter -Portgroup $network -Confirm:$false
  296. 		$vm | New-AdvancedSetting -Name disk.enableUUID -Value TRUE -Confirm:$false -Force:$true
  298. 		Start-VM $vm
  300. 		Wait-Tools -VM $vm
  301. 		Start-Sleep 10
  302. 		My-Logger "  VMKeystrokes: Sending user account to $vm"
  303. 		Set-VMKeystrokes -VMName $VM -StringInput $sshuser -ReturnCarriage $true
  304. 		Start-Sleep 2
  305. 		My-Logger "  VMKeystrokes: Sending inital OVA password to $vm"
  306. 		Set-VMKeystrokes -VMName $VM -StringInput $ovainitialpassword -ReturnCarriage $true
  307. 		Start-Sleep 2
  308. 		My-Logger "  VMKeystrokes: Sending initial OVA password to $vm"
  309. 		Set-VMKeystrokes -VMName $VM -StringInput $ovainitialpassword -ReturnCarriage $true
  310. 		Start-Sleep 2
  311. 		My-Logger "  VMKeystrokes: Sending new password to $vm"
  312. 		Set-VMKeystrokes -VMName $VM -StringInput $newsshpassword -ReturnCarriage $true
  313. 		Start-Sleep 2
  314. 		My-Logger "  VMKeystrokes: Sending new password to $vm"
  315. 		Set-VMKeystrokes -VMName $VM -StringInput $newsshpassword -ReturnCarriage $true
  317. 		# Install and update PhotonOS
  318. 		# https://github.com/Vanarga/vmware/wiki/2a.-Configuring-Photon-OS
  319. 		# insert 'yum update -y' last in the $cmd section if an automatic update should be performed
  320. 		Start-Sleep 10
  321. 		$ipofvm = $iplistverified[$ipincrement]
  322. 		My-Logger "  Configuring $ProxyName with IP: $ipofvm"
  324. 		$cmd = @"
  325. 		yum install nano perl mlocate -y
  326. 		hostnamectl set-hostname $ProxyName
  327. 		iptables -A INPUT -p tcp --match multiport --dports 2500:3300 -j ACCEPT
  328. 		iptables -A OUTPUT -p icmp -j ACCEPT
  329. 		iptables -A INPUT  -p icmp  -j ACCEPT
  330. 		iptables-save >/etc/systemd/scripts/ip4save
  331. 		timedatectl set-timezone Europe/Stockholm
  332. 		echo "[Match]" > /etc/systemd/network/10-static-en.network
  333. 		echo "Name=eth0" >> /etc/systemd/network/10-static-en.network
  334. 		echo " " >> /etc/systemd/network/10-static-en.network
  335. 		echo "[Network]" >> /etc/systemd/network/10-static-en.network
  336. 		echo "Address=$ipofvm/24" >> /etc/systemd/network/10-static-en.network
  337. 		echo "Gateway=$gateway" >> /etc/systemd/network/10-static-en.network
  338. 		echo "DNS=$dns" >> /etc/systemd/network/10-static-en.network
  339. 		echo "Domains=$domains" >> /etc/systemd/network/10-static-en.network
  340. 		echo "NTP=$ntpserver" >> /etc/systemd/network/10-static-en.network
  341. 		echo "LinkLocalAddressing=no" >> /etc/systemd/network/10-static-en.network
  342. 		echo "IPv6AcceptRA=no" >> /etc/systemd/network/10-static-en.network
  343. 		chmod 644 /etc/systemd/network/10-static-en.network
  344. 		systemctl restart systemd-networkd.service
  345. 		systemctl daemon-reload
  346. "@
  348. 		My-Logger "  Invoking script on $ProxyName"
  349. 		Invoke-VMScript -VM $VM -ScriptText $cmd -GuestUser $sshuser -GuestPassword $newsshpassword -ScriptType Bash
  351. 		# Add Proxy VM to VBR as managed server and add proxy role
  352. 		if ((Get-PSSnapin VeeamPSSnapin).Version.Major -ge 10) {
  353. 			My-Logger "  Adding $ipofvm as a Veeam Managed Server"
  354. 			Add-VBRLinux -Name $ipofvm -SSHUser $sshuser -SSHPassword $newsshpassword -Description "Linux Server based on PhotonOS 3.0 Revision 2"
  355. 			My-Logger "  Adding Backup Proxy Role"
  356. 			Add-VBRViLinuxProxy -Server (Get-VBRServer -Name $ipofvm -Type Linux) -Description "Linux Backup Proxy based on PhotonOS 3.0 Revision 2" -MaxTasks $proxyvcpu -Force
  357. 			My-Logger "  Veeam Proxy was deployed to host: $targethost"
  358. 		} else {
  359. 			My-Logger "  [ERROR] You need at least Veeam Backup & Replication version 10 to add a Linux Proxy"
  360. 			Disconnect-VBRServer
  361. 			Disconnect-VIServer $vcserver -Confirm:$false
  362. 			Break
  363. 		}
  366. 		# Collect information on Proxies in hashtable for future use
  367. 		$deployedproxies.add($proxyname,$ipofvm)
  368. 		$ipincrement++
  369. 		$ipofvm = $null
  370. 		My-Logger "  Deployment of proxy took: $($sw.Elapsed.Minutes) minutes and $($sw.Elapsed.Seconds) seconds"
  371. 	}	
  372. }
  374. Clear-Host
  375. if (($deployedproxies).Count -eq 0) {
  376. 	My-Logger "[ERROR] Something went wrong, no proxies were deployed!"
  377. 	My-Logger "Check log file $verboseLogFile"
  378. 	Disconnect-VBRServer
  379. 	Disconnect-VIServer $vcserver -Confirm:$false
  380. } else {
  381. 	My-Logger "The following proxies were deployed:" 
  382. 	My-Logger $deployedproxies
  383. 	Disconnect-VBRServer
  384. 	Disconnect-VIServer $vcserver -Confirm:$false
  385. }


  1. {
  2.   "General": {
  3.     "location": "C:\\temp",
  4.     "ovasource": "http://dl.bintray.com/vmware/photon/3.0/Rev2/ova/photon-hw13_uefi-3.0-9355405.ova",
  5.     "ovaname": "photon-hw13_uefi-3.0-9355405.ova",
  6.     "ProxyBaseName": "VeeamProxy",
  7.     "ProxyvCPU": "4",
  8.     "ProxyvRAM": "8",
  9.     "sshuser": "root",
  10.     "ovainitialpassword": "changeme",
  11.     "newsshpassword": "newsecretpassword"
  12.   },
  13.   "Test": {
  14. 	"vbrserver": "",
  15. 	"vcserver": "",
  16. 	"network": "VM Network",
  17. 	"startipaddress": "",
  18. 	"endipaddress": "",
  19. 	"vbruser": "Administrator",
  20. 	"vcuser": "administrator@vsphere.local",
  21. 	"vcdatacenter": "Datacenter",
  22. 	"gateway": "",
  23. 	"dns": "",
  24. 	"domains": "mydomain.local",
  25. 	"ntpserver": "se.pool.ntp.org"
  26.   },
  27.   "Prod": {
  28. 	"vbrserver": "",
  29. 	"vcserver": "",
  30. 	"network": "Prod",
  31. 	"startipaddress": "",
  32. 	"endipaddress": "",
  33. 	"vbruser": "Administrator",
  34. 	"vcuser": "administrator@vsphere.local",
  35. 	"vcdatacenter": "Datacenter",
  36. 	"gateway": "",
  37. 	"dns": "",
  38. 	"domains": "mydomain.local",
  39. 	"ntpserver": "se.pool.ntp.org"
  40.   }
  41. }

PowerShell function for menu creation revisited

Sometimes when creating blog posts you just get excited to publish a post as soon as it’s finished, but then you realize you can do something better. So yesterday I published a post on how to use a function in PowerShell to create an interactive dynamic menu but in hindsight it could have been more “generic” – so today I’m back with a new post on how you can create that generic function by providing the query in a separate string instead as part of the function itself. This allows you to merely change the query (the input for creating the menu) without having to change anything inside the function. Now it’s a “real” function! 🙂

I’ve added two mandatory parameters to the function, the first is the query to retrieve data and the second parameter is the task that you’d like to perform on the selected object. I’m calling the parameters -ListItem and -GetItem but the actual query is set in the $MyQuery and $MyTask string and they are then passed on to the function to perform the task. So if I’d like to get a list of VMs I just change the $MyQuery to something like “(Get-VM | Sort-Object)” and if I’d like to get a list of Datastores I set $MyQuery = “(Get-Datastore | Where {$_.Type -eq ‘VMFS’} | Sort-Object -Descending FreeSpaceGB)”. Once the list is presented and you select an object you then pass on what’s going to happen with it with the $MyTask – so if I want to retrieve a specific VM I set it to “Get-VM” but it could be anything (like Remove-VM but there’s no warning so be careful!)

Get a list of VMs
Get a list of VMFS datastores and sort them based on free space

Calling the function is then done using this syntax:
Set-Menu -ListItem $MyQuery -GetItem $MyTask

$VIServer = "vcsa.mydomain.com"
$VIUsername = "administrator@vsphere.local"
$VIPassword = "MyPassword"

Connect-VIServer $VIServer -User $VIUsername -Password $VIPassword -WarningAction SilentlyContinue

$MyQuery = (Get-Datastore | Where {$_.Type -eq 'VMFS'} |Sort-Object -Descending FreeSpaceGB)
$MyTask = "Get-Datastore"

Function Set-Menu {
    Param (


    Try {
    $menu = @{}
    For ($i=1;$i -le $ListItem.count; $i++)
        { Write-Host "$i. $($ListItem[$i-1].Name)"
        $menu.Add($i,($ListItem[$i-1].Name)) }
    [int]$ans = if(($ans = Read-Host "Select item or press enter to accept default value [1]") -eq '') {"1"} else {$ans}
    $Selection = $menu.Item($ans); Invoke-Expression $($GetItem +  " " + $Selection)

    Catch {
    Write-Host "The selection you made is unavailable, please make a valid selection between 1 and $($i-1)..."


Set-Menu -ListItem $MyQuery -GetItem $MyTask

Disconnect-VIServer $VIServer -Confirm:$false
Set-Menu waiting for user input
Set-Menu has executed the task defined in $MyTask

The way you can use the function is up to you but I use it in William Lams vghetto-vsphere-automated-lab-deployment script

Simplify your life with dynamic menus in PowerShell

If you are anything like me you’re setting up test or demo environments and then tearing them down a few hours later when you’re done with what ever testing you were doing. While setting up a VMware vSphere test environment is super easy using powershell/powercli (if you haven’t already visited William Lams web page, I highly recommend it and while you’re there grab the PowerShell scripts to deploy vSphere/vSAN/NSX environments. Kudos to William for everything you do for the vCommunity!!), I use his powershell scripts almost on a daily basis!

Now while setting up a single lab environment is usually not a big problem using the scripts provided by Willam but when you start setting up multiple labs (I typically have like 3 or 4 labs set up on any given time for different purposes) you might run out of some resources statically configured in the script, for instance your datastore configured doesn’t have enough capacity. So why not enhance the experience with dynamic selection of for instance datastore (or in my example: DatastoreCluster). Turns out it’s really easy to build an interactive dynamic menu of your datastoresclusters and use the menu to select where to install the lab environment.

I’m using a function to call the datastoreclusters I can use. I have a basic error handling included that you might need to extend. The code below includes 2 examples, the first function is calling available DatastoreClusters from a vCenter server and ordering them based on available free space and the second example gets a list of port groups available on my distributed switch. I’ve also assigned a default selection/value for faster deployments, just press enter to select the default value.

$VIServer = "vcsa.mydomain.com"
$VIUsername = "administrator@vsphere.local"
$VIPassword = "MyPassword"

Connect-VIServer $VIServer -User $VIUsername -Password $VIPassword -WarningAction SilentlyContinue

Function Show-DatastoreClusters ($Title = 'My Datastores'){
Try {
$AvailDatastores = (Get-Datastore | Where {$_.Type -eq "VMFS"} |Sort-Object -Descending FreeSpaceGB)
$menuds = @{}
For ($i=1;$i -le $AvailDatastores.count; $i++)
{ Write-Host "$i. $($AvailDatastores[$i-1].Name) - $([math]::Round($AvailDatastores[$i-1].FreespaceGB,1)) GB free space"
$menuds.Add($i,($AvailDatastores[$i-1].Name)) }
[int]$ansds = if(($ansds = Read-Host "Select Datastore or press enter to accept default value [1]") -eq ''){"1"} else {$ansds}
$DatastoreSelection = $menuds.Item($ansds) ; Get-Datastore $DatastoreSelection
Catch { Write-Host "The selection you made is unavailable, please make a valid selection between 1 and $($i-1)..." Start-Sleep 2 }

Function Show-PortGroups ($Title = 'My Portgroups'){
Try {
$AvailPortgroups = (Get-VDSwitch -Name "DSwitch" | Get-VDPortgroup)
$menupg = @{}
For ($i=1;$i -le $AvailPortgroups.count; $i++)
{ Write-Host "$i. $($AvailPortgroups[$i-1].Name)"
$menupg.Add($i,($AvailPortgroups[$i-1].Name)) }
[int]$anspg = if(($anspg = Read-Host "Select portgroup or press enter to accept default value [7]") -eq ''){"7"}else{$anspg}
$PortgroupSelection = $menupg.Item($anspg) ; Get-VDPortgroup $PortgroupSelection
Catch { Write-Host "The selection you made is unavailable, please make a valid selection between 1 and $($i-1)..." Start-Sleep 2 }
Write-Host `n
$VMDatastore = Show-DatastoreClusters
Write-Host `n
$VMNetwork = Show-Portgroups
Write-Host `n

Disconnect-VIServer $VIServer -Confirm:$false

The function above can then be included in the script from William Lam. When calling the function the output will look something like this (the menus being produced are highlighted in red):

Function to build dynamic menu

If you’d like you can add some extra granularity when enumerating the datastore, expand the “Where” statement to just get the datastores with a specific name:

{$_.Type -eq "VMFS" -and $_.Name -like "VMFS-ESXi7*"}

Once you got you script fetching the desired data you can add the function to any script, I’ve added it to William’s script for some added flexibility.

Veeam, PowerShell and SAN snapshot

So a week or so ago I wrote a post about using PowerShell to add snapshots to a SAN found in Veeam Backup & Replication. It was a quick test to see if it worked, now I’ve slightly improved the script.

The Get-HP4* cmdlets is specifically for HPE StoreVirtual VSA/P4000/LeftHand line. If you have another supported SAN Storage system use the correct cmdlets:

NetAPP Storage Systems
HPE 3Par StoreServ Storage Systems
HPE StoreVirtual Storage Systems
EMC VNX Storage Systems

You can accomplish the same thing using the management tool for the SAN, taking recurring snapshots. But in the case of HPE StoreVirtual it’s a licensed feature and it can only occur every 30 minutes so if you need it more often or you’re lacking the license you can use the PowerShell script instead.

Add-PSSnapin VeeamPSSnapIn -ErrorAction SilentlyContinue

$source_storage = ‘Veeam-VSA-MGMTG’
$source_cluster = ‘veeam-vsa-cluster’
$source_vol_name = ‘datastore1’
$snapshot_name = ‘Oh_snap_’+(Get-Date -Format MMddhhmm)

#Create a new snapshot
try {
$getvolume = (Get-HP4Storage -Name $source_storage | Get-HP4cluster -Name $source_cluster | Get-HP4Volume -name $source_vol_name)
‘getvolume-Name: ‘ + $getvolume.Name
‘getvolume-InternalId: ‘ + $getvolume.InternalId
‘getvolume-IsThin: ‘ + $getvolume.IsThinProvision
‘getvolume-Size: ‘ + $getvolume.Size

$getvolume | Add-HP4Snapshot -name $snapshot_name
} catch {
‘Failed to find storage, cluster or datastore’
‘Unable to create snapshot’

#Remove the oldest snapshot if more than 4 are available
$getsnapshot = ($getvolume | Get-HP4Snapshot | Where-Object {$_.Name -like ‘Oh_snap_*’})
$snapshot_count = @($getsnapshot).Count

if ($snapshot_count -ge 4) {
$getsnapshot | Sort-Object creationtimeutc | Select-Object -First 1 | Remove-HP4Snapshot

Populate your Veeam lab with PowerShell

If you, like me, have the need to constantly rebuild a lab environment where the servers are installed already but it lacks any configuration you probably realized that PowerShell is you friend. I have a lab environment that I tear down and build up again really often using templates in my VMware environment. In this environment I have all the infrastructure components installed but not configured in Veeam Backup & Replication so whenever I want to show-and-tell I first need to configure stuff. It might take a while to do, so why not automate with PowerShell?

The script below adds a few managed servers, adds backup proxies, creates a Scale-Out Backup Repository with 2 extents, adds 2 WAN accelerators. On top of that it adds a Tape proxy, connects to a HP VSA and takes a snapshot.

Add-PSSnapin VeeamPSSnapIn -ErrorAction SilentlyContinue

$Infra_Administrator = "Domain1\Administrator"
$Infra_Password = 'Password1'
$Lab_Administrator = "Domain2\Administrator"
$Lab_Password = 'Password2'
$ESXi_root = "root"
$ESXi_Password = 'Password3'
$Oracle_User = "oracle"
$Oracle_Password = 'Password4'
$HPE_User = 'HpeUser'
$HPE_Password = 'Password5'
$VBRserver = Get-VBRServer -Name "VEEAM-VBR.domain1.local"

Add-VBRCredentials -Type Windows -User $Infra_Administrator -Password $Infra_Password -Description $Infra_Administrator
Add-VBRCredentials -Type Windows -User $Lab_Administrator -Password $Lab_Password -Description $Lab_Administrator
Add-VBRCredentials -Type Linux -User $Oracle_User -Password $Oracle_Password -SshPort 23 -ElevateToRoot -AddToSudoers -RootPassword $Oracle_Password -Description "oracle"

#Add servers as ”managed servers”
Add-VBRESXi –Name "VEEAM-ESX" -user root -password 'Password3'
Add-VBRWinServer -Name "VEEAM-HYPERV" -credentials $Infra_Administrator
Add-VBRWinServer -Name "VEEAM-Remote" -credentials $Infra_Administrator
Add-VBRHvHost -Name "VEEAM-HYPERV" -credentials $Infra_Administrator

#Remove/Add proxy with 1 concurrent task limit
Get-VBRViProxy -Name "VMware Backup Proxy" | Remove-VBRViProxy -Confirm
Add-VBRViProxy -Server $VBRserver -Description "VMware Backup Proxy" -MaxTasks 1

#Add Backup Repositories and Scale-Out Backup Repository
Add-VBRBackupRepository -Server $VBRserver -Name "Remote Repository" -Folder "X:\Backups" -Type WinLocal -MaxConcurrentJobs 4 -Credentials $Infra_Administrator
Add-VBRBackupRepository -Name "Local Backup Repository" -Server $VBRserver -Folder "E:\Backups" -Type WinLocal -MountServer $VBRserver -VPowerNFSFolder "C:\ProgramData\Veeam\Backup\NfsDatastore" -MaxConcurrentJobs 4 -Credentials $Infra_Administrator
Set-VBRConfigurationBackupJob -Repository "Remote Repository"
Add-VBRScaleOutBackupRepository -Name "Main Backup Repository" –PolicyType DataLocality –Extent “Default Backup Repository”, “Local Backup Repository”

#Add WAN accelerators
Add-VBRWANAccelerator -Server "VEEAM-Remote" -Description "Remote WAN Accelerator" -CachePath "X:\VeeamWAN" -CacheSize 10 -CacheSizeUnit GB
Get-VBRLocalhost | Add-VBRWANAccelerator -Description "Local WAN Accelerator" -CachePath "X:\VeeamWAN" -CacheSize 10 -CacheSizeUnit GB

#Add a Virtual Lab and Application group
$VLABhost = Get-VBRServer -Type ESXi
$VLABdatastore = Find-VBRViDatastore -Name "datastore1" -Server $VLABhost
Add-VSBVirtualLab -Name "VEEAM-ESX VLAB1" -Server $VLABhost -Datastore $VLABdatastore

Find-VBRViEntity -Name "VEEAM-DC01", "VEEAM-EX01" | Add-VSBViApplicationGroup -Name "Exchange"

#Add SAN and Tape and make a snapshot on the SAN
Add-HP4Storage -DnsOrIpAddress "" -User $HPE_User -Password $HPE_Password -Description "HPE Storage"
Get-VBRServer -Name "VEEAM-Remote" | Add-VBRTapeServer
Get-HP4Volume -name "datastore1" | Add-HP4Snapshot -name "datastore1_SS_1"

Veeam and PowerShell: A perfect match!

I read a really good and useful blog post a while ago from Preben Berg from Veeam describing how to use PowerShell to restore a database from backup to a dev environment. This made me think on another scenario that would be fun to script that might come in handy someday.

What if you have a SAN that you would want do snapshots on once an hour and save some of those historical snapshots rotating the oldest one.

Now for the disclaimer part, this is merely meant to showcase how you might accomplish this. There are no safety features built in. Do not use it in production and use it at your own risk. However if you’d like to do some further testing of your own, you can download a virtual SAN from HP – free of charge up to 1 TB. Nice!

First things first. We need to create a PowerShell script.

Let’s define which volume to use:
$source_vol_name = "datastore1"

Then we add a naming convention to use for the snapshots with a timestamp at the end:
$snapshot_name = "Oh_snap_"+(Get-Date -Format MMddhhmm)

Let’s count how many snapshots exist on the volume:
$snapshot_count = @(Get-HP4Volume -name $source_vol_name | Get-HP4Snapshot).Count

Now let’s create a snapshot:
$snapshot_create_session = Get-HP4Volume -name $source_vol_name | Add-HP4Snapshot -name $snapshot_name -description "Automated snapshot"

I would like to save the 4 latest snapshots on the volume and delete the oldest:
if ($snapshot_count -ge 4 {
$snapshot_remove_session = Get-HP4Volume -name $source_vol_name | Get-HP4Snapshot | Sort-Object creationtimeutc | Select-Object -First 1 | Remove-HP4Snapshot

If I would leave out “Get-HP4Volume -name $source_vol_name”  in the above statement then I would delete to oldest snapshot available on any of the volumes (not just datastore1) which is not our intent.

Let’s put it all together:

Add-PSSnapin VeeamPSSnapIn -ErrorAction SilentlyContinue

$source_vol_name = “datastore1”
$snapshot_name = “Oh_snap_”+(Get-Date -Format MMddhhmm)
$snapshot_count = @(Get-HP4Volume -name $source_vol_name | Get-HP4Snapshot).Count

$snapshot_create_session = Get-HP4Volume -name $source_vol_name | Add-HP4Snapshot -name $snapshot_name -description “Automated snapshot”

if ($snapshot_count -ge 4 {
$snapshot_remove_session = Get-HP4Volume -name $source_vol_name | Get-HP4Snapshot | Sort-Object creationtimeutc | Select-Object -First 1 | Remove-HP4Snapshot

That’s all, just a few lines of code and we can accomplish cool things.

Then all we have to do is add the script as a scheduled task on the VBR server and run it once an hour or what ever intervall we’d like.

But let’s see how the infrastructure looks prior to running the script:

1. SAN datastores


Open Task Scheduler and create a new task:

2. Create scheduled task


Let’s configure it to run once an hour starting at 10 PM:

3. Run at 1 hour intervall


We add and action, starting the PowerShell script:

4. Add PowerShell script to run


Looks ok once it’s added:

5. Scheduled task has been added


First time the script ran a snapshot was created as expected:

6. First run of the script


(I changed the script to run every 5 minutes to speed up the process), now we have 4 snapshots:

7. 4 initial snaps


When the fifth snapshot has been taken the oldest snapshot is deleted (the snapshot created at 10:07 PM in the previous picture):

8. 4 snaps rolling