Refactoring

Refactoring. What is it and how does it fit into the pragmatist approach to scripting?
From Wikipedia:
“In computer programming and software design, code refactoring is the process of restructuring existing computer code without changing its external behavior. Refactoring is intended to improve the design, structure, and/or implementation of the software (its non-functional attributes), while preserving its functionality.”

From this definition it’s clear that Refactoring should be in every pragmatic scripter’s toolbox.

Wikipedia goes on to say that Refactoring usually addresses maintainability and or
extensibility aspects of code. Both of these should be built into any script developed that is expected to have a long life.


I was given the task of creating a monthly capacity report of our VMware farms with the goals of automating generation of a report that is actionable and auditable. The output must be in MS Word format, and it should also be available as HTML.
As a follower of the pragmatic approach, I first performed research on the Internet to determine if there exists an available preexisting solution that fits in my budget of 0.00 dollar’s. I further refined my search to PowerShell scripts that do not require that MS Word be installed.

I selected as my starting point the Windows PS script published at this URL,
https://dailytechsaga.wordpress.com/2021/01/02/powershell-script-for-vmware-capacity-performance-report.

In my opinion this script has going for it a structure that breaks the functionality down into reusable functions and it does not have a dependency on VMware PowerCLI “slot” functions to estimate capacity for additional VM deployments. Those have a heavy dependency on reservations for CPU, which we don’t do. The downside to the script is that it produces only HTML output and intermixes the HTML code directly into the data gathering functions. Addressing the mangling of data load and transform into human actionable information is my primary target for Refactoring.

The execution of the refactoring must result in data objects that can easily be ingested by PS modules that provide commandlets for writing data out to Word, HTML and or Excel files. In particular PScribo and ImportExcel. Both provide comandlets for easy handling of custom PS Objects and Hashetables.

It’s worth noting that the original author created several functions that can be used as is. These have function names that start with “Get”. Refactor only what needs to be refactored to meet the goals you have set.

Reading down the the first function that has this mix of data and view is the ListVCenterInventory function. Reproduced below. I’ll use this as an example. The code snippets I’m showing are not functional without the other bits and bobs of the full script but are enough to illustrate the concept.

The original function:

Function ListVCenterInventory () {
	$InventoryTemp = "<h4>"
	$HostTemp = Get-VMHost | Select-Object -First 1
	#$InventoryTemp += "what's going on here ???  <br>" 
	$InventoryTemp += "vCenter version " + (($HostTemp | Select-Object @{N="vCenterVersion";E={$global:DefaultVIServers | where {$_.Name.ToLower() -eq ($HostTemp.ExtensionData.Client.ServiceUrl.Split('/')[2]).ToLower()} | %{"$($_.Version) Build $($_.Build)"}   }}).vCenterVersion) + "<br>"
	$InventoryTemp += [String]$DC.Count + " Datacenters <br>"
	$InventoryTemp += [String]$Clusters.Count + " Clusters <br>"
	$InventoryTemp += [String]$VMHosts.Count + " ESXi Hosts with a total of " + (GetTotalCPUCyclesInGhz ($VMHosts)) + " GHz on " + `
				 (GetTotalNumberofCPUs ($VMHosts)) + " CPUs and " + (GetTotalMemoryInGB ($VMHosts)) + " GB of RAM <br>"
	$InventoryTemp += [String]$Datastores.Count + " Datastores with a total of " + (GetTotalDatastoreDiskSpaceinGB ($Datastores)) + " GB of disk space <br>"
	$InventoryTemp += [String]$VM.Count + " Virtual Machines <br>"
	$InventoryTemp += [String]$Templates.Count + " Templates <br>"
	$InventoryTemp += [String]$ResourcePools.Count + " Resource Pools <br>"
	$InventoryTemp += [String][system.math]::floor($VM.Count / $VMHosts.Count) + ":1" + " Consolidation ratio <br>"
	$InventoryTemp += "<br>"

	$InventoryTemp += BuildESXiSoftwareAndHardwareInfoTable ($VMHosts)
	
	$InventoryTemp += "</h4>"
	return $InventoryTemp
}

The returned object is a String of text with HTML code designed as a list. The function is limited to only supporting HTML output.
PScribo works with Word Sections and Paragraphs. It does not support the concept of a ‘line’ of text. If that is needed, I suggest looking at the Python solution to this problem space, python-docx. It provides a much more comprehensive coverage of Word document structure.

With that limitation of PScribo in mind, I refactored this function by:

  1. Taking each of the ‘$InventoryTemp +=’, removing the HTML components and creating individual variables for each of those lines that produce unstructured text.
  2. Refactoring the BuildESXiSoftwareAndHardwareInfoTable to produce a PSCustom object.

Here is the refactored code:

$VM = Get-VM | Sort-Object -Property Name
$Datastores = Get-Datastore | Sort-Object -Property Name
$Templates = Get-Template | Sort-Object -Property Name
$ResourcePools = Get-ResourcePool | Sort-Object -Property Name
$Snapshots = Get-VM | Get-Snapshot | Select-Object VM, Name, @{ Label = "Size"; Expression = { "{0:N2} GB" -f ($_.SizeGB) } }, Created
$hostname = HOSTNAME.EXE
$HostTemp = Get-VMHost | Select-Object -First 1
$VCenterInfo = "vCenter version " + (($HostTemp | Select-Object @{ N = "vCenterVersion"; E = { $global:DefaultVIServers | Where-Object { $_.Name.ToLower() -eq ($HostTemp.ExtensionData.Client.ServiceUrl.Split('/')[2]).ToLower() } | ForEach-Object { "$($_.Version) Build $($_.Build)" } } }).vCenterVersion)
$VcenterInfo1 = [String]$DC.Count + " Datacenters"
$VcenterInfo2 = [String]$Clusters.Count + " Clusters"
$VcenterInfo3 = [String]$VMHosts.Count + " ESXi Hosts with a total of " + (GetTotalCPUCyclesInGhz ($VMHosts)) + " GHz on " + (GetTotalNumberofCPUs ($VMHosts)) + " CPU and " + (GetTotalMemoryInGB ($VMHosts)) + " GB of RAM"
$VcenterInfo4 = [String]$Datastores.Count + " Datastores with a total of " + (GetTotalDatastoreDiskSpaceinGB ($Datastores)) + " GB of disk space"
$VcenterInfo5 = [String]$VM.Count + " Virtual Machines"
$VcenterInfo6 = [String]$Templates.Count + " Templates"
$VcenterInfo7 = [String]$ResourcePools.Count + " Resource Pools"
$VcenterInfo8 = [String]$Snapshots.Count + " Snapshots"
$VcenterInfo9 = [String][system.math]::floor($VM.Count / $VMHosts.Count) + ":1" + " Consolidation ratio VM's to ESXI Hosts"

Function CreateESXiSoftwareAndHardwareInfoObj ($VMHostsTemp)
{
	Write-Host "Gathering ESXi Hardware and Software Information..."
	$ESXiSoftwareAndHardwareInfoTemp = @()
	foreach ($VMhost in $VMHostsTemp)
	{
		$ram = GetTotalMemoryInGB ($VMhost)
		#        $uptime = ($VMhost | Get-View | select @{N = "Uptime"; E = { (Get-Date) - $_.Summary.Runtime.BootTime } }).Uptime.Days
		$hostObj = [PSCustomObject][ordered]@{
			'Name' = $VMhost.Name;
			'Manufacturer' = $VMhost.Manufacturer;
			'Model' = $VMhost.Model;
			'CPU Core' = $VMhost.NumCpu;
			'RAM (GB)' = $ram;
			'ESXi Version' = $VMhost.Version
			#   'Build'        = $VMhost.Build
			#   'Uptime'       = $uptime;
		}
		$ESXiSoftwareAndHardwareInfoTemp += $hostObj
	}
	return $ESXiSoftwareAndHardwareInfoTemp
}
EsxiHostInfo = CreateESXiSoftwareAndHardwareInfoObj -VMHostsTemp $VMHosts

Here is how the refactored code is used in the PScribo document:

Paragraph $vCenterinfo
Paragraph $Vcenterinfo1
Paragraph $vCenterinfo2
Paragraph $Vcenterinfo3
Paragraph $vCenterinfo4
Paragraph $Vcenterinfo5
Paragraph $vCenterinfo6
Paragraph $Vcenterinfo7
Paragraph $Vcenterinfo8
Paragraph $Vcenterinfo9

Section -Name "ESXi Software and Hardware Information" -Style 'Heading2' -ScriptBlock {
   Table -Object $EsxiHostInfo -Style listGrid
}

In this example I took the original function and refactored it in a way that makes sense for my goals and easily integrates with PScribo.