AZSBTools.psm1


#region Azure specific functions

#region Azure Storage

function Login-AzureRMSubscription {

    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String]$LogFile 
    )
    
    Begin { }

    Process {
        $LoggedIn = $false
        if ($Login = Get-AzureRmContext) {
            if ($Login.Account.Id -eq $LoginName -and $Login.Name -match $SubscriptionName) {
                Write-Log 'Already connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan $LogFile
                $LoggedIn = $true
            } 
        } 

        if (-not $LoggedIn) {
            Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud
            Write-Log 'Connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan $LogFile
            try {
                Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 -EA 1 | Set-AzureRmContext | Out-Null
                Write-Log ' Set Azure subscription context to',$SubscriptionName Green,Cyan $LogFile
            } catch {
                Write-Log $PSItem.Exception.Message Magenta
                break
            }         
        }
         
    }

    End { Get-AzureRmContext }
}

function Retry-OnRequest {

# Requires -Modules Azure, Azure.Storage
# Requires -Version 5

<#
 .SYNOPSIS
  Function to retry storage requests when encountering temporary/transient errors
 
 .DESCRIPTION
  Function to retry storage requests when encountering temporary/transient errors,
  like network errors, or storage server busy errors
 
 .PARAMETER Action
  This is a script block to get the block list of a given BLOB
  This is invoked by this function
  Example:
    $action = {
        param ($requestOption)
        return $Blob.ICloudBlob.DownloadBlockList([Microsoft.WindowsAzure.Storage.Blob.BlockListingFilter]::All, $null, $requestOption)
    }
  where $Blob is a Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBlob object
  that can be obtained from the Get-AzureStorageBlob cmdlet for example
 
 .PARAMETER TimeOutInMinutes
  This is the time span in minutes on which the Microsoft.WindowsAzure.Storage.RetryPolicies.ExponentialRetry object is configured
  This is an optional parameter. Default is (New-TimeSpan -Minutes 15)
 
 .PARAMETER maxRetryCountOnException
  This is the maximum number of times the function will retry the call.
  This is an optional parameter. Default is 3 times
 
 .EXAMPLE
    $action = {
        param ($requestOption)
        return $Blob.ICloudBlob.DownloadBlockList([Microsoft.WindowsAzure.Storage.Blob.BlockListingFilter]::All, $null, $requestOption)
    }
    $blocks = Retry-OnRequest $action
  where $Blob is a Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBlob object
  that can be obtained from the Get-AzureStorageBlob cmdlet for example
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros, based on script by Emma Zhu - Microsoft/ShangHai - emmazhu@microsoft.com
  v0.1 - 5 December 2018
#>


    param(
        [Parameter(Mandatory=$true)]$Action,
        [Parameter(Mandatory=$false)][System.TimeSpan]$TimeOutInMinutes = (New-TimeSpan -Minutes 15),
        [Parameter(Mandatory=$false)][Int16]$maxRetryCountOnException = 3
    )
    
    Begin { }

    Process {
        $requestOption = @{
            RetryPolicy = (New-Object -TypeName Microsoft.WindowsAzure.Storage.RetryPolicies.ExponentialRetry -ArgumentList @($TimeOutInMinutes, 10))
        }
        $shouldRetryOnException = $false   
        $retryCount = 0  

        do {
            try {
                return $Action.Invoke($requestOption)
            } catch {
                if ($_.Exception.InnerException -ne $null -And $_.Exception.InnerException.GetType() -Eq [System.TimeoutException] -And $maxRetryCountOnException -gt 0) {
                    $shouldRetryOnException = $true
                    $maxRetryCountOnException --
                    $retryCount ++
                    Write-Log 'retrying request.. #',$retryCount Yellow,Cyan
                } else {
                    $shouldRetryOnException = $false
                    throw
                }
            }
        } while ($shouldRetryOnException)
    }

    End { }
}

function Get-BlobBytes {

# Requires -Modules Azure, Azure.Storage
# Requires -Version 5

<#
 .SYNOPSIS
  Function to calculate the amount of storage used by a BLOB
 
 .DESCRIPTION
  Function to calculate the amount of storage used by a BLOB
 
 .PARAMETER Blob
  This is a Microsoft.WindowsAzure.Commands.Common.Storage.ResourceModel.AzureStorageBlob object
  that can be obtained from the Get-AzureStorageBlob cmdlet for example
 
 .PARAMETER IsPremiumAccount
  An optional Boolean (True/False) parameter that defaults to False
 
 .EXAMPLE
    $LoginName = 'samb@mydomain.com'
    $SubscriptionName = 'my azure subscription name'
    $StorageAccountName = 'mystorageacct'
 
    # Import-Module Azure, Azure.Storage, AZSBTools -DisableNameChecking
    Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud
    $Subsciption = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0
    $Subsciption | Set-AzureRmContext | Out-Null
    Write-Log 'Connected to',$Subsciption.Name,'as',$LoginName Green,Cyan,Green,Cyan
 
    $StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -eq $StorageAccountName
    $IsPremiumAccount = ($StorageAccount.Sku.Tier -eq "Premium")
    Write-Log 'Processing storage account',$StorageAccount.StorageAccountName,'in RG',$StorageAccount.ResourceGroupName Green,Cyan,Green,Cyan
 
    $ContainerList = Get-AzureStorageContainer -Context $StorageAccount.Context
    $Container = $ContainerList | select -First 1
    Write-Log ' Processing container',$Container.Name Green,Cyan
 
    $BlobList = Get-AzureStorageBlob -Context $StorageAccount.Context -Container $Container.Name
    $Blob = $BlobList | select -First 1
    Write-Log ' Processing blob',$Blob.Name Green,Cyan -NoNewLine
 
    $SizeInBytes = Get-BlobBytes $Blob $IsPremiumAccount
    $myOutput = [PSCustomObject][Ordered]@{
        Name = $Blob.Name
        StorageAccount = $storageAccount.StorageAccountName
        Container = $Container.Name
        Type = $Blob.BlobType
        SizeInBytes = $SizeInBytes
        LastModified = $Blob.LastModified
    }
    Write-log $SizeInBytes,'bytes' Yellow,Cyan
 
    $myOutput | select Name,Type,StorageAccount,Container,
        @{n='SizeInGB';e={[Math]::Round($_.SizeInBytes/1GB,1)}},LastModified |
            sort SizeInGB -Descending | FL
 
    This example calculates the size of the first Blob in the first container of the provided storage account
 
 .EXAMPLE
    $LoginName = 'samb@mydomain.com'
    $SubscriptionName = 'my azure subscription name'
    $StorageAccountName = 'mystorageacct'
 
    # Import-Module Azure, Azure.Storage, AZSBTools -DisableNameChecking
    Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud
    $Subsciption = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0
    $Subsciption | Set-AzureRmContext | Out-Null
    Write-Log 'Connected to',$Subsciption.Name,'as',$LoginName Green,Cyan,Green,Cyan
 
    $StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -eq $StorageAccountName
    $IsPremiumAccount = ($StorageAccount.Sku.Tier -eq "Premium")
    Write-Log 'Processing storage account',$StorageAccount.StorageAccountName,'in RG',$StorageAccount.ResourceGroupName Green,Cyan,Green,Cyan
 
    $BlobList = foreach ($Container in (Get-AzureStorageContainer -Context $StorageAccount.Context)) {
        Write-Log ' Processing container',$Container.Name Green,Cyan
        $Token = $Null
        do {
            $Blobs = Get-AzureStorageBlob -Context $StorageAccount.Context -Container $Container.Name -ContinuationToken $Token
            if ($Blobs -eq $Null) { break }
 
            if ($Blobs.GetType().Name -eq 'AzureStorageBlob') {
                $Token = $Null
            } else {
                $Token = $Blobs[-1].ContinuationToken
            }
 
            $Blobs | ForEach {
                Write-Log ' Processing blob',$_.Name Green,Cyan -NoNewLine
                $SizeInBytes = Get-BlobBytes $_ $IsPremiumAccount
                [PSCustomObject][Ordered]@{
                    Name = $_.Name
                    StorageAccount = $storageAccount.StorageAccountName
                    Container = $Container.Name
                    Type = $_.BlobType
                    SizeInBytes = $SizeInBytes
                    LastModified = $_.LastModified
                }
                Write-log $SizeInBytes,'bytes' Yellow,Cyan
            }
        } While ($Token -ne $Null)
    }
 
    $BlobList | select Name,Type,StorageAccount,Container,
        @{n='SizeInGB';e={[Math]::Round($_.SizeInBytes/1GB,1)}},LastModified |
            sort SizeInGB -Descending | FT -a
 
    This example calculates blob sizes for all blobs in all containers of the provided storage account
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros, based on script by Emma Zhu - Microsoft/ShangHai - emmazhu@microsoft.com
  v0.1 - 5 December 2018
#>

    param(
        [Parameter(Mandatory=$true)]$Blob,
        [Parameter(Mandatory=$false)][bool]$IsPremiumAccount = $false
    )

    Begin {
        if (-not ([System.Management.Automation.PSTypeName]'PageRange').Type) {
            Add-Type -TypeDefinition "
                public class PageRange {
                    public long StartOffset;
                    public long EndOffset;
                }
            "
 
        }
    }

    Process {
        # Base + blobname
        $blobSizeInBytes = 124 + $Blob.Name.Length * 2

        # Size of metadata
        $metadataEnumerator = $Blob.ICloudBlob.Metadata.GetEnumerator()
        while($metadataEnumerator.MoveNext()) {
            $blobSizeInBytes += 3 + $metadataEnumerator.Current.Key.Length + $metadataEnumerator.Current.Value.Length
        }

        if (-not $IsPremiumAccount) {
            if ($Blob.BlobType -eq [Microsoft.WindowsAzure.Storage.Blob.BlobType]::BlockBlob) {
                $blobSizeInBytes += 8
                
                $action = { # Default is Microsoft.WindowsAzure.Storage.Blob.BlockListingFilter.Committed. Need All
                    param ($requestOption) 
                    return $Blob.ICloudBlob.DownloadBlockList([Microsoft.WindowsAzure.Storage.Blob.BlockListingFilter]::All, $null, $requestOption) 
                } 
                $blocks = Retry-OnRequest $action 

                if ($blocks -eq $null) {
                    $blobSizeInBytes += $Blob.ICloudBlob.Properties.Length
                } else {
                    $blocks | ForEach { $blobSizeInBytes += $_.Length + $_.Name.Length }
                }  
            } elseif ($Blob.BlobType -eq [Microsoft.WindowsAzure.Storage.Blob.BlobType]::PageBlob) {
                # It could cause server time out issue when trying to get page ranges of highly fragmented page blob
                # Get page ranges in segment can mitigate chance of meeting such kind of server time out issue
                # See https://blogs.msdn.microsoft.com/windowsazurestorage/2012/03/26/getting-the-page-ranges-of-a-large-page-blob-in-segments/ for details.
                $pageRangesSegSize = 148 * 1024 * 1024L
                $totalSize = $Blob.ICloudBlob.Properties.Length
                $pageRangeSegOffset = 0
        
                $pageRangesTemp = New-Object System.Collections.ArrayList
                while ($pageRangeSegOffset -lt $totalSize) {
                    $action = {
                        param($requestOption) 
                        return $Blob.ICloudBlob.GetPageRanges($pageRangeSegOffset, $pageRangesSegSize, $null, $requestOption) 
                    }

                    Retry-OnRequest $action | ForEach { $pageRangesTemp.Add($_) }  | Out-Null
                    $pageRangeSegOffset += $pageRangesSegSize
                }

                $pageRanges = New-Object System.Collections.ArrayList
                foreach ($pageRange in $pageRangesTemp) {
                    if($lastRange -eq $Null) {
                        $lastRange = New-Object PageRange
                        $lastRange.StartOffset = $pageRange.StartOffset
                        $lastRange.EndOffset   =  $pageRange.EndOffset
                    } else {
                        if (($lastRange.EndOffset + 1) -eq $pageRange.StartOffset) {
                            $lastRange.EndOffset = $pageRange.EndOffset
                        } else {
                            $pageRanges.Add($lastRange)  | Out-Null
                            $lastRange = New-Object PageRange
                            $lastRange.StartOffset = $pageRange.StartOffset
                            $lastRange.EndOffset   =  $pageRange.EndOffset
                        }
                    }
                }

                $pageRanges.Add($lastRange) | Out-Null
                $pageRanges | ForEach { $blobSizeInBytes += 12 + $_.EndOffset - $_.StartOffset }
            } else {
                $blobSizeInBytes += $Blob.ICloudBlob.Properties.Length
            }

        } else {
            $blobSizeInBytes += $Blob.ICloudBlob.Properties.Length
        }        
    }

    End { $blobSizeInBytes }
}

function Get-ContainerBytes {

# Requires -Modules Azure, Azure.Storage
# Requires -Version 5

<#
 .SYNOPSIS
  Function to calculate container overhead storage size
 
 .DESCRIPTION
  Function to calculate container overhead storage size
 
 .PARAMETER Container
  This is an object of type Microsoft.WindowsAzure.Storage.Blob.CloudBlobContainer
  that can be obtained from the CloudBlobContainer property of the output object of
  the Get-AzureStorageContainer cmdlet - see example below
 
 .EXAMPLE
    $LoginName = 'samb@mydomain.com'
    $SubscriptionName = 'my subscription name'
    $StorageAccountName = 'mystorageacct'
 
    # Import-Module Azure, Azure.Storage, AZSBTools -DisableNameChecking
    Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud
    $Subsciption = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0
    $Subsciption | Set-AzureRmContext | Out-Null
    Write-Log 'Connected to',$Subsciption.Name,'as',$LoginName Green,Cyan,Green,Cyan
 
    $StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -eq $StorageAccountName
    $IsPremiumAccount = ($StorageAccount.Sku.Tier -eq "Premium")
    Write-Log 'Processing storage account',$StorageAccount.StorageAccountName,'in RG',$StorageAccount.ResourceGroupName Green,Cyan,Green,Cyan
 
    Get-AzureStorageContainer -Context $StorageAccount.Context | foreach {
        Write-Log ' Calculating overhead bytes for container',$_.Name Green,Cyan -NoNewLine
        $ContainerOverheadBytes = Get-ContainerBytes -Container $_.CloudBlobContainer
        Write-Log $ContainerOverheadBytes,'bytes' Yellow,Cyan
    }
  This example calculate overhead bytes for all containers in the provided storage account
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros, based on script by Emma Zhu - Microsoft/ShangHai - emmazhu@microsoft.com
  v0.1 - 5 December 2018
#>

    param(
        [Parameter(Mandatory=$true)][Microsoft.WindowsAzure.Storage.Blob.CloudBlobContainer]$Container
    )

    Begin { }

    Process {
        # Base + name of container
        $ContainerOverheadBytes = 48 + $Container.Name.Length * 2

        # Get size of metadata
        $metadataEnumerator = $Container.Metadata.GetEnumerator()
        while($metadataEnumerator.MoveNext()) {
            $ContainerOverheadBytes += 3 + $metadataEnumerator.Current.Key.Length + $metadataEnumerator.Current.Value.Length
        }

        # Get size for SharedAccessPolicies
        $ContainerOverheadBytes += $Container.GetPermissions().SharedAccessPolicies.Count * 512
    }

    End { $ContainerOverheadBytes }
}

function Get-AzureRMDiskSpace {
<#
 .SYNOPSIS
  Function to obtain used disk space of one or more Azure VMs
 
 .DESCRIPTION
  Function to obtain used disk space of one or more Azure VMs
  This function calculates disk space of unmanaged disks only
  Microsoft charges for the entire allocated space of a managed disk regardless
  of how much is used, so finding the actual used size is irrelevent
 
 .PARAMETER AzureVM
  One or more of type Microsoft.Azure.Commands.Compute.Models.PSVirtualMachineList
  which can be obtained from the output of the AzureRM cmdlet Get-AzureRmVM
 
 .PARAMETER RetryCount
  This is an optional number between 0 and 99
  The cmdlet will retry the disks that fail to get used disk space amount that many times
 
 .EXAMPLE
    Login-AzureRmAccount -Credential (Get-SBCredential 'nam@domain.com') | Out-Null # -Environment AzureCloud
    Get-AzureRmSubscription -SubscriptionName 'my subscription anme' -WA 0 | Set-AzureRmContext | Out-Null
    $VMList = (Get-AzureRmVM -WA 0)[0..2]
    $DiskSpaceUsage = Get-AzureRMDiskSpace -AzureVM $VMList -RetryCount 1 -Verbose
    $DiskSpaceUsage | FT -a
 
. OUTPUTS
  PSCustom object (one for each disk) containing the following properties/example:
    VMName DiskName StorageAccount BlobName TotalSizeGB UsedSizeGB Source DateReported RetryCount
    ------ -------- -------------- -------- ----------- ---------- ------ ------------ ----------
    MigrationAdmin1 MigrationAdmin1 devgdisks756 MigrationAdmin104435.vhd 127 ? AzureStorage 8/8/2018 11:04 AM 5
    DEBCSV01 DEBCSV01 debcssa DEBCSV0120180802110039.vhd 32 3.96 AzureStorage 8/8/2018 10:49 AM 0
    DECEX16VO1 DECEX16VO1 decsa DECEX16VO120180403203752.vhd 127 30.33 AzureStorage 8/8/2018 10:50 AM 0
    DECEX16VO1 DECEX16VO1-DD1 decsa DECEX16VO1-DD1.vhd 40 ? AzureStorage 8/8/2018 11:06 AM 5
 
 .LINK
  https://superwidgets.wordpress.com/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 20 July 2018 - Known issue: not able to get used space of some disks, getting:
    $Blob.ICloudBlob.GetPageRanges(): Exception calling "GetPageRanges" with "0" argument(s):
    "Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host."
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)]
            [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachineList[]]$AzureVM,
        [Parameter(Mandatory=$false)][ValidateRange(0,99)][Int16]$RetryCount = 0
    )

    Begin { 
        $myOutput = @() 

        function Get-DiskBlobSize {
            [CmdletBinding(ConfirmImpact='Low')] 
            Param(
                [Parameter(Mandatory=$true)][PSCustomObject]$Disk,
                [Parameter(Mandatory=$true)][Microsoft.Azure.Commands.Compute.Models.PSVirtualMachineList]$VM
            )

            Write-Log 'Processing disk:' Green
            Write-Log ($Disk | Out-String).Trim() Cyan
            $StorageAccount = Get-AzureRmStorageAccount -ResourceGroupName $VM.ResourceGroupName -Name $Disk.StorageAccount
            $Blob = Get-AzureStorageBlob -Container vhds -Context $StorageAccount.Context -Blob $Disk.BlobName
            $blobSize  = 124 + $Blob.Name.Length * 2
            $blobSize += ($Blob.ICloudBlob.Metadata.Keys.Length   | measure -Sum).Sum
            $blobSize += ($Blob.ICloudBlob.Metadata.Values.Length | measure -Sum).Sum
            $PageRanges = $Blob.ICloudBlob.GetPageRangesAsync() # $Blob.ICloudBlob.GetPageRanges()
            Write-Verbose ($PageRanges | Out-String) 
            if ($PageRanges.Result) {                          
                $PageRanges.Result | foreach { $blobSize += 12 + $_.EndOffset - $_.StartOffset }
                [Math]::Round($blobSize/1GB,2)
            } else { 
                '?' 
            }
        }
    }

    Process {
        #region First run
        foreach ($VM in $AzureVM) {
            
            #region Get list of unmanaged VM disks
            $DiskList = @()
            if ($VM.StorageProfile.OsDisk.ManagedDisk) {
                Write-Log 'Disk',$VM.StorageProfile.OsDisk.Name,'is a managed disk, skipping..' Yellow,Cyan,Yellow
            } else {
                $DiskList += @($VM.StorageProfile.OsDisk | select Name,DiskSizeGB,
                    @{n='StorageAccount';e={$_.Vhd.Uri.Split('.')[0].Split('/')[2]}},
                    @{n='BlobName';e={$_.Vhd.Uri.Split('/')[-1]}})
            }
            foreach ($VMDisk in $VM.StorageProfile.DataDisks) {
                if ($VMDisk.ManagedDisk) {
                    Write-Log 'Disk',$VMDisk.Name,'is a managed disk, skipping..' Yellow,Cyan,Yellow
                } else {
                    $DiskList += $VMDisk | select Name,DiskSizeGB,
                        @{n='StorageAccount';e={$_.Vhd.Uri.Split('.')[0].Split('/')[2]}},
                        @{n='BlobName';e={$_.Vhd.Uri.Split('/')[-1]}}
                }
            }
            #endregion

            if ($DiskList) {
                Write-Log 'Calculating used disk space for',$DiskList.Count,'disk(s) of VM',$VM.Name Green,Cyan,Green,Cyan
                foreach ($Disk in $DiskList) {
                    $myOutput += [PSCustomObject][Ordered]@{
                        VMName         = $VM.Name
                        DiskName       = $Disk.Name
                        StorageAccount = $Disk.StorageAccount
                        BlobName       = $Disk.BlobName
                        TotalSizeGB    = $Disk.DiskSizeGB
                        UsedSizeGB     = Get-DiskBlobSize -Disk $Disk -VM $VM
                        Source         = 'AzureStorage'
                        DateReported   = Get-Date -Format g
                        RetryCount     = 0
                    }
                }
            }

        }
        #endregion

        #region Retries
        if ($RetryCount -gt 0) {
            foreach ($Retry in 1..$RetryCount) { 
                Write-Log 'Retry #',$Retry Cyan,Yellow
                foreach ($Disk in ($myOutput | where { $PSItem.UsedSizeGB -eq '?' })) {
                    $Disk.UsedSizeGB   = Get-DiskBlobSize -Disk $Disk -VM ($AzureVM | where { $Disk.VMName -eq $PSItem.Name })
                    $Disk.DateReported = Get-Date -Format g
                    $Disk.RetryCount   = $Retry
                }
            }
        }
        #endregion
    }

    End { $myOutput }
}

function Get-AzureStorageAccountList {

# Requires -Modules AzureRM
# Requires -Version 5

<#
 .SYNOPSIS
  Function to get Azure storage accounts in a given subscription
 
 .DESCRIPTION
  Function to get Azure storage accounts in a given subscription
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER SubscriptionName
  The Azure subscription name such as 'My Dev EA subscription'
 
 .EXAMPLE
  Get-AzureStorageAccountList -LoginName 'sam.boutros@mydomain.com' -SubscriptionName 'my subscription name'
 
 .OUTPUTS
  This function returns a PS object for each Stprage Account containing the following properties/example:
    Name : maybcstorage
    Type : ARM-GPv1 # This is either ASM, ARM-GPv1, ARM-GPv2, or ARM-BlobOnly
    GeoReplication : Standard_RAGRS # This is either Standard_LRS, Standard_GRS, Standard_RAGRS, Standard_ZRS
    Tier : Standard # This is either Standard (HDD) or Enhanced (SSD)
    ResourceGroup : myrs1
    Location : uksouth
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
  https://superwidgets.wordpress.com/2018/07/02/azure-storage-features-and-pricing-june-2018/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 24 October 2018
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName
    )

    Begin {
        if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break }
    }

    Process {
        Get-AzureRmResource | where ResourceType -Match 'storage' | foreach {
            [PSCustomObject][Ordered]@{
                Name           = $_.Name 
                Type           = $(
                    if ($_.ResourceType -eq 'Microsoft.ClassicStorage/storageAccounts') { 'ASM' } 
                    elseif ($_.ResourceType -eq 'Microsoft.Storage/storageAccounts') { 
                        if ($_.Kind -eq 'StorageV2') { 'ARM-GPv2' }
                        elseif ($_.Kind -eq 'BlobStorage') { 'ARM-BlobOnly' }
                        else { 'ARM-GPv1' }
                    }
                    else { '???' }
                )
                GeoReplication = $_.sku.name
                Tier           = $_.sku.tier
                ResourceGroup  = $_.ResourceGroupName
                Location       = $_.Location
            }
        }
    } 

    End {}
}

Function Delete-AzureRMUnattachedManagedDisks {

# Requires -Modules AzureRM,ImportExcel
# Requires -Version 5

<#
 .SYNOPSIS
  Function to delete Azure unused/unattached managed disks
 
 .DESCRIPTION
  Function to delete Azure unused/unattached managed disks
  This applies to ARM disks only not classic ASM disks
  This function depends on AzureRM and ImportExcel PowerShell modules available in the PowerShell Gallery
  To install: Install-Module AzureRM,ImportExcel
  This function has been tested to work with PowerShell version 5
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER SubscriptionName
  The Azure subscription name such as 'My Dev EA subscription'
 
 .PARAMETER OutputFile
  This is an optional parameter that specifies the path to output Excel file
  This defaults to a file in the current folder where the script is running
 
 .PARAMETER LogFile
  This is an optional parameter that specifies the path to the log file where the script logs its progress
  This defaults to a file in the current folder where the script is running
 
 .EXAMPLE
  Delete-AzureRMUnattachedManagedDisks -LoginName 'samb@mydomain.com' -SubscriptionName 'my Azure subscription name here'
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 20 December 2018
#>


    [CmdletBinding(ConfirmImpact='High')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$false)][String]$OutputFile = ".\Unattached Managed Disk List - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').xlsx",
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Delete-AzureRMUnattachedManagedDisks - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin {
        Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud
        try {
            Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 -EA 1 | Set-AzureRmContext | Out-Null
            Write-Log 'Connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan $LogFile
        } catch {
            Write-Log $PSItem.Exception.Message Yellow $LogFile
            break
        }
    }

    Process{

        $ManagedDisks = Get-AzureRmDisk

        if ($ManagedDisks) {
            Write-Log 'Identified',$ManagedDisks.Count,'managed disks' Green,Yellow,Green $LogFile
            $MDList = $ManagedDisks | foreach { 
                [PSCustomObject][Ordered]@{
                    DiskName      = $_.Name
                    SizeGB        = $_.DiskSizeGB
                    ResourceGroup = $_.ResourceGroupName
                    AttachedTo    = $_.ManagedBy
                }
            }
            Write-Log ($MDList | FT -a | Out-String).Trim() Cyan $LogFile
            $UnattachedMDList = $MDList | where {-not $_.AttachedTo }
            if ($UnattachedMDList) {
                Write-Log ' of which',$UnattachedMDList.Count,'disks are not attached to or used by any VM' Green,Yellow,Green $LogFile
                Write-Log ($UnattachedMDList | FT -a | Out-String).Trim() Yellow $LogFile
                
                Write-Log 'Exporting list of unattached managed disks to file',$OutputFile Green,Cyan $LogFile
                $UnattachedMDList | Export-Excel -Path $OutputFile -ConditionalText $(
                    ($UnattachedMDList | Get-Member -MemberType NoteProperty).Name | foreach { New-ConditionalText $_ White SteelBlue }
                ) -AutoSize -FreezeTopRowFirstColumn
                
                Write-Log 'Deleting',$UnattachedMDList.Count,'unattached managed disks' Green,Cyan,Green $LogFile -NoNewLine
                $Result = $UnattachedMDList | 
                    foreach { Remove-AzureRmDisk -ResourceGroupName $_.ResourceGroup -DiskName $_.DiskName -Force }
                Write-Log 'done, task details:' Cyan $LogFile
                Write-Log ($Result | FT -a | Out-String).Trim() Green $LogFile
            } else {
                Write-Log ' all of which are attached/used by VMs' Green $LogFile
            }
        } else {
            Write-Log 'No managed disks found' Green $LogFile
        }

    } 

    End { }
}

Function Remove-AzureRMUnmanagedDiskSnapshot {

# Requires -Modules AzureRM.Compute,Azure.Storage
# Requires -Version 5

<#
 .SYNOPSIS
  Function to delete Azure disk snapshot(s) for unmanaged disks
 
 .DESCRIPTION
  Function to delete disk snapshot(s) for a given unmanaged disk
  This applies to unmanaged ARM disk snapshots only not classic ASM disks or managed ARM disks
  This function depends on AzureRM, Azure PowerShell modules available in the PowerShell Gallery
  To install required module: Install-Module AzureRM, Azure
  This function has been tested to work with PowerShell version 5
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER StorageAccountName
  The Azure storage account name such as 'storfluxwidget3vm'
 
 .PARAMETER ContainerName
  The Container name such as 'Vhds'
 
 .PARAMETER BlobName
  The disk name such as 'Widget3VM-20181226-093810.vhd'
 
 .PARAMETER FromDate
  Snapshots with datetime stamp after this point and before the ToDate will be deleted
  Example: 1/1/2018, or 12/11/2018 11:00 AM
  If either ToDate or FromDate is not provided, all snapshots of the provided page blob will be deleted
 
 .PARAMETER ToDate
  Snapshots with datetime stamp before this point and after the FromDate will be deleted
  Example: 1/10/2018, or 12/12/2018 12:00 AM
  If either ToDate or FromDate is not provided, all snapshots of the provided page blob will be deleted
 
 .PARAMETER LogFile
  This is an optional parameter that specifies the path to the log file where the script logs its progress
  This defaults to a file in the current folder where the script is running
 
 .EXAMPLE
  $ParameterList = @{
    LoginName = 'sam@dmain.com'
    SubscriptionName = 'my subscription name'
    StorageAccountName = 'storfluxwidget3vm'
    ContainerName = 'vhds'
    BlobName = 'Widget3VM-20181226-093810.vhd'
  }
  Remove-AzureRMUnmanagedDiskSnapshot @ParameterList
  This example deletes all snapshots of the provided disk
 
 .EXAMPLE
  $ParameterList = @{
    LoginName = 'sam@dmain.com'
    SubscriptionName = 'my subscription name'
    StorageAccountName = 'storfluxwidget3vm'
    ContainerName = 'vhds'
    FromDate = '1/1/2019'
    ToDate = Get-Date
    BlobName = 'Widget3VM-20181226-093810.vhd'
  }
  Remove-AzureRMUnmanagedDiskSnapshot @ParameterList
  This example deletes all snapshots of the provided disk from 1/1/2019 to now
 
 .EXAMPLE
    $LoginName = 'sam@dmain.com'
    $SubscriptionName = 'my subscription name'
    $DiskList = Get-AzureRMVMUnmanagedDisk -LoginName $LoginName -SubscriptionName $SubscriptionName -VMName (Get-AzureRMVM).Name
    # By defining the $LogFile variable before the loop, we get to put all the logs in one file
    $LogFile = ".\Remove-AzureRMUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    $SnapShotList = foreach ($Disk in $DiskList) {
        $ParameterList = @{
            LoginName = $LoginName
            SubscriptionName = $SubscriptionName
            StorageAccountName = $Disk.StorageAccountName
            ContainerName = $Disk.ContainerName
            BlobName = $Disk.BlobName
            LogFile = $LogFile
        }
        Remove-AzureRMUnmanagedDiskSnapshot @ParameterList
    }
    This example lists all unmanaged disks of all ARM VMs in the given subscription, then deletes all their snapshots
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 20 December 2018
  v0.2 - 1 January 2019 - Rewrite based on Logan Zhao (zhezhao@microsoft.com) input regarding
         $storageContainer.CloudBlobContainer interface, and .CloudBlobContainer.ListBlobs() method
#>


    [CmdletBinding(ConfirmImpact='High')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String]$StorageAccountName,
        [Parameter(Mandatory=$true)][String]$ContainerName,
        [Parameter(Mandatory=$true)][String]$BlobName,
        [Parameter(Mandatory=$false)][String]$FromDate,
        [Parameter(Mandatory=$false)][String]$ToDate,
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Remove-AzureRMUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin {
        if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break }
    }

    Process{

        #region Validate Input

        if ($StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -EQ $StorageAccountName) {
            Write-Log 'Validated Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to find Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile
            break
        }
        if ($StorageKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) {
            Write-Log 'Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
            Write-Log $Error[0].Exception.Message Yellow $LogFile
            break
        }
        if ($Context = New-AzureStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) {
            Write-Log 'Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
            Write-Log $Error[0].Exception.Message Yellow $LogFile
            break
        }
        if ($Container = Get-AzureStorageContainer -Context $Context -Name $ContainerName) {
            Write-Log 'Read Storage Container',$ContainerName,'under',$StorageAccountName Green,Cyan,Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to read Storage Container',$ContainerName,'under',$StorageAccountName Magenta,Yellow,Magenta,Yellow $LogFile 
            break
        }

        if ($FromDate) {
            if ($FromDate -as [DateTime]) {
                $FromDate = [DateTime]$FromDate
                Write-Log 'From Date received:',$FromDate Green,Cyan $LogFile
            } else {
                Write-Log 'Bad Date/Time format received as -FromDate:',$FromDate,'stopping' Magenta,Yellow,Magenta $LogFile
                break
            }
        } else {
            Write-Log 'No From Date received, deleting all snapshots named',$BlobName,'in',"$StorageAccountName\$ContainerName" Yellow,Cyan,Green,Cyan $LogFile
            $FromDate = [DateTime]'1/1/1900'
        }
        if ($ToDate) {
            if ($ToDate -as [DateTime]) {
                $ToDate = [DateTime]$ToDate
                Write-Log 'To Date received:',$ToDate Green,Cyan $LogFile
            } else {
                Write-Log 'Bad Date/Time format received as -ToDate:',$ToDate,'stopping' Magenta,Yellow,Magenta $LogFile
                break
            }
        } else {
            Write-Log 'No To Date received, deleting all snapshots named',$BlobName,'in',"$StorageAccountName\$ContainerName" Yellow,Cyan,Green,Cyan $LogFile
            $ToDate = Get-Date
        }

        if ( $SnapshotList = $Container.CloudBlobContainer.ListBlobs($BlobName, $true,'Snapshot') | where { $_.IsSnapShot } ) {
            Write-Log 'Identified',$SnapshotList.Count,'disk snapshots for the disk/page Blob',$BlobName Green,yellow,Green,Cyan $LogFile 
            Write-Log ' dated',($SnapshotList.SnapShotTime -join ', ') Green,Cyan $LogFile 
        } else {
            Write-Log 'No disk snapshots found for the disk/page Blob',$BlobName Magenta,Yellow $LogFile
        }

        #endregion

        #region Delete snapshots
        foreach ($Snapshot in $SnapshotList) {            
            if ( ($Snapshot.SnapshotTime -le $ToDate -and $Snapshot.SnapshotTime -ge $FromDate) -or $DeleteAll ) { 
                Write-Log 'Deleting Snapshot',$Snapshot.SnapshotTime Green,Cyan $LogFile -NoNewLine
                $Snapshot.Delete() 
                $Container = Get-AzureStorageContainer -Context $Context -Name $ContainerName
                if ($Container.CloudBlobContainer.ListBlobs($BlobName, $true,'Snapshot') | where { $_.SnapshotTime -eq $Snapshot.SnapshotTime }) {
                    Write-Log 'failed' Yellow $LogFile
                } else {
                    Write-Log 'done' DarkYellow $LogFile
                }
            }
        }
        #endregion

    } 

    End { }
}

Function Get-AzureRMUnmanagedDiskSnapshot {

# Requires -Modules AzureRM.Compute,Azure.Storage
# Requires -Version 5

<#
 .SYNOPSIS
  Function to get Azure disk snapshot for unmanaged disks
 
 .DESCRIPTION
  Function to get disk snapshots for a given unmanaged disk
  This applies to unmanaged ARM disk snapshots only not classic ASM disks or managed ARM disks
  This function depends on AzureRM,Azure PowerShell modules available in the PowerShell Gallery
  To install required module: Install-Module AzureRM, Azure
  This function has been tested to work with PowerShell version 5
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER StorageAccountName
  The Azure storage account name such as 'storfluxwidget3vm'
 
 .PARAMETER ContainerName
  The Container name such as 'Vhds'
 
 .PARAMETER BlobName
  The disk name such as 'Widget3VM-20181226-093810.vhd'
 
 .PARAMETER LogFile
  This is an optional parameter that specifies the path to the log file where the script logs its progress
  This defaults to a file in the current folder where the script is running
 
 .EXAMPLE
  $ParameterList = @{
    LoginName = 'sam@dmain.com'
    SubscriptionName = 'my subscription name'
    StorageAccountName = 'storfluxwidget3vm'
    ContainerName = 'vhds'
    BlobName = 'Widget3VM-20181226-093810.vhd'
  }
  Get-AzureRMUnmanagedDiskSnapshot @ParameterList
  This example lists all snapshots of the provided disk
 
 .EXAMPLE
    $LoginName = 'sam@dmain.com'
    $SubscriptionName = 'my subscription name'
    $DiskList = Get-AzureRMVMUnmanagedDisk -LoginName $LoginName -SubscriptionName $SubscriptionName -VMName (Get-AzureRMVM).Name
    # By defining the $LogFile variable before the loop, we get to put all the logs in one file
    $LogFile = ".\Get-AzureRMUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    $SnapShotList = foreach ($Disk in $DiskList) {
        $ParameterList = @{
            LoginName = $LoginName
            SubscriptionName = $SubscriptionName
            StorageAccountName = $Disk.StorageAccountName
            ContainerName = $Disk.ContainerName
            BlobName = $Disk.BlobName
            LogFile = $LogFile
        }
        Get-AzureRMUnmanagedDiskSnapshot @ParameterList
    }
    This example lists all unmanaged disks of all ARM VMs in the given subscription, then lists all their snapshots
 
 .OUTPUTS
  This function returns objects of type Microsoft.WindowsAzure.Storage.Blob.CloudPageBlob
  for each snapshot found that matches the provided storageaccount/container/blob parameters
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 2 January 2019
#>


    [CmdletBinding(ConfirmImpact='High')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String]$StorageAccountName,
        [Parameter(Mandatory=$true)][String]$ContainerName,
        [Parameter(Mandatory=$true)][String]$BlobName,
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-AzureRMUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin {
        if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break }
    }

    Process{

        #region Validate Input
        if ($StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -EQ $StorageAccountName) {
            Write-Log 'Validated Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to find Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile
            break
        }
        if ($StorageKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) {
            Write-Log 'Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
            Write-Log $Error[0].Exception.Message Yellow $LogFile
            break
        }
        if ($Context = New-AzureStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) {
            Write-Log 'Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
            Write-Log $Error[0].Exception.Message Yellow $LogFile
            break
        }
        if ($Container = Get-AzureStorageContainer -Context $Context -Name $ContainerName) {
            Write-Log 'Read Storage Container',$ContainerName,'under',$StorageAccountName Green,Cyan,Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to read Storage Container',$ContainerName,'under',$StorageAccountName Magenta,Yellow,Magenta,Yellow $LogFile 
            break
        }
        #endregion

        #region Get snapshots
        if ($SnapshotList = $Container.CloudBlobContainer.ListBlobs($BlobName, $true,'Snapshot') | where { $_.IsSnapShot } ) {
            Write-Log 'Identified',$SnapshotList.Count,'disk snapshots for the disk/page Blob',$BlobName Green,yellow,Green,Cyan $LogFile 
            Write-Log ' dated',($SnapshotList.SnapShotTime -join ', ') Green,Cyan $LogFile 
        } else {
            Write-Log 'No disk snapshots found for the disk/page Blob',$BlobName Magenta,Yellow $LogFile
        }
        #endregion

    } 

    End { $SnapshotList }
}

Function New-AzureRMUnmanagedDiskSnapshot {

# Requires -Modules AzureRM.Compute,Azure.Storage
# Requires -Version 5

<#
 .SYNOPSIS
  Function to create Azure disk snapshot for unmanaged disks
 
 .DESCRIPTION
  Function to create disk snapshots for a given unmanaged disk
  This applies to unmanaged ARM disk snapshots only not classic ASM disks or managed ARM disks
  This function depends on AzureRM,Azure PowerShell modules available in the PowerShell Gallery
  To install required module: Install-Module AzureRM, Azure
  This function has been tested to work with PowerShell version 5
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER StorageAccountName
  The Azure storage account name such as 'storfluxwidget3vm'
 
 .PARAMETER ContainerName
  The Container name such as 'Vhds'
 
 .PARAMETER BlobName
  The disk name such as 'Widget3VM-20181226-093810.vhd'
 
 .PARAMETER LogFile
  This is an optional parameter that specifies the path to the log file where the script logs its progress
  This defaults to a file in the current folder where the script is running
 
 .EXAMPLE
  $ParameterList = @{
    LoginName = 'sam@dmain.com'
    SubscriptionName = 'my subscription name'
    StorageAccountName = 'storfluxwidget3vm'
    ContainerName = 'vhds'
    BlobName = 'Widget3VM-20181226-093810.vhd'
  }
  New-AzureRMUnmanagedDiskSnapshot @ParameterList
  This example creates a new snapshot of the provided disk
 
 .OUTPUTS
  This function returns object of type Microsoft.WindowsAzure.Storage.Blob.CloudPageBlob for the snapshot created
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 2 January 2019
#>


    [CmdletBinding(ConfirmImpact='High')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String]$StorageAccountName,
        [Parameter(Mandatory=$true)][String]$ContainerName,
        [Parameter(Mandatory=$true)][String]$BlobName,
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-AzureRMUnmanagedDiskSnapshot - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin {
        if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break }
    }

    Process{

        #region Validate Input

        if ($StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -EQ $StorageAccountName) {
            Write-Log 'Validated Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to find Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile
            break
        }
        if ($StorageKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) {
            Write-Log 'Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
            Write-Log $Error[0].Exception.Message Yellow $LogFile
            break
        }
        if ($Context = New-AzureStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) {
            Write-Log 'Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log 'Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
            Write-Log $Error[0].Exception.Message Yellow $LogFile
            break
        }
        
        if ($Blob = Get-AzureStorageBlob -Container $ContainerName -Context $Context | Where { $_.Name -eq $BlobName -and (-not $_.ICloudBlob.IsSnapshot)}) {
            Write-Log 'Validated page blob/disk',$BlobName,'under',"$StorageAccountName\$ContainerName" Green,Cyan,Green,Cyan $LogFile
        } else {
            Write-Log 'Page blob/disk',$BlobName,'not found under',"$StorageAccountName\$ContainerName" Magenta,Yellow,Magenta,Yellow $LogFile 
            break
        }

        #endregion

        #region New snapshot

        $SnapShot = $Blob.ICloudBlob.CreateSnapshot()

        #endregion

    } 

    End { $SnapShot }
}

function Get-AzureRMVMUnmanagedDisk {

# Requires -Modules AzureRM
# Requires -Version 5

<#
 .SYNOPSIS
  Function to return unmanaged disk information of a given Azure VM
 
 .DESCRIPTION
  Function to return unmanaged disk information of a given Azure VM
  This function is intended for ARM disks and VMs not ASM
  This function is intended for unmanaged disks only
  It returns information on OS disk and data disks if any
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER SubscriptionName
  The Azure subscription name such as 'My Dev EA subscription'
 
 .PARAMETER VMName
  The name of the Virtual Machine
 
 .PARAMETER LogFile
  This is an optional parameter that specifies the path to the log file where the script logs its progress
  This defaults to a file in the current folder where the script is running
 
 .EXAMPLE
  Get-AzureRMVMUnmanagedDisk -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -VMName 'Widget3VM'
  This example lists the unmanaged disks of a given VM
 
 .EXAMPLE
  Get-AzureRMVMUnmanagedDisk -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -VMName (Get-AzureRMVM).Name | FT -a
  This example lists all unmanaged disks in the given subscription
 
 .OUTPUTS
  Array of PS Custom objects, one for each disk found with the following properties:
    BlobName
    ContainerName
    StorageAccountName
    VMName
    ResourceGroup ==> this is the Resource Group Name
    IsOSDisk ==> True/False
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 2 January, 2019 - original relase and minor updates
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String[]]$VMName,
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Get-AzureRMVMUnmanagedDisk - $SubscriptionName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin {
        if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break }
    }

    Process {

        #region Get VM List
        $AllVMs = Get-AzureRMVM -WA 0 
        $VMList = @()
        foreach ($VMItem in $VMName) {
            if ($MatchingVMs = $AllVMs | where Name -EQ $VMItem) {
                $VMList += $MatchingVMs
                Write-Log 'Validated VM',$VMItem Green,Cyan $LogFile
            } else {
                Write-Log 'Unable to find VM',$VMItem,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile
            }
        }
        #endregion

        #region Get VM disks
        $DiskList = @()
        foreach ($VM in $VMList) {
            if ($VM.StorageProfile.OsDisk.Vhd.Uri) {
                $DiskName = Split-Path $VM.StorageProfile.OsDisk.Vhd.Uri -Leaf
                $DiskList += [PSCustomObject][Ordered]@{
                    BlobName           = $DiskName 
                    ContainerName      = (Split-Path $VM.StorageProfile.OsDisk.Vhd.Uri).Split('\')[3]
                    StorageAccountName = (Split-Path $VM.StorageProfile.OsDisk.Vhd.Uri).Split('\')[2].Split('.')[0]
                    VMName             = $VM.Name 
                    ResourceGroup      = $VM.ResourceGroupName 
                    IsOSDisk           = $true
                }
                Write-Log 'Identified VM',$VM.Name,'OS disk',$DiskName Green,Cyan,Green,Cyan $LogFile
            } else {
                Write-Log 'VM',$VM.Name,'OS disk is a Managed disk, skipping..' Magenta,Yellow,Magenta $LogFile    
            }

            if ($VM.StorageProfile.DataDisks) {
                foreach ($Disk in $VM.StorageProfile.DataDisks) {
                    if ($Disk.Vhd.Uri) {
                        $DiskName = Split-Path $Disk.Vhd.Uri -Leaf
                        $DiskList += [PSCustomObject][Ordered]@{
                            BlobName           = $DiskName
                            ContainerName      = (Split-Path $Disk.Vhd.Uri).Split('\')[3]
                            StorageAccountName = (Split-Path $Disk.Vhd.Uri).Split('\')[2].Split('.')[0]
                            VMName             = $VM.Name 
                            ResourceGroup      = $VM.ResourceGroupName 
                            IsOSDisk           = $false
                        }
                        Write-Log 'Identified VM',$VM.Name,'data disk',$DiskName Green,Cyan,Green,Cyan $LogFile
                    } else {
                        Write-Log 'VM',$VM.Name,'data disk',$DiskName,'is a Managed disk, skipping..' Magenta,Yellow,Magenta,Yellow,Magenta $LogFile    
                    }
                }
                
            } else {
                Write-Log 'VM',$VM.Name,'has no data disks, skipping..' Magenta,Yellow,Magenta $LogFile    
            }
        }
        #endregion

    } 

    End { $DiskList }
}

function Delete-AzureRMBlobAndContainerAndAccount {

# Requires -Modules AzureRM
# Requires -Version 5

<#
 .SYNOPSIS
  Function to delete an Azure Blob, its container if empty, and its storage account if empty
 
 .DESCRIPTION
  Function to delete an Azure Blob, its container if empty, and its storage account if empty
 
 .LINK
  https://superwidgets.wordpress.com/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 14 January 2019
#>


    [CmdletBinding(ConfirmImpact='High')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String]$StorageAccountName,
        [Parameter(Mandatory=$true)][String]$ContainerName,
        [Parameter(Mandatory=$true)][String]$BlobName,              
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Delete-AzureRMBlobAndContainerAndAccount - $BlobName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin { 
        # Validate Azure access
        if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break }
    }

    Process {
        $Go =$true
        if ($StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -EQ $StorageAccountName) {
            Write-Log ' Identified Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log ' Unable to find Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile
            $Go = $false
        }
        if ($StorageKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) {
            Write-Log ' Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log ' Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
            Write-Log $Error[0].Exception.Message Yellow $LogFile
            $Go = $false
        }
        if ($Context = New-AzureStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) {
            Write-Log ' Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile
        } else {
            Write-Log ' Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
            Write-Log $Error[0].Exception.Message Yellow $LogFile
            $Go = $false
        }

        try { 
            $Container = Get-AzureStorageContainer -Context $Context -Name $ContainerName -EA 1 
            Write-Log ' Read Storage Container',$ContainerName,'under',$StorageAccountName Green,Cyan,Green,Cyan $LogFile

            #region Delete Blob(s)
            if ($BlobList = $Container.CloudBlobContainer.ListBlobs() | where Name -Match $BlobName) {
                foreach ($Blob in $BlobList) {
                    Write-Log ' Deleting Blob',$Blob.Name Green,Yellow $LogFile
                    $Blob.Delete()
                }
                $Container = Get-AzureStorageContainer -Context $Context -Name $ContainerName -EA 1 
                if ($BlobList = $Container.CloudBlobContainer.ListBlobs() | where Name -Match $BlobName) {
                    Write-Log ' Failed to delete 1 or more blobs' Magenta $LogFile
                } else {
                    Write-Log ' Blob deletion successful' Cyan $LogFile
                }
            } else {
                Write-Log ' Blob',$BlobName,'not found in',"$StorageAccountName/$ContainerName" Magenta,Yellow,Magenta,Yellow $LogFile
            } 
            #endregion

            #region Delete container if empty
            if ($BlobList = $Container.CloudBlobContainer.ListBlobs()) {
                Write-Log ' Container',$ContainerName,'is not empty - skipping, it has the following blobs:' Green,Yellow,Green $LogFile
                $BlobList | foreach { Write-Log " $($_.Name)" Cyan $LogFile } 
            } else {
                Write-Log ' Deleting empty container',$ContainerName Green,Yellow $LogFile -NoNewLine
                try {
                    $Result = $Container | Remove-AzureStorageContainer -PassThru -Force -EA 1 
                    Write-Log 'done' DarkYellow $LogFile
                } catch {
                    Write-Log 'failed' Magenta $LogFile
                }
            } 
            #endregion
                                      
        } catch {
            Write-Log ' Unable to read Storage Container',$ContainerName,'under',$StorageAccountName Magenta,Yellow,Magenta,Yellow $LogFile 
        } 

        #region Delete Storage Account if empty
        if ($Go) {                    
            if ($ContainerList = Get-AzureStorageContainer -Context $Context) {
                Write-Log ' Storage account',$StorageAccountName,'is not empty - skipping, currently has the following containers' Cyan,Yellow,Cyan $LogFile
                $ContainerList.Name | foreach { Write-Log " $_" Green $LogFile }
            } else {
                Write-Log 'Deleting empty Storage Account',$StorageAccountName Green,Cyan $LogFile -NoNewLine
                $StorageAccount | Remove-AzureRmStorageAccount -Force
                Write-Log 'done' Green $LogFile
            }
        } 
        #endregion
    }

    End {  }
}

function Delete-AzureRMVM {

# Requires -Modules AzureRM
# Requires -Version 5

<#
 .SYNOPSIS
  Function to delete an Azure ARM VM and all its objects
 
 .DESCRIPTION
  Function to delete an Azure ARM VM and all its objects including:
  - Boot Diagnostics blob(s), storage container, storage account if empty
  - VM object
  - OS disk, storage container if ampty, storage account if ampty
  - Data disk(s) if any, storage container(s) if ampty, storage account(s) if ampty
  - VM NIC(s)
  - VM public IP objects if any
  NSG's are not deleted by this function since they may be linked to many NICs
  This function will not delete a running VM by design
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER SubscriptionName
  The Azure subscription name such as 'My Dev EA subscription'
 
 .PARAMETER ARMVMName
  The name of one or more ARM Virtual Machines
 
 .PARAMETER LogFile
  This is an optional parameter that specifies the path to the log file where the script logs its progress
  This defaults to a file in the current folder where the script is running
 
 .EXAMPLE
  Delete-AzureRMVM -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -ARMVMName 'Widget3VM'
 
 .LINK
  https://superwidgets.wordpress.com/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 14 January 2019
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String]$VMName,   
        [Parameter(Mandatory=$false)][String]$ResourceGroupName,   
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Delete-AzureRMVM - $($VMName -join '-') - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin { 
        # Validate Azure access, Input
        if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break }

        Try {
            $StorageAccountList = Get-AzureRmStorageAccount -EA 1 
            if (-not $StorageAccountList) {
                Write-Log 'No storage accounts found' Magenta $LogFile; Break
            }
        } catch {
            Write-Log 'Unable to list Storage Accounts in Subscription',$SubscriptionName Magenta,Yellow $LogFile; Break
        }

        Try {
            $RawVMList = Get-AzureRmVM -EA 1 
            if (-not $RawVMList) {
                Write-Log 'No VMs found' Magenta $LogFile; Break
            }
        } catch {
            Write-Log 'Unable to list VMs in Subscription',$SubscriptionName Magenta,Yellow $LogFile; Break
        }

    }

    Process {

        if ($VM = $RawVMList | where Name -EQ $VMName) {
            if ($VM.Count -gt 1) {
                if ($ResourceGroupName) {
                    $VM = Get-AzureRmVM -Name $VMName -ResourceGroupName $ResourceGroupName
                } else {
                    Write-Log 'Delete-AzureRMVM input error:','Found more than 1 VM named',$VMName Magenta,Yellow,Magenta $LogFile
                    Write-Log ($VM|Out-String).Trim() Yellow $LogFile
                    Write-Log 'If more than 1 VM exist in the same subscription with the same name, you must specify the ResourceGroupName' Magenta $LogFile
                    break
                }
            }
        } else {
            Write-Log 'VM',$VMName,'not found in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile
            break
        } 

    if ($VM) {
            Write-Log 'Processing VM' Green $LogFile
            Write-Log ($VM|Out-String).Trim() Cyan $LogFile

            $VMStatus = (Get-AzureRmVM -ResourceGroupName $VM.ResourceGroupName -Name $VMName -Status).Statuses[1].DisplayStatus
            if ($VMStatus -eq 'VM deallocated') { 

                #region Delete Boot Diagnostics blob(s) if configured, container, storage account if empty
                    if ($VM.DiagnosticsProfile.bootDiagnostics.storageUri) {
                        $StorageAccountName = ($VM.DiagnosticsProfile.bootDiagnostics.storageUri).Split('/')[2].Split('.')[0]
                        $ContainerName = "bootdiagnostics-$($vm.Name.ToLower().Substring(0, 9))-$($VM.vmId)"
                
                        $Go =$true
                        if ($StorageAccount = Get-AzureRmStorageAccount | where StorageAccountName -EQ $StorageAccountName) {
                            Write-Log ' Identified Diagnostics Storage Account',$StorageAccountName Green,Cyan $LogFile
                        } else {
                            Write-Log ' Unable to find Diagnostics Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile
                            $Go = $false
                        }
                        if ($StorageKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $StorageAccount.ResourceGroupName -Name $StorageAccount.StorageAccountName)[0].Value) {
                            Write-Log ' Acquired access key for Storage Account',$StorageAccountName Green,Cyan $LogFile
                        } else {
                            Write-Log ' Unable to acquire access key for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
                            Write-Log $Error[0].Exception.Message Yellow $LogFile
                            $Go = $false
                        }
                        if ($Context = New-AzureStorageContext -StorageAccountName $StorageAccount.StorageAccountName -StorageAccountKey $StorageKey) {
                            Write-Log ' Acquired context for Storage Account',$StorageAccountName Green,Cyan $LogFile
                        } else {
                            Write-Log ' Unable to acquire context for Storage Account',$StorageAccountName,'in subscription',$SubscriptionName Magenta,Yellow,Magenta,Yellow $LogFile 
                            Write-Log $Error[0].Exception.Message Yellow $LogFile
                            $Go = $false
                        }
                        try { 
                            $Container = Get-AzureStorageContainer -Context $Context -Name $ContainerName -EA 1 
                            Write-Log ' Read Storage Container',$ContainerName,'under',$StorageAccountName Green,Cyan,Green,Cyan $LogFile
                            if ($BlobList = $Container.CloudBlobContainer.ListBlobs() ) {
                                Write-Log ' Found the following blobs in',"$StorageAccountName/$ContainerName" Green,Cyan $LogFile
                                $BlobList.Name | foreach { Write-Log " $_" Cyan $LogFile }
                            } else {
                                Write-Log ' No Blobs found in',"$StorageAccountName/$ContainerName" Magenta,Yellow $LogFile
                            }
                    
                            Write-Log ' Deleting the container',$ContainerName,'and all its Blobs..' Green,Yellow,Green $LogFile -NoNewLine
                            try {
                                $Result = $Container | Remove-AzureStorageContainer -PassThru -Force -EA 1 
                                Write-Log 'done' Cyan $LogFile
                            } catch {
                                Write-Log 'failed' Magenta $LogFile
                            }
                        } catch {
                            Write-Log ' Unable to read Storage Container',$ContainerName,'under',$StorageAccountName Magenta,Yellow,Magenta,Yellow $LogFile 
                        } # Delete Container

                        if ($Go) {                    
                            if ($ContainerList = Get-AzureStorageContainer -Context $Context) {
                                Write-Log ' Storage account',$StorageAccountName,'is not empty - skipping, currently has the following containers' Cyan,Yellow,Cyan $LogFile
                                $ContainerList.Name | foreach { Write-Log " $_" Green $LogFile }
                            } else {
                                Write-Log 'Deleting empty Storage Account',$StorageAccountName Green,Cyan $LogFile -NoNewLine
                                $StorageAccount | Remove-AzureRmStorageAccount -Force
                                Write-Log 'done' Green $LogFile
                            }
                        } # Delete Storage Account if empty
                    } else {
                        Write-Log ' Boot diagnostics not configured for VM',$VM.Name Green,Yellow $LogFile
                    }
                    #endregion

                #region Delete VM
                Write-Log ' Deleting VM',$VMName Green,Cyan $LogFile -NoNewLine
                $Result = $VM | Remove-AzureRmVM –Force
                Write-Log 'done' DarkYellow $LogFile
                #endregion

                #region Delete OS disk, status blob

                if($VM.StorageProfile.OsDisk.ManagedDisk) {
                    Write-Log ' Deleting managed OS disk',$VM.StorageProfile.OSDisk.Name,'for VM',$VM.Name Green,Cyan,Green,Cyan $LogFile -NoNewLine
                    Get-AzureRmDisk -ResourceGroupName $VM.ResourceGroupName -DiskName $VM.StorageProfile.OSDisk.Name | Remove-AzureRmDisk -Force
                    Write-Log 'done' DarkYellow $LogFile
                } else {
                    $StorageAccountName = ($VM.StorageProfile.OSDisk.Vhd.Uri).Split('/')[2].Split('.')[0]
                    $ContainerName = ($VM.StorageProfile.OSDisk.Vhd.Uri).Split('/')[3]
                    $BlobName = ($VM.StorageProfile.OSDisk.Vhd.Uri).Split('/')[4]
                    Write-Log 'Identified OS disk',$BlobName,'in Storage Account/Container',"$StorageAccountName/$ContainerName" Green,Cyan,Green,Cyan $LogFile
                    $ParameterList = @{
                        LoginName          = $LoginName
                        SubscriptionName   = $SubscriptionName
                        StorageAccountName = $StorageAccountName
                        ContainerName      = $ContainerName
                        BlobName           = $BlobName             
                        LogFile            = $LogFile                         
                    }
                    Delete-AzureRMBlobAndContainerAndAccount @ParameterList     
                }               

                #endregion

                #region Delete data disks

                foreach ($DataDisk in $VM.StorageProfile.DataDisks) { 
                    if($DataDisk.ManagedDisk) {
                        Write-Log ' Deleting managed data disk',$DataDisk.Name,'for VM',$VM.Name Green,Cyan,Green,Cyan $LogFile -NoNewLine
                        Get-AzureRmDisk -ResourceGroupName $VM.ResourceGroupName -DiskName $DataDisk.Name | Remove-AzureRmDisk -Force
                        Write-Log 'done' DarkYellow $LogFile
                    } else {
                        $StorageAccountName = ($DataDisk.Vhd.Uri).Split('/')[2].Split('.')[0]
                        $ContainerName = ($DataDisk.Vhd.Uri).Split('/')[3]
                        $BlobName = ($DataDisk.Vhd.Uri).Split('/')[4]
                        Write-Log 'Identified data disk',$BlobName,'in Storage Account/Container',"$StorageAccountName/$ContainerName" Green,Cyan,Green,Cyan $LogFile
                        $ParameterList = @{
                            LoginName          = $LoginName
                            SubscriptionName   = $SubscriptionName
                            StorageAccountName = $StorageAccountName
                            ContainerName      = $ContainerName
                            BlobName           = $BlobName             
                            LogFile            = $LogFile                         
                        }
                        Delete-AzureRMBlobAndContainerAndAccount @ParameterList  
                    }
                }

                #endregion

                #region delete vNIC(s)
                foreach ($VMNIC in ($VM.NetworkProfile.NetworkInterfaces | where {$_.ID})) {
                    $NICName = Split-Path -Path $VMNIC.ID -leaf
                    Write-Log ' Deleting VM NIC',$NICName Green,Cyan $LogFile -NoNewLine
                    Get-AzureRmNetworkInterface -ResourceGroupName $VM.ResourceGroupName -Name $NICName | Remove-AzureRmNetworkInterface -Force
                    Write-Log 'done' DarkYellow $LogFile
                }
                #endregion

                #region delete public IP if any
                Remove-Variable FoundPublicIP -EA 0 
                foreach ($VMNIC in $VM.NetworkProfile.NetworkInterfaces.Id) {
                    foreach ($PublicIP in (Get-AzureRmPublicIpAddress -ResourceGroupName $VM.ResourceGroupName | Where { $_.IpConfiguration.Id })) {
                        if (($PublicIP.IpConfiguration.Id).Split('/')[8] -eq $VMNIC.Split('/')[8]) {
                            Write-Log 'Identified Public IP object',$PublicIP.Name,'associated with VM NIC',($VMNIC.Split('/')[8]),'of VM',$VM.Name Green,Cyan,Green,Cyan ,Green,Cyan 
                            $FoundPublicIP = $PublicIP
                        }        
                    }
                }

                if ($FoundPublicIP) {
                    Write-Log ' Deleting VM public IP object',$PublicIP.Name Green,Cyan $LogFile -NoNewLine
                    Get-AzureRmPublicIpAddress -ResourceGroupName $VM.ResourceGroupName -Name $PublicIP.Name | Remove-AzureRmPublicIpAddress -Force 
                    Write-Log 'done' DarkYellow $LogFile
                } else {
                    Write-Log ' No public IP object found for VM',$VM.Name Green,Cyan $LogFile
                }
                #endregion

                # Not deleting NSG's here, since they may apply to several NICs that belong to several VMs
                # Will have a separate function to delete unused NSG's (not linked to any NICs)

            } else {
                Write-Log 'VM',$VMName,'is not powered off. Current status is:',$VMStatus,'skipping..' Magenta,Yellow,Magenta,Yellow,Magenta $LogFile
            }
        }

    }

    End {  }
}

function Report-AzureRMSubscriptionVMBackup {
<#
 .SYNOPSIS
  Function to list backup recovery points of Azure VMs in one or more subscriptions
 
 .DESCRIPTION
  Function to list backup recovery points of Azure VMs in one or more subscriptions
  The script provides interim output to the console indicating its progress through the hierarchy of:
    Subscriptions
      Recovery Services Vaults
        Registered AzureVM Backup containers
          Backup Items
            Recovery points
 
 .PARAMETER SubscriptionName
  Name of Azure subscription
  If not provided it will default to all accessible Azure subscriptions
 
 .EXAMPLE
    Login-AzureRmAccount -Credential (Get-SBCredential 'name@domain.com') | Out-Null # -Environment AzureCloud
    Report-AzureRMSubscriptionVMBackup
 
 .EXAMPLE
    $VMBackupList = Report-AzureRMSubscriptionVMBackup -SubscriptionName 'my subscription name'
    $VMBackupList | Format-Table -Auto # to display to the console
    $VMBackupList | Out-GridView # to display to ISE GridView
    $VMBackupList | Export-Csv .\VMBackupList1.csv -NoType # to export to CSV
 
. OUTPUTS
  PSCustom object (one for each recovery point) containing the following properties/example:
    VMName VaultName ResourceGroup SubscriptionName RecoveryPointType RecoveryPointTime EncryptionEnabled
    ------ ------------ ------------- ---------------- ----------------- ----------------- -----------------
    ab123xyzw01 xyz abc my subscription name CrashConsistent 8/9/2018 6:01:25 AM False
    ab123xyzw01 xyz abc my subscription name CrashConsistent 8/8/2018 6:08:09 AM False
    ab123xyzw01 xyz abc my subscription name CrashConsistent 8/7/2018 6:11:49 AM False
 
 .LINK
  https://superwidgets.wordpress.com/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 9 August 2018
  v0.2 - 24 September 2018 - Fixed bug with Get-AzureRmResource line
  v0.3 - 25 September 2018 - Added Vault Name in output
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$false,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)]
            [String[]]$SubscriptionName
    )

    Begin { 
        $myOutput = @() 

        # Validate AzureRM PowerShell module is available
        if(-not (Get-Module -ListAvailable AzureRM)) {
            Write-Log 'Required AzureRM PowerShell module not found. You can install it from the PowerShell Gallery by running:' Magenta
            Write-Log 'Install-Module AzureRM' Yellow
            break
        }

        # Validate that we're logged in to Azure
        try { Get-AzureRmSubscription -EA 1 -WA 0 | Out-Null  } catch { Write-Log $_.exception.message Yellow; break }
    }

    Process {

        if (-not $SubscriptionName) { $SubscriptionName = (Get-AzureRmSubscription -WA 0).Name }

        foreach ($Subscription in $SubscriptionName) {
            Write-Log 'Processing subscription',$Subscription Green,Cyan
            try {
                Get-AzureRmSubscription -SubscriptionName $Subscription -EA 1 -WA 0 | Set-AzureRmContext | Out-Null  
                $VaultList = Get-AzureRmResource | where ResourceType -EQ Microsoft.RecoveryServices/vaults | select Name,ResourceGroupName,Location
                if ($VaultList) {
                    Write-Log ' Identified',$VaultList.Count,'Recovery Services Vaults;',($VaultList.Name -join ', ') Green,Cyan,Green,Cyan
                    foreach ($Vault in $VaultList) {
                        Write-Log ' Processing Recovery Services Vault',$Vault.Name Green,Cyan 
                        Set-AzureRmRecoveryServicesVaultContext -Vault $Vault
                        $ContainerList = Get-AzureRmRecoveryServicesBackupContainer  -ContainerType 'AzureVM' -Status 'Registered' 
                        if ($ContainerList) {
                            Write-Log ' Identified',$ContainerList.Count,'Azure VM backup sets/containers;',($ContainerList.FriendlyName -join ', ') Green,Cyan,Green,Cyan
                            foreach ($Container in $ContainerList) {
                                $backupitem = Get-AzureRmRecoveryServicesBackupItem -Container $Container -WorkloadType 'AzureVM'
                                if ($backupitem) {
                                    $RecoveryPointList = Get-AzureRmRecoveryServicesBackupRecoveryPoint -Item $backupitem 
                                    if ($RecoveryPointList) {
                                        Write-Log ' Identified',$RecoveryPointList.Count,'recovery points for VM',$Container.FriendlyName Green,Cyan,Green,Cyan
                                        foreach ($RecoveryPoint in $RecoveryPointList) {
                                            $myOutput += [PSCustomObject][Ordered]@{
                                                VMName            = $RecoveryPoint.ItemName.Split(';')[2]
                                                ResourceGroup     = $RecoveryPoint.ItemName.Split(';')[1]
                                                VaultName         = $Vault.Name 
                                                SubscriptionName  = $Subscription
                                                RecoveryPointType = $RecoveryPoint.RecoveryPointType
                                                RecoveryPointTime = $RecoveryPoint.RecoveryPointTime
                                                EncryptionEnabled = $RecoveryPoint.EncryptionEnabled
                                            }
                                        }
                                    } else {
                                        Write-Log ' No recovery points found for VM',$Container.FriendlyName Green,yellow
                                    }
                                }
                            }
                        } else {
                            Write-Log ' No registered VM backup containers found in Recovery Services Vault',$Vault.Name Green,Yellow
                        }
                    }
                } else {
                    Write-Log ' No Recovery Services Vaults found in subscription',$Subscription Green,Yellow
                }
            } catch {
                Write-Log $_.exception.message Yellow
            }
            
        }
    }

    End { $myOutput }
}

function Remove-AzureRMVMBackup {
<#
 .SYNOPSIS
  Function to disable backup of a given VM and delete existing backups (recovery points)
 
 .DESCRIPTION
  Function to disable backup of a given VM and delete existing backups (recovery points)
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER SubscriptionName
  The Azure subscription name such as 'My Dev EA subscription'
 
 .PARAMETER VMName
  The name of one or more Virtual Machines
 
 .PARAMETER LogFile
  This is an optional parameter that specifies the path to the log file where the script logs its progress
  This defaults to a file in the current folder where the script is running
 
 .EXAMPLE
    Remove-AzureRMVMBackup -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -VMName 'Widget3VM'
 
 .LINK
  https://superwidgets.wordpress.com/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 16 January 2019
#>


    [CmdletBinding(ConfirmImpact='High')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String]$VMName,
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Remove-AzureRMVMBackup - $VMName - $(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin {
        if (-not ($Login = Login-AzureRMSubscription -LoginName $LoginName -SubscriptionName $SubscriptionName -LogFile $LogFile)) { break }
    }

    Process {
        
        # List backup containers because ASM VM backups may not show in which Vault their container is located
        $BackupContainerList = foreach ($RSVault in Get-AzureRmRecoveryServicesVault) {
            Get-AzureRmRecoveryServicesBackupContainer -ContainerType AzureVM -Status Registered -VaultId $RSVault.ID | select FriendlyName,
                @{n='VaultId'  ;e={$RSVault.ID}},
                @{n='Vault'    ;e={Split-Path $RSVault.ID -Leaf}},
                @{n='Container';e={$_.FriendlyName}}
        }
        Write-Verbose 'Identified list of backup vaults and containers:'
        Write-Verbose ($BackupContainerList | FT Vault,Container -a | Out-String).Trim()

        if ($BackupContainer = $BackupContainerList | where FriendlyName -EQ $VMName) {
            Write-Log 'Identified VM Backup Container',$BackupContainer.FriendlyName,'for VM',$VMName Green,Cyan,Green,Cyan $LogFile
            Write-Log ($BackupContainer | FL | Out-String).Trim() Cyan
            if ($BackupContainer.Count -gt 1) {
                Write-Log 'Remove-AzureRMVMBackup: Found more than 1 backup container for VM',$VMName,'skipping..' Magenta,Yellow,Magenta $LogFile
            } else {
                $FoundVault = $BackupContainerList | where FriendlyName -EQ $BackupContainer.FriendlyName
                $BackupItem = Get-AzureRmRecoveryServicesBackupItem -Container $BackupContainer -WorkloadType AzureVM -VaultId $FoundVault.VaultId -EA 0 
                if ($BackupItem) {
                    Write-Log ' Identified',($BackupItem.Name.Split(';')[2]),'VM Backup Item' Green,Cyan,Green 
                    Write-Log ($BackupItem  | FL | Out-String).Trim() Cyan
                    if ($BackupItem.Count -gt 1) {
                        Write-Log 'Remove-AzureRMVMBackup: Found more than 1 Backup item for VM',$VMName,'skipping..' Magenta,Yellow,Magenta $LogFile
                    } else {
                        Write-Log ' Disabling backup for VM',$VMName,'and deleting existing backups' Green,Cyan,Green $LogFile -NoNewLine
                        $Result = Disable-AzureRmRecoveryServicesBackupProtection -Item $BackupItem -RemoveRecoveryPoints -Force
                        Write-Log 'done' DarkYellow $LogFile
                    }
                } else {
                    Write-Log ' No Backup Item found for VM',$VMName Green,Yellow
                }
            }
        } else {
            Write-Log ' No Backup Container found for VM',$VMName Green,Yellow
        }            
        
    }

    End { }
}

#endregion

function New-SBAZServicePrincipal {
<#
 .SYNOPSIS
  Function to create Azure AD Service Principal
 
 .DESCRIPTION
  Function to create Azure AD Service Principal
  The use case intended for this function is to use the Service Principal to run PowerShell scripts against an Azure subscription
 
 .PARAMETER ServicePrincipalName
  One or more Service Principal Names
 
 .PARAMETER Environment
  Name of the Azure cloud. This parameter default to Azure Commercial cloud.
  As of 15 March 2018 that list is:
    AzureGermanCloud
    AzureCloud
    AzureUSGovernment
    AzureChinaCloud
    To see an updated list, use:
        (Get-AzureRMEnvironment).Name
 
 .PARAMETER Role
  This parameter is used to assign Role/Permissions for te Service Principal in the current subscription.
  The default value is 'Owner' role.
  As of 16 March 2018 the following default roles are defined:
    API Management Service Contributor
    Application Insights Component Contributor
    Automation Operator
    BizTalk Contributor
    Classic Network Contributor
    Classic Storage Account Contributor
    Classic Storage Account Key Operator Service Role
    Classic Virtual Machine Contributor
    ClearDB MySQL DB Contributor
    Contributor
    Cosmos DB Account Reader Role
    Data Factory Contributor
    Data Lake Analytics Developer
    DevTest Labs User
    DNS Zone Contributor
    DocumentDB Account Contributor
    Intelligent Systems Account Contributor
    Log Analytics Contributor
    Log Analytics Reader
    Network Contributor
    New Relic APM Account Contributor
    Owner
    Reader
    Redis Cache Contributor
    Scheduler Job Collections Contributor
    Search Service Contributor
    Security Manager
    SQL DB Contributor
    SQL Security Manager
    SQL Server Contributor
    Storage Account Contributor
    Storage Account Key Operator Service Role
    Traffic Manager Contributor
    User Access Administrator
    Virtual Machine Contributor
    Web Plan Contributor
    Website Contributor
  For more details on roles, type in:
    Get-AzureRmRoleDefinition | select name,description,actions | Out-GridView
 
 .EXAMPLE
  $SPList = New-SBAZServicePrincipal -ServicePrincipalName samtest1,sam1demo
 
 .EXAMPLE
  $SPN = New-SBAZServicePrincipal -ServicePrincipalName PowerShell05 -Environment AzureUSGovernment
  # The above line creates the SPN and gives it 'Owner' permission/role in the current subscription
  $SPN | Export-Csv .\PowerShell05-SPN.csv -NoTypeInformation # This line saves the $SPN to CSV (not the password)
 
  # To use the SPN in future automations:
  # $SPN = Import-Csv .\PowerShell05-SPN.csv
  # Login-AzureRmAccount -Credential (Get-SBCredential $SPN.ServicePrincipalName) -ServicePrincipal -TenantId $SPN.TenantID -Environment $SPN.Environment
 
 .OUTPUTS
  The function returns a PS Object for each input Service Principal Name containing the following properties:
    ServicePrincipalName
    TenantId
    Environment
    Role
 
 .LINK
  https://superwidgets.wordpress.com/2018/03/15/new-sbazserviceprincipal-cmdlet-to-create-new-azure-ad-service-principal-added-to-azsbtools-powershell-module/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 14 March 2018
  v0.2 - 15 March 2018 - Added 'Environment' parameter
  v0.3 - 16 March 2018 - Added 'Role' parameter, changed output to a custom PS Object
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String[]]$ServicePrincipalName,
        [Parameter(Mandatory=$false)][ValidateSet('AzureCloud','AzureUSGovernment','AzureGermanCloud','AzureChinaCloud')][String]$Environment = 'AzureCloud',
        [Parameter(Mandatory=$false)][String]$Role = 'Owner'
    )

    Begin { 
        $Subscription = Connect-AzureRmAccount -Environment $Environment  
    }

    Process {
        
        if ($Subscription.Context.Subscription.Name) { 

            Write-Log 'Identified',$Subscription.Context.Subscription.Name,'subscription in the',$Subscription.Context.Environment.Name,'cloud' Green,Cyan,Green,Cyan,Green

            $SPList = foreach ($AppName in $ServicePrincipalName) {

                $AppCred = Get-SBCredential -UserName $AppName
        
                #region Create/Validate Azure AD App
                Remove-Variable App -EA 0 
                if ($App = Get-AzureRmADApplication -DisplayName $AppName) {
                    Write-Log 'Validated app:',$App.Displayname Green,Cyan 
                } else {
                    $App = New-AzureRmADApplication -DisplayName $AppName -IdentifierUris $AppName
                    Write-Log 'Created app:',$App.Displayname Green,Cyan 
                }            
                #endregion

                #region Create/Validate Azure AD Service Principal
                Remove-Variable ServicePrincipal -EA 0 
                if ($ServicePrincipal = Get-AzureRmADServicePrincipal | where { $PSItem.ApplicationId -eq $App.ApplicationId.Guid }) {
                    Write-Log 'Validated Service Principal:',($ServicePrincipal.SerVicePrincipalNames -join ', ') Green,Cyan 
                } else {
                    $ServicePrincipal = New-AzureRmADServicePrincipal -ApplicationId $App.ApplicationId.Guid -Password $AppCred.Password
                    Write-Log 'Created Service Principal:',($ServicePrincipal.SerVicePrincipalNames -join ', ') Green,Cyan 
                }            
                #endregion

                #region Assign Role (Permissions)
                Write-Log 'Assigning role',$Role Green,Cyan -NoNewLine
                $Result = try {
                    New-AzureRmRoleAssignment -ObjectId $ServicePrincipal.Id -RoleDefinitionName $Role -Scope "/subscriptions/$($Subscription.Context.Subscription.Id)" -EA 1
                    Write-Log 'done' Green
                } catch {
                    Write-Log $PSItem.Exception.Message Yellow
                }
                #endregion

                [PSCustomObject][Ordered]@{
                    ServicePrincipalName = $AppName
                    TenantId             = (Get-AzureRmTenant).Id 
                    Environment          = $Environment
                    Role                 = $Role
                }

            }
                    
        } else {
            Write-Log 'No subscriptions found for account',$Subscription.Context.Account.Id,'in the',$Subscription.Context.Environment.Name,'cloud' Magenta,Yellow,Magenta,Yellow,Magenta
        }        

    }

    End {
        $SPList
    }
}

function Deploy-AzureARMVM {
<#
 .SYNOPSIS
  Function to automate provisioning of Azure ARM VM(s)
 
 .DESCRIPTION
  Function to automate provisioning of Azure ARM VM(s)
 
 .PARAMETER SubscriptionName
    Name of existing Azure subscription
 
 .PARAMETER Location
    Name of Azure Data center/Location
    Example: 'eastus'
    To see location list use:
        Get-AzureRmLocation | sort Location | Select Location
 
 .PARAMETER ResourceGroup
    Name of Resource Group.
    Example: 'VMGroup17'
    The script will create it if it does not exist
 
 .PARAMETER AvailabilitySetName
    Example: 'Availability17'
     The script will create it if it does not exist
 
 .PARAMETER ConfirmShutdown
    This switch accepts $true or $False, and defaaults to $False
    If adding existing VMs to Availaibility set, the script must shut down the VMs
 
 .PARAMETER StorageAccountPrefix
    Only lower case letters and numbers, must be Azure (globally) unique
 
 .PARAMETER AdminName
    Example: 'myAdmin17'
    This will be the new VM local administrator
 
 .PARAMETER VMName
    Example: ('vm01','vm02')
    Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set
 
 .PARAMETER VMSize
    Example: 'Standard_A1_v2'
    To see available sizes in this Azure location use:
        (Get-AzureRoleSize).RoleSizeLabel
 
 .PARAMETER WinOSImage
    This defaults to '2012-R2-Datacenter'
    Available options:
        '2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server'
    To see current options in a given Azure Location use:
        (Get-AzureRMVMImageSku -Location usgovvirginia -Publisher MicrosoftWindowsServer -Offer WindowsServer).Skus
    For more information see https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage
 
 .PARAMETER vNetName
    Example: 'Seventeen'
    This will be the name of the virtual network to be created/updated if exist
 
 .PARAMETER vNetPrefix
    Example: '10.17.0.0/16'
    To be created/updated
 
 .PARAMETER SubnetName
    Example: 'vmSubnet'
    This will be the name of the subnet to be created/updated
 
 .PARAMETER SubnetPrefix
    Example: '10.17.0.0/24'
    Must be subset of vNetPrefix above - to be created/updated
 
 .PARAMETER LogFile'
    Path to log file where this scrit will log its commands and output
    Default is ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
 
 .EXAMPLE
    Connect-AzureRmAccount -Environment AzureUSGovernment
    $myParamters = @{
        SubscriptionName = 'Azure Government T1'
        Location = 'usgovvirginia'
        ResourceGroup = 'EncryptionTest01'
        AvailabilitySetName = 'AvailabilityTest01'
        ConfirmShutdown = $false
        StorageAccountPrefix = 'sam150318a'
        AdminName = 'myAdmin150318a'
        VMName = @('vm01','vm02','vm03')
        VMSize = 'Standard_A0'
        WinOSImage = '2016-Datacenter'
        vNetName = 'EncryptionTest01VNet'
        vNetPrefix = '10.3.0.0/16'
        SubnetName = 'vmSubnet'
        SubnetPrefix = '10.3.15.0/24'
    }
    Deploy-AzureARMVM @myParamters
 
 .LINK
  http://www.exigent.net/blog/microsoft-azure/provisioning-and-tearing-down-azure-virtual-machines/
 
 .NOTES
  Function by Sam Boutros
    3 January 2017 - v0.1 - Initial release
    19 January 2017 - v0.2
        Updated parameters - set to mandatory
        Updated Storage Account creation region, create a separate storage account for each VM
        Updated Initialize region; removing subscription login, adding input echo, adding error handling
        Added functionality to configure VMs in availability set
    5 March 2018 - v0.3 Cosmetic updates
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$SubscriptionName         , # Example: 'Sam Test 1' # Name of existing Azure subscription
        [Parameter(Mandatory=$true)][String]$Location                 , # Example: 'eastus' # Get-AzureRmLocation | sort Location | Select Location
        [Parameter(Mandatory=$true)][String]$ResourceGroup            , # Example: 'VMGroup17' # To be created if not exist
        [Parameter(Mandatory=$false)][String]$AvailabilitySetName     , # Example: 'Availability17' # To be created if not exist
        [Parameter(Mandatory=$false)][Switch]$ConfirmShutdown = $false, # If adding existing VMs to Availaibility set, the script must shut down the VMs
        [Parameter(Mandatory=$false)][String]$StorageAccountPrefix    , # To be created if not exist, only lower case letters and numbers, must be Azure unique
        [Parameter(Mandatory=$true)][String]$AdminName                , # Example: 'myAdmin17' # This will be the new VM local administrator
        [Parameter(Mandatory=$true)][String[]]$VMName                 , # Example: ('vm01','vm02') # Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set
        [Parameter(Mandatory=$true)][String]$VMSize                   , # Example: 'Standard_A1_v2' # (Get-AzureRoleSize).RoleSizeLabel to see available sizes in this Azure location
        [Parameter(Mandatory=$false)][ValidateSet('2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server')]
            [String]$WinOSImage = '2012-R2-Datacenter'               , # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage
        [Parameter(Mandatory=$true)][String]$vNetName                 , # Example: 'Seventeen' # This will be the name of the virtual network to be created/updated if exist
        [Parameter(Mandatory=$true)][String]$vNetPrefix               , # Example: '10.17.0.0/16' # To be created/updated
        [Parameter(Mandatory=$true)][String]$SubnetName               , # Example: 'vmSubnet' # This will be the name of the subnet to be created/updated
        [Parameter(Mandatory=$true)][String]$SubnetPrefix             , # Example: '10.17.0.0/24' # Must be subset of vNetPrefix above - to be created/updated
        [Parameter(Mandatory=$false)][String]$LogFile = ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt"
    )

    Begin {

        #region Initialize
        if (!(Test-Path (Split-Path $LogFile))) { New-Item -Path (Split-Path $LogFile) -ItemType directory -Force | Out-Null }
        Write-Log 'Input received:' Green $LogFile
        write-log " SubscriptionName: $SubscriptionName" Cyan $LogFile
        write-log " Location: $Location" Cyan $LogFile
        write-log " ResourceGroup: $ResourceGroup" Cyan $LogFile
        write-log " AvailabilitySetName: $AvailabilitySetName" Cyan $LogFile
        write-log " ConfirmShutdown: $ConfirmShutdown" Cyan $LogFile
        write-log " StorageAccountPrefix: $StorageAccountPrefix" Cyan $LogFile
        write-log " AdminName: $AdminName" Cyan $LogFile
        write-log " VMName(s): $($VMName -join ', ')" Cyan $LogFile
        write-log " VMSize: $VMSize" Cyan $LogFile
        write-log " vNetName: $vNetName" Cyan $LogFile
        write-log " vNetPrefix: $vNetPrefix" Cyan $LogFile
        write-log " SubnetName: $SubnetName" Cyan $LogFile
        write-log " SubnetPrefix: $SubnetPrefix"  Cyan $LogFile
        $Cred = Get-SBCredential -UserName $AdminName 
        #endregion

        #region Connect to Azure subscription
        Write-Log 'Connecting to Azure subscription',$SubscriptionName Green,Cyan $LogFile -NoNewLine
        try { 
            $Result = Get-AzureRmSubscription –SubscriptionName $SubscriptionName -ErrorAction Stop | Select-AzureRmSubscription 
            Write-Log 'done' Green $LogFile    
            Write-Log ($Result | Out-String).Trim() Cyan $LogFile
        } catch {
            throw "unable to get Azure Subscription '$SubscriptionName'"
        }
        #endregion

        #region Create/Update Resource group
        Write-Log 'Create/Update Resource group',$ResourceGroup Green,Cyan $LogFile -NoNewLine
        try {
            $Result = New-AzureRmResourceGroup -Name $ResourceGroup -Location $Location -Force -ErrorAction Stop
            Write-Log 'done' Green $LogFile    
            Write-Log ($Result | Out-String).Trim() Cyan $LogFile
        } catch {
            throw "Failed to create Resource Group '$ResourceGroup'"
        }
        #endregion

        #region Create/Update Subnet and vNet
        Write-Log 'Creating/updating vNet',$vNetName,$vNetPrefix,'and subnet',$SubnetName,$SubnetPrefix Cyan,Green,DarkYellow,Cyan,Green,DarkYellow $LogFile -NoNewLine
        $Subnet = New-AzureRmVirtualNetworkSubnetConfig -Name $SubnetName -AddressPrefix $SubnetPrefix
        $vNet = New-AzureRmVirtualNetwork -Name $vNetName -ResourceGroupName $ResourceGroup -Location $Location -AddressPrefix $vNetPrefix -Subnet $Subnet -Force
        Write-Log 'done' Green
        #endregion

    }

    Process {
        foreach ($Name in $VMName) { # Provision Azure VM(s)

            #region Create Storage Account if it does not exist
            $StorageAccountName = "stor$($StorageAccountPrefix.ToLower())$($Name.ToLower())"
            if ($StorageAccountName.Length -gt 20) { 
                Write-Log 'Storage account name',$StorageAccountName,'is too long, using first 20 characters only..' Green,Yellow,Green $LogFile
                $StorageAccountName = $StorageAccountName.Substring(0,19) 
            }  
            Write-Log 'Creating Storage Account',$StorageAccountName Green,Cyan $LogFile
            try {
                $StorageAccount = Get-AzureRmStorageAccount -Name $StorageAccountName -ResourceGroupName $ResourceGroup -ErrorAction Stop
                Write-Log 'Using existing storage account',$StorageAccountName Green,Cyan $LogFile
            } catch {
                $i=0
                $DesiredStorageAccountName = $StorageAccountName
                while (!(Get-AzureRmStorageAccountNameAvailability $StorageAccountName).NameAvailable) {
                    $i++
                    $StorageAccountName = "$StorageAccountName$i"
                }
                if ($DesiredStorageAccountName -ne $StorageAccountName ) {
                    Write-Log 'Storage account',$DesiredStorageAccountName,'is taken, using',$StorageAccountName,'instead (available)' Greem,Yellow,Green,Cyan,Green $LogFile
                }
                try {
                    $Splatt = @{
                        ResourceGroupName = $ResourceGroup
                        Name              = $StorageAccountName 
                        SkuName           = 'Standard_LRS' 
                        Kind              = 'Storage' 
                        Location          = $Location 
                        ErrorAction       = 'Stop'
                    }
                    $StorageAccount = New-AzureRmStorageAccount @Splatt
                    Write-Log 'Created storage account',$StorageAccountName Green,Cyan $LogFile
                } catch {
                    Write-Log 'Failed to create storage account',$StorageAccountName Magenta,Yellow $LogFile
                    throw $PSItem.exception.message
                }
            }
            #endregion

            #region Create/validate Availability Set
            if ($AvailabilitySetName) {
                Write-Log 'Creating/verifying Availability Set',$AvailabilitySetName Green,Cyan $LogFile
                try {
                    $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -ErrorAction Stop
                    Write-Log 'Availability Set',$AvailabilitySetName,'already exists' Green,Yellow,Green $LogFile
                    Write-Log ($AvailabilitySet | Out-String).Trim() Cyan $LogFile
                } catch {
                    try {
                        $AvailabilitySet = New-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -Location $Location -ErrorAction Stop
                        Write-Log 'Created Availability Set',$AvailabilitySetName Green,Cyan $LogFile
                    } catch {
                        Write-Log 'Failed to create Availability Set',$AvailabilitySetName Magenta,Yellow $LogFile
                        throw $PSItem.exception.message
                    }
                }
                if ($AvailabilitySet.Location -ne $Location) {
                    Write-Log 'Unable to proceed, Availability set must be in the same location',$AvailabilitySet.Location,'as the desired VM location',$Location Magenta,Yellow,Magenta,Yellow $LogFile
                    break
                }
            }
        #endregion

            try {
                $ExistingVM = Get-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -ErrorAction Stop
                Write-Log 'VM',$ExistingVM.Name,'already exists' Green,Yellow,Gree $LogFile
                if ($AvailabilitySetName) {
                    if ($ConfirmShutdown) {
                        Write-Log 'Shutting down VM',$Name,'to add it to Availability set',$AvailabilitySetName Green,Cayn,Green,Cyan $LogFile
                        Stop-AzureRmVM -Name $Name -Force -StayProvisioned -ResourceGroupName $ResourceGroup -Confirm:$false

                        # Remove current VM
                        Remove-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -Force -Confirm:$false

                        # Prepare to recreate VM
                        $VM = New-AzureRmVMConfig -VMName $ExistingVM.Name -VMSize $ExistingVM.HardwareProfile.VmSize -AvailabilitySetId $AvailabilitySet.Id
                        Set-AzureRmVMOSDisk -VM $VM -VhdUri $ExistingVM.StorageProfile.OsDisk.Vhd.Uri -Name $ExistingVM.Name -CreateOption Attach -Windows

                        #Add Data Disks
                        foreach ($Disk in $ExistingVM.StorageProfile.DataDisks) { 
                            Add-AzureRmVMDataDisk -VM $VM -Name $Disk.Name -VhdUri $Disk.Vhd.Uri -Caching $Disk.Caching -Lun $Disk.Lun -CreateOption Attach -DiskSizeInGB $Disk.DiskSizeGB
                        }

                        #Add NIC(s)
                        foreach ($NIC in $ExistingVM.NetworkInterfaceIDs) {
                            Add-AzureRmVMNetworkInterface -VM $VM -Id $NIC
                        }

                        # Recreate the VM as part of the Availability Set
                        New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $ExistingVM.Location -VM $VM -DisableBginfoExtension
                    } else {
                        Write-Log 'To add existing VM(s) to availability set, the VM(s) must be shut down. Use the','-ConfirmShutdown:$true','switch' Yellow,Cyan,Yellow $LogFile
                        break
                    }
                }
            } catch {
                Write-Log 'Preparing to create new VM',$Name Green,Cyan $LogFile

                Write-Log 'Requesting/updating public IP address assignment',"$Name-PublicIP" Green,Cyan $LogFile 
                $PublicIp = New-AzureRmPublicIpAddress -Name "$Name-PublicIP" -ResourceGroupName $ResourceGroup -Location $Location -AllocationMethod Dynamic -Force

                Write-Log 'Provisining/updating vNIC',"$Name-vNIC" Green,Cyan $LogFile
                $vNIC = New-AzureRmNetworkInterface -Name "$Name-vNIC" -ResourceGroupName $ResourceGroup -Location $Location -SubnetId $vNet.Subnets[0].Id -PublicIpAddressId $PublicIp.Id -Force

                Write-Log 'Provisioning VM configuration object for VM',$Name Green,Cyan $LogFile
                if ($AvailabilitySetName) {
                    $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize -AvailabilitySetId $AvailabilitySet.Id
                } else {
                    $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize 
                }

                Write-Log 'Configuring VM OS (Windows),',$Cred.UserName,'local admin' Green,Cyan,Green $LogFile
                $VM = Set-AzureRmVMOperatingSystem -VM $VM -Windows -ComputerName $Name -Credential $Cred -ProvisionVMAgent -EnableAutoUpdate

                Write-Log 'Selecting VM image - Latest',$WinOSImage Green,Cyan $LogFile
                $VM = Set-AzureRmVMSourceImage -VM $VM -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus $WinOSImage -Version "latest"

                Write-Log 'Adding vNIC' Green $LogFile
                $VM = Add-AzureRmVMNetworkInterface -VM $VM -Id $vNIC.Id 

                $VhdUri = "$($StorageAccount.PrimaryEndpoints.Blob.ToString())vhds/$($Name)-OsDisk1.vhd"
                Write-Log 'Configuring OS Disk',$VhdUri Green,Cyan $LogFile
                $VM = Set-AzureRmVMOSDisk -VM $VM -Name 'OSDisk' -VhdUri $VhdUri -CreateOption FromImage

                Write-Log 'Creating VM..' Green -NoNewLine
                New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $Location -VM $VM 
                Write-Log 'done' Green $LogFile
                $DoneVM = Get-AzureRmVM | where { $_.Name -eq $Name } | FT -a 
                Write-Log ($DoneVM | Out-String).Trim() cyan $LogFile
            }
        }
    }

    End {
        if ($AvailabilitySetName) {
            $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName
            $VMDomains = $AvailabilitySet.VirtualMachinesReferences | foreach { 
                $VM = Get-AzureRMVM -Name (Get-AzureRmResource -Id $_.id).Name -ResourceGroup $ResourceGroup -Status
                [PSCustomObject][Ordered]@{
                    Name         = $VM.Name
                    FaultDomain  = $VM.PlatformFaultDomain
                    UpdateDomain = $VM.PlatformUpdateDomain
                }
            }
            Write-Log ($VMDomains | sort Name | FT -a | Out-String).Trim() Cyan $LogFile
        } 
    }

}

function Tag-AzureVM {

# Requires -Modules AzureRM
# Requires -Version 5

<#
 .SYNOPSIS
  Function to apply one or more Azure tags to one or more VM and its related objects
 
 .DESCRIPTION
  Function to apply one or more Azure tags to one or more VM and its related objects including its:
    NICs
    Disks
    Extensions
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER SubscriptionName
  The Azure subscription name such as 'My Dev EA subscription'
 
 .PARAMETER ResourceGroupName
  The name of the Azure Resource Group where the VM(s) reside
 
 .PARAMETER VMName
  The name(s) or one or more VMs
 
 .PARAMETER TagList
  One or more Azure tags to be applied.
  This is a hash table that takes key/value pairs in the format:
    @{
        COMPANY = 'MyCompany'
        OWNER = 'Sam.Boutros'
    }
 
 .EXAMPLE
    $Splatt = @{
        LoginName = 'sam.boutros@mydomain.com'
        SubscriptionName = 'My Enterprise subscription'
        ResourceGroupName = 'My RG1'
        VMName = @(
            'VM01'
            'VM02'
        )
        TagList = @{
            COMPANY = 'my company'
            OWNER = 'Sam.Boutros'
        }
    }
    Tag-AzureVM @Splatt
 
 .OUTPUTS
  None
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 June 2018 - Initial release
  v0.2 - 14 June 2018 - Parameterized, added error handling and documentation
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$true)][String]$ResourceGroupName,
        [Parameter(Mandatory=$true)][String[]]$VMName,
        [Parameter(Mandatory=$false)][HashTable]$TagList = @{
                COMPANY = 'MyCompany'
                OWNER   = 'Sam.Boutros'
            }
    )

    Begin {
        Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud
        try {
            Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 -EA 1 | Set-AzureRmContext | Out-Null
            Write-Log 'Connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan
        } catch {
            Write-Log $PSItem.Exception.Message Magenta
            break
        }
    }

    Process {
        foreach ($VM in $VMName) {
            Write-Log 'Processing VM',$VM Green,Cyan
            try { 
                $VMObj = Get-AzureRmVM -Name $VM -ResourceGroupName $ResourceGroupName -WA 0 -EA 1
                Write-Log 'Found VM',$VM,'in resource group',$ResourceGroupName Green,Cyan,Green,Cyan
            } catch {
                Write-Log 'VM',$VM,'not found in resource group',$ResourceGroupName Magenta,Yellow,Magenta,Yellow
                break
            }

            if ($VMObj) {
                $ObjList = @()
                $ObjList += $VMObj
                # Get VM NIC Objects
                foreach ($NicId in $VMObj.NetworkInterfaceIDs) {
                    $NICObj = Get-AzureRmResource -ResourceId $NicId
                    $ObjList += $NICObj
                    Write-Log ' Identified VM NIC ',$NICObj.Name Green,Cyan
                }

                # Get VM Disk Objects
                foreach ($DiskName in $VMObj.DataDiskNames) {
                    $DiskObj = Get-AzureRmDisk -ResourceGroupName $ResourceGroupName -DiskName $DiskName
                    $ObjList += $DiskObj
                    Write-Log ' Identified VM disk',$DiskObj.Name Green,Cyan
                }

                # Get VM Extension Objects
                foreach ($VMExtension in $VMObj.Extensions) {
                    $VMExtensionObj = Get-AzureRmResource -ResourceId $VMExtension.Id
                    $ObjList += $VMExtensionObj
                    Write-Log ' Identified VM extension',$VMExtensionObj.Name Green,Cyan
                }
            } # Get VM related objects

            foreach ($Resource in $ObjList) {
                # The Microsoft team is being inconsistent the way they make these objects
                # For example, the resource Id is called Id on some objects but called ResourceId on others
                if (! $Resource.ResourceId) {
                    $Resource | Add-Member -MemberType NoteProperty -Name ResourceId -Value $Resource.Id
                }
                if (! $Resource.ResourceType) {
                    $Resource | Add-Member -MemberType NoteProperty -Name ResourceType -Value $Resource.Type
                }

                Write-Log 'Processing resource',$Resource.Name,"($($Resource.ResourceType))" Green,Cyan,Green

                if ($ResourceTagList = (Get-AzureRmResource -ResourceId $Resource.ResourceId).Tags) {
                    $OK2Save = $false
                    foreach ($key in $TagList.Keys) {
                        if (-not($ResourceTagList.ContainsKey($key))) {
                            Write-Log ' Tag',$key,'is not set for resource',$Resource.Name,'setting..' Green,Cyan,Yellow,Cyan,Green
                            $ResourceTagList.Add($key, $TagList[$key])
                            $OK2Save = $true
                        } elseif ($Resource.Tags[$key] -eq $TagList[$key]) {
                            Write-Log ' Tag',$key,'is already set for resource',$Resource.Name,'value:',$Resource.Tags[$key],'skipping..' Green,Cyan,Green,Cyan,Green,Cyan,Green
                        } else {
                            Write-Log ' Tag',$key,'is already set for resource',$Resource.Name,'value:',$Resource.Tags[$key],'updating..' Green,Cyan,Green,Cyan,Green,Yellow,Green
                            $Resource.Tags.$key = $TagList.$key # not working for some reason !?
                            $OK2Save = $true
                        }

                    }        
                    if ($OK2Save) {
                        Write-Log 'saving tags to resource..' DarkYellow -NoNewLine
                        try {
                            Set-AzureRmResource -Tag $ResourceTagList -ResourceId $Resource.ResourceId -Force -EA 1 | Out-Null
                        } catch {
                            Write-Log 'failed' Magenta
                            Write-Log $PSItem.Exception.Message Yellow
                        }
                    }    
                } else {
                    Write-Log ' No tags configured for resource',$Resource.Name,'adding tags',($TagList.Keys -join ',') Green,Cyan,Green,Cyan
                    try {
                        Set-AzureRmResource -Tag $TagList -ResourceId $Resource.ResourceId -Force -EA 1 | Out-Null
                    } catch {
                        Write-Log 'failed' Magenta
                        Write-Log $PSItem.Exception.Message Yellow
                    }
                }
            }

        }
    } 

    End {}
}

function Expand-Json {
<#
 .SYNOPSIS
  Function to expand a custom PowerShell object in a more readable format
 
 .DESCRIPTION
  Function to expand a custom PowerShell object in a more readable format
  The ConvertFrom-Json cmdlet of the Microsoft.PowerShell.Utility module outputs
  a PS Custom Object that often contains sub objects and so on.
  This function expands all objects and displays the key/value pairs in a
  more humanly readable format - see the example
 
 .PARAMETER Json
  PS Custom Object, typically the output of ConvertFrom-Json cmdlet - see the example
 
 .PARAMETER Parent
  This is optional parameter used to show sub-objects when using the function recursively
 
 .EXAMPLE
  Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-Json
  where the contents of Storage1.json file are:
 
    {
      "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "parameters": {
        "storageAccountType": {
          "type": "string",
          "defaultValue": "Standard_LRS",
          "allowedValues": [
            "Standard_LRS",
            "Standard_GRS",
            "Standard_ZRS",
            "Premium_LRS"
          ],
          "metadata": {
            "description": "Storage Account type"
          }
        }
      },
      "variables": {
        "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'standardsa')]"
      },
      "resources": [
        {
          "type": "Microsoft.Storage/storageAccounts",
          "name": "[variables('storageAccountName')]",
          "apiVersion": "2016-01-01",
          "location": "[resourceGroup().location]",
          "sku": {
              "name": "[parameters('storageAccountType')]"
          },
          "kind": "Storage",
          "properties": {
          }
        }
      ],
      "outputs": {
          "storageAccountName": {
              "type": "string",
              "value": "[variables('storageAccountName')]"
          }
      }
    }
   
  The output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json would look like:
 
    $schema : https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#
    contentVersion : 1.0.0.0
    parameters : @{storageAccountType=}
    variables : @{storageAccountName=[concat(uniquestring(resourceGroup().id), 'standardsa')]}
    resources : {@{type=Microsoft.Storage/storageAccounts; name=[variables('storageAccountName')]; apiVersion=2016-01-01; location=[resourceGroup().location]; sku=; kind=Storage; properties=}}
    outputs : @{storageAccountName=}
 
  which does not show sub-objects such as parameters.storageAccountType.allowedValues, parameters.storageAccountType.defaultValue, ...
 
  However, the output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-Json
  shows all objects, sub-objects, and their key/pair values:
 
    $schema: https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#
    contentVersion: 1.0.0.0
    outputs.storageAccountName.type: string
    outputs.storageAccountName.value: [variables('storageAccountName')]
    parameters.storageAccountType.allowedValues: Standard_LRS, Standard_GRS, Standard_ZRS, Premium_LRS
    parameters.storageAccountType.defaultValue: Standard_LRS
    parameters.storageAccountType.metadata.description: Storage Account type
    parameters.storageAccountType.type: string
    resources.apiVersion: 2016-01-01
    resources.kind: Storage
    resources.location: [resourceGroup().location]
    resources.name: [variables('storageAccountName')]
    resources.sku.name: [parameters('storageAccountType')]
    resources.type: Microsoft.Storage/storageAccounts
    variables.storageAccountName: [concat(uniquestring(resourceGroup().id), 'standardsa')]
 
 .LINK
  https://superwidgets.wordpress.com/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 28 March 2018
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)][PSCustomObject]$JSON,
        [Parameter(Mandatory=$false)][String[]]$Parent
    )

    Begin {
        Write-Verbose "JSON: $($JSON | Out-String)"
        Write-Verbose "Parent: $($Parent -join '.')"
    }

    Process {
        foreach ($NoteProperty in ($JSON | Get-Member -MemberType NoteProperty)) {
            if ($NoteProperty.Definition -match 'PSCustomObject') {
                Expand-Json -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name)
            } else {
                if (($JSON.($NoteProperty.Name) -join '').Trim()) {
                    Write-Log "$(($Parent + $NoteProperty.Name) -join '.'):",($JSON.($NoteProperty.Name) -join ', ') Green,Cyan
                } else {
                    Expand-Json -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name) -EA 0
                }
            } 
        }
    }

    End { }
}

function Report-AzureRMVM {

# Requires -Modules AzureRM, ImportExcel
# Requires -Version 5

<#
 .SYNOPSIS
  Function to report on Azure VM population in a given Azure subscription
 
 .DESCRIPTION
  Function to report on Azure VM population in a given Azure subscription
  The report is saved to xlsx file
  This function uses ImportExcel PowerShell module available in the PowerShell gallery
  This function reports on Azure ARM VMs only (not classic ASM VMs)
 
 .PARAMETER LoginName
  The username required to authenticate to Azure
  Example: samb@mydomain.com
 
 .PARAMETER SubscriptionName
  The Azure subscription name such as 'My Dev EA subscription'
 
 .PARAMETER OutputFile
  Path to xlsx file, where the function will write its output
 
 .EXAMPLE
  Report-AzureRMVM -LoginName 'samb@mydomain.com' -SubscriptionName 'my azure subscription name here' -Verbose
 
 .OUTPUTS
  Array of PS Custom objects, one for each ARM VM found with the following properties/example:
    VMName : AZ-abcBDEV-01
    ResourceGroup : AZ-abcEV-RG
    Subscription : abc Enterprise Dev/Test
    Size : Standard_D2s_v3
    Cores : 2
    RAM(GB) : 8
    Location : eastus
    MACAddress : 00-0D-3A-1C-87-11
    IPv4Address : 172.129.132.112
    AdminName : cdabcadmin
    OperatingSystem : Windows
    OSDiskSize(GB) : 127
    DataDisks : (AZ-abcBDEV-01_SQLDATA, 1028 GB, LUN 0), (AZ-abcBDEV-01_SQLLOG, 1028 GB, LUN 1)
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 June 2018 - Initial release
  v0.2 - 14 June 2018 - Parameterized, added error handling and documentation
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$LoginName,
        [Parameter(Mandatory=$true)][String]$SubscriptionName,
        [Parameter(Mandatory=$false)][String]$OutputFile = ".\Report-AzureRMVM-$SubscriptionName.xlsx"
    )

    Begin {
        Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud
        try {
            Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 -EA 1 | Set-AzureRmContext | Out-Null
            Write-Log 'Connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan
        } catch {
            Write-Log $PSItem.Exception.Message Magenta
            break
        }

        if ($OutputFile -match '/' ) { $OutputFile = $OutputFile.Replace('/','_') }
    }

    Process {
        $VMList = Get-AzureRmVM -WA 0 
        $LocationList = $VMList.Location | select -Unique
        $VMTagList = $VMList | % { $_.Tags.Keys } | % { $_.ToString().ToLower().Trim() } | select -Unique | sort
        $ResourceGroupList = $VMList.ResourceGroupName | select -Unique
        Write-Log 'Identified',$VMList.Count,'VMs' Green,Cyan,Green
        Write-Log 'Identified Azure site(s)',($LocationList -join ', ') Green,Cyan
        Write-Log 'Identified',$ResourceGroupList.Count,'Resource Groups' Green,Cyan,Green

        $myVMList = foreach ($VM in $VMList) {

            Write-Verbose "Processing VM ($($VM.Name)) in Resource Group ($($VM.ResourceGroupName))"
            $VMSize = Get-AzureRmVMSize -Location $VM.Location | where { $_.Name -eq $VM.HardWareProfile.VmSize }
            $myOutput = [PSCustomObject][Ordered]@{
                VMName           = $VM.Name
                ResourceGroup    = $VM.ResourceGroupName
                Subscription     = $SubscriptionName
                Size             = $VM.HardWareProfile.VmSize
                Cores            = $VMSize.NumberOfCores              
                'RAM(GB)'        = $VMSize.MemoryInMB/1KB
                Location         = $VM.Location
                MACAddress       = ($VM.NetworkProfile.NetworkInterfaces.id | foreach { (Get-AzureRmResource -ResourceId $_).Properties.MacAddress }) -join ', '
                IPv4Address      = ((Get-AzureRmNetworkInterface -ResourceGroupName $VM.ResourceGroupName | 
                                    where {$PSItem.virtualmachine.id -match $VM.Name } | Get-AzureRmNetworkInterfaceIpConfig).PrivateIPAddress) -join ', '
                AdminName        = $VM.OSProfile.AdminUsername
                OperatingSystem  = $VM.StorageProfile.OsDisk.OsType
                'OSDiskSize(GB)' = $VM.StorageProfile.OsDisk.DiskSizeGB
                DataDisks        = $(
                    if ($VM.StorageProfile.DataDisks) {
                        ($VM.StorageProfile.DataDisks | foreach { "($($_.Name), $($_.DiskSizeGB) GB, LUN $($_.Lun))" }) -join ', '
                    } else {
                        'None'
                    }
                )
            }

            foreach ($TagName in $VMTagList) {
                $myOutput | Add-Member -MemberType NoteProperty -Name "Tag: $TagName" -Value $(
                    $myTagList = $VM.Tags.Keys | foreach { "$_=$($VM.Tags.$_)" }
                    if  ($myTagList | where { $_ -match $TagName }) {
                        ($myTagList | where { $_ -match $TagName }).Split('=')[1]
                    }
                ) 
            }

            $myOutput
        } 
    } 

    End {
        try {
            if (Test-Path $OutputFile) { Remove-Item -Path $OutputFile -Force -Confirm:$false -EA 1 }
            $myVMList | Export-Excel -Path $OutputFile -ConditionalText $(
                ($myVMList | Get-Member -MemberType NoteProperty).Name | foreach { New-ConditionalText $_ White SteelBlue }
                New-ConditionalText 'Basic' Black Gold
            ) -AutoSize -FreezeTopRowFirstColumn 
        } catch {
            Write-Log 'Output file',$OutputFile,'already open!!??' Magenta,Yellow,Magenta
        }

        $myVMList
    }
}

#endregion

#region Core functions

function Write-Log {
<#
 .SYNOPSIS
  Function to log input string to file and display it to screen
 
 .DESCRIPTION
  Function to log input string to file and display it to screen.
  Log entries in the log file are time stamped.
  Function allows for displaying text to screen in different colors.
 
 .PARAMETER String
  The string to be displayed to the screen and saved to the log file
 
 .PARAMETER Color
  The color in which to display the input string on the screen
  Default is White
  16 valid options for [System.ConsoleColor] type are
    Black
    Blue
    Cyan
    DarkBlue
    DarkCyan
    DarkGray
    DarkGreen
    DarkMagenta
    DarkRed
    DarkYellow
    Gray
    Green
    Magenta
    Red
    White
    Yellow
 
 .PARAMETER LogFile
  Path to the file where the input string should be saved.
  Example: c:\log.txt
  If absent, the input string will be displayed to the screen only and not saved to log file
 
 .EXAMPLE
  Write-Log -String "Hello World" -Color Yellow -LogFile c:\log.txt
  This example displays the "Hello World" string to the console in yellow, and adds it as a new line to the file c:\log.txt
  If c:\log.txt does not exist it will be created.
  Log entries in the log file are time stamped. Sample output:
    2014.08.06 06:52:17 AM: Hello World
 
 .EXAMPLE
  Write-Log "$((Get-Location).Path)" Cyan
  This example displays current path in Cyan, and does not log the displayed text to log file.
 
 .EXAMPLE
  "$((Get-Process | select -First 1).name) process ID is $((Get-Process | select -First 1).id)" | Write-Log -color DarkYellow
  Sample output of this example:
    "MDM process ID is 4492" in dark yellow
 
 .EXAMPLE
  Write-Log 'Found',(Get-ChildItem -Path .\ -File).Count,'files in folder',(Get-Item .\).FullName Green,Yellow,Green,Cyan .\mylog.txt
  Sample output will look like:
    Found 520 files in folder D:\Sandbox - and will have the listed foreground colors
 
 .EXAMPLE
  Write-Log (Get-Volume | sort DriveLetter | Out-String).Trim() Cyan .\mylog.txt
  Sample output will look like (in Cyan, and will also be written to .\mylog.txt):
    DriveLetter FriendlyName FileSystemType DriveType HealthStatus OperationalStatus SizeRemaining Size
    ----------- ------------ -------------- --------- ------------ ----------------- ------------- ----
                Recovery NTFS Fixed Healthy OK 101.98 MB 450 MB
    C NTFS Fixed Healthy OK 7.23 GB 39.45 GB
    D Unknown CD-ROM Healthy Unknown 0 B 0 B
    E Data NTFS Fixed Healthy OK 26.13 GB 49.87 GB
 
 .LINK
  https://superwidgets.wordpress.com/2014/12/01/powershell-script-function-to-display-text-to-the-console-in-several-colors-and-save-it-to-log-with-timedate-stamp/
 
 .NOTES
  Function by Sam Boutros
  v1.0 - 6 August 2014
  v1.1 - 1 December 2014 - added multi-color display in the same line
  v1.2 - 8 August 2016 - updated date time stamp format, protect against bad LogFile name
  v1.3 - 22 September 2017 - Re-write: Error handling for no -String parameter, bad color(s), and bad -LogFile without errors
                                        Add Verbose messages
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$false,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [String[]]$String, 
        [Parameter(Mandatory=$false,Position=1)][String[]]$Color, 
        [Parameter(Mandatory=$false,Position=2)][String]$LogFile,
        [Parameter(Mandatory=$false,Position=3)][Switch]$NoNewLine
    )

    if ($String) {
        $ConsoleColor = @('Black','Blue','Cyan','DarkBlue','DarkCyan','DarkGray','DarkGreen','DarkMagenta',
                          'DarkRed','DarkYellow','Gray','Green','Magenta','Red','White','Yellow')
        $ColorList = $Color | % { if ($_ -in $ConsoleColor) { $_ } else { 'White' } }
        if (!$ColorList) { $ColorList = 'Green' }

        if ($String.Count -gt 1) {
            $i=0
            foreach ($item in $String) { 
                if ($ColorList.Count -gt 1) {
                    if ($ColorList[$i]) { $col = $ColorList[$i] } else { $col = 'White' }
                } else {
                    if ($i -eq 0) { $col = $ColorList } else { $col = 'White' }
                }
                Write-Host "$item " -ForegroundColor $col -NoNewline
                $i++
            }
        } else { # 1 String
            if ($ColorList.Count -gt 1) { $col = $ColorList[0] } else { $col = $ColorList }
            Write-Host "$String " -ForegroundColor $col -NoNewline
        }

        if (!$NoNewLine) { Write-Host ' ' }

        try {
            "$(Get-Date -format 'dd MMMM yyyy hh:mm:ss tt'): $($String -join ' ')" | Out-File -Filepath $Logfile -Append -ErrorAction Stop
        } catch {
            Write-Verbose 'Write-Log: Missing -LogFile parameter or bad LogFile name. Will not save input string(s) to log file..'
        }
    } else {
        Write-Verbose 'Write-Log: Missing -String parameter - nothing to write or log..'
    }
}

function Get-SBCredential {
<#
 .SYNOPSIS
  Function to get AD credential, save encrypted password to file for future automation
 
 .DESCRIPTION
  Function to get AD credential, save encrypted password to file for future automation
  The function will use saved password if the password file exists
  The function will prompt for the password if the password file does not exist,
    or the -Refresh switch is used
  Note that the function does not validate whether the UserName exists in any directory,
  or that the password entered is valid. It merely creates a Credential object to be used
  securely for future automation, eleminating the need to type in the password everytime
  the function is needed, or the need to type in password in clear text in scripts.
 
 .PARAMETER UserName
  This can be in the format 'myusername' or 'domain\username'
  If not provided, the function assumes username under which the function is executed
   
 .PARAMETER Refresh
  This switch will force the function to prompt for the password and over-write the password file
 
 .OUTPUTS
  The function returns a PSCredential object that can be used with other cmdlets that use the -Credential parameter
 
 .EXAMPLE
  $MyCred = Get-SBCredential
 
 .EXAMPLE
  $Cred2 = Get-SBCredential -UserName 'sboutros' -Verbose -Refresh
 
 .EXAMPLE
  $Cred3 = 'domain2\ADSuperUser' | Get-SBCredential
  Disable-ADAccount -Identity 'Someone' -Server 'MyDomainController' -Credential $Cred3
  This example obtains and saves credential of 'domain2\ADSuperUser' in $Cred3 varialble
  Second line uses that credential to disable an AD account of 'Someone'
 
 .NOTES
  Sam Boutros
  5 August 2016 - v1.0
  For more information see
  https://superwidgets.wordpress.com/2016/08/05/powershell-script-to-provide-a-ps-credential-object-saving-password-securely/
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$false,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [String]$UserName = "$env:USERDOMAIN\$env:USERNAME", 
        [Parameter(Mandatory=$false,Position=1)][Switch]$Refresh = $false 
    )

    $CredPath = "$env:Temp\$($UserName.Replace('\','_')).txt"
    if ($Refresh) { Remove-Item -Path $CredPath -Force -Confirm:$false -ErrorAction SilentlyContinue } 
    if (!(Test-Path -Path $CredPath)) {
        Read-Host "Enter the pwd for $UserName" -AsSecureString | ConvertFrom-SecureString | Out-File $CredPath            
    }
    $Pwd = Get-Content $CredPath | ConvertTo-SecureString 
    New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $UserName, $Pwd 
}

function ConvertTo-EnhancedHTML {
<#
.SYNOPSIS
Provides an enhanced version of the ConvertTo-HTML command that includes
inserting an embedded CSS style sheet, JQuery, and JQuery Data Tables for
interactivity. Intended to be used with HTML fragments that are produced
by ConvertTo-EnhancedHTMLFragment. This command does not accept pipeline
input.
 
.PARAMETER jQueryURI
A Uniform Resource Indicator (URI) pointing to the location of the
jQuery script file. You can download jQuery from www.jquery.com; you should
host the script file on a local intranet Web server and provide a URI
that starts with http:// or https://. Alternately, you can also provide
a file system path to the script file, although this may create security
issues for the Web browser in some configurations.
 
Tested with v1.8.2.
 
Defaults to http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js, which
will pull the file from Microsoft's ASP.NET Content Delivery Network.
 
.PARAMETER jQueryDataTableURI
A Uniform Resource Indicator (URI) pointing to the location of the
jQuery Data Table script file. You can download this from www.datatables.net;
you should host the script file on a local intranet Web server and provide a URI
that starts with http:// or https://. Alternately, you can also provide
a file system path to the script file, although this may create security
issues for the Web browser in some configurations.
 
Tested with jQuery DataTable v1.9.4
 
Defaults to http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.3/jquery.dataTables.min.js,
which will pull the file from Microsoft's ASP.NET Content Delivery Network.
 
.PARAMETER CssStyleSheet
The CSS style sheet content - not a file name. If you have a CSS file,
you can load it into this parameter as follows:
 
    -CSSStyleSheet (Get-Content MyCSSFile.css)
 
Alternately, you may link to a Web server-hosted CSS file by using the
-CssUri parameter.
 
.PARAMETER CssUri
A Uniform Resource Indicator (URI) to a Web server-hosted CSS file.
Must start with either http:// or https://. If you omit this, you
can still provide an embedded style sheet, which makes the resulting
HTML page more standalone. To provide an embedded style sheet, use
the -CSSStyleSheet parameter.
 
.PARAMETER Title
A plain-text title that will be displayed in the Web browser's window
title bar. Note that not all browsers will display this.
 
.PARAMETER PreContent
Raw HTML to insert before all HTML fragments. Use this to specify a main
title for the report:
 
    -PreContent "<H1>My HTML Report</H1>"
 
.PARAMETER PostContent
Raw HTML to insert after all HTML fragments. Use this to specify a
report footer:
 
    -PostContent "Created on $(Get-Date)"
 
.PARAMETER HTMLFragments
One or more HTML fragments, as produced by ConvertTo-EnhancedHTMLFragment.
 
    -HTMLFragments $part1,$part2,$part3
.EXAMPLE
The following is a complete example script showing how to use
ConvertTo-EnhancedHTMLFragment and ConvertTo-EnhancedHTML. The
example queries 6 pieces of information from the local computer
and produces a report in C:\. This example uses most of the
avaiable options. It relies on Internet connectivity to retrieve
JavaScript from Microsoft's Content Delivery Network. This
example uses an embedded stylesheet, which is defined as a here-string
at the top of the script.
 
$computername = 'localhost'
$path = 'c:\'
$style = @"
<style>
body {
    color:#333333;
    font-family:Calibri,Tahoma;
    font-size: 10pt;
}
h1 {
    text-align:center;
}
h2 {
    border-top:1px solid #666666;
}
 
 
th {
    font-weight:bold;
    color:#eeeeee;
    background-color:#333333;
    cursor:pointer;
}
.odd { background-color:#ffffff; }
.even { background-color:#dddddd; }
.paginate_enabled_next, .paginate_enabled_previous {
    cursor:pointer;
    border:1px solid #222222;
    background-color:#dddddd;
    padding:2px;
    margin:4px;
    border-radius:2px;
}
.paginate_disabled_previous, .paginate_disabled_next {
    color:#666666;
    cursor:pointer;
    background-color:#dddddd;
    padding:2px;
    margin:4px;
    border-radius:2px;
}
.dataTables_info { margin-bottom:4px; }
.sectionheader { cursor:pointer; }
.sectionheader:hover { color:red; }
.grid { width:100% }
.red {
    color:red;
    font-weight:bold;
}
</style>
"@
 
function Get-InfoOS {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)][string]$ComputerName
    )
    $os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName
    $props = @{'OSVersion'=$os.version;
               'SPVersion'=$os.servicepackmajorversion;
               'OSBuild'=$os.buildnumber}
    New-Object -TypeName PSObject -Property $props
}
 
function Get-InfoCompSystem {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)][string]$ComputerName
    )
    $cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName
    $props = @{'Model'=$cs.model;
               'Manufacturer'=$cs.manufacturer;
               'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB);
               'Sockets'=$cs.numberofprocessors;
               'Cores'=$cs.numberoflogicalprocessors}
    New-Object -TypeName PSObject -Property $props
}
 
function Get-InfoBadService {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)][string]$ComputerName
    )
    $svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName `
           -Filter "StartMode='Auto' AND State<>'Running'"
    foreach ($svc in $svcs) {
        $props = @{'ServiceName'=$svc.name;
                   'LogonAccount'=$svc.startname;
                   'DisplayName'=$svc.displayname}
        New-Object -TypeName PSObject -Property $props
    }
}
 
function Get-InfoProc {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)][string]$ComputerName
    )
    $procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName
    foreach ($proc in $procs) {
        $props = @{'ProcName'=$proc.name;
                   'Executable'=$proc.ExecutablePath}
        New-Object -TypeName PSObject -Property $props
    }
}
 
function Get-InfoNIC {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)][string]$ComputerName
    )
    $nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName `
           -Filter "PhysicalAdapter=True"
    foreach ($nic in $nics) {
        $props = @{'NICName'=$nic.servicename;
                   'Speed'=$nic.speed / 1MB -as [int];
                   'Manufacturer'=$nic.manufacturer;
                   'MACAddress'=$nic.macaddress}
        New-Object -TypeName PSObject -Property $props
    }
}
 
function Get-InfoDisk {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True)][string]$ComputerName
    )
    $drives = Get-WmiObject -class Win32_LogicalDisk -ComputerName $ComputerName `
           -Filter "DriveType=3"
    foreach ($drive in $drives) {
        $props = @{'Drive'=$drive.DeviceID;
                   'Size'=$drive.size / 1GB -as [int];
                   'Free'="{0:N2}" -f ($drive.freespace / 1GB);
                   'FreePct'=$drive.freespace / $drive.size * 100 -as [int]}
        New-Object -TypeName PSObject -Property $props
    }
}
 
foreach ($computer in $computername) {
    try {
        $everything_ok = $true
        Write-Verbose "Checking connectivity to $computer"
        Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null
    } catch {
        Write-Warning "$computer failed"
        $everything_ok = $false
    }
 
    if ($everything_ok) {
        $filepath = Join-Path -Path $Path -ChildPath "$computer.html"
 
        $params = @{'As'='List';
                    'PreContent'='<h2>OS</h2>'}
        $html_os = Get-InfoOS -ComputerName $computer |
                   ConvertTo-EnhancedHTMLFragment @params
 
        $params = @{'As'='List';
                    'PreContent'='<h2>Computer System</h2>'}
        $html_cs = Get-InfoCompSystem -ComputerName $computer |
                   ConvertTo-EnhancedHTMLFragment @params
 
        $params = @{'As'='Table';
                    'PreContent'='<h2>&diams; Local Disks</h2>';
                    'EvenRowCssClass'='even';
                    'OddRowCssClass'='odd';
                    'MakeTableDynamic'=$true;
                    'TableCssClass'='grid';
                    'Properties'='Drive',
                                 @{n='Size(GB)';e={$_.Size}},
                                 @{n='Free(GB)';e={$_.Free};css={if ($_.FreePct -lt 80) { 'red' }}},
                                 @{n='Free(%)';e={$_.FreePct};css={if ($_.FreeePct -lt 80) { 'red' }}}}
        $html_dr = Get-InfoDisk -ComputerName $computer |
                   ConvertTo-EnhancedHTMLFragment @params
 
        $params = @{'As'='Table';
                    'PreContent'='<h2>&diams; Processes</h2>';
                    'MakeTableDynamic'=$true;
                    'TableCssClass'='grid'}
        $html_pr = Get-InfoProc -ComputerName $computer |
                   ConvertTo-EnhancedHTMLFragment @params
 
        $params = @{'As'='Table';
                    'PreContent'='<h2>&diams; Services to Check</h2>';
                    'EvenRowCssClass'='even';
                    'OddRowCssClass'='odd';
                    'MakeHiddenSection'=$true;
                    'TableCssClass'='grid'}
        $html_sv = Get-InfoBadService -ComputerName $computer |
                   ConvertTo-EnhancedHTMLFragment @params
 
        $params = @{'As'='Table';
                    'PreContent'='<h2>&diams; NICs</h2>';
                    'EvenRowCssClass'='even';
                    'OddRowCssClass'='odd';
                    'MakeHiddenSection'=$true;
                    'TableCssClass'='grid'}
        $html_na = Get-InfoNIC -ComputerName $Computer |
                   ConvertTo-EnhancedHTMLFragment @params
 
        $params = @{'CssStyleSheet'=$style;
                    'Title'="System Report for $computer";
                    'PreContent'="<h1>System Report for $computer</h1>";
                    'HTMLFragments'=@($html_os,$html_cs,$html_dr,$html_pr,$html_sv,$html_na)}
        ConvertTo-EnhancedHTML @params |
        Out-File -FilePath $filepath
    }
}
 
 .Notes
  Function by Don Jones
  Generated on: 9/10/2013
  For more information see Powershell.org
  included in AZSBTools module with permission by Don Jones
   
#>

    [CmdletBinding()]
    param(
        [string]$jQueryURI = 'http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js',
        [string]$jQueryDataTableURI = 'http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.3/jquery.dataTables.min.js',
        [Parameter(ParameterSetName='CSSContent')][string[]]$CssStyleSheet,
        [Parameter(ParameterSetName='CSSURI')][string[]]$CssUri,
        [string]$Title = 'Report',
        [string]$PreContent,
        [string]$PostContent,
        [Parameter(Mandatory=$True)][string[]]$HTMLFragments
    )


    <#
        Add CSS style sheet. If provided in -CssUri, add a <link> element.
        If provided in -CssStyleSheet, embed in the <head> section.
        Note that BOTH may be supplied - this is legitimate in HTML.
    #>

    Write-Verbose "Making CSS style sheet"
    $stylesheet = ""
    if ($PSBoundParameters.ContainsKey('CssUri')) {
        $stylesheet = "<link rel=`"stylesheet`" href=`"$CssUri`" type=`"text/css`" />"
    }
    if ($PSBoundParameters.ContainsKey('CssStyleSheet')) {
        $stylesheet = "<style>$CssStyleSheet</style>" | Out-String
    }


    <#
        Create the HTML tags for the page title, and for
        our main javascripts.
    #>

    Write-Verbose "Creating <TITLE> and <SCRIPT> tags"
    $titletag = ""
    if ($PSBoundParameters.ContainsKey('title')) {
        $titletag = "<title>$title</title>"
    }
    $script += "<script type=`"text/javascript`" src=`"$jQueryURI`"></script>`n<script type=`"text/javascript`" src=`"$jQueryDataTableURI`"></script>"


    <#
        Render supplied HTML fragments as one giant string
    #>

    Write-Verbose "Combining HTML fragments"
    $body = $HTMLFragments | Out-String


    <#
        If supplied, add pre- and post-content strings
    #>

    Write-Verbose "Adding Pre and Post content"
    if ($PSBoundParameters.ContainsKey('precontent')) {
        $body = "$PreContent`n$body"
    }
    if ($PSBoundParameters.ContainsKey('postcontent')) {
        $body = "$body`n$PostContent"
    }


    <#
        Add a final script that calls the datatable code
        We dynamic-ize all tables with the .enhancedhtml-dynamic-table
        class, which is added by ConvertTo-EnhancedHTMLFragment.
    #>

    Write-Verbose "Adding interactivity calls"
    $datatable = ""
    $datatable = "<script type=`"text/javascript`">"
    $datatable += '$(document).ready(function () {'
    $datatable += "`$('.enhancedhtml-dynamic-table').dataTable();"
    $datatable += '} );'
    $datatable += "</script>"


    <#
        Datatables expect a <thead> section containing the
        table header row; ConvertTo-HTML doesn't produce that
        so we have to fix it.
    #>

    Write-Verbose "Fixing table HTML"
    $body = $body -replace '<tr><th>','<thead><tr><th>'
    $body = $body -replace '</th></tr>','</th></tr></thead>'


    <#
        Produce the final HTML. We've more or less hand-made
        the <head> amd <body> sections, but we let ConvertTo-HTML
        produce the other bits of the page.
    #>

    Write-Verbose "Producing final HTML"
    ConvertTo-HTML -Head "$stylesheet`n$titletag`n$script`n$datatable" -Body $body  
    Write-Debug "Finished producing final HTML"


}

function ConvertTo-EnhancedHTMLFragment {
<#
.SYNOPSIS
Creates an HTML fragment (much like ConvertTo-HTML with the -Fragment switch
that includes CSS class names for table rows, CSS class and ID names for the
table, and wraps the table in a <DIV> tag that has a CSS class and ID name.
 
.PARAMETER InputObject
The object to be converted to HTML. You cannot select properties using this
command; precede this command with Select-Object if you need a subset of
the objects' properties.
 
.PARAMETER EvenRowCssClass
The CSS class name applied to even-numbered <TR> tags. Optional, but if you
use it you must also include -OddRowCssClass.
 
.PARAMETER OddRowCssClass
The CSS class name applied to odd-numbered <TR> tags. Optional, but if you
use it you must also include -EvenRowCssClass.
 
.PARAMETER TableCssID
Optional. The CSS ID name applied to the <TABLE> tag.
 
.PARAMETER DivCssID
Optional. The CSS ID name applied to the <DIV> tag which is wrapped around the table.
 
.PARAMETER TableCssClass
Optional. The CSS class name to apply to the <TABLE> tag.
 
.PARAMETER DivCssClass
Optional. The CSS class name to apply to the wrapping <DIV> tag.
 
.PARAMETER As
Must be 'List' or 'Table.' Defaults to Table. Actually produces an HTML
table either way; with Table the output is a grid-like display. With
List the output is a two-column table with properties in the left column
and values in the right column.
 
.PARAMETER Properties
A comma-separated list of properties to include in the HTML fragment.
This can be * (which is the default) to include all properties of the
piped-in object(s). In addition to property names, you can also use a
hashtable similar to that used with Select-Object. For example:
 
 Get-Process | ConvertTo-EnhancedHTMLFragment -As Table `
               -Properties Name,ID,@{n='VM';
                                     e={$_.VM};
                                     css={if ($_.VM -gt 100) { 'red' }
                                          else { 'green' }}}
 
This will create table cell rows with the calculated CSS class names.
E.g., for a process with a VM greater than 100, you'd get:
 
  <TD class="red">475858</TD>
   
You can use this feature to specify a CSS class for each table cell
based upon the contents of that cell. Valid keys in the hashtable are:
 
  n, name, l, or label: The table column header
  e or expression: The table cell contents
  css or csslcass: The CSS class name to apply to the <TD> tag
   
Another example:
 
  @{n='Free(MB)';
    e={$_.FreeSpace / 1MB -as [int]};
    css={ if ($_.FreeSpace -lt 100) { 'red' } else { 'blue' }}
     
This example creates a column titled "Free(MB)". It will contain
the input object's FreeSpace property, divided by 1MB and cast
as a whole number (integer). If the value is less than 100, the
table cell will be given the CSS class "red." If not, the table
cell will be given the CSS class "blue." The supplied cascading
style sheet must define ".red" and ".blue" for those to have any
effect.
 
.PARAMETER PreContent
Raw HTML content to be placed before the wrapping <DIV> tag.
For example:
 
    -PreContent "<h2>Section A</h2>"
 
.PARAMETER PostContent
Raw HTML content to be placed after the wrapping <DIV> tag.
For example:
 
    -PostContent "<hr />"
 
.PARAMETER MakeHiddenSection
Used in conjunction with -PreContent. Adding this switch, which
needs no value, turns your -PreContent into clickable report
section header. The section will be hidden by default, and clicking
the header will toggle its visibility.
 
When using this parameter, consider adding a symbol to your -PreContent
that helps indicate this is an expandable section. For example:
 
    -PreContent '<h2>&diams; My Section</h2>'
 
If you use -MakeHiddenSection, you MUST provide -PreContent also, or
the hidden section will not have a section header and will not be
visible.
 
.PARAMETER MakeTableDynamic
When using "-As Table", makes the table dynamic. Will be ignored
if you use "-As List". Dynamic tables are sortable, searchable, and
are paginated.
 
You should not use even/odd styling with tables that are made
dynamic. Dynamic tables automatically have their own even/odd
styling. You can apply CSS classes named ".odd" and ".even" in
your CSS to style the even/odd in a dynamic table.
 
.EXAMPLE
 $fragment = Get-WmiObject -Class Win32_LogicalDisk |
             Select-Object -Property PSComputerName,DeviceID,FreeSpace,Size |
             ConvertTo-HTMLFragment -EvenRowClass 'even' `
                                    -OddRowClass 'odd' `
                                    -PreContent '<h2>Disk Report</h2>' `
                                    -MakeHiddenSection `
                                    -MakeTableDynamic
 
 You will usually save fragments to a variable, so that multiple fragments
 (each in its own variable) can be passed to ConvertTo-EnhancedHTML.
.NOTES
Consider adding the following to your CSS when using dynamic tables:
 
    .paginate_enabled_next, .paginate_enabled_previous {
        cursor:pointer;
        border:1px solid #222222;
        background-color:#dddddd;
        padding:2px;
        margin:4px;
        border-radius:2px;
    }
    .paginate_disabled_previous, .paginate_disabled_next {
        color:#666666;
        cursor:pointer;
        background-color:#dddddd;
        padding:2px;
        margin:4px;
        border-radius:2px;
    }
    .dataTables_info { margin-bottom:4px; }
 
This applies appropriate coloring to the next/previous buttons,
and applies a small amount of space after the dynamic table.
 
If you choose to make sections hidden (meaning they can be shown
and hidden by clicking on the section header), consider adding
the following to your CSS:
 
    .sectionheader { cursor:pointer; }
    .sectionheader:hover { color:red; }
 
This will apply a hover-over color, and change the cursor icon,
to help visually indicate that the section can be toggled.
 
 .Notes
  Function by Don Jones
  Generated on: 9/10/2013
  For more information see Powershell.org
  included in AZSBTools module with permission by Don Jones
 
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$True,ValueFromPipeline=$True)]
        [object[]]$InputObject,
        [string]$EvenRowCssClass,
        [string]$OddRowCssClass,
        [string]$TableCssID,
        [string]$DivCssID,
        [string]$DivCssClass,
        [string]$TableCssClass,
        [ValidateSet('List','Table')]
        [string]$As = 'Table',
        [object[]]$Properties = '*',
        [string]$PreContent,
        [switch]$MakeHiddenSection,
        [switch]$MakeTableDynamic,
        [string]$PostContent
    )
    BEGIN {
        <#
            Accumulate output in a variable so that we don't
            produce an array of strings to the pipeline, but
            instead produce a single string.
        #>

        $out = ''
        <#
            Add the section header (pre-content). If asked to
            make this section of the report hidden, set the
            appropriate code on the section header to toggle
            the underlying table. Note that we generate a GUID
            to use as an additional ID on the <div>, so that
            we can uniquely refer to it without relying on the
            user supplying us with a unique ID.
        #>

        Write-Verbose "Precontent"
        if ($PSBoundParameters.ContainsKey('PreContent')) {
            if ($PSBoundParameters.ContainsKey('MakeHiddenSection')) {
               [string]$tempid = [System.Guid]::NewGuid()
               $out += "<span class=`"sectionheader`" onclick=`"`$('#$tempid').toggle(500);`">$PreContent</span>`n"
            } else {
                $out += $PreContent
                $tempid = ''
            }
        }
        <#
            The table will be wrapped in a <div> tag for styling
            purposes. Note that THIS, not the table per se, is what
            we hide for -MakeHiddenSection. So we will hide the section
            if asked to do so.
        #>

        Write-Verbose "DIV"
        if ($PSBoundParameters.ContainsKey('DivCSSClass')) {
            $temp = " class=`"$DivCSSClass`""
        } else {
            $temp = ""
        }
        if ($PSBoundParameters.ContainsKey('MakeHiddenSection')) {
            $temp += " id=`"$tempid`" style=`"display:none;`""
        } else {
            $tempid = ''
        }
        if ($PSBoundParameters.ContainsKey('DivCSSID')) {
            $temp += " id=`"$DivCSSID`""
        }
        $out += "<div $temp>"
        <#
            Create the table header. If asked to make the table dynamic,
            we add the CSS style that ConvertTo-EnhancedHTML will look for
            to dynamic-ize tables.
        #>

        Write-Verbose "TABLE"
        $_TableCssClass = ''
        if ($PSBoundParameters.ContainsKey('MakeTableDynamic') -and $As -eq 'Table') {
            $_TableCssClass += 'enhancedhtml-dynamic-table '
        }
        if ($PSBoundParameters.ContainsKey('TableCssClass')) {
            $_TableCssClass += $TableCssClass
        }
        if ($_TableCssClass -ne '') {
            $css = "class=`"$_TableCSSClass`""
        } else {
            $css = ""
        }
        if ($PSBoundParameters.ContainsKey('TableCSSID')) {
            $css += "id=`"$TableCSSID`""
        } else {
            if ($tempid -ne '') {
                $css += "id=`"$tempid`""
            }
        }
        $out += "<table $css>"
        <#
            We're now setting up to run through our input objects
            and create the table rows
        #>

        $fragment = ''
        $wrote_first_line = $false
        $even_row = $false

        if ($properties -eq '*') {
            $all_properties = $true
        } else {
            $all_properties = $false
        }

    }
    PROCESS {

        foreach ($object in $inputobject) {
            Write-Verbose "Processing object"
            $datarow = ''
            $headerrow = ''

            <#
                Apply even/odd row class. Note that this will mess up the output
                if the table is made dynamic. That's noted in the help.
            #>

            if ($PSBoundParameters.ContainsKey('EvenRowCSSClass') -and $PSBoundParameters.ContainsKey('OddRowCssClass')) {
                if ($even_row) {
                    $row_css = $OddRowCSSClass
                    $even_row = $false
                    Write-Verbose "Even row"
                } else {
                    $row_css = $EvenRowCSSClass
                    $even_row = $true
                    Write-Verbose "Odd row"
                }
            } else {
                $row_css = ''
                Write-Verbose "No row CSS class"
            }


            <#
                If asked to include all object properties, get them.
            #>

            if ($all_properties) {
                $properties = $object | Get-Member -MemberType Properties | Select -ExpandProperty Name
            }


            <#
                We either have a list of all properties, or a hashtable of
                properties to play with. Process the list.
            #>

            foreach ($prop in $properties) {
                Write-Verbose "Processing property"
                $name = $null
                $value = $null
                $cell_css = ''


                <#
                    $prop is a simple string if we are doing "all properties,"
                    otherwise it is a hashtable. If it's a string, then we
                    can easily get the name (it's the string) and the value.
                #>

                if ($prop -is [string]) {
                    Write-Verbose "Property $prop"
                    $name = $Prop
                    $value = $object.($prop)
                } elseif ($prop -is [hashtable]) {
                    Write-Verbose "Property hashtable"
                    <#
                        For key "css" or "cssclass," execute the supplied script block.
                        It's expected to output a class name; we embed that in the "class"
                        attribute later.
                    #>

                    if ($prop.ContainsKey('cssclass')) { $cell_css = $Object | ForEach $prop['cssclass'] }
                    if ($prop.ContainsKey('css')) { $cell_css = $Object | ForEach $prop['css'] }


                    <#
                        Get the current property name.
                    #>

                    if ($prop.ContainsKey('n')) { $name = $prop['n'] }
                    if ($prop.ContainsKey('name')) { $name = $prop['name'] }
                    if ($prop.ContainsKey('label')) { $name = $prop['label'] }
                    if ($prop.ContainsKey('l')) { $name = $prop['l'] }


                    <#
                        Execute the "expression" or "e" key to get the value of the property.
                    #>

                    if ($prop.ContainsKey('e')) { $value = $Object | ForEach $prop['e'] }
                    if ($prop.ContainsKey('expression')) { $value = $tObject | ForEach $prop['expression'] }


                    <#
                        Make sure we have a name and a value at this point.
                    #>

                    if ($name -eq $null -or $value -eq $null) {
                        Write-Error "Hashtable missing Name and/or Expression key"
                    }
                } else {
                    <#
                        We got a property list that wasn't strings and
                        wasn't hashtables. Bad input.
                    #>

                    Write-Warning "Unhandled property $prop"
                }


                <#
                    When constructing a table, we have to remember the
                    property names so that we can build the table header.
                    In a list, it's easier - we output the property name
                    and the value at the same time, since they both live
                    on the same row of the output.
                #>

                if ($As -eq 'table') {
                    Write-Verbose "Adding $name to header and $value to row"
                    $headerrow += "<th>$name</th>"
                    $datarow += "<td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$value</td>"
                } else {
                    $wrote_first_line = $true
                    $headerrow = ""
                    $datarow = "<td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$name :</td><td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$value</td>"
                    $out += "<tr$(if ($row_css -ne '') { ' class="'+$row_css+'"' })>$datarow</tr>"
                }
            }


            <#
                Write the table header, if we're doing a table.
            #>

            if (-not $wrote_first_line -and $as -eq 'Table') {
                Write-Verbose "Writing header row"
                $out += "<tr>$headerrow</tr><tbody>"
                $wrote_first_line = $true
            }


            <#
                In table mode, write the data row.
            #>

            if ($as -eq 'table') {
                Write-Verbose "Writing data row"
                $out += "<tr$(if ($row_css -ne '') { ' class="'+$row_css+'"' })>$datarow</tr>"
            }
        }
    }
    END {
        <#
            Finally, post-content code, the end of the table,
            the end of the <div>, and write the final string.
        #>

        Write-Verbose "PostContent"
        if ($PSBoundParameters.ContainsKey('PostContent')) {
            $out += "`n$PostContent"
        }
        Write-Verbose "Done"
        $out += "</tbody></table></div>"
        Write-Output $out
    }
}

Function Get-SBWMI {
<#
 .SYNOPSIS
  Function query WMI with Timeout
 
 .DESCRIPTION
  Function query WMI with Timeout
 
 .PARAMETER Class
  Class name such as 'Win32_computerSystem'
 
 .PARAMETER Property
  Property name such as 'NumberofLogicalProcessors'
 
 .PARAMETER Filter
  In the format Property=Value such as DriveLetter=G:
 
 .PARAMETER ComputerName
  Computer name
 
 .PARAMETER NameSpace
  Default is 'root\cimv2'
  To see name spaces type:
    (Get-WmiObject -Namespace 'root' -Class '__Namespace').Name
 
 .PARAMETER Cred
  PS Credential object
 
 .PARAMETER TimeOut
  In seconds
 
 .EXAMPLE
  Get-SBWMI -Class Win32_computerSystem -Property NumberofLogicalProcessors
 
 .EXAMPLE
  Get-SBWMI -Class Win32_Volume -Filter 'DriveType=3'
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 20 September 2017
  v0.2 - 29 September 2017 - Added parameter to use a different credential other than the one running the script
                             Added error checking for failure to WMI connect
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)][string]$Class,
        [Parameter(Mandatory=$false)][String[]]$Property = '*',
        [Parameter(Mandatory=$false)][String]$Filter,
        [Parameter(Mandatory=$false)][String]$ComputerName = $env:COMPUTERNAME,
        [Parameter(Mandatory=$false)][String]$NameSpace = 'root\cimv2',
        [Parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Cred = (Get-SBCredential -UserName "$env:USERDOMAIN\$env:USERNAME"),
        [Parameter(Mandatory=$false)][int]$TimeOut=20
    )

    Begin {
        if ($Filter) {
            if ($Filter -match '=') {
                $FilterProperty = $Filter.Split('=')[0].Trim()
                $FilterValue    = $Filter.Split('=')[1].Trim()
            } else {
                Write-Log 'Get-SBWMI Input Error:','Filter',', supported syntax is','Property=Value','such as','DriveLetter=G' Magenta,Yellow,Magenta,Yellow,Magenta,Yellow
                Write-Log ' ignoring filter',$Filter Magenta,Yellow 
            }
        }
    }

    Process{
        $ConnOpt = New-Object System.Management.ConnectionOptions 
        if ($ComputerName -ne $env:COMPUTERNAME) { # User credentials cannot be used for local connections
            $ConnOpt.EnablePrivileges = $true
            $ConnOpt.Username = $Cred.UserName
            $ConnOpt.SecurePassword = $Cred.Password
        }
        $Scope = New-Object System.Management.ManagementScope “\\$ComputerName\$NameSpace", $ConnOpt
        try { 
            $Scope.Connect()
        } catch {
            $Message = $_.Exception.InnerException
        }
        if ($Scope.IsConnected) {
            $EnumOptions = New-Object System.Management.EnumerationOptions
            $EnumOptions.set_timeout((New-TimeSpan -seconds $TimeOut))
            $Search = New-Object System.Management.ManagementObjectSearcher 
            $Search.set_options($EnumOptions) 
            $Search.Query = “SELECT $Property FROM $Class” 
            $Search.Scope = $Scope
            $Result = $Search.get()
        } else {
            Write-Warning "Get-SBWMI: Error: $(($Message|Out-String).Trim())"
        }
    } 

    End {
        if ($Result){
            if ($Filter) {
                if ($FilterProperty -in ($Result | Get-Member -MemberType Property).Name) {
                    $Result | where { $_.$FilterProperty -eq $FilterValue }
                } else {
                    Write-Log 'Class',$Class,'doesn''t contain filter property',$FilterProperty Magenta,Yellow,Magenta,Yellow
                    Write-Log 'Class',$Class,'has the following properties:' Cyan,Yellow,Cyan
                    Write-Log (($Result | Get-Member -MemberType Property).Name | ? { $_ -notmatch '__' } | Out-String).Trim() Cyan
                }
            } else {
                $Result
            }
        }
    }
}

function Get-SBDisk {
<#
 .SYNOPSIS
  Function to get disk information including block (allocation unit) size
 
 .DESCRIPTION
  Function to get disk information including block (allocation unit) size
  Function returns information on all fixed disks (Type 3)
  Function will fail to return computer disk information if:
    - Target computer is offline or name is misspelled
    - Function/script is run under an account with no read permission on the target computer
    - WMI services not running on the target computer
    - Target computer firewall or AntiVirus blocks WMI or RPC calls
 
 .PARAMETER ComputerName
  The name or IP address of computer(s) to collect disk information on
  Default value is local computer name
 
 .PARAMETER WMITimeOut
  Timeout in seconds. The default value is 20
 
 .PARAMETER Cred
  PS Credential object
 
 .PARAMETER IncludeRecoveryVolume
  This parameter takes a $true or $false value, and is set to $false by default
  When set to $true the script will return information on Recovery Volume
 
 .EXAMPLE
  Get-SBDisk
  Returns fixed disk information of local computer
 
 .EXAMPLE
  Get-SBDisk computer1, 192.168.19.26, computer3 -Verbose
  Returns fixed disk information of the 3 listed computers
  The 'verbose' parameter will display a message if the target computer cannot be reached
 
 .OUTPUTS
  The script returns a PS Object with the following properties:
    ComputerName
    VolumeName
    DriveLetterOrMountPoint
    BlockSizeKB
    SizeGB
    FreeGB
    'Free%'
    FileSystem
    Compressed
 
 .LINK
  https://superwidgets.wordpress.com/2017/01/09/powershell-script-to-get-disk-information-including-block-size/
 
 .NOTES
  Function by Sam Boutros - v1.0 - 9 January 2017
    v2.0 - 24 January 2017
        Used WMI object Win32_Volume instead of Win32_LogicalDisk to capture mount points as well
        Added parameter to skip Recovery Volume
        Updated output object properties
    v3.0 - 12 July 2017
        Updated output object to change data types to Int32 instead of the default String for BlockSizeKB,SizeGB,FreeGB,'Free%'
    v4.0 - 20 September 2017 - Used Get-SBWMI instead to take advanrage of the default 20 sec Timeout
    v4.1 - 22 September 2017 - Added WMITimeout parameter,
        removed -Filter parameter from Get-SBWMI call and filtered via updated if statement to speed processing by 200%
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$false,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [String[]]$ComputerName = $env:COMPUTERNAME,
        [Parameter(Mandatory=$false)][Int32]$WMITimeOut = 20,
        [Parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Cred = (Get-SBCredential -UserName "$env:USERDOMAIN\$env:USERNAME"),
        [Parameter(Mandatory=$false)][Switch]$IncludeRecoveryVolume
    )

    foreach ($Computer in $ComputerName) {
        try {
            Get-SBWMI -ComputerName $Computer -Class Win32_Volume -TimeOut $WMITimeOut -Cred $Cred -ErrorAction Stop | % {
                if ($_.DriveType -eq 3 -and ($_.Label-notlike'Recovery' -or $IncludeRecoveryVolume)) {
                    [PSCustomObject][Ordered]@{
                        ComputerName    = $Computer
                        VolumeName      = $_.Label
                        DriveLetterOrMountPoint = $(if ($_.Name.Contains(':')) {$_.Name} else {'<Not mounted>'})
                        BlockSizeKB     = [Int32]($_.Blocksize/1KB)
                        SizeGB          = [Math]::Round($_.Capacity/1GB,1)
                        FreeGB          = [Math]::Round($_.FreeSpace/1GB,1)
                        'Free%'         = [Math]::Round($_.FreeSpace/$_.Capacity*100,1)
                        FileSystem      = $_.FileSystem
                        Compressed      = $_.Compressed
                        Indexed         = $_.IndexingEnabled
                        Automount       = $_.Automount
                        QuotasEnabled   = $_.QuotasEnabled
                        PageFilePresent = $_.PageFilePresent
                        BootVolume      = $_.BootVolume
                        SystemVolume    = $_.SystemVolume
                    } # PSCustomObject
                } # if
            } # Get-SBWMI
        } catch {
            Write-Verbose "Unable to read disk information from computer $Computer"
        }
    }
}

#endregion

#region Networking

function Validate-NameResolution {
<#
 .SYNOPSIS
  Function to validate that a given computer name resolves to the same IP address by all domain controllers
 
 .DESCRIPTION
  Function to validate that a given computer name resolves to the same IP address by all domain controllers
 
 .PARAMETER ComputerName
  One or more computer names
 
 .EXAMPLE
  Validate-NameResolution -ComputerName 'myTestPC'
 
 .EXAMPLE
  $DNSValidationResult = Validate-NameResolution @('comp1','comp2','comp3')
 
 .OUTPUTS
  This cmdlet returns PSCustom Objects, one for each resolved IP address with the following properties/example:
    ComputerName ResolvesTo DNSServer
    ------------ ---------- ---------
    devtestaaav47 10.70.122.134 {DEVaaaDCRWV01.dev.tst.local, DEVaaaDCRWV02.dev.tst.local, tstCJRDCRWV01.tst.local, tstJUNDCRWV01.tst.local...}
    devtestaaav47 10.19.133.168 {DEVCJRDCRWV01.dev.tst.local, tstaaaDCRWV03.tst.local}
       
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 20 July 2018
#>


    [CmdletBinding(ConfirmImpact='Low')]
    Param([Parameter(Mandatory=$true,ValueFromPipeLineByPropertyName=$true)][String[]]$ComputerName)

    Begin { $DCList = Get-DCList }

    Process {       
        $myOutput = foreach ($Computer in $ComputerName) {
            $NameResolutionList = foreach ($DC in ($DCList | where { $_.Forest })) { Resolve-DnsName -Name $Computer -Server $DC.Name | 
                select @{n='ComputerName';e={$_.Name}},Type,TTL,IPAddress,@{n='DNSServer';e={$DC.Name}} | sort IPAddress
            }

            if (($Groups = $NameResolutionList | group IPAddress).Count.Count -gt 1) { # Yes .Count twice, not a typo :)
                Write-Log 'Identified name resolution inconsistency:',$Computer,'resolves to',(($NameResolutionList.IPAddress | select -Unique) -join ', ') Magenta,Yellow,Magenta,Yellow
            } else {
                Write-Log 'All DNS servers resolved',$Computer,'to the same IP address',($NameResolutionList.IPAddress | select -Unique) Green,Cyan,Green,Cyan
            }  
                          
            $Groups | foreach {
                [PSCustomObject][Ordered]@{
                    ComputerName = $Computer
                    ResolvesTo   = $_.Name
                    DNSServer    = $_.Group.DNSServer
                }
            }
        }
    }

    End { $myOutput }
} 
  
function Test-SBNetConnection {
<#
 .SYNOPSIS
  Function to test open TCP ports
 
 .DESCRIPTION
  Function to test open TCP ports
  Compared to the Test-NetConnection native function of the NetTCPIP module,
  this command is much faster particularly when it comes across closed ports.
  In addition, the timeout value is adjustable by using the TimeoutSec parameter.
 
 .PARAMETER ComputerName
  This parameter accepts a computer name or IPv4 Address.
  If a computer name is provided, the function attempts to resolve it to an IP address
 
 .PARAMETER PortNumber
  This is one or more TCP port number(s) with valid values from 1 to 65535
  It defaults to 111,135,22,3389,25,80,5985,5986
  Ports 111,135 help identify the system as a Linux or Windows system respectively
  Ports 22,3389 are Linux/SSH and Windows/RDP ports
  Ports 25,80 are SMTP and HTTP ports
  Ports 5895,5986 are PowerShell/WinRM ports over HTTP and HTTPS respectively
 
 .PARAMETER TimeoutSec
  Time out in seconds
  This defaults to 1, and accepts valid values from 1 to 300 seconds.
 
 .OUTPUTS
  The script outputs a PS array of objects, one for each open port including the following properties/example:
    ComputerName RemotePort TcpTestSucceeded
    ------------ ---------- ----------------
    10.127.73.195 53 True
    10.127.73.195 135 True
    10.127.73.195 389 True
    10.127.73.195 443 False
    10.127.73.195 5723 False
    10.127.73.195 5985 True
    10.127.73.195 5986 True
 
 .EXAMPLE
  Test-SBNetConnection -ComputerName 10.127.73.195
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 18 October 2017
  v0.2 - 5 January 2018 - Fixed bug to account for computers that resolve to more than 1 IP
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][String]$ComputerName,
        [Parameter(Mandatory=$false)][uInt16[]]$PortNumber = @(111,135,22,3389,25,80,5985,5986),
        [Parameter(Mandatory=$false)][ValidateRange(1,300)][Int16]$TimeoutSec = 1
    )

    Begin { }

    Process{
        if (!($IPv4Address = $Computername -as 'IPAddress')) {
            try {
                [IPAddress[]]$IPv4Address = (Resolve-DnsName -Name $ComputerName -EA 1).IPAddress
            } catch {
                Write-Warning "Unable to resolve computer name '$ComputerName'"
            }        
        }
        if ($IPv4Address) {
            foreach ($IP in $IPv4Address.IPAddressToString) {
                foreach ($Item in $PortNumber) {
                    $TCP = New-Object System.Net.Sockets.TcpClient
                    $AsyncResult = $TCP.BeginConnect("$IP","$Item",$null,$null)
                    $PortOpen = $false
                    if ($AsyncResult.AsyncWaitHandle.WaitOne($TimeoutSec*1000,$false)) {
                        try {
                            $TCP.EndConnect($AsyncResult)
                            $PortOpen = $true
                        } catch {
                            Write-Warning $_.Exception.InnerException 
                        }                        
                    } else {
                        Write-Warning "TCP connect to $($IP):$Item timed out ($TimeoutSec sec)"
                    } # if $AsyncResult
                    $TCP.Close()
                    [PSCustomObject][Ordered]@{
                        ComputerName     = $IP
                        RemotePort       = $Item
                        TcpTestSucceeded = $PortOpen
                    } # PSCustomObject
                } # foreach port
            } # foreach IP
        } # if $IPv4Address
    } # Process

    End { }
}

function Convert-IpAddressToMaskLength {
<#
 .SYNOPSIS
  Function to return the length of an IPv4 subnet mask
 
 .DESCRIPTION
  Function to return the length of an IPv4 subnet mask
  For example, 255.255.255.0 will return 24
 
 .PARAMETER DottedDecimalIP
  Dotted IPv4 address (subnet mask) such as 255.255.224.0
 
 .EXAMPLE
  Convert-IpAddressToMaskLength -DottedDecimalIP 255.255.255.0
  This will return 24
 
 .EXAMPLE
  Convert-IpAddressToMaskLength 255.0.0.0,255.192.0.0,255.255.255.224
  This will return 8, 10, and 27
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 October 2018
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [String[]]$DottedDecimalIP
    )

    Begin {  }

    Process{
        foreach ($Address in $DottedDecimalIP) {
            $Result = 0
            [IPAddress]$IPv4 = $Address
            foreach ($Octet in ($IPv4.IPAddressToString.Split('.'))) {
                while ($Octet -ne 0) {
                    $Octet = ($Octet -shl 1) -band [byte]::MaxValue
                    $Result ++
                } # while
            } # foreach
            $Result        
        } # foreach
    } # Process

    End {  }
}

function Convert-MaskLengthToIpAddress {
<#
 .SYNOPSIS
  Function to return the IPv4 subnet mask provided a mask length
 
 .DESCRIPTION
  Function to return the IPv4 subnet mask provided a mask length
  For example, 10 will return 255.192.0.0
 
 .PARAMETER MaskLength
  IPv4 subnet mask length. Valid values are 1 to 32
 
 .EXAMPLE
  Convert-MaskLengthToIpAddress -MaskLength 12
  This will return 255.240.0.0
 
 .EXAMPLE
  8,10,20,27 | Convert-MaskLengthToIpAddress
  This will return
    255.0.0.0
    255.192.0.0
    255.255.240.0
    255.255.255.224
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 October 2018
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [ValidateRange(1,32)]
            [UInt32[]]$MaskLength
    )

    Begin {    }

    Process{
        foreach ($Item in $MaskLength) { 
            if ($Item -lt 9) {
                "$((1..$Item | % { [math]::Pow(2,8-$_) } | measure -Sum).Sum).0.0.0"
            } elseif ($Item -lt 17) {
                "255.$((1..$($Item-8) | % { [math]::Pow(2,8-$_) } | measure -Sum).Sum).0.0"
            } elseif ($Item -lt 25) {
                "255.255.$((1..$($Item-16) | % { [math]::Pow(2,8-$_) } | measure -Sum).Sum).0"
            } else {
                "255.255.255.$((1..$($Item-24) | % { [math]::Pow(2,8-$_) } | measure -Sum).Sum)"
            }            
        } # foreach
    } # Process

    End {    }
}

function Get-IPv4Details {
<#
 .SYNOPSIS
  Function to return the details of a given IPv4 address
 
 .DESCRIPTION
  Function to return the details of a given IPv4 address
 
 .PARAMETER IPAddress
  Dotted decimal IPv4 address such as 11.12.13.14
 
 .PARAMETER SubnetMask
  Dotted decimal IPv4 subnet mask such as 255.255.0.0
 
 .OUTPUTS
  This function returns a PS object with the following properties (and example):
    IPDottedDecimal : 10.120.30.11
    IPDecimal : 186546186
    IPBitLength : 12
    IPDottedBinary : 00001010.01111000.00011110.00001011
    MaskDottedDecimal : 255.255.240.0
    MaskDecimal : 15794175
    MaskBitLength : 20
    MaskDottedBinary : 11111111.11111111.11110000.00000000
    NetDottedDecimal : 10.120.16.0
    NetDecimal : 1079306
    NetCIDR : 255.255.240.0/20
    NetDottedBinary : 00001010.01111000.00010000.00000000
    HostDottedDecimal : 0.0.14.11
    HostDecimal : 185466880
    HostDottedBinary : 00000000.00000000.00001110.00001011
    FirstSubnetIP : 10.120.16.1
    LastSubnetIP : 10.120.31.254
    SubnetMaximumHosts : 4094
 
 .EXAMPLE
  Get-IPv4Details -IPAddress 10.120.30.11 -SubnetMask 255.255.240.0
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 October 2018
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][Alias('IPv4','IP')][IPAddress]$IPAddress,
        [Parameter(Mandatory=$true)][Alias('Mask','NetMAsk')][IPAddress]$SubnetMask
    )

    Begin { }

    Process{
        $MaskLength = 0
        foreach($Octet in ($SubnetMask.GetAddressBytes())) {
            while ($Octet -ne 0) { $Octet = $Octet*2 -band 255; $MaskLength ++ }
        }
        $IPLength = 32 - $MaskLength
        $IPBinary = $IPAddress.GetAddressBytes()  | % { [Convert]::ToString($_,2).PadLeft(8,'0') }
        $MaskBinary = $SubnetMask.GetAddressBytes() | % { [Convert]::ToString($_,2).PadLeft(8,'0') }
        $NetAddress = [IPAddress]($IPAddress.Address -band $SubnetMask.Address)
        $NetBinary = $NetAddress.GetAddressBytes() | % { [Convert]::ToString($_,2).PadLeft(8,'0') }
        $Temp = foreach ($Octet in $MaskBinary) { 0..7 | % { if ($Octet[$_] -eq '1') { '0' } else { '1' } } }
        $MaskMirrorBinary = @(); 0,8,16,24 | % {$MaskMirrorBinary += ($Temp -join '').Substring($_,8)  }
        [IPAddress]$MaskMirror = ($MaskMirrorBinary | % { [Convert]::ToInt32($_,2) }) -join '.'
        $HostAddress = [IPAddress]($IPAddress.Address -band $MaskMirror.Address)
        $HostBinary = $HostAddress.GetAddressBytes() | % { [Convert]::ToString($_,2).PadLeft(8,'0') }
        [PSCustomObject]@{
            IPDottedDecimal    = $IPAddress.IPAddressToString
            IPDecimal          = $IPAddress.Address
            IPBitLength        = $IPLength
            IPDottedBinary     = $IPBinary -join '.'

            MaskDottedDecimal  = $SubnetMask.IPAddressToString
            MaskDecimal        = $SubnetMask.Address
            MaskBitLength      = $MaskLength
            MaskDottedBinary   = $MaskBinary -join '.'

            NetDottedDecimal   = $NetAddress.IPAddressToString
            NetDecimal         = $NetAddress.Address
            NetCIDR            = "$($NetAddress.IPAddressToString)/$MaskLength"
            NetDottedBinary    = $NetBinary -join '.'

            HostDottedDecimal  = $HostAddress.IPAddressToString
            HostDecimal        = $HostAddress.Address
            HostDottedBinary   = $HostBinary -join '.'

            FirstSubnetIP      = Next-IP -IPAddress $NetAddress.IPAddressToString
            LastSubnetIP       = Next-IP -IPAddress $NetAddress.IPAddressToString -Increment ([Math]::Pow(2,$IPLength) - 2)
            SubnetMaximumHosts = [Math]::Pow(2,$IPLength) - 2
        }
    }

    End { }
}

function Next-IP {
<#
 .SYNOPSIS
  Function to return an IP address relative to the input IP address
 
 .DESCRIPTION
  Function to return an IP address relative to the input IP address
 
 .PARAMETER IPAddress
  Dotted IPv4 address such as 10.12.13.15
 
 .PARAMETER Increment
  A whole number between -4294967294 and 4294967295
  For example when using 1, the function will return the next IP address
  This defaults to 1
 
 .EXAMPLE
  Next-IP -IPAddress 10.10.10.11 -Increment 1
    Will return 10.10.10.12
 
 .EXAMPLE
  Next-IP -IPAddress 201.120.252.253 -Verbose
    Will return 201.120.252.254
 
 .EXAMPLE
  Next-IP -IPAddress 201.120.252.253 -Increment 100 -Verbose
    Will return 201.120.253.97
 
 .EXAMPLE
  Next-IP -IPAddress 201.120.252.253 -Increment -500 -Verbose
    Will return 201.120.251.9
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 October 2018
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][Alias('IPv4','IP')][IPAddress]$IPAddress,
        [Parameter(Mandatory=$false)][ValidateRange(-4294967294,4294967295)][Int64]$Increment = 1
    )

    Begin { }

    Process{
        $DecimalArray = $IPAddress.GetAddressBytes()
        [Array]::Reverse($DecimalArray)
        $Decimal = ([IPAddress]($DecimalArray -join '.')).Address
        $Decimal = $Decimal + $Increment
        if ($Decimal -le 4294967295 -and $Decimal -ge -4294967294) {
            $DecimalArray = ([IPAddress]$Decimal).GetAddressBytes()
            [Array]::Reverse($DecimalArray)
            $DecimalArray -join '.'  
        } else {
            Write-Verbose "Cannot increment/decrement the provided IP addresses '$($IPAddress.IPAddressToString)' by '$Increment'"
            Write-Verbose "The resulting address '$Decimal' would exceed a 32-bit address (-4294967294 to 4294967295)"
        }      
    }

    End { }
}

function Test-SameSubnet {
<#
 .SYNOPSIS
  Function to compare a pair of IPv4 addresess and their subnet masks and
  identify if they're on the same subnet or not
 
 .DESCRIPTION
  Function to compare a pair of IPv4 addresess and their subnet masks
  If the 2 IPs are on the same subnet, the function retirns the subnet ID in CIDR format,
  otherwise it returns False
 
 .PARAMETER IP1
  Dotted decimal IPv4 address such as 11.12.13.14
 
 .PARAMETER Mask1
  Dotted decimal IPv4 subnet mask such as 255.255.0.0
 
 .PARAMETER IP2
  Dotted decimal IPv4 address such as 11.12.13.15
 
 .PARAMETER Mask2
  Dotted decimal IPv4 subnet mask such as 255.255.240.0
 
 .EXAMPLE
  Test-SameSubnet -IP1 10.124.170.1 -Mask1 255.255.252.0 -IP2 10.124.170.2 -Mask2 255.255.252.0
  This will return 10.124.168.0/22
 
 .EXAMPLE
  Test-SameSubnet -IP1 10.124.170.117 -Mask1 255.255.255.240 -IP2 10.124.170.2 -Mask2 255.255.255.240
  This will return False
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 October 2018
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][IPAddress]$IP1,
        [Parameter(Mandatory=$true)][IPAddress]$Mask1,
        [Parameter(Mandatory=$true)][IPAddress]$IP2,
        [Parameter(Mandatory=$true)][IPAddress]$Mask2
    )

    Begin { }

    Process{
        $Network1 = (Get-IPv4Details -IPAddress $IP1 -SubnetMask $Mask1).NetDecimal
        $Network2 = (Get-IPv4Details -IPAddress $IP2 -SubnetMask $Mask2).NetDecimal
        if ($Network1 -eq $Network2) { 
            [IPAddress]$IP = 0
            $IP.Address = $Network1
            "$($IP.IPAddressToString)/$(Convert-IpAddressToMaskLength -DottedDecimalIP $Mask1)"
        } else {
            $false
        }
    }

    End { }
}

function Get-IPv4Summary {
<#
 .SYNOPSIS
  Function to return IPv4 information of enabled network adapters
 
 .DESCRIPTION
  Function to return IPv4 information of enabled network adapters
  This function requires the Convert-IpAddressToMaskLength function
  available in the SB-Tools modules in the PowerShell Gallery
 
 .PARAMETER ServiceName
  This is set to 'netvsc' by default
  To see available Service Names use:
  Get-WmiObject -Class Win32_NetworkAdapterConfiguration | FT Description,Index,IPAddress,ServiceName,DefaultIPGateway -a
 
 .EXAMPLE
  Get-IPv4Summary -Verbose
  
 .EXAMPLE
  Get-IPv4Summary -ServiceName 'vmsmp' -Verbose
  
 .OUTPUTS
  This function/cmdlet returns a PS object for each netvsc NIC with the following properties/example:
    IPv4Address : 192.168.124.44
    IPv4Subnet : 255.255.255.0
    MaskLength : 24
    DefaultGateway : 192.168.124.1
    DNSServers : {8.8.8.8,4.4.4.4}
    Description : Ethernet Network Adapter
    DHCPEnabled : False
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 October 2018
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param([Parameter(Mandatory=$false)][String]$ServiceName = 'netvsc') # 'vmsmp'

    Begin { }

    Process{
        $AdapterList = Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "servicename = ""$ServiceName""" | ? { $_.IPAddress }
        if ($AdapterList) {
            $myOutput = foreach ($NIC in $AdapterList) {
                Write-Verbose "Get-IPv4Summary: Processing adapter '$($NIC.Description)'"
                if (($NIC.IPSubnet -match '\.').Count -eq 1) { 
                    $IPv4Subnet = $NIC.IPSubnet -match '\.' | select -First 1 
                    $MaskLength = Convert-IpAddressToMaskLength $IPv4Subnet 
                } else {
                    $IPv4Subnet = $NIC.IPSubnet -match '\.'
                    $MaskLength = $IPv4Subnet | % {Convert-IpAddressToMaskLength $_}
                }
                if (($NIC.IPAddress -match '\.').Count -eq 1) { 
                    $IPv4Address = $NIC.IPAddress -match '\.' | select -First 1 
                } else {
                    $IPv4Address = $NIC.IPAddress -match '\.'
                }
                if (($NIC.DefaultIPGateway -match '\.').Count -eq 1) { 
                    $DefaultGateway = $NIC.DefaultIPGateway -match '\.' | select -First 1 
                } else {
                    $DefaultGateway = $NIC.DefaultIPGateway -match '\.'
                }
                if (($NIC.DNSServerSearchOrder -match '\.').Count -eq 1) { 
                    $DNSServers = $NIC.DNSServerSearchOrder -match '\.' | select -First 1 
                } else {
                    $DNSServers = $NIC.DNSServerSearchOrder -match '\.'
                }
                [PSCustomObject]@{
                    IPv4Address    = $IPv4Address
                    IPv4Subnet     = $IPv4Subnet
                    MaskLength     = $MaskLength
                    DefaultGateway = $DefaultGateway
                    DNSServers     = $DNSServers
                    Description    = $NIC.Description
                    DHCPEnabled    = $NIC.DHCPEnabled
                } # PSCustomObject
            } # foreach $NIC
            $myOutput
        } else {
            Write-Verbose "Bad ServiceName '$ServiceName' provided, available Service Names are: $( Get-WmiObject -Class Win32_NetworkAdapterConfiguration | FT description,Index,IPAddress,ServiceName,DefaultIPGateway -a | Out-String)"
        } 
    }

    End { }
}

function Get-FTPFileList {
<#
 .SYNOPSIS
  Function to get file list from FTP site
 
 .DESCRIPTION
  Function to get file list from FTP site
 
 .PARAMETER FTPURL
  For example: ftp://site.domain.com
  This is the URL to the FTP site
 
 .PARAMETER Port
  Optional parameter that defaults to port 21
 
 .PARAMETER Cred
  PSCredential object obtained via Get-Credential or Get-SBCredential
  It is used to authenticate to the FTP site.
  For anonymous FTP create a credential that has the name 'anonymous' and any password
 
 .PARAMETER Recurse
  Optional switch parameter. When set to True, the function will return all files and subfolders
 
 .EXAMPLE
  Get-FTPFileList -FTPURL ftp://123.45.56.78 -Cred (Get-SBCredential 'samb@mysite.ftpdomain.com') | FT -a
  This example list the files listed from the given FTP site
  
 .EXAMPLE
  $myFileList = Get-FTPFileList -FTPURL ftp://mysite.ftpsite.com -Cred (Get-SBCredential 'samb@mysite.ftpdomain.com') -Recurse
  $FileOnlyList = $myFileList | where Type -EQ 'File'
  Write-log 'File and directory listing contains', $myFileList.Count, 'items' Green,Cyan,Green
  Write-log ' including', ($myFileList.Count-$FileOnlyList.Count), 'directories' Green,Cyan,Green
  Write-log ' and', $FileOnlyList.Count, 'files' Green,Cyan,Green
  Write-log 'Calculating total size...' Green -noNew
  $SizeBytes = ($myFileList | measure SizeBytes -Sum).Sum
  Write-log $SizeBytes, 'bytes', "($([Math]::Round($SizeBytes/1GB,2)) GB)" Cyan,Green,Cyan
 
 
 .OUTPUTS
  The function returns an object for each file/directory found with the following properties/example:
    Type Name Path SizeBytes Date Permission
    ---- ---- ---- --------- ---- ----------
    File 8xxxx5 ftp://mysite.ftpsite.com//8xxxx5/8xxxx5 47 12/27/2014 12:00:00 AM -r--r--r--
    File 8xxxx5.zip ftp://mysite.ftpsite.com//8xxxx5/8xxxx5.zip 61728 12/27/2014 12:00:00 AM -r--r--r--
    Directory June Amazon ftp://mysite.ftpsite.com//Amazon/June Amazon 0 6/9/2015 12:00:00 AM drwxr-xr-x
    File MANIFEST.txt ftp://mysite.ftpsite.com//Amazon/MANIFEST.txt 636 3/18/2015 12:00:00 AM -r--r--r--
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  17 October 2018 - v0.1
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,HelpMessage='Such as ftp://site.domain.com')][String]$FTPURL,  
        [Parameter(Mandatory=$false)][Int]$Port = 21,
        [Parameter(Mandatory=$true)][PSCredential]$Cred,
        [Parameter(Mandatory=$false)][Switch]$Recurse = $false
    )

    Begin {
        # Compile URL from FTPURL and Port
        if (($FTPURL -as [system.uri]).AbsoluteUri) {
            $Temp = $FTPURL -as [system.uri]
            [system.uri]$FTPURL = "ftp://$($Temp.Host):$Port$($Temp.LocalPath)"
            Write-Log 'Get-FTPFileList: Processing URL:',$FTPURL.AbsoluteUri Green,Cyan
        } else {
            Write-Log 'Get-FTPFileList: Error: bad FTPURL received:',$FTPURL,"expecting FTP URL such as ftp://site.domain.com" Magenta,Yellow,Magenta
            break
        }
    }

    Process {
        try {
        
            $FTPRequest = [System.Net.FtpWebRequest]::Create($FTPURL)
            $FTPRequest.Credentials = $Cred
            $FTPRequest.Method = [System.Net.WebRequestMethods+Ftp]::ListDirectoryDetails 
            $FTPResponse = $FTPRequest.GetResponse() 
            $ResponseStream = $FTPResponse.GetResponseStream()
            $StreamReader = New-Object System.IO.StreamReader $ResponseStream  
            $FileList = New-Object System.Collections.ArrayList
            While ($File = $StreamReader.ReadLine()) { [void]$FileList.add($File) }

        } catch {
            Write-Log $_.Exception.InnerException.Message Yellow
            break
        }

        $StreamReader.close()
        $ResponseStream.close()
        $FTPResponse.Close()

        $myOutput = foreach ($FileLine in $FileList) {
            $Name = $FileLine.Substring(49,$FileLine.Length-49)
            [PSCustomObject][Ordered]@{
                Type       = $(if ($FileLine.Substring(0,1) -eq 'd') { 'Directory' } else { 'File' })
                Name       = $Name
                Path       = "$($FTPURL.AbsoluteUri)/$Name"
                SizeBytes  = $FileLine.Substring(20,15).Trim() -as [Int64]
                Date       = $FileLine.Substring(35,13) -as [DateTime]
                Permission = $FileLine.Substring(0,10) -as [String]
            }
        }

        if ($Recurse) {
            foreach ($Directory in ($myOutput | where Type -EQ 'Directory')) { 
                Get-FTPFileList -FTPURL $Directory.Path -Cred $Cred 
            }
        }
    } 

    End { $myOutput }
}

#endregion

#region Remoting

Function Export-SessionCommand {
<#
 .SYNOPSIS
  Function to export one or more session commands
 
 .DESCRIPTION
  Function to export one or more session commands
  This function takes one or more Powershell script functions/commands from current session
  and exports them to a remote PS session
  This function will ignore and not export binary functions
  Exported functions will persist on the remote computer for the user profile used with the PS remote session
 
 .PARAMETER Command
  This is one or more script commands available in the current PS session
  For example Update-SmbMultichannelConnection cmdlet/function of the SmbShare PS module
  To see available script commands, you can use:
    Get-Command | ? { $_.CommandType -eq 'function' }
 
 .PARAMETER ModuleName
  This is the name of the module that this function will create on the remote computer
  under the user profile of the remote PS session
  This will over-write prior existing module with the same name
 
 .PARAMETER Session
  PSSession object usually obtained by using New-PSSession cmdlet.
 
 .EXAMPLE
  Export-SessionCommand get-saervice,get-sbdisk,bla,get-bitlockerstatus,get-service -Session $Session -Verbose
 
 .OUTPUTS
  The function returns a list of successfully exported commands/functions, or $false if it fails
  Example:
    CommandType Name ModuleName
    ----------- ---- ----------
    Function Get-BitLockerStatus SBjr
    Function Get-SBDisk SBjr
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 12 July 2018
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)][string[]]$Command,
        [Parameter(Mandatory=$false)][String]$ModuleName = 'SBjr',
        [Parameter(Mandatory=$true)][System.Management.Automation.Runspaces.PSSession]$Session
    )

    Begin { 
        if ($Session.State -ne 'Opened') {
            Write-Log 'Export-SessionCommand: Error: Session State is not ''opened''' Magenta
            Write-Log ($Session|Out-String).Trim() Yellow
            break
        }

        $FunctionList = foreach ($Name in $Command) {
            try { 
                Get-Command $Name -EA 1 | Out-Null
                if ((Get-Command $Name).ScriptBlock) {
                    $Name
                } else {
                    Write-Warning "Command '$Name' is not a script command, ignoring"
                }
            } catch {
                Write-Warning "Command '$Name' not found, ignoring"
            }
        }
        $FunctionList = $FunctionList | select -Unique 
        Write-Log 'Exporting function(s):',($FunctionList -join ', ') Green,Cyan 
    }

    Process{ 
        $FirstCommand = $true
        $FunctionList | % {
            $myCommand = Get-Command $_
            Write-Verbose "Exporting command '$($myCommand.Name)' to module '$ModuleName'"
            Invoke-Command -Session $Session -ScriptBlock { 
                $ModPath = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\$Using:ModuleName"
                $PSM     = "$ModPath\$Using:ModuleName.psm1"
                if ($Using:FirstCommand) {
                    New-Item -Path $ModPath -ItemType Directory -Force | Out-Null 
                    "Function $($Using:myCommand.Name) {" | Out-File $PSM                        
                } else {
                    "Function $($Using:myCommand.Name) {" | Out-File $PSM -Append
                }
                                       
                $Using:myCommand.ScriptBlock,'}',' ' | % { $_ | Out-File $PSM -Append }
            }
            $FirstCommand = $false 
        }

    } # Process

    End {         
        Invoke-Command -Session $Session -ScriptBlock { 
            ' ','Export-ModuleMember -Function *' | % { $_ | Out-File $PSM -Append }
            Remove-Module $Using:ModuleName -Force -Confirm:$false -EA 0
            Import-Module $Using:ModuleName
            try { Get-Command -Module $Using:ModuleName -EA 1 | FT -a } catch { $false }
        }
    }

}

function Import-SessionCommands {
<#
 .SYNOPSIS
  Function to import commands from another computer
 
 .DESCRIPTION
  Function will import commands from remote computer from the module(s) listed.
 
 .PARAMETER ModuleName
  Name(s) of the module(s) that we want to import their commands into the current
  PS console.
  Note that session commands will not be available in other PS instances.
 
 .PARAMETER ComputerName
  Computer name that has the module(s) that we need to import their commands.
 
 .PARAMETER Keep
  This is a switch. When selected, the function will export the imported module(s)
  locally under "C:\Program Files\WindowsPowerShell\Modules" if it's in the PSModulePath,
  otherwise, it will export it to the default path "$home\Documents\WindowsPowerShell\Modules"
  - Note 1: Exported modules and their commands can be used directly from any PS instance
            after a module has been exported with the -keep switch
  - Note 2: Even though a module has been exported locally, everytime you try to use one of
            its commands, PS will start an implicit remoting session to the server where the
            module was imported from.
 
 .EXAMPLE
  Import-SessionCommands -ModuleName ActiveDirectory -ComputerName DC01
  This example imports all the commands from the ActiveDirectory module from the DC01 server
  So, in this PS console instance we can use AD commands like Get-ADComputer without the need
  to install AD features, tools, or PS modules on this computer!
 
 .EXAMPLE
  Import-SessionCommands SQLPS,Storage V-2012R2-SQL1 -Verbose
  This example imports all the commands from the PSSQL and Storage modules from the MySQLServer
  server into the current PS instance
 
 .EXAMPLE
  Import-SessionCommands WebAdministration,BestPractices,MMAgent CM01 -keep
  This example imports all the commands from the WebAdministration, BestPractices, and MMAgent
  modules from the CM01 server into the current PS instance, and exports them locally.
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  Requires PS 3.0
  v1.0 - 08/17/2014
    Although we need to eventually run:
    Remove-PSSession -Session $Session
    We cannot do that in the function as we'll lose the imported session commands
    Two things to consider:
    1. The session will be automatically removed when the PS console is closed
    2. If in the parent script that's using this function a blanket Remove-PSSession
    command is run, like:
    Get-PSSession | Remove-PSSession
    We'll lose this session and its commands, which could cripple the parent script
#>

    
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [String[]]$ModuleName, 
        [Parameter(Mandatory=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=1)]
            [String]$ComputerName,
        [Parameter(Mandatory=$false,
                   Position=2)]
            [Switch]$Keep
    )
    
    # Get a random session name:
    Do { $SessionName = "Import" + (Get-Random -Minimum 10000000 -Maximum 99999999) }
        While (Get-PSSession -Name $SessionName -ErrorAction SilentlyContinue) 
    Write-Verbose "New PSSession name: $SessionName"
    if ($Env:PSModulePath.Split(';') -contains 'C:\Program Files\WindowsPowerShell\Modules') {
        $ExportTo = 'C:\Program Files\WindowsPowerShell\Modules'
    } else {
        $ExportTo = "$home\Documents\WindowsPowerShell\Modules"
    }
    try { 
        Write-Log 'Connecting to computer', $ComputerName Green,Cyan
        $CurrentSessions = Get-PSSession -ErrorAction SilentlyContinue -ComputerName $ComputerName
        if ($CurrentSessions.ComputerName -Contains $ComputerName) {
            $Session = $CurrentSessions[0]
        } else {
            $Session = New-PSSession -ComputerName $ComputerName -Name $SessionName -ErrorAction Stop
        }
        Write-Verbose "Current PSSessions: $(Get-PSSession)"
        $RemoteModules = Invoke-Command -ScriptBlock { Get-Module -ListAvailable | Select Name } -Session $Session 
        $LocalModules = Get-Module -ListAvailable | Select Name
        foreach ($Module in $ModuleName) {
            if ($LocalModules.Name -Contains $Module -or $LocalModules.Name -Contains "Imported-$Module") {
                Write-Log 'Module', $Module, 'exists locally, not importing..' Yellow,Cyan,Yellow
            } else {
                if ($RemoteModules.Name -Contains $Module) {
                    Write-Log 'Found module', $Module, 'on computer', $ComputerName, 'importing its commands..' Green,Cyan,Green,Cyan,Green
                    Invoke-Command -Session $Session -ArgumentList $Module -ScriptBlock { 
                        Param($Module)
                        Import-Module $Module 
                    }
                    try { 
                        $ImportedModule = Import-PSSession -Session $Session -Module $Module -DisableNameChecking -ErrorAction Stop 
                        if ($Keep) {
                            Write-Log 'Keeping module', $Module, 'locally..' Green,Cyan,Green
                            Remove-Module -Name $ImportedModule.Name
                            Export-PSSession -Module $Module -OutputModule "$ExportTo\Imported-$Module" -Session $Session -Force
                            Import-Module -Name "Imported-$Module"
                        }
                    } catch { 
                        Write-Log 'Module', $Module, 'already imported, skipping..' Yellow,Cyan,Yellow
                    }
                } else {
                    Write-Log 'Error: module', $Module, 'not found on server', $ComputerName Magenta,Yellow,Magenta,Yellow
                }
            }
        }
    } catch {
        Write-Log 'Error: unable to connect to server', $ComputerName Magenta,Yellow
        Write-Log ' Check if', $ComputerName, 'exists, is online, ' Magenta,Yellow,Magenta
        Write-Log ' has WinRM enabled and configured, and ' Magenta
        Write-Log ' you have sufficient permissions to it' Magenta
    }
}

function Connect-Computer {
<#
 .SYNOPSIS
  Function to establish PowerShell Remoting session with a remote computer that's not domain member
 
 .DESCRIPTION
  Function to establish PowerShell Remoting session with a remote computer that's not domain member
 
 .PARAMETER ComputerName
  This can be a NetBios computer name like'mycomputer' or an IPv4 address like '10.20.30.40'
  If using a computer name, make sure it can be resolved to an IPv4 address
 
 .PARAMETER Credential
  This is a PSCredential Object not text.
 
 .EXAMPLE
  $Session = Connect-Computer -ComputerName '10.171.120.68' -Credential (Get-SBCredential -UserName '.\Administrator') -Verbose
  This establishes a session with 10.171.120.68
  To see built in help for the Get-SB-Credential function use: Get-Help Get-SBCredential -Show
  The returned PSSession object is stored in the $Session variable in this example, to used for further automation such as:
  Invoke-command -Session $Session -ScriptBlock { Get-Service }
 
 .OUTPUTS
  This function returns a PSSession object [System.Management.Automation.Runspaces.PSSession]
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 4 October 2018
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true)]
            [String]$ComputerName,
        [Parameter(Mandatory=$true)]
            [System.Management.Automation.PSCredential]$Credential
    )

    Begin {
        Write-Verbose 'Connect-Computer: Checking Trusted Hosts list'
        $TrustedHosts = Get-Item wsman:\localhost\Client\TrustedHosts
        if ($TrustedHosts.Value -match $ComputerName) {
            Write-Verbose "Connect-Computer: $ComputerName is already in Trusted Hosts"
        } else {
            Write-Verbose "Connect-Computer: Adding $ComputerName to Trusted Hosts"
            try {
                Set-Item wsman:\localhost\Client\TrustedHosts $ComputerName -Concatenate -Force -ErrorAction Stop
                Write-Verbose 'done'
            } catch {
                throw "Failed to add $ComputerName to Trusted Hosts"
            }              
        }        
    }

    Process{
        Write-Verbose "Connect-Computer: Establishing PowerShell Remoting session with $ComputerName using Credential $($Credential.UserName)"
        try {
            New-PSSession -ComputerName $ComputerName -Credential $Credential -ErrorAction Stop
            Write-Verbose 'done'
        } catch {
            Write-Error "Failed to establish PowerShell Remoting session with $ComputerName"
            throw $_
        }         
    }

    End {    }
}

#endregion

#region PageFile

function Get-PageFile {
<#
 .SYNOPSIS
  List the drives that have page file(s) and their configuration
 
 .DESCRIPTION
  List the drives that have page file(s) and their configuration
  Note that 0 value for Initial or Maximum size indicate a system-managed page file
  This function does not require or accept any parameters
 
 .OUTPUTS
  This function returns a PS object for each drive that has a page file on it,
  each having the following 3 properties/example:
    DriveLetter InitialSizeMB MaximumSizeMB
    ----------- ------------- -------------
              C 0 0
              D 1024 4096
 
 .EXAMPLE
  Get-PageFile
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  https://superwidgets.wordpress.com/category/powershell/
  18 September 2018 - v0.1
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param()

    Begin { }

    Process {
        Get-WmiObject -Class Win32_PageFileSetting | 
            select  @{n='DriveLetter';  e={$_.Name[0]}},
                    @{n='InitialSizeMB';e={$_.InitialSize}},
                    @{n='MaximumSizeMB';e={$_.MaximumSize}}
        Write-Verbose '0 value for Initial or Maximum size indicate a system-managed page file'
    }

    End { }

}

function Set-PageFile {
<#
 .SYNOPSIS
  Function to set page file to be on a given drive
 
 .DESCRIPTION
  Function to set page file to be on a given drive
  Function will create page file if it does not exist on the provided drive
 
 .PARAMETER PageFile
  This is a PS Custom Object containing the following 3 properties:
    DriveLetter such as c
    InitialSizeMB such as 1024 (0 value indicate system managed page file)
    MaximumSizeMB such as 4096 (0 value indicate system managed page file)
  This object can be constructed manually as in:
  $PageFile = [PSCustomObject]@{
    DriveLetter = 'c'
    InitialSizeMB = 0
    MaximumSizeMB = 0
  }
  or obtained from the Get-PageFile function of this PS module
 
 .EXAMPLE
  Set-PageFile -PageFile ([PSCustomObject]@{
    DriveLetter = 'c'
    InitialSizeMB = 0
    MaximumSizeMB = 0
  })
  This example configures a system-managed page file on drive c
 
 .EXAMPLE
  Get-PageFile | foreach { $_.InitialSizeMB = 0; $_.MaximumSizeMB = 0; $_ } | Set-PageFile
  This example sets every page file to system-managed size
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  20 September 2018 - v0.1
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$false,ValueFromPipeline=$true)][PSCustomObject]$PageFile = [PSCustomObject]@{
            DriveLetter   = ((Get-WmiObject Win32_Volume | where PageFilePresent).DriveLetter | foreach { $_[0] } | select -First 1)
            InitialSizeMB = 0 # 0 = System Managed Size
            MaximumSizeMB = 0 # 0 = System Managed Size
        }
    )

    Begin { 
        Write-Verbose 'Input received:'
        Write-Verbose ($PageFile | Out-String)
        
        $DriveletterList = (Get-WmiObject Win32_Volume | where PageFilePresent).DriveLetter | foreach { $_[0] } 
        if ($PageFile.DriveLetter -notin $DriveletterList) {
            Write-Log 'Set-PageFile error:','Provided drive letter',$PageFile.DriveLetter,
                'does not exist on this computer, available drive letters are',($DriveletterList -join ', ') Magenta,Yellow,Magenta,Yellow,Magenta 
            break
        } else {
            Write-Verbose "Validated that provided drive letter '$($PageFile.DriveLetter)' exists on this computer '$($env:COMPUTERNAME)'"
        }
    }

    Process {

        $CurrentPageFile = Get-PageFile | where { $_.DriveLetter -match $PageFile.DriveLetter }
        if ($CurrentPageFile.InitialSizeMB -eq $PageFile.InitialSizeMB -and $CurrentPageFile.MaximumSizeMB -eq $PageFile.MaximumSizeMB) {
            Write-Log 'Existing page file',($CurrentPageFile | Out-String),'already matches provided parameters' Green,Yellow,Green
        } else {
            Write-Log 'Updating page file',($CurrentPageFile | Out-String) Green,Cyan

            #region Disable AutomaticManagedPagefile feature
            $compObj = Get-WmiObject Win32_ComputerSystem -EnableAllPrivileges
            if ($compObj.AutomaticManagedPagefile) {
                $compObj.AutomaticManagedPagefile = $false
                $compObj.Put() | Out-Null
                $compObj = Get-WmiObject -Class Win32_compObj -EnableAllPrivileges
                if ($compObj.AutomaticManagedPagefile) { 
                    Write-Log 'Set-PageFile:','Unable to Disable AutomaticManagedPagefile feature','Get-WmiObject -Class Win32_compObj' Magenta,Yellow,Magenta
                    break
                } else {
                    Write-Log 'Disabled','AutomaticManagedPagefile','feature on',$compObj.Name Green,Cyan,Green,Cyan
                }
            } else {
                Write-Log 'Computer',$compObj.Name,'AutomaticManagedPagefile','feature is already disabled' Green,Cyan,Green,Cyan
            }
            #endregion

            # Change/Create Page File
            $pageFileSetting = Get-WmiObject -Class Win32_PageFileSetting | where { $_.Name.StartsWith($PageFile.DriveLetter) }
            if (-not $pageFileSetting) {
                Set-WmiInstance -Class Win32_PageFileSetting -Arguments @{
                    Name        = "$($PageFile.DriveLetter):\pagefile.sys"
                    InitialSize = 0
                    MaximumSize = 0
                } -EnableAllPrivileges | Out-Null
                $pageFileSetting = Get-WmiObject -Class Win32_PageFileSetting | where { $_.Name.StartsWith($PageFile.DriveLetter) }
            }
            $pageFileSetting.InitialSize = $PageFile.InitialSizeMB
            $pageFileSetting.MaximumSize = $PageFile.MaximumSizeMB
            $pageFileSetting.Put() | Out-Null
            $CurrentPageFile = Get-PageFile | where { $_.DriveLetter -match $PageFile.DriveLetter }
            Write-Verbose 'PageFile setting:'
            Write-Verbose ($PageFile | Out-String)
            Write-Verbose 'CurrentPageFile setting:'
            Write-Verbose ($CurrentPageFile | Out-String)
            if ($CurrentPageFile.InitialSizeMB -eq $PageFile.InitialSizeMB -and $CurrentPageFile.MaximumSizeMB -eq $PageFile.MaximumSizeMB) {
                Write-Log 'Successfully updated page file settings to',($CurrentPageFile | Out-String) Green,Cyan
                Write-Log 'Remember that a reboot is required to complete this process' Yellow
            } else {
                Write-log 'Unable to change Page File setting',($CurrentPageFile | Out-String) Magenta,Yellow
            }
        }

    }

    End { }

}

function Remove-PageFile {
<#
 .SYNOPSIS
  Function to remove page file from a given drive
 
 .DESCRIPTION
  Function to remove page file from a given drive
 
 .PARAMETER DriveLetter
  Drive such as 'c' or 'e' that has a page file to be removed
 
 .EXAMPLE
  Remove-PageFile 'c'
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  20 September 2018 - v0.1
 
#>


    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$false,ValueFromPipeline=$true)]
            [String]$DriveLetter = ((Get-WmiObject Win32_Volume | where PageFilePresent).DriveLetter | foreach { $_[0] } | select -First 1)
    )

    Begin { 
        Write-Verbose "Input received: DriveLetter $DriveLetter"
        
        $DriveletterList = (Get-WmiObject Win32_Volume | where PageFilePresent).DriveLetter | foreach { $_[0] } 
        if ($DriveLetter -notin $DriveletterList) {
            Write-Log 'Remove-PageFile error:','Provided drive letter',$DriveLetter,
                'does not exist on this computer, available drive letters are',($DriveletterList -join ', ') Magenta,Yellow,Magenta,Yellow,Magenta 
            break
        } else {
            Write-Verbose "Validated that provided drive letter '$($DriveLetter)' exists on this computer '$($env:COMPUTERNAME)'"
        }
    }

    Process { 
        Write-Log 'Current page file(s):', (Get-PageFile|Out-String) Green,Cyan 

        if ($DriveLetter -in (Get-PageFile).DriveLetter) {
            (Get-WmiObject -Class Win32_PageFileSetting | where { $_.Name.StartsWith($DriveLetter) }).Delete()
            Write-Log 'Removed page file from drive',$DriveLetter Green,Cyan
            Write-Log 'Current page file(s):', (Get-PageFile|Out-String) Green,Cyan 
            Write-Log 'Remember that a reboot is required to complete this process' Yellow
        } else {
            Write-Log 'No page file found on drive',$DriveLetter Yellow,Cyan
        }
    }

    End { }

}

#endregion

#region Active Directory

function Get-DCList {
<#
 .SYNOPSIS
  Function to provide domain controller information for the current AD forest
 
 .DESCRIPTION
  Function to provide domain controller information for the current AD forest
  This function will take several minutes for each unreachable domain controllers
 
 .EXAMPLE
  Get-DCList
 
 .OUTPUTS
  This cmdlet returns PCSutom Objects, one for each DC containing the following properties/example:
    Forest Name CurrentTime OSVersion Roles Domain IPAddress SiteName
    ------ ---- ----------- --------- ----- ------ --------- --------
    tst.local DExxxxxCRWV01.dev.tst.local 7/20/2018 10:57:07 PM Windows Server 2012 R2 Standard {PdcRole, RidRole, InfrastructureRole} dev.tst.local 10.19.11.250 DExxxx
    tst.local DExxxxxCRWV02.dev.tst.local 7/20/2018 10:57:07 PM Windows Server 2012 R2 Standard {} dev.tst.local 10.19.11.251 DExxxx
    tst.local DExxxxxCRWV01.dev.tst.local 7/20/2018 10:57:08 PM Windows Server 2012 R2 Standard {} dev.tst.local 10.29.11.250 DExxxx
    tst.local tstCJRDCRWV01.tst.local 7/20/2018 10:57:09 PM Windows Server 2012 R2 Standard {} tst.local 10.2.11.250 AAA
    tst.local tstJUNDCRWV01.tst.local 7/20/2018 10:57:09 PM Windows Server 2012 R2 Standard {} tst.local 10.12.11.251 AAA
    tst.local tstNPBDCRWV01.tst.local 7/20/2018 10:57:09 PM Windows Server 2012 R2 Standard {} tst.local 10.10.11.251 SSS
    tst.local tstTPADCRWV01.tst.local 7/20/2018 10:57:10 PM Windows Server 2012 R2 Standard {} tst.local 10.16.11.250 DDD
    tst.local tstMIADCRWV01.tst.local 7/20/2018 10:57:10 PM Windows Server 2012 R2 Standard {SchemaRole, NamingRole, PdcRole, RidRole...} tst.local 10.1.11.250 FFF
    tst.local tstCJRDCRWV02.tst.local 7/20/2018 10:57:11 PM Windows Server 2012 R2 Standard {} tst.local 10.2.11.251 GGG
    tst.local tstAZRDCRWV01.tst.local 7/20/2018 10:57:12 PM Windows Server 2016 Datacenter {} tst.local 10.80.2.250 RRR-tst
    tst.local tstMIADCRWV03.tst.local 7/20/2018 10:57:12 PM Windows Server 2016 Datacenter {} tst.local 10.1.11.251 WWW
              tstMIADCROV01.tst.local 192.168.254.251
    tst.local DMxxxxxODCV02.tst.local 7/20/2018 11:00:27 PM Windows Server 2016 Datacenter {} tst.local 10.80.6.11 EEE-tst
 
 .LINK
  https://superwidgets.wordpress.com/category/powershell/
 
 .NOTES
  Function by Sam Boutros
  v0.1 - 20 July 2018
#>


    [CmdletBinding(ConfirmImpact='Low')]Param()

    Begin {
        if (-not (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) {
            Write-Log 'Validate-TimeSync error: This cmdlet is designed to run from a domain joined computer' Magenta
            break
        }
    }

    Process {
        Write-log 'Identifying AD forest, domains, domain controllers...' Green
        $DCList = [system.directoryservices.activedirectory.Forest]::GetCurrentForest().domains.domaincontrollers | 
            select Forest,Name,CurrentTime,OSVersion,Roles,Domain,IPAddress,SiteName
        Write-Log 'Identified',$DCList.Count,'domain controllers in the',(($DCList.Domain.Name | select -Unique) -join ', '),
            'domain(s), in the',(($DCList | select -First 1).Forest),'forest' Green,Cyan,Green,Cyan,Green,Cyan,Green
    }

    End { $DCList }
} 

function Get-SBADComputer {
<#
.SYNOPSIS
 Function to get all computer objects' information from Active Directory
                       
.DESCRIPTION
 Function to get all computer objects' information from Active Directory using LDAP
 Does not need ActiveDirectory PowerShell module
 Must be run from a domain joined computer
                        
.EXAMPLE
 Get-SBADComputer
 Returns enabled computer information in the current AD domain
                        
.OUTPUTS
 Returns a PowerShell object containing the following properties:
    ComputerName
    OSName ==> For example: Windows Server 2012 R2 Standard
    DN ==> Distinguished name, for example: CN=Server10V,OU=Domain Computer,DC=mydomain,DC=com
    AD_OU ==> Active Directory Organization Unit where the computer object is located
    LastLogon ==> Date of last time the computer object logged on to AD
    ADCreated ==> Date the computer object was created in AD
 Returns nothing if the computer name is not found or a matching computer object is found but disabled
                       
.LINK
 https://superwidgets.wordpress.com/category/powershell/
                       
.NOTES
 Function by Sam Boutros
 v0.1 - 10 September 2018
#>

    [CmdletBinding(ConfirmImpact='Low')] 
    Param()
                      
    Begin {
        if (! (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) {
            Write-Log 'This function','Get-SBADComputer','must be run from a domain joined computer' Magenta, Yellow, Magenta
            break
        }
    }
                      
    Process{
        Write-Log 'Processing computer objects in the AD domain', $env:USERDNSDOMAIN Green, Cyan
        $adsi = [adsisearcher]"objectcategory=computer"
        $adsi.PageSize = 1000000 
        $adsi.filter = "(&(objectClass=Computer)(!userAccountControl:1.2.840.113556.1.4.803:=2))" # To return only the enabled computer objects
        $adsi.FindAll() | foreach {
            $obj = $_.Properties
            [PSCustomObject][ordered]@{
                ComputerName = [string]$obj.name
                OSName       = [string]$obj.operatingsystem
                DN           = [string]$obj.distinguishedname
                AD_OU        = [string](($obj.distinguishedname) -replace '^CN=[\w\d-_]+,\w\w=','' -replace ',OU=','/' -replace ',DC=.*')
                LastLogon    = (([datetime]::FromFileTime([string]$obj.lastlogon)).ToShortDateString())
                ADCreated    = ($obj.whencreated).ToShortDateString()
            }
        }
    }
                      
    End { }
}

function Get-SBADUser {
<#
.SYNOPSIS
 Function to get user objects information from Active Directory
                       
.DESCRIPTION
 Function to get user objects information from Active Directory using LDAP
 Does not need ActiveDirectory PowerShell module
 Must be run from a domain joined computer
 Used samaccounttype reference:
    SAM_DOMAIN_OBJECT 0x0
    SAM_GROUP_OBJECT 0x10000000
    SAM_NON_SECURITY_GROUP_OBJECT 0x10000001
    SAM_ALIAS_OBJECT 0x20000000
    SAM_NON_SECURITY_ALIAS_OBJECT 0x20000001
    SAM_USER_OBJECT 0x30000000
    SAM_NORMAL_USER_ACCOUNT 0x30000000
    SAM_MACHINE_ACCOUNT 0x30000001
    SAM_TRUST_ACCOUNT 0x30000002
    SAM_APP_BASIC_GROUP 0x40000000
    SAM_APP_QUERY_GROUP 0x40000001
    SAM_ACCOUNT_TYPE_MAX 0x7fffffff
 Used UserAccountControl reference:
    0x00000002 ADS_UF_ACCOUNTDISABLE The user account is disabled.
    0x00000010 ADS_UF_LOCKOUT The account is currently locked out.
    0x00000200 ADS_UF_NORMAL_ACCOUNT This is a default account type that represents a typical user.
    0x00000800 ADS_UF_INTERDOMAIN_TRUST_ACCOUNT This is a permit to trust account for a system domain that trusts other domains.
    0x00001000 ADS_UF_WORKSTATION_TRUST_ACCOUNT This is a computer account for a computer that is a member of this domain.
    0x00002000 ADS_UF_SERVER_TRUST_ACCOUNT This is a computer account for a system backup domain controller that is a member of this domain.
    0x00010000 ADS_UF_DONT_EXPIRE_PASSWD The password for this account will never expire.
    0x00020000 ADS_UF_MNS_LOGON_ACCOUNT This is an MNS logon account.
    0x00040000 ADS_UF_SMARTCARD_REQUIRED The user must log on using a smart card.
    0x00080000 ADS_UF_TRUSTED_FOR_DELEGATION The service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service.
    0x00100000 ADS_UF_NOT_DELEGATED The security context of the user will not be delegated to a service even if the service account is set as trusted for Kerberos delegation.
    0x00200000 ADS_UF_USE_DES_KEY_ONLY Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys.
    0x00400000 ADS_UF_DONT_REQUIRE_PREAUTH This account does not require Kerberos pre-authentication for logon.
    0x00800000 ADS_UF_PASSWORD_EXPIRED The user password has expired. This flag is created by the system using data from the Pwd-Last-Set attribute and the domain policy.
    0x01000000 ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION The account is enabled for delegation. This is a security-sensitive setting; accounts with this option enabled should be strictly controlled. This setting enables a service running under the account to assume a client identity and authenticate as that user to other remote servers on the network.
 
.PARAMETER samaccountname
 This is an optional parameter that takes the user's login name AKA samaccountname
 If omitted, the function will return all user accounts (excluding computer accounts)
 This parameter accepts wild cards such as *
 
.PARAMETER DomainController
 This is an optional parameter to contain the FQDN of the Domain Controller to query, as DC1.myDomain.com
 If omitted, the function will query the currently logged on domain controller
                        
.EXAMPLE
 Get-SBADUser
 Returns all users' information in the current AD domain
 
.Example
 Get-SBADUser *Sam*
 This will return all users that have 'sam' as part of the login name
 
.Example
 Get-SBADUser *test*
 This will return all users that have 'test' as part of the login name
 
.Example
 $UserList = Get-SBADUser
 $UserList | where useraccountcontrol -Match 'Normal' # list of normal working accounts
 $UserList | where useraccountcontrol -Match 'Disabled' # list of disabled accounts
 $UserList | where useraccountcontrol -Match 'PasswordNeverExpires' # list of account with passswords that never expire
 $UserList | where useraccountcontrol -Match 'Locked-Out' # list of locked out accounts
 $UserList | where useraccountcontrol -Match 'PasswordExpired' # list of accounts with expired passwords
 $UserList | where DN -Match 'OU=Partners,OU=Users,OU=Two,DC=One,DC=Domain,DC=com' | FT -a # list of accounts in the 'OU=Partners,OU=Users,OU=Two,DC=One,DC=Domain,DC=com' OU
 
.Example
 $UserName = 'samb' # Logon Name / SamName
 $DCList = Get-DCList # This may take a few minutes in large domains with many DCs across slow wan links
 $myUserLogins = foreach ($DC in ($DCList)) { Get-SBADUser -samaccountname $UserName -DomainController $DC.Name }
 $myUserLogins | where LastLogon -ne 'Never' | sort LastLogon |
    FT UserName,DomainController,
        @{n='DomainControllerIP';e={($DCList|where Name -eq $_.DomainController).IPAddress}},LastLogon -auto
 This example queries all domain controllers for a given user's information including lastlogon
 This is helpful to show where a given user has logged on last.
 This can be used along with event log analysis to audit user logons.
 
.OUTPUTS
 Returns a PowerShell object for each returned user containing the following properties/example:
    UserName : Small, Robert
    samaccountname : Robert.Small
    DateCreated : 2/4/2016 1:04:05 PM
    useraccountcontrol : {Disabled, Normal}
    lastlogon : 10/10/2018 1:56:14 PM
    DateExpires : AccountNeverExpires
    DN : CN=Small\, Robert,OU=MyOU,DC=Mysubdomain,DC=mydomain,DC=com
  Notice the use of the '\' in the DN (Distinguished Name) as an escape character for the ',' part of the CN (Common Name)
  Note: DateExpires property speaks to the account expiration not the password expiration
  Conditions that may appear under useraccountcontrol include one or more of:
   Normal
   Disabled
   Locked-Out
   PasswordNeverExpires
   PasswordExpired
             
.LINK
 https://superwidgets.wordpress.com/category/powershell/
                       
.NOTES
 Function by Sam Boutros
 v0.1 - 9 October 2018
#>

    [CmdletBinding(ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$false,ValueFromPipeline=$true)][String]$samaccountname,
        [Parameter(Mandatory=$false)][String]$DomainController = "$($env:LOGONSERVER.Replace('\\','')).$($env:USERDNSDOMAIN)"
    )
                      
    Begin {
        if (! (Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain) {
            Write-Log 'This function','Get-SBADUser','must be run from a domain joined computer' Magenta, Yellow, Magenta
            break
        }
    }
                      
    Process{
        Write-Log 'Input received: samaccountname:',$samaccountname,'DomainController:',$DomainController Green,Cyan,Green,Cyan
        Write-Log 'Querying domain controller', $DomainController Green, Cyan
        $adsi = [adsisearcher][adsi]"LDAP://$DomainController" 
        if ($samaccountname) {
            Write-Log 'Processing user',$samaccountname,'in the AD domain', $env:USERDNSDOMAIN Green,Cyan,Green,Cyan
            $adsi.filter = "(samaccountname=$samaccountname)" 
        } else {
            Write-Log 'Processing user objects in the AD domain', $env:USERDNSDOMAIN Green,Cyan
            $adsi.filter = "(&(objectClass=person)(samaccounttype=805306368))" # Filtering on person class objects, and type user account (not computer account)
        }
        $adsi.PageSize = 1000000 
        try {
            $adsi.FindAll() | foreach {
                $obj = $_.Properties
                [PSCustomObject][ordered]@{
                    UserName           = $($obj.name)
                    samaccountname     = $($obj.samaccountname)
                    DateCreated        = $($obj.whencreated)
                    useraccountcontrol = $(
                        $UAC = '{0:x}' -f $($obj.useraccountcontrol)
                        $mySubOutput = @()
                        if ($UAC[-1] -eq '2') { $mySubOutput += 'Disabled' }
                        if ($UAC[-2] -eq '1') { $mySubOutput += 'Locked-Out' }
                        if ($UAC[-3] -eq '2') { $mySubOutput += 'Normal' }
    # if ($UAC[-3] -eq '8') { $mySubOutput += 'INTERDOMAIN_TRUST_ACCOUNT' }
    # if ($UAC[-4] -eq '1') { $mySubOutput += 'ComputerAccount' }
    # if ($UAC[-4] -eq '2') { $mySubOutput += 'DomainControllerAccount' }
                        if ($UAC[-5] -eq '1') { $mySubOutput += 'PasswordNeverExpires' }
                        if ($UAC[-6] -eq '8') { $mySubOutput += 'PasswordExpired' }
                        $mySubOutput
                    )
    # lastlogontimestamp = [datetime]::FromFileTime($($obj.lastlogontimestamp) -as [int64]) # Replicates after 14 days by default
                    DomainController   = $DomainController
                    lastlogon          = $(if (($($obj.lastlogon) -as [int64]) -gt 0) {[datetime]::FromFileTime($($obj.lastlogon) -as [int64])} else {'Never'})
                    DateExpires        = $(try {[datetime]::FromFileTime($($obj.accountexpires) -as [int64])} catch {'AccountNeverExpires'})
                    DN                 = $($obj.distinguishedname)
                }
            }

        } catch {}
    }
                      
    End { }
}

#endregion

Export-ModuleMember -Function * -Variable *