SDM.psm1

<#
.SYNOPSIS
    PowerShell library module for automation (SDM: Stijn Denruyter Module).
.DESCRIPTION
    This PowerShell module contains functions that can be used in other PowerShell scripts for automation.
.NOTES
    FileName: SDM.psm1
    Version: 2.1
    Blog: https://blog.stijndenruyter.be
    Author: Stijn Denruyter
    Twitter: @StijnDenruyter
    Created: 02-11-2020
    Updated: 07-06-2022
 
    Version history:
    1.0 - (02-11-2020) First version.
    2.0 - (17-02-2022) Add comment-based help keywords.
    2.1 - (07-06-2022) Updated Test-SDMComputerConnection
                            - Add WinRM connectivity check.
 
.LINK
    https://www.powershellgallery.com
#>


#region Declaration variables

$Global:LogFilePath = ""            #Used to write the log entries in this module to a custom log file.
                                    #This variable must be initialized in the script that calls this module, otherwise no log file will be used.
$Global:LogFileSize = 10            #The maximum size of a log file before it gets archived. Default 10MB.
                                    #This can be overwritten in the script that calls this module.
$Global:LogFileArchiveNumber = 10    #The maximum number of archived log files. Default 10 log files.
                                    #This can be overwritten in the script that calls this module.

$ErrorActionPreference = "Stop"

#endregion Declaration variables

#region Microsoft Endpoint Manager Configuration Manager

Function Import-SDMMEMCMModule {
    <#
        .SYNOPSIS
        Imports the MEMCM PowerShell module that is locally installed.
 
        .DESCRIPTION
        Prerequisite: MEMCM PowerShell module installed on the system where this command is being used.
    #>

    Write-SDMLog -Message "Import MEMCM PowerShell module..." -Severity Info
    If (-not (Get-Module -Name "ConfigurationManager")) {
        If (Test-Path -Path "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1") {
            Try {
                Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1" -Global
                If (Get-Module -Name "ConfigurationManager") {
                    Write-SDMLog -Message "MEMCM PowerShell module imported" -Severity Info
                }
                Else {
                    Throw
                }
            }
            Catch {
                Write-SDMLog -Message "Failed to import the MEMCM PowerShell module: $($_.Exception.Message)" -Severity Error
                Break
            }
        }
        ElseIf (Test-Path -Path "C:\Program Files (x86)\Microsoft Endpoint Manager\AdminConsole\bin\ConfigurationManager.psd1") {
            Try {
                Import-Module "C:\Program Files (x86)\Microsoft Endpoint Manager\AdminConsole\bin\ConfigurationManager.psd1" -Global
                If (Get-Module -Name "ConfigurationManager") {
                    Write-SDMLog -Message "MEMCM PowerShell module imported" -Severity Info
                }
                Else {
                    Throw
                }
            }
            Catch {
                Write-SDMLog -Message "Failed to import the MEMCM PowerShell module: $($_.Exception.Message)" -Severity Error
                Break
            }
        }
        Else {
            Write-SDMLog -Message "MEMCM PowerShell module is not installed on this system" -Severity Error
            Break
        }
    }
    Else {
        Write-SDMLog -Message "MEMCM PowerShell module already imported" -Severity Info
    }
}

Function Import-SDMMEMCMDevice {
    <#
        .SYNOPSIS
        Imports a device into MEMCM.
 
        .DESCRIPTION
        Imports a device based on MAC address to MEMCM and adds it to a device collection.
 
        .PARAMETER Name
        Specifies the name of the device.
 
        .PARAMETER MACAddress
        Specifies the MAC address of the device.
 
        .PARAMETER DeviceCollection
        Specifies the name of the device collection the device should be added to.
 
        .PARAMETER MEMCMServer
        Specifies the name of the MEMCM server.
 
        .PARAMETER MEMCMSiteCode
        Specifies the site code name of the MEMCM server.
 
        .EXAMPLE
        Import-SDMMEMCMDevice -Name "CLIENT01" -MACAddress "00:00:00:00:00:01" -MEMCMServer "SERVER01.domain.local" -MEMCMSiteCode "MCM"
 
        .EXAMPLE
        Import-SDMMEMCMDevice -Name "CLIENT01" -MACAddress "00:00:00:00:00:01" -DeviceCollection "All client devices" -MEMCMServer "SERVER01.domain.local" -MEMCMSiteCode "MCM"
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Name,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MACAddress,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [String]$DeviceCollection = "",
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MEMCMServer,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MEMCMSiteCode
    )
    Write-SDMLog -Message "Import MEMCM device $($Name)..." -Severity Info
    Mount-SDMMEMCMSiteCode -MEMCMServer $MEMCMServer -MEMCMSiteCode $MEMCMSiteCode
    Try {
        If (-not ($MEMCMSiteCode[-1] -eq ":")) {
            $MEMCMSiteCodeWithSuffix = "$($MEMCMSiteCode):"
        }
        Else {
            $MEMCMSiteCodeWithSuffix = $MEMCMSiteCode
            $MEMCMSiteCode = $MEMCMSiteCode -Replace ".$"
        }
        Set-Location -Path $MEMCMSiteCodeWithSuffix
    }
    Catch {
        Write-SDMLog -Message "Failed to set the MEMCM site code to $($MEMCMSiteCode): $($_.Exception.Message)" -Severity Error
        Break
    }
    If (($DeviceCollection -ne "") -and (-not (Get-CMDeviceCollection -Name $DeviceCollection))) {
        Write-SDMLog -Message "Device collection $($DeviceCollection) does not exist" -Severity Error
        Break
    }
    If (Get-CMDevice -Name $Name) {
        Write-SDMLog -Message "MEMCM device $($Name) already exist. Remove MEMCM device $($Name)..." -Severity Info
        Try {
            Remove-CMDevice -DeviceName $Name -Force
            If (-not (Get-CMDevice -Name $Name)) {
                Write-SDMLog -Message "MEMCM device $($Name) removed" -Severity Info
            }
            Else {
                Throw
            }
        }
        Catch {
            Write-SDMLog -Message "Failed to remove MEMCM device $($Name): $($_.Exception.Message)" -Severity Error
            Break
        }
    }
    Try {
        If ($DeviceCollection -ne "") {
            Import-CMComputerInformation -ComputerName $Name -CollectionName $DeviceCollection -MacAddress $MacAddress
        }
        Else {
            Import-CMComputerInformation -ComputerName $Name -MacAddress $MacAddress
        }
        $TimeOut = 20 #Time-out after 10 minutes
        Do {
            $TimeOut = $TimeOut - 1
            Start-Sleep -Seconds 30
        } While (((Get-CMDevice -Name $Name) -eq $Null) -and ($TimeOut -ne 0))
        If (Get-CMDevice -Name $Name) {
            Write-SDMLog -Message "MEMCM device $($Name) imported" -Severity Info
        }
        Else {
            Throw
        }
    }
    Catch {
        Write-SDMLog -Message "Failed to import MEMCM device $($Name): $($_.Exception.Message)" -Severity Error
        Break
    }
    If ($DeviceCollection -ne "") {
        $ExitLoop = $False
        $RetryCount = 5
        Do {
            Try {
                $DeviceCollectionQuery = Get-WmiObject -Namespace "Root\SMS\Site_$($MEMCMSiteCode)" -Class SMS_Collection -ComputerName $MEMCMServer -Filter "Name='$DeviceCollection'"
                $DeviceCollectionQuery.RequestRefresh() | Out-Null
                $TimeOut = 180 #Time-out after 30 minutes
                Do {
                    $TimeOut = $TimeOut - 1
                    Start-Sleep -Seconds 10
                } While (((Get-CMDevice -CollectionName $DeviceCollection -Name $Name) -eq $Null) -and ($TimeOut -ne 0))
                If (Get-CMDevice -CollectionName $DeviceCollection -Name $Name) {
                    Write-SDMLog -Message "MEMCM device $($Name) added to device collection $($DeviceCollection)" -Severity Info
                }
                Else {
                    Throw
                }
                $ExitLoop = $True
            }
            Catch {
                If ($RetryCount -eq 0) {
                    Write-SDMLog -Message "Failed to add MEMCM device $($Name) to device collection $($DeviceCollection): $($_.Exception.Message)" -Severity Error
                    Break
                }
                Else {
                    Write-SDMLog -Message "Failed to add MEMCM device $($Name) to device collection $($DeviceCollection): retrying in 30 seconds..." -Severity Warning
                    Start-Sleep -Seconds 30
                    $RetryCount = $RetryCount - 1
                }
            }
        } While ($ExitLoop -eq $False)
    }
}

Function Mount-SDMMEMCMSiteCode {
    <#
        .SYNOPSIS
        Maps the MEMCM site code to the MEMCM server.
 
        .DESCRIPTION
        Creates a temporary drive with the same name as the MEMCM site code that is mapped to the MEMCM server.
 
        .PARAMETER MEMCMServer
        Specifies the name of the MEMCM server.
 
        .PARAMETER MEMCMSiteCode
        Specifies the site code name of the MEMCM server.
 
        .EXAMPLE
        Mount-SDMMEMCMSiteCode -MEMCMServer "SERVER01.domain.local" -MEMCMSiteCode "MCM"
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MEMCMServer,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MEMCMSiteCode
    )
    Try {
        If (-not ($MEMCMSiteCode[-1] -eq ":")) {
            $MEMCMSiteCodeWithSuffix = "$($MEMCMSiteCode):"
        }
        Else {
            $MEMCMSiteCodeWithSuffix = $MEMCMSiteCode
            $MEMCMSiteCode = $MEMCMSiteCode -Replace ".$"
        }
        If (-not (Get-PSDrive | Where-Object Name -eq $MEMCMSiteCode)) {
            Write-SDMLog -Message "Mount MEMCM site code $($MEMCMSiteCode) to server $($MEMCMServer)..." -Severity Info
            New-PSDrive -Name $MEMCMSiteCode -PSProvider "CMSite" -Root $MEMCMServer -Scope Global | Out-Null
            If (Get-PSDrive | Where-Object Name -eq $MEMCMSiteCode) {
                Write-SDMLog -Message "MEMCM site code $($MEMCMSiteCode) mounted to server $($MEMCMServer)" -Severity Info
            }
            Else {
                Throw
            }
        }
    }
    Catch {
        Write-SDMLog -Message "Failed to mount the MEMCM site code $($MEMCMSiteCode) to server $($MEMCMServer): $($_.Exception.Message)" -Severity Error
        Break
    }
}

Function Add-SDMMEMCMDeviceCollectionDirectMembershipRule {
    <#
        .SYNOPSIS
        Adds a device to a MEMCM device collection.
 
        .DESCRIPTION
        Adds a device to a MEMCM device collection.
 
        .PARAMETER Name
        Specifies the name of the device.
 
        .PARAMETER DeviceCollection
        Specifies the name of the device collection.
 
        .PARAMETER MEMCMServer
        Specifies the name of the MEMCM server.
 
        .PARAMETER MEMCMSiteCode
        Specifies the site code name of the MEMCM server.
 
        .EXAMPLE
        Add-SDMMEMCMDeviceCollectionDirectMembershipRule -Name "CLIENT01" -DeviceCollection "All client devices" -MEMCMServer "SERVER01.domain.local" -MEMCMSiteCode "MCM"
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Name,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$DeviceCollection,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MEMCMServer,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MEMCMSiteCode
    )
    Write-SDMLog -Message "Add MEMCM device $($Name) to device collection $($DeviceCollection)..." -Severity Info
    Mount-SDMMEMCMSiteCode -MEMCMServer $MEMCMServer -MEMCMSiteCode $MEMCMSiteCode
    Try {
        If (-not ($MEMCMSiteCode[-1] -eq ":")) {
            $MEMCMSiteCodeWithSuffix = "$($MEMCMSiteCode):"
        }
        Else {
            $MEMCMSiteCodeWithSuffix = $MEMCMSiteCode
            $MEMCMSiteCode = $MEMCMSiteCode -Replace ".$"
        }
        Set-Location -Path $MEMCMSiteCodeWithSuffix
    }
    Catch {
        Write-SDMLog -Message "Failed to set the MEMCM site code to $($MEMCMSiteCode): $($_.Exception.Message)" -Severity Error
        Break
    }
    Try {
        Add-CMDeviceCollectionDirectMembershipRule -CollectionName $DeviceCollection -ResourceId (Get-CMDevice -Name $Name).ResourceID
    }
    Catch {
        Write-SDMLog -Message "Failed to add MEMCM device $($Name) to device collection $($DeviceCollection): $($_.Exception.Message)" -Severity Error
        Break
    }
    $ExitLoop = $False
    $RetryCount = 5
    Do {
        Try {
            $DeviceCollectionQuery = Get-WmiObject -Namespace "Root\SMS\Site_$($MEMCMSiteCode)" -Class SMS_Collection -ComputerName $MEMCMServer -Filter "Name='$DeviceCollection'"
            $DeviceCollectionQuery.RequestRefresh() | Out-Null
            $TimeOut = 180 #Time-out after 30 minutes
            Do {
                $TimeOut = $TimeOut - 1
                Start-Sleep -Seconds 10
            } While (((Get-CMDevice -CollectionName $DeviceCollection -Name $Name) -eq $Null) -and ($TimeOut -ne 0))
            If (Get-CMDevice -CollectionName $DeviceCollection -Name $Name) {
                Write-SDMLog -Message "MEMCM device $($Name) added to device collection $($DeviceCollection)" -Severity Info
            }
            Else {
                Throw
            }
            $ExitLoop = $True
        }
        Catch {
            If ($RetryCount -eq 0) {
                Write-SDMLog -Message "Failed to add MEMCM device $($Name) to device collection $($DeviceCollection): $($_.Exception.Message)" -Severity Error
                Break
            }
            Else {
                Write-SDMLog -Message "Failed to add MEMCM device $($Name) to device collection $($DeviceCollection): retrying in 60 seconds..." -Severity Warning
                Start-Sleep -Seconds 60
                $RetryCount = $RetryCount - 1
            }
        }
    } While ($ExitLoop -eq $False)
}

Function Add-SDMMEMCMDeviceVariable {
    <#
        .SYNOPSIS
        Adds a variable to a device.
 
        .DESCRIPTION
        Adds a variable to a device.
 
        .PARAMETER Name
        Specifies the name of the device.
 
        .PARAMETER Variable
        Specifies the name of the variable.
 
        .PARAMETER Value
        Specifies the value of the variable.
 
        .PARAMETER MEMCMServer
        Specifies the name of the MEMCM server.
 
        .PARAMETER MEMCMSiteCode
        Specifies the site code name of the MEMCM server.
 
        .PARAMETER IsMask
        Specifies if the value should be masked.
 
        .EXAMPLE
        Add-SDMMEMCMDeviceVariable -Name "CLIENT01" -Variable "Variable01" -Value "Value01" -MEMCMServer "SERVER01.domain.local" -MEMCMSiteCode "MCM"
 
        .EXAMPLE
        Add-SDMMEMCMDeviceVariable -Name "CLIENT01" -Variable "Variable01" -Value "Value01" -MEMCMServer "SERVER01.domain.local" -MEMCMSiteCode "MCM" -IsMask:$True
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Name,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Variable,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Value,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MEMCMServer,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$MEMCMSiteCode,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [Bool]$IsMask = $False
    )
    Write-SDMLog -Message "Add MEMCM device variable $($Variable) with value $($Value) to device $($Name)..." -Severity Info
    Mount-SDMMEMCMSiteCode -MEMCMServer $MEMCMServer -MEMCMSiteCode $MEMCMSiteCode
    Try {
        If (-not ($MEMCMSiteCode[-1] -eq ":")) {
            $MEMCMSiteCodeWithSuffix = "$($MEMCMSiteCode):"
        }
        Else {
            $MEMCMSiteCodeWithSuffix = $MEMCMSiteCode
            $MEMCMSiteCode = $MEMCMSiteCode -Replace ".$"
        }
        Set-Location -Path $MEMCMSiteCodeWithSuffix
    }
    Catch {
        Write-SDMLog -Message "Failed to set the MEMCM site code to $($MEMCMSiteCode): $($_.Exception.Message)" -Severity Error
        Break
    }
    Try {
        New-CMDeviceVariable -DeviceName $Name -VariableName $Variable -VariableValue $Value -IsMask $IsMask | Out-Null
        Do {
            Start-Sleep -Seconds 5
        } Until (((Get-CMDeviceVariable -DeviceName $Name -VariableName $Variable | Select-Object Value).Value) -eq $Value)
        If (((Get-CMDeviceVariable -DeviceName $Name -VariableName $Variable | Select-Object Value).Value) -eq $Value) {
            Write-SDMLog -Message "MEMCM device variable $($Variable) with value $($Value) added to device $($Name)" -Severity Info
        }
        Else {
            Throw
        }
    }
    Catch {
        Write-SDMLog -Message "Failed to add MEMCM device variable $($Variable) with value $($Value) added to device $($Name): $($_.Exception.Message)" -Severity Error
    }
}

Function Test-SDMMEMCMDeviceExists {
    <#
        .SYNOPSIS
        Checks if a device exists in MEMCM.
 
        .DESCRIPTION
        Checks if a device exists in MEMCM.
 
        .PARAMETER Name
        Specifies the name of the device.
 
        .OUTPUTS
        Returns a boolean True or False.
 
        .EXAMPLE
        Test-SDMMEMCMDeviceExists -Name "CLIENT01"
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Name
    )
    Try {
        If (Get-CMDevice -Name $Name) {
            Return $True
        }
        Else {
            Return $False
        }
    }
    Catch {
        Write-SDMLog -Message "Failed to check if MEMCM device $($Name) exists: $($_.Exception.Message)" -Severity Error
    }
}

#endregion Microsoft EndPoint Manager Configuration Manager

#region Windows

Function Test-SDMComputerConnection {
    <#
        .SYNOPSIS
        Checks if a computer is reachable.
 
        .DESCRIPTION
        Checks if a computer responds to a ping, UNC and WinRM.
 
        .PARAMETER Name
        Specifies the computer name.
 
        .PARAMETER UNC
        Specifies if UNC reachability should be checked. Default true.
 
        .PARAMETER WSMan
        Specifies if WinRM reachability should be checked. Default false.
 
        .PARAMETER Retry
        Specifies the number of retries. Default 0.
 
        .EXAMPLE
        Test-SDMComputerConnection -Name "CLIENT01"
 
        .EXAMPLE
        Test-SDMComputerConnection -Name "CLIENT01" -Retry 10
 
        .EXAMPLE
        Test-SDMComputerConnection -Name "CLIENT01" -UNC:$True -WSman:$True -Retry 10
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Name,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [Switch]$UNC = $True,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [Switch]$WSMan = $False,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [Int]$Retry = 0
    )
    Write-SDMLog -Message "Test connectivity to device $($Name)..." -Severity Info
    Try {
        If (-not (Test-Connection -ComputerName $Name -Count 1 -ErrorAction SilentlyContinue)) {
            If ($Retry -gt 0) {
                $Retries = $Retry
                Do {
                    Write-SDMLog -Message "Failed to ping device $($Name): retrying in 10 seconds..." -Severity Warning
                    $Retries = $Retries - 1
                    Start-Sleep -Seconds 9
                } Until ((Test-Connection -ComputerName $Name -Count 1 -ErrorAction SilentlyContinue) -or ($Retries -le 0))
                If ($Retries -le 0) {
                    Write-SDMLog -Message "Failed to ping device $($Name)" -Severity Error
                    Throw
                }
            }
            Else {
                Write-SDMLog -Message "Failed to ping device $($Name)" -Severity Error
                Throw
            }
        }
        If ((-not (Test-Path -Path "filesystem::\\$($Name)\C$" -ErrorAction SilentlyContinue)) -and $UNC) {
            If ($Retry -gt 0) {
                $Retries = $Retry
                Do {
                    Write-SDMLog -Message "Failed to connect to filesystem on device $($Name): retrying in 10 seconds..." -Severity Warning
                    $Retries = $Retries - 1
                    Start-Sleep -Seconds 10
                } Until ((Test-Path -Path "filesystem::\\$($Name)\C$" -ErrorAction SilentlyContinue) -or ($Retries -le 0))
                If ($Retries -le 0) {
                    Write-SDMLog -Message "Failed to connect to filesystem on device $($Name)" -Severity Error
                    Throw
                }
            }
            Else {
                Write-SDMLog -Message "Failed to connect to filesystem on device $($Name)" -Severity Error
                Throw
            }
        }
        If ((-not (Test-WSMan -ComputerName $AzVMHostname -ErrorAction SilentlyContinue)) -and $WSMan) {
            If ($Retry -gt 0) {
                $Retries = $Retry
                Do {
                    Write-SDMLog -Message "The WinRM service is not running on device $($Name): retrying in 10 seconds..." -Severity Warning
                    $Retries = $Retries - 1
                    Start-Sleep -Seconds 10
                } Until ((Test-WSMan -ComputerName $AzVMHostname -ErrorAction SilentlyContinue) -or ($Retries -le 0))
                If ($Retries -le 0) {
                    Write-SDMLog -Message "The WinRM service is not running on device $($Name)" -Severity Error
                    Throw
                }
            }
            Else {
                Write-SDMLog -Message "The WinRM service is not running on device $($Name)" -Severity Error
                Throw
            }
        }
        Write-SDMLog -Message "Successfully tested connectivity to device $($Name)" -Severity Info
    }
    Catch {
        Write-SDMLog -Message "Failed to test connectivity to device $($Name)" -Severity Error
        Break
    }
}

#endregion Windows

#region VMWare

Function New-SDMVMGuest {
    <#
        .SYNOPSIS
        Creates a VMWare guest.
 
        .DESCRIPTION
        Creates a VMWare guest.
 
        .PARAMETER Name
        Specifies the name of the VMWare guest.
 
        .PARAMETER VMHostClusterName
        Specifies the name of the VMWare cluster.
 
        .PARAMETER VMGuestOS
        Specifies the operating system that will be installed on the VMWare guest.
 
        .PARAMETER VMGuestCPUSockets
        Specifies the number of virtual CPU's.
 
        .PARAMETER VMGuestCPUCores
        Specifies the number of cores per virtual CPU.
 
        .PARAMETER VMGuestMemory
        Specifies the amount of virtual memory.
 
        .PARAMETER VMDatastoreClusterName
        Specifies the name of the VMWare datastore cluster.
 
        .PARAMETER VMGuestDiskSpace
        Specifies the amount of disk space for the VMWare guest.
 
        .PARAMETER VMGuestDiskStorageFormat
        Specifies the type of disk space storage.
 
        .PARAMETER VMGuestVLAN
        Specifies the number of the VLAN.
 
        .PARAMETER VMGuestNotes
        Specifies notes for the VMWare guest.
 
        .PARAMETER VMGuestISOPath
        Specifies the datastore path to the ISO file.
 
        .PARAMETER VMGuestEFIBIOS
        Specifies if BIOS or EFI is used. Default false.
 
        .EXAMPLE
        New-SDMVMGuest -Name "CLIENT01" -VMGuestOS "windows9_64Guest" -VMGuestCPUSockets 1 -VMGuestCPUCores 4 -VMGuestMemory 16 -VMGuestDiskSpace 80 -VMGuestDiskStorageFormat "Thin" -VMGuestVLAN 10
 
        .EXAMPLE
        New-SDMVMGuest -Name "CLIENT01" -VMHostClusterName "CLUSTER01" -VMGuestOS "windows9_64Guest" -VMGuestCPUSockets 1 -VMGuestCPUCores 4 -VMGuestMemory 16 -VMDatastoreClusterName "DATASTORECLUSTER01" -VMGuestDiskSpace 80 -VMGuestDiskStorageFormat "Thin" -VMGuestVLAN 10 -VMGuestNotes "VMWare guest system" -VMGuestISOPath = "[VM-DS-01] ISO/Bootable.ISO" -VMGuestEFIBIOS:$False
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Name,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [String]$VMHostClusterName = "",
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$VMGuestOS,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [Int]$VMGuestCPUSockets,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [Int]$VMGuestCPUCores,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [Int]$VMGuestMemory,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [String]$VMDatastoreClusterName = "",
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [Int]$VMGuestDiskSpace,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("EagerZeroedThick", "Thick", "Thick2GB", "Thin", "Thin2GB")]
        [String]$VMGuestDiskStorageFormat,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$VMGuestVLAN,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [String]$VMGuestNotes = "",
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [String]$VMGuestISOPath = "",
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [Switch]$VMGuestEFIBIOS = $False
    )
    Write-SDMLog -Message "Creating VMWare guest $($Name)..." -Severity Info
    If (Get-VM | Where-Object {$_.Name -eq $Name}) {
        Remove-SDMVMGuest -Name $Name -Confirm:$False
    }
    Try {
        If ($VMHostClusterName -ne "") {
            $VMHostClusterObject = Get-Cluster -Name $VMHostClusterName
            $VMHostObject = $VMHostClusterObject | Get-VMHost | Select-Object Name, ConnectionState, PowerState, @{N='Load';E={[Math]::Round((($_.CpuUsageMhz/$_.CpuTotalMhz)*100)+(($_.MemoryUsageGB/$_.MemoryTotalGB)*100))}} | Where-Object {$_.ConnectionState -eq "Connected" -and $_.PowerState -eq "PoweredOn"} | Sort-Object Load | Select-Object -First 1
        }
        Else {
            $VMHostObject = Get-VMHost | Select-Object Name, ConnectionState, PowerState, @{N='Load';E={[Math]::Round((($_.CpuUsageMhz/$_.CpuTotalMhz)*100)+(($_.MemoryUsageGB/$_.MemoryTotalGB)*100))}} | Where-Object {$_.ConnectionState -eq "Connected" -and $_.PowerState -eq "PoweredOn"} | Sort-Object Load | Select-Object -First 1
        }
        If ($VMDatastoreClusterName -ne "") {
            $VMDatastoreClusterObject = Get-DatastoreCluster -Name $VMDatastoreClusterName
            $VMDataStoreObjects = $VMDatastoreClusterObject | Get-Datastore
            $VMDatastoreObject = $VMDataStoreObjects | Where-Object {$VMDataStoreObjects.Name -eq $_.Name -and $_.State -eq "Available"} | Sort-Object FreeSpaceGB -Descending | Select-Object -First 1
        }
        Else {
            $VMDatastoreObject = Get-VMHost -Name $VMHostObject.Name | Get-Datastore | Where-Object {$_.State -eq "Available"} | Sort-Object FreeSpaceGB -Descending | Select-Object -First 1
        }
        If (Get-VirtualPortGroup -VMHost $VMHostObject.Name | Where-Object {$_.VLANID -eq $VMGuestVLAN}) {
            $VMGuestVLANObject = Get-VirtualPortGroup -VMHost $VMHostObject.Name | Where-Object {$_.VLANID -eq $VMGuestVLAN}
        }
        Else {
            Write-SDMLog -Message "VLAN $($VMGuestVLAN) is not available on host $($VMHostObject.Name)" -Severity Error
            Throw
        }
        If (-not (Get-VMHost -Name $VMHostObject.Name | Get-Datastore | Where-Object {$_.State -eq "Available" -and $_.Name -eq ($VMGuestISOPath | Select-String '(?<=\[)[^]]+(?=\])' -AllMatches).Matches.Value})) {
            Write-SDMLog -Message "ISO path is not available on host $($VMHostObject.Name)" -Severity Error
            Throw
        }
        $VMWareGuest = New-VM -VMHost $VMHostObject.Name -Name $Name -Datastore $VMDatastoreObject -DiskGB $VMGuestDiskSpace -DiskStorageFormat $VMGuestDiskStorageFormat -MemoryGB $VMGuestMemory -NumCpu $VMGuestCPUCores -CD -GuestID $VMGuestOS -NetworkName $VMGuestVLANObject -Notes $VMGuestNotes
        $VMGuestMACAddress = (Get-VM -Name $Name | Get-NetworkAdapter).MacAddress.ToUpper()
        $VMWareGuest | Get-NetworkAdapter | Set-NetworkAdapter -Type vmxnet3 -Confirm:$False | Out-Null
        $VMWareGuest | Get-NetworkAdapter | Set-NetworkAdapter -NetworkName $VMGuestVLANObject -Confirm:$False | Out-Null
        New-AdvancedSetting -Name devices.hotplug -Value False -Entity $VMWareGuest -Confirm:$False | Out-Null
        $VMGuestCoresPerSocket = $VMGuestCPUCores / $VMGuestCPUSockets
        New-AdvancedSetting -Name cpuid.coresPerSocket -Value $VMGuestCoresPerSocket -Entity $VMWareGuest -Confirm:$False | Out-Null
        $VMWareGuest | Get-CDDrive | Set-CDDrive -IsoPath $VMGuestISOPath -StartConnected $True -Confirm:$False | Out-Null
        $VMGuestSpec = New-Object VMWare.Vim.VirtualMachineConfigSpec
        If ($VMGuestEFIBios) {
            $VMGuestSpec.Firmware = [VMWare.Vim.GuestOsDescriptorFirmwareType]::efi
        }
        Else {
            $VMGuestSpec.Firmware = [VMWare.Vim.GuestOsDescriptorFirmwareType]::bios
        }
        $VMWareGuest.ExtensionData.ReconfigVM_Task($VMGuestSpec) | Out-Null
        If (Get-VM | Where-Object {$_.Name -eq $Name}) {
            Write-SDMLog -Message "VMWare guest succesfully created with the following settings:" -Severity Info
            Write-SDMLog -Message "Name..................: $($Name)" -Severity Info
            Write-SDMLog -Message "Guest OS..............: $($VMGuestOS)" -Severity Info
            Write-SDMLog -Message "vCPU sockets..........: $($VMGuestCPUSockets)" -Severity Info
            Write-SDMLog -Message "vCPU cores............: $($VMGuestCPUCores)" -Severity Info
            Write-SDMLog -Message "Memory GB.............: $($VMGuestMemory)" -Severity Info
            Write-SDMLog -Message "Disk size GB..........: $($VMGuestDiskSpace)" -Severity Info
            Write-SDMLog -Message "Disk storage format...: $($VMGuestDiskStorageFormat)" -Severity Info
            If ($VMGuestEFIBIOS) {
                Write-SDMLog -Message "BIOS..................: EFI" -Severity Info
            }
            Else {
                Write-SDMLog -Message "BIOS..................: Legacy" -Severity Info
            }
            If ($VMHostClusterName -ne "") {
                Write-SDMLog -Message "Host cluster..........: $($VMHostClusterName)" -Severity Info
            }
            Else {
                $VMHostClusterName = Get-VMHost -Name $VMHostObject.Name | Get-Cluster
                If ($VMHostClusterName -ne "") {
                    Write-SDMLog -Message "Host cluster..........: $($VMHostClusterName)" -Severity Info
                }
            }
            Write-SDMLog -Message "Host..................: $($VMHostObject.Name)" -Severity Info
            If ($VMDatastoreClusterName -ne "") {
                Write-SDMLog -Message "Datastore cluster.....: $($VMDatastoreClusterName)" -Severity Info
            }
            Else {
                If (Get-Datastore -Name $VMDatastoreObject | Get-DatastoreCluster) {
                    $VMDatastoreClusterName = Get-Datastore -Name $VMDatastoreObject | Get-DatastoreCluster
                    Write-SDMLog -Message "Datastore cluster.....: $($VMDatastoreClusterName)" -Severity Info
                }
                Else {
                    Write-SDMLog -Message "Datastore cluster.....: N/A" -Severity Info
                }
            }
            Write-SDMLog -Message "Datastore.............: $($VMDatastoreObject)" -Severity Info
            Write-SDMLog -Message "VLAN..................: $($VMGuestVLAN)" -Severity Info
            Write-SDMLog -Message "MAC address...........: $($VMGuestMACAddress)" -Severity Info
            Write-SDMLog -Message "Notes.................: $($VMGuestNotes)" -Severity Info
            Write-SDMLog -Message "ISO path..............: $($VMGuestISOPath)" -Severity Info
        }
        Else {
            Throw
        }
    }
    Catch {
        Write-SDMLog -Message "Failed to create a VMWare guest with the name $($Name): $($_.Exception.Message)" -Severity Error
        Break
    }
}

Function Remove-SDMVMGuest {
    <#
        .SYNOPSIS
        Removes a VMWare guest.
 
        .DESCRIPTION
        Removes a VMWare guest.
 
        .PARAMETER Name
        Specifies the name of the VMWare guest.
 
        .PARAMETER Confirm
        Specifies if a confirmation is needed. Default true.
 
        .EXAMPLE
        Remove-SDMVMGuest -Name "CLIENT01"
 
        .EXAMPLE
        Remove-SDMVMGuest -Name "CLIENT01" -Confirm:$False
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True,ValueFromPipeline = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Name,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [Switch]$Confirm = $True
    )
    Process {
        $Result = Get-VM | Where-Object {$_.Name -eq $Name}
        If ($Result) {
            If ($Result.PowerState -eq "PoweredOn") {
                Try {
                    Write-SDMLog -Message "Powering off VMWare guest $($Name)..." -Severity Info
                    Stop-VM -VM $Name -Confirm:$Confirm | Out-Null
                    $TimeOut = 12 #Time-out after 1 minute
                    Do {
                        $TimeOut = $TimeOut - 1
                        Start-Sleep -Seconds 5
                        $Result = Get-VM | Where-Object {$_.Name -eq $Name}
                    } While (($Result.PowerState -eq "PoweredOn") -and ($TimeOut -ne 0))
                    If ($Result.PowerState -eq "PoweredOff") {
                        Write-SDMLog -Message "VMWare guest $($Name) is powered off" -Severity Info
                    }
                    Else {
                        Throw
                    }
                }
                Catch {
                    Write-SDMLog -Message "Failed to power off VMWare guest $($Name): $($_.Exception.Message)" -Severity Error
                    Break
                }
            }
            Try {
                Write-SDMLog -Message "Removing VMWare guest $($Name)..." -Severity Info
                Remove-VM -DeletePermanently -VM $Name -Confirm:$Confirm
                $TimeOut = 12 #Time-out after 1 minute
                Do {
                    $TimeOut = $TimeOut - 1
                    Start-Sleep -Seconds 5
                    $Result = Get-VM | Where-Object {$_.Name -eq $Name}
                } While (($Result) -and ($TimeOut -ne 0))
                If (-not ($Result)) {
                    Write-SDMLog -Message "VMWare guest $($Name) is removed" -Severity Info
                }
                Else {
                    Throw
                }
            }
            Catch {
                Write-SDMLog -Message "Failed to remove VMWare guest $($Name): $($_.Exception.Message)" -Severity Error
                Break
            }
        }
    }
}

#endregion VMWare

#region Logging

Function Write-SDMLog {
    <#
        .SYNOPSIS
        Displays log messages and writes them to a log file.
 
        .DESCRIPTION
        Displays log messages and writes them to a log file.
 
        .PARAMETER Message
        Specifies the message.
 
        .PARAMETER Severity
        Specifies the severity of the message.
 
        .PARAMETER Path
        Specifies the path where the log file is saved.
 
        .PARAMETER SkipLogFileRotation
        No log file rotation and cleanup will be done. Default false.
 
        .EXAMPLE
        Write-SDMLog -Message "This is a log message" -Severity Info
 
        .EXAMPLE
        Write-SDMLog -Message "This is a log message" -Severity Info -Path "C:\Logfile.log" -SkipLogFileRotation:$True
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Message,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [ValidateSet("Info", "Warning", "Error")]
        [String]$Severity,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [String]$Path = $Global:LogFilePath,
        [Parameter(Mandatory = $False)]
        [ValidateNotNullOrEmpty()]
        [Switch]$SkipLogFileRotation = $False
    )
    $DateLogFile = Get-Date -Format "MM-dd-yyyy"
    $DateConsole = Get-Date -Format "dd-MM-yyyy"
    $TimeLogFile = Get-Date -Format "HH:mm:ss.ffffff"
    $TimeConsole = Get-Date -Format "HH:mm:ss"
    $DateArchive = Get-Date -Format "yyyyMMdd"
    $TimeArchive = Get-Date -Format "HHmmss"
    $Context = $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
    If ($Null -eq $MyInvocation.PSCommandPath) {
        $Source = "Script"
    }
    Else {
        $Source = Split-Path -Path $MyInvocation.PSCommandPath -Leaf
    }
    $Component = Get-SDMFunctionName -StackNumber 2
    Switch ($Severity) {
        "Info"        {[Int]$Severity = 1}
        "Warning"    {[Int]$Severity = 2}
        "Error"        {[Int]$Severity = 3}
    }
    If ($PSBoundParameters.ContainsKey("Path") -or ($Path -ne "")) {
        If (-not (Test-Path -Path $Path)) {
            Try {
                New-Item -Path $Path -ItemType File | Out-Null
            }
            Catch {
                If (Test-SDMAzureRunbookConsole) {
                    Write-Output "[$($DateConsole) $($TimeConsole)] - [W]: Unable to create log file $($Path): $($_.Exception.Message)"
                }
                Else {
                    Write-Host "[$($DateConsole) $($TimeConsole)]: Unable to create log file $($Path): $($_.Exception.Message)" -ForegroundColor Yellow
                }
            }
        }
        $LogText = "<![LOG[$($Message)]LOG]!><time=""$($TimeLogFile)"" date=""$($DateLogFile)"" component=""$($Component)"" context=""$($Context)"" type=""$($Severity)"" thread=""$($PID)"" file=""$($Source)"">"
        Try {
            Add-Content -Value $LogText -Path $Path
        }
        Catch {
            If (Test-SDMAzureRunbookConsole) {
                Write-Output "[$($DateConsole) $($TimeConsole)] - [W]: Unable to write to log file $($Path): $($_.Exception.Message)"
            }
            Else {
                Write-Host "[$($DateConsole) $($TimeConsole)]: Unable to write to log file $($Path): $($_.Exception.Message)" -ForegroundColor Yellow
            }
        }
        #No log file rotation and cleanup will be done when using the -SkipLogFileRotation parameter.
        If (-not ($SkipLogFileRotation)) {
            $LogFileSize = (Get-Item -Path $Path).Length/1MB
            $LogFileDirectoryName = (Get-Item -Path $Path).DirectoryName
            $LogFileBaseName = (Get-Item -Path $Path).BaseName
            $LogFileExtension = (Get-Item -Path $Path).Extension
            $LogFileArchiveSuffix = "-$($DateArchive)-$($TimeArchive)"
            #Archive log file when exceeding the maximum log file size
            If ($LogFileSize -gt $Global:LogFileSize) {
                Try {
                    Rename-Item -Path $Path -NewName "$($LogFileBaseName)$($LogFileArchiveSuffix)$($LogFileExtension)"
                    Write-SDMLog -Message "Previous log file has been archived in $($LogFileBaseName)$($LogFileArchiveSuffix)$($LogFileExtension)" -Severity Info -Path $Path -SkipLogFileRotation
                }
                Catch {
                    Write-SDMLog -Message "Unable to archive log file $($Path): $($_.Exception.Message)" -Severity Warning -Path $Path -SkipLogFileRotation
                }
            }
            #Cleanup old archived log files when exceeding the maximum number
            $LogFileArchiveCount = (Get-ChildItem -Path $LogFileDirectoryName -Filter "$($LogFileBaseName)-????????-??????$($LogFileExtension)").Count
            If ($LogFileArchiveCount -gt $Global:LogFileArchiveNumber) {
                $LogFileArchiveExpiredCount = ($LogFileArchiveCount - $Global:LogFileArchiveNumber)
                $LogFileArchiveToDelete = (Get-ChildItem -Path $LogFileDirectoryName -Filter "$($LogFileBaseName)-????????-??????$($LogFileExtension)" | Select-Object Name -First $LogFileArchiveExpiredCount | Sort-Object Name).Name
                Try {
                    ForEach ($LogFile In $LogFileArchiveToDelete) {
                        Remove-Item -Path "$($LogFileDirectoryName)\$($LogFile)"
                        Write-SDMLog -Message "Delete old archived log file $($LogFile)" -Severity Info -Path $Path -SkipLogFileRotation
                    }
                }
                Catch {
                    Write-SDMLog -Message "Unable to delete old archive log files: $($_.Exception.Message)" -Severity Warning -Path $Path -SkipLogFileRotation
                }
            }
        }
    }
    #Write logging to the console
    Switch ($Severity) {
        1    {
            If (Test-SDMAzureRunbookConsole) {
                $Command = "Write-Output '[$($DateConsole) $($TimeConsole)] - [I]: $($Message)'"
            }
            Else {
                $Command = "Write-Host [$($DateConsole) $($TimeConsole)]: $($Message) -ForegroundColor Green -BackgroundColor Black"
            }
        }
        2    {
            If (Test-SDMAzureRunbookConsole) {
                $Command = "Write-Output '[$($DateConsole) $($TimeConsole)] - [W]: $($Message)'"
            }
            Else {
                $Command = "Write-Host [$($DateConsole) $($TimeConsole)]: $($Message) -ForegroundColor Yellow -BackgroundColor Black"
            }
        }
        3    {
            If (Test-SDMAzureRunbookConsole) {
                $Command = "Write-Output '[$($DateConsole) $($TimeConsole)] - [E]: $($Message)'"
            }
            Else {
                $Command = "Write-Host [$($DateConsole) $($TimeConsole)]: $($Message) -ForegroundColor Red -BackgroundColor Black"
            }
        }
    }
    Invoke-Expression -Command $Command
}

Function Get-SDMFunctionName {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [Int]$StackNumber
    )
    $Result = $(Get-PSCallStack)[$StackNumber].FunctionName
    Switch ($Result) {
    "<ScriptBlock>"     {Return "ScriptBlock"}
    Default             {Return $Result}
    }
}

#endregion Logging

#region Azure

Function Test-SDMAzureRunbookConsole {
    If ($PSPrivateMetadata.JobId) {
        Return $True
    }
    Else {
        Return $False
    }
}

#endregion Azure

#region General

Function New-SDMRandomString {
    <#
        .SYNOPSIS
        Creates a random string with predefined characters.
 
        .DESCRIPTION
        Creates a random string with predefined characters.
 
        .PARAMETER Characters
        Specifies the characters used to create the random string.
 
        .PARAMETER Length
        Specifies the length of the random string.
 
        .EXAMPLE
        New-SDMRandomString -Characters "abcdef0123456789" -Length 8
 
        .EXAMPLE
        New-SDMRandomString -Characters "abcdefABCDEF0123456789+-!?" -Length 12
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [String]$Characters,
        [Parameter(Mandatory = $True)]
        [ValidateNotNullOrEmpty()]
        [Int]$Length
    )
    $Chars = $Characters.ToCharArray()
    $String = ""
    1..$Length | ForEach {$String += $Chars | Get-Random}
    Return $String
}

#endregion General

Export-ModuleMember -Function Import-SDMMEMCMModule, Import-SDMMEMCMDevice, Mount-SDMMEMCMSiteCode, Add-SDMMEMCMDeviceCollectionDirectMembershipRule, Add-SDMMEMCMDeviceVariable, Test-SDMMEMCMDeviceExists, Test-SDMComputerConnection, New-SDMVMGuest, Remove-SDMVMGuest, Write-SDMLog, New-SDMRandomString