Functions/vmware.psm1

Function Import-PowerCLI {
    [CmdletBinding()]
    Param(
    )
 
    if (!(Get-Module -Name VMware.VimAutomation.Core)) {
        write-host ("Adding PowerCLI...")
        Get-Module -Name VMware* -ListAvailable | Import-Module -Global
        write-host ("Loaded PowerCLI.")
    }
}

function Connect-vCenter {
    [CmdletBinding()]
    Param(
        [Parameter()] [string] $vCenter,
        [Parameter()] [PSCredential] $Credential
    )

    $ConnectedvCenter = $global:DefaultVIServers
    if ($ConnectedvCenter.Count -eq 1) {
        Write-Host "You are currently connected to $($ConnectedvCenter.Name)."
        $Response = Read-Host "Do you want to disconnect? (y/n; default 'n')"
      
        if ($Response -eq 'y')
        { Disconnect-VIServer -Confirm:$false -Force; $ConnectedvCenter = $global:DefaultVIServers }
    }
  
    if ($ConnectedvCenter.Count -eq 0) {
        if ((Test-Path "$githome\powershell\etc\vCenterDict.csv") -and ($null -eq $vCenter -or $vCenter -eq "")) {
            $vCenters = Import-Csv "$githome\powershell\etc\vCenterDict.csv" | Sort-Object FriendlyName

            $vCenter = (DriveMenu -Objects $vCenters -MenuColumn FriendlyName -SelectionText "Pick a vCenter" -ClearScreen $false).VCName
        }

        if ($null -eq $vCenter -or $vCenter -eq "") { $vCenter = Read-Host "Please provide the name of a vCenter server..." }
        do {
            if ($ConnectedvCenter.Count -eq 0 -or $null -eq $ConnectedvCenter) { Write-Host "Attempting to connect to vCenter server $vCenter" }

            #Set-PowerCLIConfiguration -invalidcertificateaction ignore -Confirm:$false | Out-Null

            if ($null -eq $Credential) { Connect-VIServer -Server $vCenter -Force | Out-Null }
            else { Connect-VIServer -Server $vCenter -Credential $Credential -Force | Out-Null }
      
            $ConnectedvCenter = $global:DefaultVIServers

            if ($ConnectedvCenter.Count -eq 0 -or $null -eq $ConnectedvCenter) { Write-Host "vCenter Connection Failed. Please try again or press Control-C to exit..."; Start-Sleep -Seconds 2 }
        } while ($ConnectedvCenter.Count -eq 0)
    }
}

function Show-vCenter {
    [CmdletBinding()]
    $ConnectedvCenter = $global:DefaultVIServers

    if ($ConnectedvCenter.Count -eq 1) {
        Write-Host "You are currently connected to $($ConnectedvCenter.Name)." -ForegroundColor Green
    }
    else {
        Write-Host "You are currently not connected to a vCenter Server." -ForegroundColor Yellow
    }
}

function Wait-Shutdown {
    while ($PowerState -eq "PoweredOn") {
        Start-Sleep 5
        $PowerState = (Get-VM $($LocalGoldCopy.Name)).PowerState
    }
}

Function Find-VmByAddress {
    <# .Description
        Find all VMs w/ a NIC w/ the given MAC address or IP address (by IP address relies on info returned from VMware Tools in the guest, so those must be installed). Includes FindByIPWildcard, so that one can find VMs that approximate IP, like "10.0.0.*"
        .Example
        Get-VMByAddress -MAC 00:50:56:00:00:02
        VMName MacAddress
        ------ ----------
        dev0-server 00:50:56:00:00:02,00:50:56:00:00:04
 
        Get VMs with given MAC address, return VM name and its MAC addresses
        .Example
        Get-VMByAddress -IP 10.37.31.120
        VMName IPAddr
        ------ ------
        dev0-server2 192.168.133.1,192.168.253.1,10.37.31.120,fe80::...
 
        Get VMs with given IP as reported by Tools, return VM name and its IP addresses
        .Example
        Get-VMByAddress -AddressWildcard 10.0.0.*
        VMName IPAddr
        ------ ------
        someVM0 10.0.0.119,fe80::...
        someVM2 10.0.0.138,fe80::...
        ...
 
        Get VMs matching the given wildcarded IP address
    #>


    [CmdletBinding(DefaultParametersetName = "FindByMac")]
    param (
        ## MAC address in question, if finding VM by MAC; expects address in format "00:50:56:83:00:69"
        [parameter(Mandatory = $true, ParameterSetName = "FindByMac")][string]$MacToFind_str,
        ## IP address in question, if finding VM by IP
        [parameter(Mandatory = $true, ParameterSetName = "FindByIP")][ValidateScript({ [bool][System.Net.IPAddress]::Parse($_) })][string]$IpToFind_str,
        ## wildcard string IP address (standard wildcards like "10.0.0.*"), if finding VM by approximate IP
        [parameter(Mandatory = $true, ParameterSetName = "FindByIPWildcard")][string]$AddressWildcard_str
    ) ## end param


    Process {
        Switch ($PsCmdlet.ParameterSetName) {
            "FindByMac" {
                ## return the some info for the VM(s) with the NIC w/ the given MAC
                Get-View -Viewtype VirtualMachine -Property Name, Config.Hardware.Device | Where-Object { $_.Config.Hardware.Device | Where-Object { ($_ -is [VMware.Vim.VirtualEthernetCard]) -and ($_.MacAddress -eq $MacToFind_str) } } | Select-Object @{n = "VMName"; e = { $_.Name } }, @{n = "MacAddress"; e = { ($_.Config.Hardware.Device | Where-Object { $_ -is [VMware.Vim.VirtualEthernetCard] } | ForEach-Object { $_.MacAddress } | Sort-Object) -join "," } }
                break;
            } ## end case
            { "FindByIp", "FindByIPWildcard" -contains $_ } {
                ## scriptblock to use for the Where clause in finding VMs
                $sblkFindByIP_WhereStatement = if ($PsCmdlet.ParameterSetName -eq "FindByIPWildcard") { { $_.IpAddress | Where-Object { $_ -like $AddressWildcard_str } } } else { { $_.IpAddress -contains $IpToFind_str } }
                ## return the .Net View object(s) for the VM(s) with the NIC(s) w/ the given IP
                Get-View -Viewtype VirtualMachine -Property Name, Guest.Net | Where-Object { $_.Guest.Net | Where-Object $sblkFindByIP_WhereStatement } | Select-Object @{n = "VMName"; e = { $_.Name } }, @{n = "IPAddr"; e = { ($_.Guest.Net | ForEach-Object { $_.IpAddress } | Sort-Object) -join "," } }
            } ## end case
        } ## end switch
    } ## end process
}

function Get-FolderByPath {
    <# .SYNOPSIS Retrieve folders by giving a path .DESCRIPTION The function will retrieve a folder by it's path. The path can contain any type of leave (folder or datacenter). .NOTES Author: Luc Dekens .PARAMETER Path The path to the folder. This is a required parameter. .PARAMETER Path The path to the folder. This is a required parameter. .PARAMETER Separator The character that is used to separate the leaves in the path. The default is '/' .EXAMPLE PS> Get-FolderByPath -Path "Folder1/Datacenter/Folder2"
.EXAMPLE
  PS> Get-FolderByPath -Path "Folder1>Folder2" -Separator '>'
#>

 
    param(
        [CmdletBinding()]
        [parameter(Mandatory = $true)]
        [System.String[]]${Path},
        [char]${Separator} = '/'
    )
 
    process {
        if ((Get-PowerCLIConfiguration).DefaultVIServerMode -eq "Multiple") {
            $vcs = $defaultVIServers
        }
        else {
            $vcs = $defaultVIServers[0]
        }
 
        foreach ($vc in $vcs) {
            foreach ($strPath in $Path) {
                $root = Get-Folder -Name Datacenters -Server $vc
                $strPath.Split($Separator) | ForEach-Object {
                    $root = Get-Inventory -Name $_ -Location $root -Server $vc -NoRecursion
                    if ((Get-Inventory -Location $root -NoRecursion | Select-Object -ExpandProperty Name) -contains "vm") {
                        $root = Get-Inventory -Name "vm" -Location $root -Server $vc -NoRecursion
                    }
                }
                $root | Where-Object { $_ -is [VMware.VimAutomation.ViCore.Impl.V1.Inventory.FolderImpl] } | ForEach-Object {
                    Get-Folder -Name $_.Name -Location $root.Parent -NoRecursion -Server $vc
                }
            }
        }
    }
}

function Get-AlarmActionState {
    <#
    .SYNOPSIS Returns the state of Alarm actions.
    .DESCRIPTION The function will return the state of the
      alarm actions on a vSphere entity or on the the entity
      and all its children
    .NOTES Author: Luc Dekens
    .PARAMETER Entity
      The vSphere entity.
    .PARAMETER Recurse
      Switch that indicates if the state shall be reported for
      the entity alone or for the entity and all its children.
    .EXAMPLE
      PS> Get-AlarmActionState -Entity $cluster -Recurse:$true
    #>

     
    param(
        [CmdletBinding()]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl]$Entity,
        [switch]$Recurse = $false
    )
     
    process {
        $Entity = Get-Inventory -Id $Entity.Id
        if ($Recurse) {
            $objects = @($Entity)
            $objects += Get-Inventory -Location $Entity
        }
        else {
            $objects = $Entity
        }
     
        $objects |
        Select-Object Name,
        @{N = "Type"; E = { $_.GetType().Name.Replace("Impl", "").Replace("Wrapper", "") } },
        @{N = "Alarm actions enabled"; E = { $_.ExtensionData.alarmActionsEnabled } }
    }
}
    
function Set-AlarmActionState {
    <#
    .SYNOPSIS Enables or disables Alarm actions
    .DESCRIPTION The function will enable or disable
      alarm actions on a vSphere entity itself or recursively
      on the entity and all its children.
    .NOTES Author: Luc Dekens
    .PARAMETER Entity
      The vSphere entity.
    .PARAMETER Enabled
      Switch that indicates if the alarm actions should be
      enabled ($true) or disabled ($false)
    .PARAMETER Recurse
      Switch that indicates if the action shall be taken on the
      entity alone or on the entity and all its children.
    .EXAMPLE
      PS> Set-AlarmActionState -Entity $cluster -Enabled:$true
    #>

     
    param(
        [CmdletBinding()]
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl]$Entity,
        [switch]$Enabled,
        [switch]$Recurse
    )
     
    begin {
        $alarmMgr = Get-View AlarmManager 
    }
     
    process {
        if ($Recurse) {
            $objects = @($Entity)
            $objects += Get-Inventory -Location $Entity
        }
        else {
            $objects = $Entity
        }
        $objects | ForEach-Object {
            $alarmMgr.EnableAlarmActions($_.Extensiondata.MoRef, $Enabled)
        }
    }
}

Function Invoke-DrainHost {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)] [VMware.VimAutomation.ViCore.Impl.V1.Inventory.VMHostImpl]$VMHost
    )

    $ErrorActionPreference = "Stop"

    $ScriptStarted = Get-Date -Format MM-dd-yyyy_HH-mm-ss
    $ScriptName = $MyInvocation.MyCommand.Name

    # $LoggingSuccSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Succ"}
    $LoggingInfoSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Info" }
    # $LoggingWarnSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Warn"}
    # $LoggingErrSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Err"}

    try {
        $Cluster = $VMHost | Get-Cluster

        if ($($Cluster.DrsEnabled)) {
            Invoke-Logging @LoggingInfoSplat -LogString "Storing current cluster DRS Automation level."
            $Stored_DRS_Level = $Cluster.DrsAutomationLevel
            Invoke-Logging @LoggingInfoSplat -LogString "Setting DRS Automation level to manual."
            Set-Cluster -Cluster $Cluster -DrsAutomationLevel Manual -Confirm:$false | Out-Null
        }
    
        $VmsToMigrate = $VMHost | Get-VM | Where-Object { $_.PowerState -eq "PoweredOn" }
        $VmsToMigrateCount = $VmsToMigrate.Count
        $VMc = 1
    
        foreach ($VmToMigrate in $VmsToMigrate) {
            # Write-Host "Determining host to migrate to."
            Invoke-Logging @LoggingInfoSplat -LogString "Determining host to migrate to."
            $ClusterHosts = $Cluster | Get-VMHost | Where-Object { $_.ConnectionState -eq "Connected" }
            $TargetHost = $ClusterHosts | Where-Object { $_ -ne $VMHost } | Sort-Object MemoryUsageGB | Select-Object -First 1
    
            # Write-Host "Moving VM $($VmToMigrate.Name) ($VMc of $VmsToMigrateCount) to $($TargetHost.Name)."
            Invoke-Logging @LoggingInfoSplat -LogString "Moving VM $($VmToMigrate.Name) ($VMc of $VmsToMigrateCount) to $($TargetHost.Name)."
            Move-VM -VM $VmToMigrate -Destination $TargetHost | Out-Null
    
            $VMc++
            Start-Sleep 5
        }
    
        # Write-Host "Setting cluster DRS to 'FullyAutomated'."
        Invoke-Logging @LoggingInfoSplat -LogString "Setting cluster DRS to 'FullyAutomated'."
        Set-Cluster -Cluster $Cluster -DrsAutomationLevel FullyAutomated -Confirm:$false | Out-Null
    
        # Write-Host "Verifying host is empty and setting to MM."
        Invoke-Logging @LoggingInfoSplat -LogString "Verifying host is empty and setting to MM."
        $Check = $VMHost | Get-VM | Where-Object { $_.PowerState -eq "PoweredOn" }
        if ($null -eq $Check) { Set-VMHost -VMHost $VMHost -State Maintenance -Evacuate:$true -Confirm:$false | Out-Null }
        else { Invoke-Logging @LoggingErrSplat -LogString "Host did not completely drain. Please check VMs left on the host for VMotion errors, resolve and run the script again."; throw "Host did not completely drain. Please check VMs left on the host for VMotion errors, resolve and run the script again." }
    
        #Waiting for vCenter to do stuff
        Start-Sleep 30

        # Write-Host "Setting DRS mode to pre-script setting."
        Invoke-Logging @LoggingInfoSplat -LogString "Setting DRS mode to pre-script setting."
        Set-Cluster -Cluster $Cluster -DrsAutomationLevel $Stored_DRS_Level -Confirm:$false | Out-Null
    }
    catch {
        throw
    }
}

Function Invoke-PatchESXHost {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $true)] [VMware.VimAutomation.ViCore.Impl.V1.Inventory.VMHostImpl]$HostToPatch,
        [Parameter()][ValidateSet("DRS", "DrainHost")] [string]$EvacType = "DrainHost",
        [bool]$AutoExitMm = $false,
        [string]$emailTo = ([DC.Automation]::TeamEmail)
    )

    $ErrorActionPreference = "Stop"

    $ScriptStarted = Get-Date -Format MM-dd-yyyy_HH-mm-ss
    $ScriptName = $MyInvocation.MyCommand.Name

    $LoggingSuccSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Succ" }
    $LoggingInfoSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Info" }
    $LoggingWarnSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Warn" }
    $LoggingErrSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Err" }

    try {
        #Obtain a count of host datastores before applying updates
        $DsCountStart = ($HostToPatch | Get-Datastore).Count
        #Write-Host "Scanning $($HostToPatch.Name) baselines."
        Invoke-Logging $LoggingInfoSplat -LogString "Scanning $($HostToPatch.Name) baselines."
        Scan-Inventory -Entity $HostToPatch.Name

        #Write-Host "Determining if there are non-compliant baselines"
        Invoke-Logging $LoggingInfoSplat -LogString "Determining if there are non-compliant baselines"
        $NcBaselines = Get-Compliance -Entity $HostToPatch.Name -ComplianceStatus NotCompliant

        if ($null -eq $NcBaselines) { Invoke-Logging $LoggingSuccSplat -LogString "Host is already compliant with all applied baselines." }
        else {
            switch ($EvacType) {
                "DRS" { 
                    #Write-Host "Putting host in MM using DRS."
                    Invoke-Logging $LoggingInfoSplat -LogString "Putting host in MM using DRS."
                    Set-VMHost -VMHost $HostToPatch -State Maintenance -Evacuate:$true -Confirm:$false
                }
                "DrainHost" { 
                    #Write-Host "Putting host in MM using DrainHost Function."
                    Invoke-Logging $LoggingInfoSplat -LogString "Putting host in MM using DrainHost Function."
                    Invoke-DrainHost -VMHost $HostToPatch
                }
                Default {}
            }
    
            #Verify host is in MM
            # Write-Host "Verifying host is in MM."
            Invoke-Logging $LoggingInfoSplat -LogString "Verifying host is in MM."
            if ((Get-VMHost $HostToPatch).ConnectionState -ne "Maintenance") { Throw "$($HostToPatch.Name) is not in MM." }
    
            # Write-Host "Staging non-compliant baselines."
            Invoke-Logging $LoggingInfoSplat -LogString "Staging baselines."
            $Baselines = Get-PatchBaseline -Entity $HostToPatch -Inherit
            Copy-Patch -Entity $HostToPatch -Baseline $Baselines
            Invoke-Logging $LoggingInfoSplat -LogString "Remediating baselines: `r`n`t$($Baselines.Name -join "`n`t")"
            Remediate-Inventory -Entity $HostToPatch -Baseline $Baselines -ClusterDisableDistributedPowerManagement:$true -Confirm:$false -ErrorAction "Ignore"
    
            #Waiting for 10 successful pings
            # Write-Host "Performing ping checks."
            Invoke-Logging $LoggingInfoSplat -LogString "Performing ping checks."
            $PingCheck = 0
            while ($PingCheck -lt 10) {
                $PingCheck += 1
                if (!(Test-Connection -ComputerName $HostToPatch.Name -Count 1 -Quiet)) { Invoke-Logging @LoggingErrSplat -LogString "Post patch ping checks failed for $($HostToPatch.Name)"; throw "Post patch ping checks failed for $($HostToPatch.Name)" }
                Start-Sleep 3
            }
            
            #Rescan host and verify compliance
            # Write-Host "Rescanning $($HostToPatch.Name) baselines."
            Invoke-Logging $LoggingInfoSplat -LogString "Rescanning $($HostToPatch.Name) baselines."
            Scan-Inventory -Entity $HostToPatch.Name
            # Write-Host "Determining if there are non-compliant baselines"
            Invoke-Logging $LoggingInfoSplat -LogString "Determining if there are non-compliant baselines"
            $PostNcBaselines = Get-Compliance -Entity $HostToPatch.Name -ComplianceStatus NotCompliant
            if ($null -ne $PostNcBaselines) { Invoke-Logging $LoggingWarnSplat -LogString "Patching was attempted on $($HostToPatch.Name) but there are still non-compliant baselines." }
    
            #Verify datastore count matches pre-upgrade count
            Invoke-Logging $LoggingInfoSplat -LogString "Verifying datastore count matches pre-upgrade count."
            $DsCountEnd = ($HostToPatch | Get-Datastore).Count
            if ($DsCountStart -ne $DsCountEnd) { Invoke-Logging @LoggingErrSplat -LogString "Post upgrade datastore count on $($HostToPatch.Name) does not match the pre upgrade count"; throw "Post upgrade datastore count on $($HostToPatch.Name) does not match the pre upgrade count" }
    
            if ($AutoExitMm) {
                Invoke-Logging $LoggingInfoSplat -LogString "$($HostToPatch.Name) exiting Maintenance Mode."
                Set-VMHost -VMHost $HostToPatch -State Connected -Confirm:$false | Out-Null
            }
    
            # Write-Host "Sending success message."
            Invoke-Logging $LoggingSuccSplat -LogString "$($HostToPatch.Name) was successfully patched. Baselines installed: `r`n`t$($Baselines.Name -join "`n`t")"
        }
    }
    catch {
        # Write-Host "Sending failure message."
        Invoke-Logging $LoggingErrSplat -LogString "Attempt to patch $($HostToPatch.Name) failed. The error encountered was:`r`n$($_.Exception.Message)`n$($_.ScriptStackTrace)"
        Invoke-SendEmail -Subject "Host Patch Error" -EmailBody "Attempt to patch $($HostToPatch.Name) failed. The error encountered was:`r`n$($_.Exception.Message)`n$($_.ScriptStackTrace)"
        throw
    }
}

Function Invoke-PatchESXCluster {
    [cmdletbinding()]
    param (
        [Parameter()] [VMware.VimAutomation.ViCore.Impl.V1.Inventory.ClusterImpl]$ClusterToPatch,
        [Parameter()][ValidateSet("DRS", "DrainHost")] [string]$EvacType = "DrainHost",
        [string]$emailTo = ([DC.Automation]::TeamEmail)
    )

    $ErrorActionPreference = "Stop"

    $ScriptStarted = Get-Date -Format MM-dd-yyyy_HH-mm-ss
    $ScriptName = $MyInvocation.MyCommand.Name

    $LoggingSuccSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Succ" }
    $LoggingInfoSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Info" }
    # $LoggingWarnSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Warn"}
    # $LoggingErrSplat = @{ScriptStarted = $ScriptStarted; ScriptName = $ScriptName; LogType = "Err"}

    try {
        if ($null -eq $ClusterToPatch) {
            $Clusters = Get-Cluster | Sort-Object Name
            $ClusterToPatch = Invoke-Menu -Objects $Clusters -MenuColumn "Name" -SelectionText "Please select a cluster for ESX host upgrades" -ClearScreen:$true
        }

        $AreYouSure = Read-Host "Are you sure you want to apply ESX updates to the hosts in cluster $ClusterToPatch (You must respond with 'yes' to continue)?"
        if ($AreYouSure -ne "yes") { Write-Host "You did not respond with 'yes'." }
        else {
            # Write-Host "Getting all the hosts in the cluster sorted by Name."
            Invoke-Logging $LoggingInfoSplat -LogString "Getting all the hosts in the cluster sorted by Name."

            $ClusterHosts = $ClusterToPatch | Get-VMHost | Sort-Object Name

            foreach ($ClusterHost in $ClusterHosts) {
                Invoke-Logging $LoggingInfoSplat -LogString "Calling patch host function for $($ClusterHost.Name)"
                Invoke-PatchESXHost -HostToPatch $ClusterHost -EvacType $EvacType -AutoExitMm:$true
            }
            Invoke-Logging $LoggingSuccSplat -LogString "$($ClusterToPatch.Name) patch process compelete. Check email for server patch failures."
            Invoke-SendEmail -Subject "Cluster Patch Success" -EmailBody "$($ClusterToPatch.Name) was successfully patched."
        }
    }
    catch {
        throw
    }
}