Dimmo.psm1

function Set-AzStorageAccountBlobAccessTier {
<#
.SYNOPSIS
Move blobs in an Azure storage account between hot, cool and archive access tiers. By default, this function
#>


    [cmdletbinding(SupportsShouldProcess)]param(

        [Parameter(Mandatory=$true)]
        [string]$StorageAccountName,

        [Parameter(Mandatory=$true)]
        [string]$StorageAccountKey,

        [Parameter(Mandatory=$true)]
        [string]$ContainerName,

        [ValidateSet('Hot', 'Cool', 'Archive')]
        [string]$CurrentAccessTier = 'Hot',

        [ValidateSet('Hot', 'Cool', 'Archive')]
        [string]$DesiredAccessTier = 'Archive',

        [int]$MinimumFileSize = 1MB
    )

    Process {
        Write-Verbose "Connect to Azure storage account $StorageAccountName"
        $ctx = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey

        Write-Verbose "Retrieving all blobs from $ContainerName"
        $blobs = Get-AzStorageBlob -Container $ContainerName -Context $ctx

        # Set the tier of all the blobs in the container to Archive.

        $targetBlobs = $blobs | Where-Object AccessTier -EQ $CurrentAccessTier | Where-Object Length -gt $MinimumFileSize

        if ($targetBlobs.count -eq 0) { Write-Warning 'No blobs found that satisfy requirements.' }
        else {
            $Activity = "Setting access tier from $CurrentAccessTier to $DesiredAccessTier"
            $i = 0
            Write-Progress -Activity $Activity
            $targetBlobs | ForEach-Object { 
                $i++

                $msg =  "{0} {1} {2}" -f $_.LastModified, $_.Length, $_.Name
                Write-Verbose "$Activity $msg"

                if ($pscmdlet.ShouldProcess($_.Name, "Set access tier to $DesiredAccessTier")) {
                    $_.ICloudBlob.SetStandardBlobTier($DesiredAccessTier)
                }
                Write-Progress -Activity $Activity -CurrentOperation $msg -PercentComplete ([int]($i/$targetBlobs.count*100))
            }
            Write-Progress -Activity $Activity -Completed
        }
    }
}




function Install-Chocolatey {
<#
.SYNOPSIS
This command installs the Chocolatey package manager.
#>
    

    Set-ExecutionPolicy Bypass -Scope Process -Force
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
    Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
}




function Add-VMDisks {
<#
.SYNOPSIS
This script creates several virtual disks on a specific VM and initializes the disks.
 
.DESCRIPTION
Can be used to demo several things:
- capacity limits of VMs, controllers, file systems
- distribution point storage locations in SCCM-training
 
.NOTES
(c) 2018 Dimitri Koens
 
Revert:
 Get-VM $vm.Name | Get-VMHardDiskDrive | where ControllerType -eq scsi | Remove-VMHardDiskDrive
 rm $targetpath -Recurse
 
#>


    [cmdletbinding()]
    param(
        $VMName = (Get-VM | Out-GridView -OutputMode single -Title 'Select VM to add disks to').Name,
        [double]$Size = 120GB, # max 64TB
        [string]$DisksPerController = 5, # max 64
        [string]$DiskName = 'DataDisk',
        [string]$DefaultUserName = 'ADATUM\Administrator',
        [string]$DefaultPassword = 'Pa55w.rd',
        [switch]$SkipDiskInit,
        [string]$DiskLabel = 'Data'
    )

    if ($VMName -eq $null) { throw 'No VM specified' }
    $VM = Get-VM -Name $VMName
    if ($VM.count -ne 1) { throw 'you can specify only one VM' }

    [string]$TargetPath  = ((Get-VM $VMName | Get-VMHardDiskDrive).Path | Select-Object -First 1 | Split-Path)

    $Activity = "Creating disks in $TargetPath"
    0..($DisksPerController - 1) | ForEach-Object {
        $pct = $_ / $DisksPerController * 100
        $currentfile = Join-Path $TargetPath "$DiskName$_.vhdx"
        Write-Progress -Activity $Activity -PercentComplete $pct -CurrentOperation $currentfile
        New-VHD -Path $currentfile -SizeBytes $Size
        Add-VMHardDiskDrive -VMName $VM.Name -Path $currentfile -ControllerType SCSI -ControllerNumber 0 -ControllerLocation $_
    }

    if (!$SkipDiskInit) {
        if ($VM.State -ne 'Running') { throw 'VM not running. Not able to format disks.' }
        $Cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $DefaultUserName, (ConvertTo-SecureString $DefaultPassword -AsPlainText -Force)
        Write-Progress -Activity $Activity -PercentComplete 100 -CurrentOperation 'Initializing disks'
        Invoke-Command -VMName $VM.Name -Credential $Cred -ScriptBlock {
            Get-Disk |
                Where-Object PartitionStyle -eq 'RAW'| 
                Initialize-Disk -Passthru | 
                New-Partition -AssignDriveLetter -UseMaximumSize |
                Format-Volume -FileSystem NTFS -NewFileSystemLabel $DiskLabel -Confirm:$false -Force
        }
    }
    Write-Progress -Activity $Activity -Completed
}




function Show-PercentBar {
    <#
    .SYNOPSIS
    Shows an ASCII-based percent bar.
    .EXAMPLE
    Show-PercentBar -Percent 50
    Show an ASCII-based percent that is 50% full (or 50% empty).
    #>


    param
    (
        [Parameter(Mandatory = $true,
                   Position = 0,
                   ValueFromPipeline = $true,
                   ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [decimal]$Percent,

        [char]$BarCharacter = 'x',

        [char]$RemainderCharacter = '.',

        [int]$Length = 10,

        [Parameter(Mandatory = $false,
                   ParameterSetName = 'Border')]
        [ValidateLength(2,2)]
        [string]$Border = '[]',

        [Parameter(Mandatory = $true,
                   ParameterSetName = 'NoBorder')]
        [switch]$NoBorder
    )

    process
    {
        [string]$PercentBar = $BarCharacter.ToString() * [int]($Percent/100*$Length)

        $PercentBar += $RemainderCharacter.ToString() * ($Length - $PercentBar.Length)

        if (!$NoBorder)
        {
            $PercentBar = $Border[0] + $PercentBar + $Border[1]
        }

        Write-Output $PercentBar
    }
}




function Show-SystemUsage {
    <#
    .SYNOPSIS
    Shows memory and disk space usage. Nice to include in PowerShell profile.ps1.
    #>


    [CmdletBinding()]
    param(
        [string[]]$ComputerName=$env:COMPUTERNAME
    )

    foreach($computer in $ComputerName) {
        #memory
        $os = Get-CimInstance Win32_OperatingSystem -ComputerName $computer
        "{0} {1}GB {2} Memory" -f (Show-PercentBar -Percent (100-($os.FreePhysicalMemory/$os.TotalVisibleMemorySize*100)) -Length 20), ([int]($os.TotalVisibleMemorySize/1MB)).ToString().PadLeft(5), $computer

        # disks
        Get-CimInstance Win32_LogicalDisk -ComputerName $computer -Filter 'DriveType = 3' | ForEach-Object {
            "{0} {1}GB {2} {3}" -f (Show-PercentBar -Percent (100-($_.FreeSpace/$_.Size*100)) -Length 20), ([int]($_.Size/1GB)).ToString().PadLeft(5), $computer, $_.DeviceID
        }
    }
}




function Remove-VMAndAllFiles {

    <#
    .SYNOPSIS
    Removes VMs including attached disks. Does not remove parent disks from differencing disks.
    .EXAMPLE
    Remove-VMAndAllFiles VM1 -Force
    .NOTES
    https://4sysops.com/archives/confirm-confirmpreference-and-confirmimpact-in-powershell/
    https://powershell.org/forums/topic/proper-use-of-confirmimpact-and-a-force-parameter/
    http://www.iheartpowershell.com/2013/05/powershell-supportsshouldprocess-worst.html
    #>
      
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory=$true)]
        [string]$VMName,
        [switch]$Force
    )

    Process
    {
        $VMList = Get-VM -Name $VMName -ErrorAction Stop
        if ($Force) {
            foreach ($VM in $VMList) {
                if ($pscmdlet.ShouldProcess("$($VM.Name) $($VM.id)", 'Removing VM including files')) {
                    Write-Warning "Destroying $($VM.Name)"
                    $vmpath = $VM.Path
                    Get-VM $VMName | Stop-VM -TurnOff -Force -ErrorAction Stop
                    Get-VM $VMName | Get-VMHardDiskDrive | ForEach-Object {
                        # this excludes parent disks
                        Remove-Item $_.Path -ErrorAction Stop
                    }
                    Get-VM $VMName | Remove-VM -Force -ErrorAction Stop
                    $vmpathobject = Get-Item $vmpath
                    if ($vmpathobject.GetFiles().Count -eq 0 -and $vmpathobject.GetDirectories().Count -eq 0) {
                        Remove-Item $vmpathobject
                    }
                }
            } 
        } else {
            Write-Warning "You didn't specify the -Force parameter. Too bad! $VMName not destroyed."
        }
    }
}




function Convert-VMToHyperVHost {
    <#
    .SYNOPSIS
    This function converts an existing VM to a hyper-v host-capable VM. It sets:
    - CPU Count when specified
    - ExposeVirtualizationExtensions
    - Disables Dynamic mem
    - MacAddressSpoofing
    - Memory amount when specified
    #>


    [cmdletbinding()]
    param(
        $VMName,
        $CpuCount,
        $StartupBytes   # in bytes
    )

    # test if VM is off
    if ((Get-VM $VMName).State -ne 'Off') {
        Write-Error "VM $VMName is not off"
    }
    else {
        # Expose virtualization extensions
        Write-Verbose "Expose virtualization extensions for VM: $VMName"
        Get-VMProcessor -VMName $VMName | Set-VMProcessor -ExposeVirtualizationExtensions $true

        # set cpu count when specified
        if ($CpuCount) {
            $maxCpu = (Get-WmiObject win32_computersystem).NumberOfLogicalProcessors
            if ($CpuCount -gt $maxCpu) {
                $CpuCount = $maxCpu
                Write-Warning "Downgrading Cpu to $CpuCount for VM $VMName"
            } else {
                Write-Warning "Setting Cpu to $CpuCount for VM $VMName"
            }
            Get-VMProcessor -VMName $VMName | Set-VMProcessor -Count $CpuCount
        }

        # Dynamic memory is not supported
        Write-Verbose "Disabling Dynamic Memory for VM: $VMName"
        Set-VMMemory -VMName $VMName -DynamicMemoryEnabled $false

        # set memory when specified
        if ($StartupBytes) {
            Write-Verbose "Set memory to $StartupBytes for VM: $VMName"
            Set-VMMemory -VMName $VMName -StartupBytes $StartupBytes
        }

        # Enable MAC Address Spoofing
        Write-Verbose "Turning on MAC address spoofing for VM: $VMName"
        Get-VMNetworkAdapter -VMName $VMName | Set-VMNetworkAdapter -MacAddressSpoofing On
    }
}




function New-UnattendXML {
    <#
    .SYNOPSIS
    Creates output that can be used to create an Unattend.XML file for unattended deployments of Windows.
    #>


    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$true,
                    ValueFromPipelineByPropertyName=$true)]
        [string]$ComputerName,

        [Parameter(Mandatory=$false,
                    ValueFromPipelineByPropertyName=$true)]
        [string]$Timezone='Eastern Standard Time',

        [Parameter(Mandatory=$false,
                    ValueFromPipelineByPropertyName=$true)]
        [string]$RegisteredOwner='Owner',

        [Parameter(Mandatory=$false,
                    ValueFromPipelineByPropertyName=$true)]
        [string]$RegisteredOrganization='Organization',

        [Parameter(Mandatory=$false,
                    ValueFromPipelineByPropertyName=$true)]
        [string]$AdminPassword,

        [Parameter(Mandatory=$false,
                    ValueFromPipelineByPropertyName=$true)]
        [string]$DomainName,

        [Parameter(Mandatory=$false,
                    ValueFromPipelineByPropertyName=$true)]
        [string]$DomainAccount,

        [Parameter(Mandatory=$false,
                    ValueFromPipelineByPropertyName=$true)]
        [string]$DomainPassword,
        [string]$IPv4,
        [string]$SubnetMask,
        [string]$Gateway
    )

    $UnattendXML=@"
<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="specialize">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComputerName>$ComputerName</ComputerName>
            <RegisteredOrganization>$RegisteredOrganization</RegisteredOrganization>
            <RegisteredOwner>$RegisteredOwner</RegisteredOwner>
            <TimeZone>$Timezone</TimeZone>
        </component>
"@


    If($DomainName)
    {
    $UnattendXML=$UnattendXML+@"
     
        <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
                <Identification>
                    <Credentials>
                        <Domain>$DomainName</Domain>
                        <Password>$Domainpassword</Password>
                        <Username>$Domainaccount</Username>
                    </Credentials>
                    <JoinDomain>$DomainName</JoinDomain>
                    <UnsecureJoin>False</UnsecureJoin>
                </Identification>
            </component>
"@

    }


    if($IPv4)
    {
    $UnattendXML=$UnattendXML+@"
 
    <component name="Microsoft-Windows-TCPIP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <Interfaces>
        <Interface wcm:action="add">
        <Identifier>$MAC</Identifier>
        <Ipv4Settings>
            <DhcpEnabled>false</DhcpEnabled>
            <Metric>10</Metric>
            <RouterDiscoveryEnabled>false</RouterDiscoveryEnabled>
        </Ipv4Settings>
        <UnicastIpAddresses>
            <IpAddress wcm:action="add" wcm:keyValue="1">$IPv4/24</IpAddress>
        </UnicastIpAddresses>
        <Routes>
            <Route wcm:action="add">
                <Identifier>1</Identifier>
                <Metric>10</Metric>
                <NextHopAddress>$Gateway</NextHopAddress>
                <Prefix>0.0.0.0/0</Prefix>
            </Route>
        </Routes>
        </Interface>
    </Interfaces>
    </component>
"@

    }

    $UnattendXML=$UnattendXML+@"
     
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <UserAccounts>
                <AdministratorPassword>
                    <Value>$Adminpassword</Value>
                    <PlainText>true</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <RegisteredOrganization>$Organization</RegisteredOrganization>
            <RegisteredOwner>$Owner</RegisteredOwner>
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <SkipMachineOOBE>true</SkipMachineOOBE>
            </OOBE>
        </component>
    </settings>
 
    <cpi:offlineImage cpi:source="" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
    </unattend>
"@


<#
            <AutoLogon>
                <Password>
                    <Value>$Adminpassword</Value>
                    <PlainText>true</PlainText>
                </Password>
                <Username>administrator</Username>
                <LogonCount>1</LogonCount>
                <Enabled>true</Enabled>
            </AutoLogon>
#>


    Write-Output $UnattendXML
}



function New-VMFromBaseDisk {   # was CreateVM, was New-VMAsHyperVHostServer
    <#
    .SYNOPSIS
    This function creates a VM from a specified base disk.
    #>


    [cmdletbinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$VMName,

        [int]$Generation=1,   # required for VHD images

        [int]$CpuCount=1,

        [long]$MemoryStartupBytes=1GB,   # in bytes

        [Parameter(Mandatory=$true)]
        [string]$BaseDisk,

        [Parameter(Mandatory=$true)]
        [string]$DestinationFolder,

        [Parameter(Mandatory=$true)]
        [string]$VirtualSwitchName
    )

    if ((Get-VM ($VMName) -ErrorAction SilentlyContinue).count -gt 0)
    {
        Write-Warning "VM $VMName already exists. Skipping."
    } else {

        $DestinationDisk = Join-Path $DestinationFolder "$VMName.vhd"

        if (Test-Path $DestinationDisk)
        {
            Write-Warning "Virtual disk already exists: $DestinationDisk"
        } else {
            Write-Verbose " - Creating disk: $DestinationDisk from $BaseDisk"
            New-VHD -Differencing -Path $DestinationDisk -ParentPath $BaseDisk | Out-Null
        }

        Write-Verbose " - Creating VM $VMName from $BaseDisk"
        $vm = New-VM -Name $VMName -VHDPath $DestinationDisk -MemoryStartupBytes $MemoryStartupBytes -Generation $Generation -SwitchName $VirtualSwitchName
        Write-Verbose "VM $VMName created from $BaseDisk"
        Write-Output $vm
    }
}




function Install-WindowsFeatureInVHD {
    <#
    .SYNOPSIS
    Installs a Windows Feature in a VHD and waits for the VHD to dismount.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Name,

        [Parameter(Mandatory=$true)]
        [string]$Vhd

        # to do [switch]$IncludeManagementTools,

        # to do [switch]$IncludeAllSubFeature
    )

    Write-Warning "Installing $Name in $Vhd, please do not mount or unmount any disks"
    $t1vol = Get-Volume
    $IncludeAllSubFeature
    $result = Install-WindowsFeature -Name $Name -VHD $Vhd -IncludeManagementTools
    Write-Verbose 'Waiting for VHD to dismount'
    do { $t2vol = Get-Volume; Start-Sleep 1 } until ($t1vol.count -eq $t2vol.count)
    Write-Output $result
}




function New-HNVDemoEnvironment {

    <#
    .SYNOPSIS
    Creates a number of virtual machines to demonstrate Hyper-V Network Virtualization (HNV).
    .DESCRIPTION
    Required: domain name, base disk path, switch name, destination folder path
    .EXAMPLE
    New-HNVDemoEnvironment -SourceBaseFile 'D:\Hyper-V\Parent\Base17C-WS16-1607.vhd' -DestinationFolder D:\Hyper-V -SwitchName 'Internal Network' -DomainName 'adatum.com' -Password 'Pa55w.rd' -Verbose
    This command creates two hyper-v host VMs as members of the adatum.com domain, each containing two VMs.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$SourceBaseFile,#='D:\Hyper-V\Parent\Base17C-WS16-1607.vhd',

        [Parameter(Mandatory=$true)]
        [string]$DestinationFolder,#= 'D:\Hyper-V\',

        [Parameter(Mandatory=$true)]
        [string]$SwitchName,

        [Parameter(Mandatory=$true)]
        [string]$DomainName,

        [Parameter(Mandatory=$true)]
        [string]$Password,   # required for domain, also used as local admin password

        [switch]$CreateDomain
    )

    # constants
    $Servers  = 'Server1', 'Server2'
    $ProgressActivity = 'Deploying HNV demo environment'
    #$ErrorActionPreference = 'stop'
    #$VerbosePreference = 'continue'
 
    $DomainUserName = "$DomainName\Administrator"
    $SecurePassword = (ConvertTo-Securestring â€“asplaintext â€“force $password)
    $DomainCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $DomainUserName, $SecurePassword

    Write-Progress -Activity $ProgressActivity -PercentComplete 5

    # check if this machine is member of specified domain!!!!

    # prereq check
    if (!(Test-Path $SourceBaseFile)) { Write-Error "$SourceBaseFile does not exist!" } 
    $DestinationBaseFile = Join-Path $DestinationFolder 'Hyper-V-Host-VM.vhd'

    # to do: check for Convert-WindowsImage command when using iso as a source

    # get VMSwitch
    Write-Progress -Activity $ProgressActivity -PercentComplete 10 -CurrentOperation 'Examining switches'
    if ($SwitchName -eq $null) {
        $VMSwitch = Get-VMSwitch | Out-GridView -OutputMode Single -Title 'Please select switch to use'
    } else {
        $VMSwitch = Get-VMSwitch | Where-Object Name -eq $SwitchName
        # multiple switches with same name can exist
        if ($VMSwitch.count -ne 1) { Write-Error "Please make sure there is only switch with name $VMSwitch"}    
    }

    # to do optional: create base disk from iso
    # Write-Progress -Activity $ProgressActivity -PercentComplete 10 -CurrentOperation ''
    # New-BaseDiskFromIso, or Convert-WindowsImage, or check commands in DISM module

    # to do: create domain VM (for example: create demo env on stand-alone laptop)
    if ($CreateDomain) {
        Write-Progress -Activity $ProgressActivity -PercentComplete 12 -CurrentOperation 'Install Domain Controller'
        $VMDC = New-VMFromBaseDisk -VMName DC -Generation 1 -CpuCount 2 -MemoryStartupBytes 2GB -BaseDisk $SourceBaseFile -DestinationFolder $DestinationFolder -VirtualSwitchName $SwitchName
        $VMDCVhd = $VMDC | Get-VMHardDiskDrive | Select-Object -ExpandProperty Path -First 1
        Install-WindowsFeatureInVHD -Name AD-Domain-Services -Vhd $VMDCVhd | Out-Null
        Install-WindowsFeatureInVHD -Name DHCP -Vhd $VMDCVhd | Out-Null
        # write unattended file
        $VMDC | Start-VM
        Write-Host 'Please finish setup yourself (stil work to be done).'
        Write-Host 'Create AD Domain and DHCP Scope'
        Read-Host 'Press enter when done'
    }

    # create diff disk from source base file
    Write-Progress -Activity $ProgressActivity -PercentComplete 15 -CurrentOperation "Creating new differencing VHD $DestinationBaseFile from $SourceBaseFile"
    Write-Verbose "Creating new differencing VHD"
    Write-Verbose " - source $SourceBaseFile"
    Write-Verbose " - destination $DestinationBaseFile"
    if (Test-Path $DestinationBaseFile) {
        Write-Warning "$DestinationBaseFile exists."
    } else {
        New-VHD -Path $DestinationBaseFile -ParentPath $SourceBaseFile -Differencing | Out-Null
    }

    # install hyper-v role in hyper-v base VHD
    Write-Progress -Activity $ProgressActivity -PercentComplete 20 -CurrentOperation "Installing Hyper-V in $DestinationBaseFile"
    Install-WindowsFeatureInVHD -Name 'Hyper-V' -VHD $DestinationBaseFile | Out-Null

    # copy original base disk into new base disk
    Write-Progress -Activity $ProgressActivity -PercentComplete 20 -CurrentOperation "Copy $SourceBaseFile into $DestinationBaseFile"
    $DriveLetter = (Mount-VHD -Path $DestinationBaseFile -Passthru | Get-Disk | Get-Partition | Where-Object Size -gt 1GB).DriveLetter
    if ($DriveLetter.count -ne 1) { Write-Error "Incorrect drive letter: $driveletter" }
    try {
        mkdir ($DriveLetter + ':\Parent') -ErrorAction SilentlyContinue | Out-Null
        $destt = $DriveLetter + ':\Parent\' + (Split-Path -Leaf $SourceBaseFile)
        if (test-path $destt) {
            Write-Warning "$destt already exist"
        } else {
            # write-progress
            Copy-Item -Path $SourceBaseFile -Destination $destt
        }
    }
    finally {
        Dismount-VHD $DestinationBaseFile
    }

    $CurrentIP = 0
    Write-Progress -Activity $ProgressActivity -PercentComplete 25 -CurrentOperation "Creating VMs"
    foreach($server in $Servers) {
        $CurrentIP++
        # creating VM
        Write-Verbose "Creating VM: $server in $DestinationFolder from $DestinationBaseFile"
        $vm = New-VMFromBaseDisk -VMName $server -BaseDisk $DestinationBaseFile -DestinationFolder $DestinationFolder -CpuCount 2 -MemoryStartupBytes 4GB -VirtualSwitchName $VMSwitch.Name

        $unattend = New-UnattendXML -ComputerName $server -Timezone 'W. Europe Standard Time' -AdminPassword 'Pa55w.rd' -DomainName 'adatum.com' -DomainAccount 'Administrator' -DomainPassword 'Pa55w.rd' # these VMs don't need static IPs

        # mount VHD and write unattend.xml
        $VHDFromVM = (Get-VM -Name $server).HardDrives.Path
        Write-Verbose "Writing unattended file to $VHDFromVM"
        $DriveLetter = (Mount-VHD -Path $VHDFromVM -Passthru | Get-Disk | Get-Partition | Where-Object Size -gt 1GB).DriveLetter
        $unattend | Out-File ($DriveLetter + ':\unattend.xml') -Encoding utf8
        $unattend | Out-File ($DriveLetter + ':\unattend-original.xml') -Encoding utf8   # debug output
        Dismount-VHD $VHDFromVM

        Write-Verbose "Converting VM to Hyper-V Host VM: $($vm.Name)"
        Convert-VMToHyperVHost -VMName $vm.Name

        # start vm
        Write-Verbose "Starting VM: $($vm.Name)"
        Start-VM $vm.Name
    }

    # wait for boot and create virtual switch inside host vm
    Write-Progress -Activity $ProgressActivity -PercentComplete 30 -CurrentOperation "Waiting for VM's to boot"
    foreach ($server in $Servers) {
        Write-Verbose "waiting for $server to boot"
        Wait-VM -VMName $server -For IPAddress
        Write-Verbose "$server has an ip address"
        Invoke-Command -VMName $server -Credential $DomainCred {
            $nic = Get-NetAdapter -Name Ethernet
            write-verbose "Nic found $($nic.Name)"
            New-VMSwitch -Name 'External Network' -NetAdapterName $nic.Name -AllowManagementOS $true
            Write-Verbose 'Switch created'
        }
    }

    Write-Progress -Activity $ProgressActivity -PercentComplete 25 -CurrentOperation "Create four vms inside host server vms using winrm"

    $ServerList = 
    'Server1 PROD1 IP? MAC?',
    'Server1 TEST1 IP? MAC?',
    'Server2 PROD2 IP? MAC?',
    'Server2 TEST2 IP? MAC?' 
    
    foreach($server in $ServerList) {

        [string]$ComputerName=$server.split()[0]
        Write-Verbose "Connecting to $ComputerName"
        Invoke-Command -VMName $ComputerName -Credential $DomainCred {

            [string]$BaseDisk    ='C:\Parent\Base17C-WS16-1607.vhd'
            #[string]$ComputerName=$Using_.split()[0]
            [string]$VMName      = "VM$(get-random (1000))"  # $Using:server.split()[1]
            #[string]$IPAddress =$Using_.split()[2]
            #[string]$MACAddress =$Using_.split()[3]

            $VMSwitch = Get-VMSwitch #-SwitchType External
            Write-Verbose "VMSwitch: $($VMSwitch.Name)"

            $Destination = "C:\$VMName"
            mkdir $Destination
    
            # New Diff disk
            New-VHD -Differencing -ParentPath $BaseDisk -Path ($Destination + '\' + $VMName + '.vhd')
            $vhd = Get-VHD -Path ($Destination + '\' + $VMName + '.vhd')

            $vm = New-VM $VMName -Generation 1 -Path $Destination -SwitchName $VMSwitch.Name -MemoryStartupBytes 700MB -VHDPath $vhd.Path
            # MAC address? Add-VMNetworkAdapter -StaticMacAddress ??????
    
            # IP! using New-UnattendXML?: -IPv4 "172.16.0.19$CurrentIP" -SubnetMask '255.255.0.0' -Gateway '172.16.0.1'
            # nodig in hyper-v host vm: unattend-xml, dan ook gebruik maken van new-vmfrombasedisk
            # bij voorkeur uitvoeren met invoke-command $function.definition

            $vm | Start-VM
        }
    }
    Write-Progress -Activity $ProgressActivity -Completed
    Write-Host 'Hyper-V environment created.'
}




function Remove-HNVDemoEnvironment {
    <#
    .SYNOPSIS
    Removes virtual machines used to demonstrate a Hyper-V Network Virtualization (HNV).
    #>

    $VMsToDestroy = Get-VM | Get-VMProcessor | Where ExposeVirtualizationExtensions | Out-GridView -OutputMode Multiple -Title 'select VMs to destroy'

    # to do: remove AD accounts for hyper-v host VMs
    # werkt alleen als host server domain member is en RSAT-AD-PowerShell geinstalleerd is
    $VMsToDestroy | ForEach-Object { Get-ADComputer $_.VMName | Remove-ADObject -Recursive }

    $VMsToDestroy | ForEach-Object { Remove-VMAndAllFiles -VMName $_.VMName -Force }   # to do: including empty folder
    # to do: optionally delete hyper-v.vhd
    # to do: clear DNS cache??? and/or DNS records?
}




function Enable-IEProxy {
    <#
    .SYNOPSIS
    This command enables the proxy server as configured in Internet Explorer.
    #>

    Set-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -Name ProxyEnable -value 1
}




function Disable-IEProxy {
    <#
    .SYNOPSIS
    This command disables the proxy server as configured in Internet Explorer.
    #>

    Set-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -Name ProxyEnable -value 0 
}




function Get-NetworkControllerInstallationStatus {
    <#
    .SYNOPSIS
    This function summarizes several important configuration settings during deployment of a Network Controller VM from System Center Virtual Machine Manager (VMM).
     
    .NOTES
     
    Info from Network Controller deployment scripts
     
    PrepareNodeForNetworkController.ps1 configures:
    Installation directory
    IPv6
    NetworkController installation
    Add domain admin as local admin
    Certificates: server + trusted root
     
     
    InstallNetworkController-AllNodes.ps1 runs:
    New-NetworkControllerNodeObject -Name $vmName -Server $fqdn -FaultDomain $fd -RestInterface $nicName -Verbose
    Install-NetworkControllerCluster -ClusterAuthentication Kerberos -ManagementSecurityGroup $mgmtSecurityGroupName -Node $nodes -CredentialEncryptionCertificate $sslCertificate -Verbose
    Install-NetworkController -Node $nodes -ClientAuthentication Kerberos -ClientSecurityGroup $clientSecurityGroupName -ServerCertificate $sslCertificate -RestIPAddress $restEndPoint
    Install-NetworkController -Node $nodes -ClientAuthentication Kerberos -ClientSecurityGroup $clientSecurityGroupName -ServerCertificate $sslCertificate -RestName $restEndPoint
    Install-NetworkController -Node $nodes -ClientAuthentication Kerberos -ClientSecurityGroup $clientSecurityGroupName -ServerCertificate $sslCertificate # local
     
    UITWERKEN
     
    Get-Service SlbHostAgent -comp nc-vm01 # on hyper-v host server
    Get-Service NcHostAgent -comp nc-vm01 # on hyper-v host server
     
    #>

    
        param(
            [Parameter(Mandatory=$true)]
            $ComputerName,
            [switch]$Monitor,
            [int]$MonitorInterval=60
        )
    
        do {
            Invoke-Command -ComputerName $ComputerName {
    
                # suppress errors because many items will not yet exist during installation
                $ErrorActionPreference = 'SilentlyContinue'
    
                # check computername and domain membership
                $Computername = Get-ChildItem Env:\COMPUTERNAME
                $DomainName   = Get-ChildItem  Env:\USERDOMAIN
    
                # verify installation folder
                $NCInstallDir = Get-Item 'C:\NCInstall'
    
                # verify certificate file
                $CertFile = Get-ChildItem -Path 'C:\NCInstall\certificate-ssl' -Filter '*.pfx'
    
                # verify IPv6: not supported by Network Controller
                $IPv6Config   = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\' -Name 'DisabledComponents'   # -Value 0xffffffff
    
                # verify registry settings
                $NCReady      = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\SCVMM Network Controller' -Name 'NCReady'   # MarkAsReadyForNetworkControllerDeployment = 1
                $NCThumbprint = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\SCVMM Network Controller' -Name 'NCThumbprint'
    
                # verify certificate installation
                $CertFromStore = Get-ChildItem Cert:\LocalMachine\My | Where-Object Thumbprint -EQ $NCThumbprint.NCThumbprint
    
                # Verify GivePermissionToNetworkService
                #(Get-Item -path "$ENV:ProgramData\Microsoft\Crypto\RSA\MachineKeys\*" | Get-ACL).Access | Where-Object 'IdentityReference' -EQ 'NT AUTHORITY\NETWORK SERVICE'
    
                $Access = (Get-Item -path "$ENV:ProgramData\Microsoft\Crypto\RSA\MachineKeys\*" | Get-ACL).Access
                $AccessControl = ($Access | Where-Object { $_.IdentityReference -EQ 'NT AUTHORITY\NETWORK SERVICE' -and $_.AccessControlType -eq 'Allow' }).FileSystemRights | foreach { $_ -match 'Read|FullControl' }
    
                # verify SCVMM Guest Agent Service
                $ScvmmGuestService = Get-Service -Name ScvmmGuestServiceV7
    
                # check for VMM Agent installation
                $MsiEvent = Get-EventLog -LogName application -source msiinstaller -InstanceId 1033 | where Message -match agent
                
                # Verify installation status of role
                $WindowsFeature = Get-WindowsFeature #-Name NetworkController, RSAT-NetworkController
                $NCRole = $WindowsFeature | Where-Object Name -eq 'NetworkController'
                $NCRSAT = $WindowsFeature | Where-Object Name -eq 'RSAT-NetworkController'
    
                # custom output
                $properties = [ordered]@{
                    'DateTime'          = (Get-Date -format s);
                    'ComputerName'      = $Computername.Value;       # ordered, optioneel, geef in deze volgorde terug
                    'DomainName'        = $DomainName.Value;
                    'InstallDir'        = $NCInstallDir.FullName;
                    'CertificateFile'   = $CertFile.FullName;
                    'ScvmmGuestService' = $ScvmmGuestService.Status;
                    'IPv6Config'        = $IPv6Config.DisabledComponents;
                    'ThumbprintFromStore' = $CertFromStore.Thumbprint;
                    'ThumbprintFromRegistry' = $NCThumbprint.NCThumbprint;
                    'MSIEvent'          = $MsiEvent.timegenerated;
                    'AccessControl'     = $AccessControl;
                    'NCReady'           = $NCReady.NCReady;
                    'NCRole'            = $NCRole.InstallState;
                    'NCRSAT'            = $NCRSAT.InstallState;
                }
                $output = New-Object -TypeName PSObject -Property $properties
                Write-Output $output
            }
        if ($Monitor) { Start-Sleep $MonitorInterval }
        } while ($Monitor)
    }
    
    
    
    
function Get-EmptyFolders {
    param(
        $Path='.'
    )
    
    Get-ChildItem -Path $Path -Recurse -Directory | Where-Object { $_.GetFiles().Count -eq 0 -and $_.GetDirectories().Count -eq 0 }
}




function Get-VMHardDiskDriveSize {
<#
.SYNOPSIS
Displays VM Disk Drive space usage, including parent disks, excluding state files. FileInfo output by default. Use NiceReport parameter to get a summary.
#>


    param(
        [switch]$NiceReport,
        [switch]$ExcludeParentDisks
    )

    function Get-VhdSizeRecursive {
        param($Path)
        Get-ChildItem $path
        if (!$ExcludeParentDisks) {
            $vhd = Get-VHD -Path $Path
            if ($vhd.parentpath) { Get-VhdSizeRecursive $vhd.parentpath }
        }
    }

    $totOutput = $null
    Get-VM | Get-VMHardDiskDrive | ForEach-Object {
        $output = Get-VhdSizeRecursive $_.Path | Add-Member -NotePropertyName VMName -NotePropertyValue $_.VMName -PassThru
        if ($NiceReport) { $totOutput += $output } else { Write-Output $output }
    }

    if ($NiceReport) {
        $totOutput | Group-Object VMName | ForEach-Object {
            $sum = $_.Group | Measure-Object Length -Sum
            $properties = [ordered]@{
                'SizeInGB' = [int]($sum.sum/1GB)
                'VMName'   = $_.Group.VMName[0]
            }
            $output = New-Object -TypeName PSObject -Property $properties
            Write-Output $output
        }
    }
}




function Invoke-DefragAnalysis {
    <#
    .SYNOPSIS
    Performs a defragmentation analysis on the specified drive.
    .EXAMPLE
    Invoke-DefragAnalysis
    Performs a defragmentation analysis on the current drive.
    .EXAMPLE
    Invoke-DefragAnalysis -Drive D
    Performs a defragmentation analysis on the D-drive.
    #>

    
    [cmdletbinding()]
    param([string]$Drive=(Get-Location).Drive.Name)

    Write-Verbose "Analyzing $Drive..."

    $result = Get-WmiObject Win32_Volume | Where-Object { $_.name.SubString(0,1) -EQ $Drive } | Invoke-WmiMethod -Name DefragAnalysis

    switch ($result.ReturnValue) {
            0 { $e = "OK" }
            1 { $e = "Access Denied" }
            2 { $e = "Not Supported" }
            3 { $e = "Volume Dirty Bit Is Set" }
            4 { $e = "Not Enough Free Space" }
            5 { $e = "Corrupt Master File Table Detected" }
            6 { $e = "Call Canceled" }
            7 { $e = "Call Cancellation Request Too Late" }
            8 { $e = "Defrag Engine Is Already Running" }
            9 { $e = "Unable To Connect To Defrag Engine" }
        10 { $e = "Defrag Engine Error" }
        11 { $e = "Unknown Error (maybe run this elevated?)" }
    }    # switch

    if ($result.ReturnValue -eq 0) {
        
        $properties = @{
            'DefragRecommended' = $result.DefragRecommended;
            'ComputerName' = $result.DefragAnalysis.PSComputername;
            'ReturnValue' = $result.ReturnValue;
            'AverageFragmentsPerFile' = $result.DefragAnalysis.AverageFragmentsPerFile;
            'FilePercentFragmentation' = $result.DefragAnalysis.FilePercentFragmentation;
            'MFTPercentInUse' = $result.DefragAnalysis.MFTPercentInUse;
        }
        
        $output = New-Object -TypeName PSObject -Property $properties
        Write-Output $output
        
    }
    else { Write-Error "Error $($result.ReturnValue): $e" }
    
}    # function




function Get-DhcpMostRecentLease {
<#
.SYNOPSIS
Shows most recent leases from the Microsoft DHCP Server. Requires RSAT.
#>

    param(
        $ComputerName=$env:COMPUTERNAME,
        $Number=40
    )

    Get-DhcpServerv4Scope -ComputerName $ComputerName | ForEach-Object {
        Get-DhcpServerv4Lease -ComputerName $ComputerName -ScopeId $_.ScopeID | 
            Sort-Object LeaseExpiryTime -Descending | 
            Select-Object -First $Number
    }
}




function Invoke-RestMethodDemo {
<#
.SYNOPSIS
Demonstrates REST with several public URLs.
.EXAMPLE
Invoke-RestMethodDemo -MetaWeather
Displays weather information from metaweather.com
.EXAMPLE
Invoke-RestMethodDemo -RestCountries
Displays country information from restcountries.eu
.EXAMPLE
Invoke-RestMethodDemo -LaunchLibrary
Displays rocket launch information from launchlibrary.net
#>

    [cmdletbinding()]
    param(
        [switch]$ipapi,
        [switch]$ipinfo,
        [switch]$LaunchLibrary,
        [switch]$MetaWeather,
        [switch]$octocat,        
        [switch]$RestCountries,
        [switch]$SpaceXData,
        [switch]$WhereTheIssAt
    )

    if ($ipapi)         { $url = 'https://ipapi.co/8.8.8.8/json' }
    if ($ipinfo)        { $url = 'https://ipinfo.io/json' }
    if ($LaunchLibrary) { $url = 'https://launchlibrary.net/1.3/launch' }
    if ($MetaWeather)   { $url = 'https://www.metaweather.com/api/location/727232' }
    if ($octocat)       { $url = 'https://api.github.com/octocat' }
    if ($RestCountries) { $url = 'https://restcountries.eu/rest/v2/all' }
    if ($SpaceXData)    { $url = 'http://api.spacexdata.com/v3/launches/latest' }
    if ($WhereTheIssAt) { $url = 'http://api.wheretheiss.at/v1/satellites/25544' }
    
    Write-Verbose "Connecting to $url"
    $result = Invoke-RestMethod $url

    if ($LaunchLibrary) {
        $result | Select-Object -ExpandProperty Launches 
    } else {
        Write-Output $result 
    }
}




function Start-MOCLab2 {
    <#
    .SYNOPSIS
    Start required VMs for a particular lab from a MOC training (Microsoft Official Curriculum). Internet connection required.
     
    .NOTES
    By Dimitri Koens
     
    TO DO: implement confirm and whatif
     
    15-04-2014: first version
    16-05-2014: more generic for 10747 and 10748 and possibly other trainings, other CSV selection, sleep in variable
    09-11-2015: include title in out-gridview,
                                start hyper-v manager %windir%\system32\mmc.exe "%windir%\system32\virtmgmt.msc",
                                run %windir%\system32\vmconnect.exe localhost 10747D-LON-CFG-A/B/C
                                do { } while (1)
    15-12-2018: more generic for most courses based on MOC material. Source CSV hosted on Github
    10-01-2019: implemented -StartVMRemoteConsole
    #>

    

    [cmdletbinding()]
    param(
        [string]$SourceFile = 'https://raw.githubusercontent.com/Dimtemp/MOC/master/MOCLabs.csv',
        [switch]$StartHyperVConsole,
        [switch]$StartVMRemoteConsole,
        [int]$DCBootTimeout = 60
    )

    # retrieving CSV file
    Write-Progress -Activity "Reading $SourceFile"
    $WebReq = Invoke-WebRequest $SourceFile -UseBasicParsing
    $Csv = $WebReq.Content | ConvertFrom-Csv
    Write-Progress -Activity "Reading $SourceFile" -Completed

    # request training and lab
    $Training = $Csv | Select-Object MOC -Unique | Out-GridView -Title 'Select training' -OutputMode Single
    $Lab = $Csv | Where-Object MOC -EQ $Training.MOC | Select-Object Module, Lab, Time | Out-GridView -Title 'Select lab' -OutputMode Single
    Write-Verbose "Training: $($Training.MOC), Lab: $($Lab.Module)"

    #determine VMs
    $VMs = $Csv | Where-Object { $_.MOC -EQ $Training.MOC -and $_.Module -EQ $Lab.Module } # -and $_.Lab -EQ $Lab.Lab }
    $VMs = $VMs.VMs
    if ($VMs.Count -eq 0) { throw 'No VMs found' }
    $VMs = $VMs.split('/')
    Write-Verbose "VMs found: $VMs"

    # optionally start hyper-v console
    if ($StartHyperVConsole) {
        if (Get-WmiObject win32_process | Where-Object CommandLine -match 'virtmgmt.msc') {
            Write-Verbose 'Hyper-V console already running'
        } else {
            Write-Verbose 'Starting Hyper-V console'
            virtmgmt.msc
        }
    }

    # suspend VMs to minimize conflicts
    Write-Verbose 'Suspending all VMs...'
    Get-VM | Where State -eq 'Running' | Suspend-VM

    # prepare to start VMs
    $PercentComplete = 0
    $Activity = "Starting VMs for Lab {0}{1}" -f $Lab.Module, $Lab.Lab.Replace('-', '')
    # Write-Verbose $Activity

    # starting VMs
    ForEach ($CurrentVM in $VMs) {
        Write-Progress -Activity $Activity -PercentComplete $PercentComplete -CurrentOperation $CurrentVM
        Write-Verbose "Starting VM $CurrentVM"
        $VMObject = Get-VM -Name $CurrentVM

        # state can be Off, Running, Saved, Paused
        Switch ($VMObject.State) {
            'Paused' { Resume-VM -Name $CurrentVM }
            'Saved' { Start-VM -Name $CurrentVM }
            'Off' {
                Start-VM -Name $CurrentVM
                if ($CurrentVM -match 'DC1') {
                    Write-Warning "Sleeping $DCBootTimeout seconds for DC to boot..."
                    Start-Sleep $DCBootTimeout
                }
            }
        }

        # optionally start vm remote console
        if ($StartVMRemoteConsole) {
            Write-Verbose "Starting Virtual Machine Connection for $CurrentVM"
            vmconnect.exe localhost $CurrentVM
        }

        $PercentComplete += [int](100/$VMs.Count)
    }
    Write-Progress -Activity $Activity -Completed
    
    Write-Verbose 'Saving VMs that were previously paused...'
    Get-VM | Where State -eq 'paused' | Save-VM
}

    


function Get-Ticker {
<#
.SYNOPSIS
Retrieves ticker info from Alpha Vantage.
 
.DESCRIPTION
ApiKey published on https://gist.github.com/quonic/fc0a76e8a8925e91dc1937bfc19aef28
A Free API key provides for 5 API requests per minute; 500 API requests per day.
 
.EXAMPLE
Get-Ticker -Ticker MSFT
Gets ticker info based on symbol name.
 
.EXAMPLE
Get-Ticker -CompanyName Microsoft
Gets ticker info based on Company name.
 
.NOTES
Based on https://gist.github.com/quonic/fc0a76e8a8925e91dc1937bfc19aef28
#>


    [CmdletBinding()]
    param(
        [string]$Symbol,
        [string]$CompanyName,
        [string]$apiKey='KPCCCRJVMOGN9L6T'
    )

    $Query = ""
    if ($Symbol) {
        $Query = $Symbol.Replace(' ', '+')
    }
    elseif ($CompanyName) {
        $Query = $CompanyName.Replace(' ', '+')
    }
    Write-Verbose "Query: $Query"
    if (!$Query) { throw 'you must provide a value for company or symbol' }

    # intermediate code to retrieve most popular symbol
    $response = Invoke-WebRequest "http://d.yimg.com/autoc.finance.yahoo.com/autoc?query=$Query&region=1&lang=en%22" -UseBasicParsing
    $data = $response.Content | ConvertFrom-Json
    $ySymbol = $data.ResultSet.Result[0].symbol
    Write-Verbose "Symbol: $ySymbol"

    # use Invoke-RestMethod instead of Invoke-WebRequest
    $data = Invoke-RestMethod "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=$($ySymbol)&apikey=$($apiKey)"
    Write-Verbose "Result size: $($data.length)"
    Write-Verbose $data.ToString()
    if ($data.note -match 'Thank you for using Alpha Vantage') { throw '' }

    #$URI = 'https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=' + $APIKey
    # &datatype=csv
    # 'https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=15min&outputsize=full&apikey=demo'
    # 'https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=demo'

    $stockInfo = $data.Content | ConvertFrom-Json

    $properties = [ordered]@{
        'symbol'      = $ySymbol;   #$($stockInfo.'Global Quote'.'01. symbol')
        'open'        = $($stockInfo.'Global Quote'.'02. open')
        'high'        = $($stockInfo.'Global Quote'.'03. high')
        'low'         = $($stockInfo.'Global Quote'.'04. low')
        'latestPrice' = $($stockInfo.'Global Quote'.'05. price')
        'volume'      = $($stockInfo.'Global Quote'.'06. volume')
        'lastUpdated' = $($stockInfo.'Global Quote'.'07. latest trading day')
        'close'       = $($stockInfo.'Global Quote'.'08. previous close')
        'priceChange' = $($stockInfo.'Global Quote'.'09. change')
        'priceChangePercentage' = $($stockInfo.'Global Quote'.'10. change percent')
    }
    $output = New-Object -TypeName PSObject -Property $properties
    Write-Output $output
}




function Measure-HashSpeed {
<#
.SYNOPSIS
Measures the hash speed of several algorithms on a specified file. Use a large file (> 1GB) to get accurate results.
#>

    param($filename=(throw 'you must specify a filename'))
    $hashes = 'SHA1', 'SHA256', 'SHA384', 'SHA512', 'MACTripleDES', 'MD5', 'RIPEMD160'
    $hashes | ForEach-Object { 
        $timeTaken = (Measure-Command { Get-FileHash -Algorithm $_ -Path $filename }).TotalSeconds
        "{0,12}: {1,9:N5} seconds" -f $_, $timeTaken
    }
}




function Send-Tweet {

    <#
    .SYNOPSIS
    Sends Twitter tweets
    #>

    
    [cmdletbinding(SupportsShouldProcess=$true)]
    param (
        [Parameter(Mandatory=$true)]
        [string]$Message,
        
        [Parameter(Mandatory=$true)]
        [string]$ConsumerKey,
        
        [Parameter(Mandatory=$true)]
        [string]$ConsumerSecret,

        [Parameter(Mandatory=$true)]
        [string]$AccessToken,

        [Parameter(Mandatory=$true)]
        [string]$AccessTokenSecret

    )

    process {      
        $MaxMessageLength = 280
        [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
        [Reflection.Assembly]::LoadWithPartialName("System.Net") | Out-Null
        
        if ($Message.Length -gt $MaxMessageLength) { $Message = $Message.Substring(0, $MaxMessageLength-1) }
        Write-Verbose "Message length: $($Message.length) characters"
        $status = [System.Uri]::EscapeDataString($Message)
        $oauth_nonce = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes([System.DateTime]::Now.Ticks.ToString()))
        $ts = [System.DateTime]::UtcNow - [System.DateTime]::ParseExact("01-01-1970", "dd/MM/yyyy", $null).ToUniversalTime()
        # to do: test with diff language settings. Works with: Dutch, d-M-yyyy, location NL, non-unicode: English (US)
        $oauth_timestamp = [System.Convert]::ToInt64($ts.TotalSeconds).ToString()
    
        $signature = "POST&"
        $signature += [System.Uri]::EscapeDataString("https://api.twitter.com/1.1/statuses/update.json") + "&"
        $signature += [System.Uri]::EscapeDataString("oauth_consumer_key=" + $ConsumerKey + "&")
        $signature += [System.Uri]::EscapeDataString("oauth_nonce=" + $oauth_nonce + "&")
        $signature += [System.Uri]::EscapeDataString("oauth_signature_method=HMAC-SHA1&")
        $signature += [System.Uri]::EscapeDataString("oauth_timestamp=" + $oauth_timestamp + "&")
        $signature += [System.Uri]::EscapeDataString("oauth_token=" + $AccessToken + "&")
        $signature += [System.Uri]::EscapeDataString("oauth_version=1.0&")
        $signature += [System.Uri]::EscapeDataString("status=" + $status)
    
        $signature_key = [System.Uri]::EscapeDataString($ConsumerSecret) + "&" + [System.Uri]::EscapeDataString($AccessTokenSecret)
    
        $hmacsha1 = new-object System.Security.Cryptography.HMACSHA1
        $hmacsha1.Key = [System.Text.Encoding]::ASCII.GetBytes($signature_key)
        $oauth_signature = [System.Convert]::ToBase64String($hmacsha1.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($signature)))
    
        $oauth_authorization = 'OAuth '
        $oauth_authorization += 'oauth_consumer_key="' + [System.Uri]::EscapeDataString($ConsumerKey) + '",'
        $oauth_authorization += 'oauth_nonce="' + [System.Uri]::EscapeDataString($oauth_nonce) + '",'
        $oauth_authorization += 'oauth_signature="' + [System.Uri]::EscapeDataString($oauth_signature) + '",'
        $oauth_authorization += 'oauth_signature_method="HMAC-SHA1",'  
        $oauth_authorization += 'oauth_timestamp="' + [System.Uri]::EscapeDataString($oauth_timestamp) + '",'
        $oauth_authorization += 'oauth_token="' + [System.Uri]::EscapeDataString($AccessToken) + '",'
        $oauth_authorization += 'oauth_version="1.0"'
    
        $post_body = [System.Text.Encoding]::ASCII.GetBytes("status=" + $status)

        if ($pscmdlet.ShouldProcess("$post_body", "Send-Tweet")) {
            try {
                [System.Net.HttpWebRequest] $request = [System.Net.WebRequest]::Create("https://api.twitter.com/1.1/statuses/update.json")
                $request.Method = "POST"
                $request.Headers.Add("Authorization", $oauth_authorization)
                $request.ContentType = "application/x-www-form-urlencoded"
                $body = $request.GetRequestStream()
                $body.write($post_body, 0, $post_body.length)
            }
            catch {
                Write-Warning "Something went wrong: $($error[0])"
            }
            finally {
                $body.flush()  
                $body.close()
                $response = $request.GetResponse()
                Write-Output $response
                if ($response.statuscode -ne 'OK') { Write-Warning 'Statuscode not OK' }
            }
        }
    }
}
    
    


function Get-EventLogInterestingEvents {
<#
.SYNOPSIS
Displays popular events from the eventlog. For example: unexpected shutdown, failed logon, account locked out.
.NOTES
To do:
    * Domain Controller disk caching: 1539, ActiveDirectory_DomainService
    * 13512, NTFRS: The File Replication Service has detected an enabled disk write cache on the drive containing the directory c:\windows\ntfrs\jet on the computer DC4. The File Replication Service might not recover when power to the drive is interrupted and critical updates are lost.
    * Syslog/USER32/ID 1076: reason na unexpected shutdown
    * Syslog/USER32/ID 1074: reason na expected shutdown
    * Syslog/Disk/ID 7: bad block error
    * Syslog/Disk/ID 51: paging warning
    * Syslog/Ntfs/ID 55: fs corrupt (deze zit al in Win-MP? NTFS - File System Corrupt. Event id 41??? Genereert alert???)
    * Applog/wininit/1001: Checking File System Occurred on Startup (zit al in Win-MP)
    * System log, Source: Browser, IDs: 8021, 8032
 
    * 200 cap auth, user, ip addr, rdp gateway destination?
    * 300 rap auth, user, ip addr, destination
    * RD Gateway Connection Established * 302: * connect, user, ip addr, destination
    * RD Gateway failed logon: 304
    * RD Gateway Connection disconnected 303 disco, user, ip addr, destination, xfer and rcvd bytes
 
    Get-WinEvent [-FilterHashtable] <hashtable[]> [-MaxEvents <long>] [-ComputerName <string>]
#>


    param(
        [string[]]$ComputerName = $env:COMPUTERNAME,
        [switch]$UnexpectedShutdown,
        [switch]$Bootstrap,
        [switch]$STOPError,
        [switch]$BugCheck,
        [switch]$FailedLogon,
        [switch]$AccountLockedOut,
        [switch]$PreAuthenticationFailed,
        [switch]$MsiInstalled,
        [switch]$RDGatewayConnected,
        [switch]$RDGatewayFailedLogon,
        [switch]$RDGatewayDisconnected
    )

    foreach($Computer in $ComputerName) {
        if ($UnexpectedShutdown) { Get-EventLog -ComputerName $Computer -LogName System -Source 'EventLog' | Where-Object EventId -eq 6008 }
        if ($Bootstrap)          { Get-EventLog -ComputerName $Computer -LogName System -Source 'EventLog' | Where-Object EventId -eq 1001 }
        if ($STOPError)          { Get-EventLog -ComputerName $Computer -LogName System -Source 'System'   | Where-Object EventId -eq 1003 }
        if ($BugCheck)           { Get-EventLog -ComputerName $Computer -LogName System -Source 'BugCheck'   | Where-Object EventId -eq 1001 }
        if ($MsiInstalled)       { Get-EventLog -ComputerName $Computer -LogName Application -Source 'MsiInstaller' | Where-Object EventId -eq 1033 }

        if ($FailedLogon)        { Get-EventLog -ComputerName $Computer -LogName Security -Source 'Microsoft-Windows-Security-Auditing' | Where-Object EventId -eq 4625 }
        if ($AccountLockedOut)   { Get-EventLog -ComputerName $Computer -LogName Security -Source 'Microsoft-Windows-Security-Auditing' | Where-Object EventId -eq 4740 }
        if ($PreAuthenticationFailed) { Get-EventLog -ComputerName $Computer -LogName Security -Source 'Microsoft-Windows-Security-Auditing' | Where-Object EventId -eq 4771 }

        if ($RDGatewayConnected)    { Get-EventLog -ComputerName $Computer -LogName 'Microsoft-Windows-TerminalServices-Gateway/Operational' -Source 'Microsoft-Windows-Security-Auditing' | Where-Object EventId -eq 4771 }   # werkt
        if ($RDGatewayDisconnected) { Get-EventLog -ComputerName $Computer -LogName 'Microsoft-Windows-TerminalServices-Gateway/Operational' -Source 'Microsoft-Windows-Security-Auditing' | Where-Object EventId -eq 4771 }   # werkt
        if ($RDGatewayFailedLogon)  { Get-EventLog -ComputerName $Computer -LogName 'Microsoft-Windows-TerminalServices-Gateway/Operational' -Source 'Microsoft-Windows-Security-Auditing' | Where-Object EventId -eq 4771 }   # werkt
    }
}




function Get-ServiceMemoryUsage {
<#
.SYNOPSIS
Get memory usage by service.
#>

    param($ComputerName = $env:COMPUTERNAME)

    Get-WmiObject win32_service -ComputerName $ComputerName |
        Where-Object processid -gt 0 | foreach {
        $svc = $_
        $getsvc = Get-Service -ComputerName $ComputerName
        $proc = Get-Process -ComputerName $ComputerName -id $svc.processid
        $props = [ordered]@{
            #'ServiceDisplayName' = $svc.name;
            'NPM' = $proc.NPM;
            'PM' = $proc.PM;
            'WS' = $proc.WS;
            'ProcessName' = $proc.name;
            'ServiceName' = $svc.name;
            'ServiceDisplayName' = ($getsvc | Where-Object Name -eq $svc.Name).DisplayName;
        }
        New-Object -TypeName PSObject -Property $props
    }
}




function Get-AzureStackDeploymentStatus {
    $file = Join-Path $env:programdata 'Microsoft\AzureStack\AzureStackDeploymentStatus.xml'
    [xml]$asdeployfile = Get-Content $file
    $s1 = $asdeployfile.AzureStackDeploymentStatus.TaskGroups.TaskGroup.task

    do {
        sleep 3
        [xml]$asdeployfile = Get-Content $file
        $s2 = $asdeployfile.AzureStackDeploymentStatus.TaskGroups.TaskGroup.task
    
        Compare-Object $s1 $s2 -Property id, status -passthru | where sideindicator -eq '=>' | foreach {
            Write-Host ("{0:hh:mm:ss} {1} {2}" -f (get-date), $_.id, $_.status)
        }
        $s1 = $s2
    } while (1)
}




function Get-DuplicateFiles {
<#
.SYNOPSIS
    Generates a list of duplicate files in the current or specified directory.
.DESCRIPTION
    Duplicate files are calculated using a filehash. This can be very time consuming and resource intensive. Only files between 1 MB and 1 GB are scanned.
.EXAMPLE
    Get-DuplicateFiles
    This command scans for duplicate files in the current directory.
.EXAMPLE
    Get-DuplicateFiles -MinimumFileSize 1KB -MaximumFileSize 1GB
    This command scans for duplicate files in the current directory specifying alternate file sizes.
.EXAMPLE
    Get-DuplicateFiles -Path C:\Data -Filter *.PDF -Verbose
    This command scans for duplicate PDF files in the specified directory, and displays verbose output.
.EXAMPLE
    Get-DuplicateFiles -Path C:\Data -Verbose | Export-CliXml Dups.xml
    This command scans for duplicate files in the specified directory, displays verbose output, and writes to a CliXml file for future reference.
.EXAMPLE
    Get-DuplicateFiles | Out-GridView -OutputMode Multiple | Remove-Item -Confirm
    This command scans for duplicate files in the current directory and displays a graphical listing. Any selected files will be removed after confirmation.
.LINK
    www.dimensionit.nl
.NOTES
    By Dimitri Koens
 
    Contact me through:
    http://www.DimensionIT.nl
    Twitter: @DimensionIT https://twitter.com/DimensionIT
    Linkedin: http://nl.linkedin.com/in/dimitrikoens
    Facebook: http://www.facebook.com/dimitri.koens
 
    This function uses Get-FileHash function introduced in PowerShell 4.
    requires ps4?
 
    To do: implement workflows?, start-job?
 
#>


    [CmdletBinding()]
    param(
        # Specify a path with files to be checked for duplicates.
        [string[]]$Path = (Get-Location),   #(Get-Location).ProviderPath

        # Specify a filesystem filter for filenames or extensions to be checked
        [string]$Filter = '*',

        # Minimum file size to be checked.
        [int64]$MinimumFileSize = 1MB,

        # Maximum file size to be checked. Large files can take a long time to check.
        [int64]$MaximumFileSize = 1TB,

        # Different algorithms can have a huge impact on performance. SHA1 and MD5 are fast but regarded least reliable.
        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MACTripleDES', 'MD5', 'RIPEMD160')]
        [string]$Algorithm='SHA256',

        # When specified sorts files by length ascending.
        [switch]$Sort
    
        # When specified will only calculate filehash on a section of the file. This can produce unreliable results.
        #[switch]$FastAndUnreliable
    
        #implement filter, exclude, params from get-childitem
    )

    Write-Progress -Activity "Traversing" -CurrentOperation "$path" -Percent 0

    $Files = Get-ChildItem -Path $Path -Recurse -File |
        Where-Object { $_.Length -ge $MinimumFileSize -and $_.Length -le $MaximumFileSize }

    # sort on length when required
    if ($Sort) { $Files = $Files | Sort-Object Length }

    $Files = $Files |
        Group-Object -Property Length |
        Where-Object { $_.Count -gt 1 }

    Write-Progress -Activity "Traversing" -CurrentOperation "Calculating total file size and number of suspects..." -Percent 80
    $SuspectsFound = 0
    $TotalSize = 0
    $Files | Foreach {
        $_.Group | Foreach { 
            $TotalSize += $_.Length
            $SuspectsFound++
        }
    }
    Write-Progress -Activity "Traversing" -Completed
    Write-Verbose "$("{0:N2}" -f ($TotalSize/1GB)) GB in $SuspectsFound suspect files"
    # if ($FastAndUnreliable) { Write-Warning "Calculating filehash on a section of the file. This can produce unreliable results!" }

    $start = Get-Date
    $i = 0
    $bytesProcessed = 0
    $DuplicateFilesFound = 0

    # PROCESS
    $Files | ForEach-Object {

        $_.Group | ForEach-Object {

            $i++

            $CurrentOp = "{0} Duplicates found. File {1} of {2}, {3:N2} GB in {4}" -f $DuplicateFilesFound, $i, $SuspectsFound, ($_.length/1GB), $_.FullName

            # only include remaining time when reliable
            if ($bytesProcessed -ge 1 -and ((Get-Date)-$start).TotalSeconds -gt 3) { 

                $TotalEstTime = ((Get-Date)-$start).TotalSeconds/$bytesProcessed*$TotalSize   # in seconds
                $secRemaining = $TotalEstTime - ((Get-Date)-$start).totalseconds

                Write-Progress -Activity "Calculating hash value" -CurrentOperation $CurrentOp -Percent ($bytesProcessed/$TotalSize*100) -SecondsRemaining $secRemaining

            } else {

                Write-Progress -Activity "Calculating hash value" -CurrentOperation $CurrentOp -Percent ($bytesProcessed/$TotalSize*100)

            }

            #if ($FastAndUnreliable) {
            $FileHash = (Get-FileHash -Path $_.FullName -Algorithm $Algorithm).Hash   # rewrite with try ... catch
            
            $_ | Add-Member -MemberType NoteProperty -Name FileHash -Value $FileHash
            $bytesProcessed += $_.length

        } # $_.Group

      $_.Group |
        Where-Object { $_.FileHash -ne $null } |   # if FileHash is null file could probably not be opened for reading - filehash is still in properties, CHECK !!!!!!!!!!!!!!!!!!!!!!!!
        Group-Object -Property FileHash |
        Where-Object { $_.Count -gt 1 } |
        Foreach {
            $DuplicateFilesFound++
            $_.Group # don't select, because piping to remove-item won't work anymore, use type ps1xml instead
            $_.Group | ForEach { Write-Debug ($("{0,12} bytes {1}" -f $_.Length, $_.FullName)) }
        }

    } # $Files

    Write-Progress -Activity "Calculating hash value" -Completed

    $TimeTaken = ((Get-Date) - $start).TotalSeconds
    if     ($TimeTaken -lt 60)   { Write-Verbose ("{0} Duplicates found, time taken: {1:N1} seconds" -f $DuplicateFilesFound,  $TimeTaken) }
    elseif ($TimeTaken -lt 3600) { Write-Verbose ("{0} Duplicates found, time taken: {1:N1} minutes" -f $DuplicateFilesFound, ($TimeTaken/60)) }
    else                         { Write-Verbose ("{0} Duplicates found, time taken: {1:N1} hours"   -f $DuplicateFilesFound, ($TimeTaken/3600)) }
    
    if ($DuplicateFilesFound -lt 1) { Write-Warning 'No duplicate files found' }
    
}   # end of function




function Get-FolderSpaceReport2 {
    <#
    .SYNOPSIS
        Returns a list of total space usage per folder
    #>

    [cmdletbinding()]
    param([string]$Path='.')

    Get-ChildItem -Directory -Path $Path |    ForEach-Object {
        Write-Verbose $_
        $measure = Get-ChildItem -Path $_.FullName -Recurse -File | Measure-Object -Property Length -Sum
        $properties = [ordered]@{
            Count    = $measure.count
            'Sum(G)' = [int]($measure.sum/1GB)
            Name     = $_.Name
        }
        $output = New-Object -TypeName PSObject -Property $properties
        Write-Output $output
     }
}




function Enable-RDPThroughWMI {
    <#
    .SYNOPSIS
        This function enables Remote Desktop connections through WMI
    .DESCRIPTION
        Remote Desktop is disabled by default. With this function you can enable Remote Desktop through the WMI interface, which is enabled by default on many Windows systems.
    .EXAMPLE
        Enable-RDPThroughWmi -ComputerName server5
        This command enables Remote Desktop on server5.
    .EXAMPLE
        Enable-RDPThroughWmi -ComputerName server5 -Verbose
        This command enables Remote Desktop on server5 and displays verbose output.
    #>

    
    [cmdletbinding()]param(
        $ComputerName=$env:COMPUTERNAME
    )

    Write-Verbose "Invoking WMI"
    Try {
        $cmd = "cmd /c powershell Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\' -Name 'fDenyTSConnections' -Value 0 -Force"
        $wmi = Invoke-WmiMethod -class Win32_process -name Create -ArgumentList $cmd -ComputerName $ComputerName -ErrorAction Stop
    Get-NetFirewallRule | Where-Object DisplayGroup -eq 'Remote Desktop' | Enable-NetFirewallRule   # VERIFY
        if ($wmi.ReturnValue -eq 0) { Write-Verbose "Success. RDP is now enabled." } else { Write-Error "WMI failed. ReturnValue: $($wmi.ReturnValue)" }
    }
    Catch {
        Write-Error "WMI failed. Make sure Remote WMI service is running and allowed in firewall."
    }
}


function Enable-PSRemotingThroughWmi {
    <#
    .SYNOPSIS
        This function enables PowerShell Remoting through WMI
    .DESCRIPTION
        PowerShell Remoting is disabled by default. It can be enabled by running the command Enable-PSRemoting. With this function you can enable PowerShell Remoting through the WMI interface, which is enabled on many Windows systems.
    .EXAMPLE
        Enable-PSRemotingThroughWmi -ComputerName server5
        This command enables PowerShell Remoting on server5.
    .EXAMPLE
        Enable-PSRemotingThroughWmi -ComputerName server5 -Verbose
        This command enables PowerShell Remoting on server5 and displays verbose output.
    #>

    
    [cmdletbinding()]param(
        $ComputerName=$env:COMPUTERNAME
    )

    Write-Verbose 'Invoking WMI'
    Try {
        $cmd = "cmd /c powershell enable-psremoting -force -skipnetworkprofilecheck && net stop WinRM && net start WinRM && net stop MpsSvc && net start MpsSvc"
        $wmi = Invoke-WmiMethod -class Win32_process -name Create -ArgumentList $cmd -ComputerName $ComputerName -ErrorAction Stop
        if ($wmi.ReturnValue -eq 0) { Write-Verbose "Success. It may take up to 60 seconds to complete..." } else { Write-Error "WMI failed. ReturnValue: $($wmi.ReturnValue)" }
    }
    Catch {
        Write-Error "WMI failed. Make sure Remote WMI service is running and allowed in firewall."
    }
        
}


function NetworkMonitor {

    <#
    .SYNOPSYS
        Sends a ping to all hosts on the local subnet.
 
    .DESCRIPTION
        This function sends a ping to all hosts on the local subnet. This is repeated every 3 seconds to monitor hosts that are coming online or are going offline. The interval is configurable through the interval parameter.
 
    .EXAMPLE
        NetworkMonitor
        This command sends a ping to all hosts with the default interval of 3 seconds.
 
    .EXAMPLE
        NetworkMonitor -interval 30
        This command sends a ping to all hosts with an interval of 30 seconds.
 
    .EXAMPLE
        NetworkMonitor -noloop
        This command sends a ping to all hosts on the local subnet and stops. Output is pipeline-ready.
 
    .NOTES
        By Dimitri Koens
        http://www.dimensionit.nl
        Doesn't run on W7SP1
    #>


    [cmdletbinding()]param
    (
        [int]$interval=3,
        [switch]$noloop
    )

    function Get-IPrange
    { 
        param  
        (  
          [string]$start,  
          [string]$end,  
          [string]$ip,  
          [string]$mask,  
          [int]$cidr  
        )  
  
    function IP-toINT64 () {  
      param ($ip)  
      $octets = $ip.split(".")  
      return [int64]([int64]$octets[0]*16777216 +[int64]$octets[1]*65536 +[int64]$octets[2]*256 +[int64]$octets[3])  
    }  
  
    function INT64-toIP() {  
      param ([int64]$int)  
      return (([math]::truncate($int/16777216)).tostring()+"."+([math]::truncate(($int%16777216)/65536)).tostring()+"."+([math]::truncate(($int%65536)/256)).tostring()+"."+([math]::truncate($int%256)).tostring() ) 
    }  
  
    if ($ip) {$ipaddr = [Net.IPAddress]::Parse($ip)}  
    if ($cidr) {$maskaddr = [Net.IPAddress]::Parse((INT64-toIP -int ([convert]::ToInt64(("1"*$cidr+"0"*(32-$cidr)),2)))) }  
    if ($mask) {$maskaddr = [Net.IPAddress]::Parse($mask)}  
    if ($ip) {$networkaddr = new-object net.ipaddress ($maskaddr.address -band $ipaddr.address)}  
    if ($ip) {$broadcastaddr = new-object net.ipaddress (([system.net.ipaddress]::parse("255.255.255.255").address -bxor $maskaddr.address -bor $networkaddr.address))}  
  
    if ($ip) {  
      $startaddr = IP-toINT64 -ip $networkaddr.ipaddresstostring  
      $endaddr = IP-toINT64 -ip $broadcastaddr.ipaddresstostring  
    } else {  
      $startaddr = IP-toINT64 -ip $start  
      $endaddr = IP-toINT64 -ip $end  
    }  
  
  
    for ($i = $startaddr; $i -le $endaddr; $i++)  
    {  
      INT64-toIP -int $i  
    } 
 
    } # function get-iprange

    function PingAsync {
        param($ComputerName)
        $t=$ComputerName | foreach { (New-Object Net.NetworkInformation.Ping).SendPingAsync($_,250) }
        [Threading.Tasks.Task]::WaitAll($t)
        $t.Result
    }

    Write-Host "NetworkMonitor: ping entire subnet, use with care! (interrupt with Ctrl-C)" -ForegroundColor Cyan

    $netip = Get-NetIPAddress -AddressFamily ipv4 | 
        Where-Object ipaddress -notmatch '127\.0\.0\.1'   # exclude apipa range: |169\.254\.

    if ($netip.count -gt 1)
    {
        $netip = $netip | 
            Out-GridView -OutputMode single -Title 'Select an IP address' 
    }

    $ips = Get-IPrange -ip $netip.IPAddress -cidr $netip.PrefixLength
    $ips = $ips[1..($ips.count-2)]   # exclude network address and broadcast address

    if ($ips.count -gt 256)
    {
        Write-Warning "IP Range very large!"
    }

    Write-Host ("{0:hh:mm:ss} Sending initial ping to {1} addresses... " -f (get-date), $ips.count) -NoNewline

    $res1 = PingAsync $ips

    Write-Host "$(($res1 | Where-Object Status -eq 'Success').count) IP addresses responding"
    
    if (!$noloop) {
        $res1 | 
            Where-Object Status -eq 'Success' | 
            foreach { Write-Host ("{0:hh:mm:ss} {1,-15} is responding" -f (get-date), $_.address) -foregroundcolor green }
    }
    else {
        $res1 | 
            Where-Object Status -eq 'Success' |
            Select-Object Status, Address, RoundtripTime
    }

    while(!$noloop) {

        Start-Sleep $interval
        $res2 = PingAsync $ips

        Compare-Object $res1 $res2 -Property status, address -passthru | foreach {

            if ($_.sideIndicator -eq "=>" -and $_.status -eq 'success') {
                Write-Host ("{0:hh:mm:ss} {1,-15} is responding"     -f (get-date), $_.address) -foregroundcolor green  
            }   # success

            if ($_.sideIndicator -eq "<=" -and $_.status -eq 'success') { 
                Write-Host ("{0:hh:mm:ss} {1,-15} is not responding" -f (get-date), $_.address) -foregroundcolor yellow
            }   # prev success

        } # foreach

      $res1 = $res2

    }  # while
} # function


function ProcessMonitor {

    <#
    .SYNOPSIS
    Displays changes in the process list on this or a remote PC.
    .DESCRIPTION
    Great for monitoring logon/startup scripts, batch jobs, software installations, etc... Especially on terminal servers. Works for local and remote computers.
    .EXAMPLE
    ProcessMonitor
    Compares changes in the process list every second on the local computer.
    .EXAMPLE
    ProcessMonitor -Interval 30
    Compares changes in the process list for every 30 seconds.
    .EXAMPLE
    ProcessMonitor -Computername ServerB
    Compares changes in the process list on ServerB.
    .NOTES
    Created by Dimitri Koens, www.dimensionit.nl
    Version 1.3: display current time when results in compare are empty
    Version 1.4: commandlineWidth implemented
    Next version: adapt for ISE
    #>


    param([int]$Interval=1, [string]$Computername='.')

    Write-Host 'ProcessMonitor (interrupt with Ctrl-C)' -ForegroundColor Cyan

    $minimumWidth = 40
    $refProcs = Get-WmiObject win32_process -ComputerName $Computername

    Do {
      Start-Sleep $Interval
      $diffProcs = Get-WmiObject win32_process -ComputerName $Computername
      $result = Compare-Object $refProcs $diffProcs -Property ProcessId -passthru
      $result | foreach {

        # construct primary string
        $msg = "{0:hh:mm:ss} {1,5} pid {2,15} " -f (Get-Date) , $_.ProcessId, $_.Name

        # construct rest of string, .commandline also contains .path
        $commandlineWidth = $Host.UI.RawUI.WindowSize.Width - $msg.Length   # measure everty time to address screen resize
        If ($commandlineWidth -lt $MinimumWidth) { $commandlineWidth = $MinimumWidth }
        If ($_.commandline.length -lt $commandlineWidth) { 
            $msg = $msg + $_.commandline   
        } else {
            $msg = $msg + $_.commandline.SubString(0,$commandlineWidth-1)
        }

        if ($_.sideIndicator -eq "=>") { Write-Host $msg -foregroundcolor green  }   # new process running
        if ($_.sideIndicator -eq "<=") { Write-Host $msg -foregroundcolor yellow }   # existing process stopped

      } # foreach
      if ($result -eq $null) { 
        $msg = "{0:hh:mm:ss}" -f (Get-Date)
        Write-Host -NoNewline $msg
        $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates 0,($Host.UI.RawUI.CursorPosition.y)
      }
      $refProcs = $diffProcs
    } while (1)

} # function


function ServiceMonitor {
    <#
    .SYNOPSIS
    Displays changes in services list
    .DESCRIPTION
    version 1.0
    .EXAMPLE
    ServiceMonitor
    Compares changes in the services list every second on the local computer or remote computers.
    .EXAMPLE
    ServiceMonitor -Interval 30
    Compares changes in the services list for every 30 seconds.
    .NOTES
    Niet te zien of een service wijzigt of nieuw is
    #>


    param($computername='localhost', [int]$Interval = 1)

    Write-Host "ServiceMonitor (interrupt with Ctrl-C)" -fore cyan

    $svcBaseline = Get-Service -computername $computername

    Do {

      Start-Sleep $Interval

      $svcCurrent = Get-Service -computername $computername

      Compare-Object $svcBaseline $svcCurrent -Property machinename, name, displayname, status | 
        where { $_.sideindicator -eq '=>' } | foreach {
          $msg = "{0:hh:mm:ss} {1} {2} ({3})" -f (get-date) , $_.machinename, $_.name, $_.displayname
          if ($_.status -eq "running") { Write-Host $msg -foregroundcolor green  }   # service started
          if ($_.status -eq "stopped") { Write-Host $msg -foregroundcolor yellow }   # service stopped
      } # foreach

      $svcBaseline = $svcCurrent

    } while ( 1 -eq $true)

} # function


function EventLogMonitor {

    param( $Log='system' , $computername='.', $interval =1, $window= 100, $color="cyan" )

    # nog verwerken: echo $log tijdens init
    # nog verwerken: echo warning over breedte van console
    # nog verwerken: afbreken einde regel werkt niet goed

    <#
    .SYNOPSIS
    Monitors the eventlog for new entries and displays them on screen.
    .DESCRIPTION
    With this tool you can monitor the eventlog for new entries as they occur. Great for troubleshooting.
    #>


    # security events opsommen, ook over rdp
    # scom acs kevin holman
    # ook mogelijk om ipv compare een where te doen icm index property, maar dan misschien moeilijker werkend te maken over meerdere computers

    $MinimumWidth = 160

    function GetEventLogFromMultipleComputers {
        $tmpEvents = @()   # array leegmaken
        foreach ($computer in $computername) {
            $tmpEvents += Get-EventLog $Log -newest $window -ComputerName $computer
        }
        $tmpEvents | sort timegenerated   # need to sort because output is chronological and default sort is probably by machinename
    }

    function SimplifyEventLogMessage {
        param([string]$Message)
        $returnmessage = ""
        Foreach ($line in $message) {
            $returnmessage += $line.ToString()
        }
        $returnmessage = $returnmessage.replace([char]10, " ").replace([char]13, " ").replace(" ", " ").replace(" ", " ").replace(" ", " ")
    if ($returnmessage.length -ge $width) { $returnmessage.SubString(0,$width) } else { $returnmessage }
    # if ($returnmessage.length -gt $width) { $returnmessage = $returnmessage.SubString(0,$width) }
     # $returnmessage
    }

    Write-Host "EventLog Monitor v1.0" -foregroundcolor cyan
    If ($computername.count -gt 1) { "running across $($computername.count) computers: $computername" }

    Write-Warning "Events will not show up when source computers log more than $window events in $interval seconds"

    $Width = $Host.UI.RawUI.WindowSize.Width - 30
    If ($Width -lt $MininmumWidth) { $Width = $MinimumWidth; Write-Warning "Output width set to $Width" }

    write-progress -Activity 'reading eventlog'
    $a = GetEventLogFromMultipleComputers
    write-progress -activity 'reading eventlog' -Completed

    Do {
      Sleep $interval
      $b = GetEventLogFromMultipleComputers

      Compare-Object $a $b -Property MachineName, Index -passthru | where { $_.sideindicator -eq "=>" } | foreach {
        if ($_ -ne $null) {
            if ($_.MachineName.length -gt 15) { $MachineName = $_.MachineName.SubString(0,15) } else { $MachineName = $_.MachineName }
            $msg = "{0,15} {1:HH:mm:ss} {2,6} {3} | {4}" -f $MachineName, $_.TimeGenerated, $_.EventID, $_.Source, (SimplifyEventLogMessage $_.Message)   # colored output
            switch ($_.entrytype)
            {
                'error'   { Write-Host $msg -foregroundcolor 'red' }
                'warning' { Write-Host $msg -foregroundcolor 'yellow' }
                Default   { Write-Host $msg -foregroundcolor 'cyan' }
            }
         }
      } # foreach
      $a = $b
    } while ( 1 -eq $true)

}




function FolderMonitor {

    <#
    .SYNOPSIS
    Displays changes in a folder
    .DESCRIPTION
    version 1.1
    .EXAMPLE
    FolderMonitor C:\SCCMContentlib, C:\SMSPKG, 'C:\SMSPKGC$', C:\SMSPKGSIG, 'C:\SMSSIG$'
    Compares changes in the specified folders every second on the local computer.
    #>


    [cmdletbinding()]param(
        [string[]]$folder,   # required
        [int]$Interval = 1
        # to do: force, recurse, output to address specific changes (mode, length, lastwritetime)
    )

    Write-Host "FolderMonitor, interrupt with Ctrl-C, inspecting $folder" -ForegroundColor cyan

    $f1 = Get-ChildItem -path $folder -Force -Recurse
    Write-Verbose "$($f1.count) items found"

    Do {
      Start-Sleep $Interval
      $f2 = Get-ChildItem -path $folder -Force -Recurse
      Compare-Object $f1 $f2 -Property name, mode, length , lastwritetime -passthru | foreach {
        $msg = "{0} {1} {2} {3}" -f $_.mode, $_.lastwritetime, $_.length, $_.fullname
        if ($_.sideindicator -eq "=>") { Write-Host $msg -foregroundcolor green  }
        if ($_.sideindicator -eq "<=") { Write-Host $msg -foregroundcolor yellow }
      } # foreach
      $f1 = $f2
    } while (1)

} # function




function Send-WakeOnLan {
    <#
    .DESCRIPTION
    Enter MAC Address in the following form: 00-00-00-00-00-00 or 00:00:00:00:00:00
    .NOTES
    A Magic Packet is a broadcast frame containing anywhere within its payload 6 bytes of all 255 (FF FF FF FF FF FF in hexadecimal), followed by sixteen repetitions of the target computer's 48-bit MAC address, for a total of 102 bytes (source: Wikipedia)
    Troubleshooting: uitsluitend wol indien pc uitstaat dmv softknop (oftewel: dient al eens aangestaan hebben)
 
    To do: retrieve mac addresses from dhcp
    To do: accept DHCP leases as pipeline input
    to do: param macaddress throw 'verplicht'
    #>


    [cmdletbinding()]param(
        [parameter(Mandatory=$true)]
        [ValidatePattern('^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$')]
        [string[]]$MacAddress, 
        [int]$repeat=1,
        [int]$interval=1
    )

    $SimplifiedMac = $MacAddress.Replace('-','').Replace(':','')
    Write-Verbose "Simplified address: $SimplifiedMac"

    #$formattedMacAddress = $SimplifiedMac | ForEach-Object { [System.Convert]::ToByte($_, 16) }
    $formattedMacAddress = 0,2,4,6,8,10 | % {[System.Convert]::ToByte($SimplifiedMac.substring($_,2),16)}

    Write-Verbose "Formatted address: $formattedMacAddress"

    # constructing magic packet
    $packet = [byte[]](,0xFF * 102)   # construct 102 bytes of FF
    6..101 | ForEach-Object { $packet[$_] = [byte]$formattedMacAddress[($_%6)] }

    $UDPclient = New-Object System.Net.Sockets.UdpClient
    $UDPclient.Connect(([System.Net.IPAddress]::Broadcast), 4000)

    for ($i=0; $i -lt $repeat; $i++) 
    {
        Write-Output ("{0:HH:mm:ss} Sending wake-up packet to {1}" -f (Get-Date), "$MacAddress")
        # preferred to output '-' seperated
        $result = $UDPclient.Send($packet, $packet.Length)   # result: number of bytes sent

        if ($result -ne 102)
        {
            Write-Error "Something went wrong: $result bytes sent"
        }

        if ($i -lt $repeat-1) { Start-Sleep $interval }
    }

    $UDPclient.Close()  # finalize? dispose? :(
    Write-Verbose 'Done'
}




function MultiPing {
    <#
    .SYNOPSIS
        Sends a ping to a specified host or several hosts. Colors the output to indicate latency.
    .DESCRIPTION
        Provides a simple network monitoring solution, without the need to install any software.
    .EXAMPLE
        MultiPing ServerX
        Sends a ping to ServerX every second. Repeats forever.
    .EXAMPLE
        MultiPing ServerX, ServerY, 10.1.1.254, www.google.com
        Sends a ping to two servers, the IP address of the default gateway and a webserver on the internet
    .NOTES
        Update jul 2016: Timestamp included.
    #>


    param(
        [string[]]$computername=$Env:COMPUTERNAME,
        [switch]$ExcludeTimeStamp=$false,
        [switch]$ExcludeDefaultGateway=$false,
        [switch]$NoRepeat=$false,
        [int]$PingCritical=100,
        [int]$PingWarning=10,
        [int]$Delay=1000
        )

    if ($ExcludeDefaultGateway -eq $false) {
        $gw = Get-WmiObject Win32_NetworkAdapterConfiguration | Where { $_.IPEnabled -and $_.DefaultIPGateway -ne $null } | Select -expand DefaultIPGateway
        $computername += $gw
    }

    Write-Host "Pinging $($computername.count) remote systems. Interrupt with Ctrl-C. v1.2" -Foregroundcolor cyan
    Write-Host "Delay: $Delay ms. Thresholds: critical=$PingCritical, warning=$PingWarning" -Foregroundcolor cyan

    $i = 0   # line numbers

    Do {
        $height = $host.ui.RawUI.WindowSize.Height - 2
        if ($height -lt 5) { $height = 5 }   # height = 0 in ISE
        $TimeStamp = "{0:HH:mm}" -f (Get-Date)
        
        # write the header
        if ([int]($i / $height) -eq ($i / $height)) { 
            if (!$ExcludeTimeStamp) { Write-Host "time " -NoNewline } 
            Write-Host " $computername" 
        }
    
        if (!$ExcludeTimeStamp) { Write-Host $TimeStamp -NoNewline } 
        
        $computername | foreach {
            $a = Test-Connection $_ -Count 1 -ErrorAction SilentlyContinue
            if (!$?) {
                $msg = "---".PadLeft($_.length) + " "
                Write-Host $msg -NoNewline -ForegroundColor red 
            }
            else {
                $msg = "$($a.ResponseTime.ToString().Padleft($_.length)) "   # used $($a.Address) to write hostname on screen
                if     ($a.ResponseTime -ge $PingCritical) { write-host $msg -nonewline -fore red }
                elseif ($a.ResponseTime -ge $PingWarning)  { write-host $msg -nonewline -fore yellow }
                else                                       { write-host $msg -nonewline }
            }
        }

        $i++
        Write-Host ""

        # read the keyboard. Q = quit.
        #$key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
        #if ($key.Character -eq 'q') { $NoRepeat = $true }
        
        if (!$NoRepeat) { Start-Sleep -Milliseconds $Delay }   # perform delay only when repeat is true
   
    } while (!$NoRepeat)   # exit loop after first round when -NoRepeat is specified
}




function Get-MessageOfTheDay {
    $msg = 
    'Knowledge is power --Sir Francis Bacon',
    'Power is the ultimate aphrodisiac --Henry Alfred Kissinger',
    'With great power comes great responsibility --Uncle Ben',
    'If computers get too powerful, we can organize them into a committee -- that will do them in.',
    "Nearly all men can stand adversity, but if you want to test a man's character, give him power. --Abraham Lincoln",
    'Never go backward. Attempt, and do it with all your might. Determination is power. --Charles Simmons ',
    "Power is like being a lady... if you have to tell people you are, you aren't. --Margaret Thatcher",
    'Power is always dangerous. Power attracts the worst and corrupts the best. --Ragnar Lodbrok',
    "There are 10 types of people who understand binary: those who do and those who don't.",
    "I'm sure the universe is full of intelligent life. It's just been too intelligent to come here. --Arthur C. Clarke",
    "If you think it's expensive to hire a professional, wait until you hire an amateur",
    'Why is it drug addicts and computer aficionados are both called users? --Clifford Stoll',
    "Progress isn't made by early risers. It's made by lazy men trying to find easier ways to do something. --Robert Heinlein",
    "That's the thing about people who think they hate computers... What they really hate are lousy programmers. --Larry Niven", 
    'Man is the best computer we can put aboard a spacecraft... and the only one that can be mass produced with unskilled labor. -- Wernher von Braun',
    'Man is a slow, sloppy and brilliant thinker; the machine is fast, accurate and stupid. -- William M. Kelly',
    'Old programmers never die... They just decompile. -- Peter Dick',
    'Walking on water and developing software from a specification are easy if both are frozen. --Edward V. Berard', 
    'Any smoothly functioning technology will have the appearance of magic. -- Arthur C. Clarke',
    'Do. Or do not. There is no try. -- Yoda',
    "I'm sorry Dave. I'm afraid I can't do that --HAL 9000"

    # select a random message
    $msg[(random($msg.length))]

}

Set-Alias motd Get-MessageOfTheDay




function Invoke-Speach {

    <#
    .Synopsis
       Reads text through the default speakers
    .EXAMPLE
       Invoke-Speach
       This command reads an inspiring message through the default speakers
    .EXAMPLE
       Invoke-Speach -Volume 50 -Rate 1 'Nearly all men can stand adversity, but if you want to test a man's character, give him PowerShell!'
       Converts the given text to speach at rate 1 and volume at 50%.
    .EXAMPLE
       'Never go backward. Attempt, and do it with all your might. Determination is power.' | Invoke-Speach
       A string being passed through the pipeline will be spoken
    .EXAMPLE
        The measure of a man is what he does with PowerShell | say
        Example of the use of the alias
    .NOTES
         Verbs that apply: Out, Invoke, Read
    #>


    [CmdletBinding()]
    Param
    (
        # The sentence or word that is being spoken
        [Parameter(ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]
        $Message = 'With great power comes great responsibility',

        # Volume between 0 and 100, default is 100
        [Parameter(ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1)]
        [int]
        $volume=100,

        # The rate, or speed of the sentence that's being spoken. -10 is extremely slow, 10 is extremely fast, 0 is the default.
        [int]
        $rate=0
    )

    Begin
    {
        $a = New-Object -ComObject SAPI.SpVoice
        $a.Volume = $Volume
        $a.Rate = $Rate
    }

    Process
    {
        $a.speak($Message)
    }

    End
    {
        $a = $null
    }
}

Set-Alias say Invoke-Speak

# Export-ModuleMember -Alias * -Cmdlet * -Function * -Variable *