NTS.Tools.General.psm1

function Start-FolderCleanUp {
    <#
        .Description
        this function can be used to remove folders and its items
 
        .Parameter FolderToRemove
        version of sql reporting services setup
 
        .Example
        Start-CleanUp -FolderToRemove $SSRSTempFolder
 
        .NOTES
 
    #>


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

    Write-Verbose "removing temp files from $($FolderToRemove)"
    try {
        Remove-Item -Path $FolderToRemove -Recurse -Force
    }
    catch {
        throw "error while cleanup - $($PSItem.Exception.Message)"
    }
    Write-Verbose "cleanup finished"
}

function Set-Interface {
    <#
        .Description
        configures the network interface, ip, dns, gateway
 
        .Parameter InterfaceObject
        nic objects
 
        .Parameter IPAddress
        ipaddress
 
        .Parameter NetPrefix
        net prefix, e.g. 24
 
        .Parameter DefaultGateway
        default gateway in the subnet
 
        .Parameter DNSAddresses
        dns server addresses
 
        .Parameter NewName
        new name of the network adapter
 
        .Example
        Set-Interface -InterfaceObject $SFP10G_NICs[0] -IPAddress $CLU1_IPAddress -NetPrefix $NetPrefix -DefaultGateway $CLU_DefaultGateway -DNSAddresses $CLU_DNSAddresses -NewName "Datacenter-1"
 
        .NOTES
         
    #>


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

        [Parameter(Mandatory = $true)]
        $IPAddress,

        [Parameter(Mandatory = $true)]
        $NetPrefix,

        [Parameter(Mandatory = $false)]
        $DefaultGateway,

        [Parameter(Mandatory = $false)]
        $DNSAddresses,

        [Parameter(Mandatory = $false)]
        $NewName
    )

    $ErrorActionPreference = 'Stop'
    try {
        try {
            $IPAddress = [IPAddress]::Parse($IPAddress).IPAddressToString
        }
        catch {
            throw "cloud not parse ip - $($PSItem.Exception.Message)"
        }
        
        Write-Verbose "configuring nic with macaddress $($InterfaceObject.MacAddress)"
        If (($InterfaceObject | Get-NetIPConfiguration).IPv4Address.IPAddress) {
            $InterfaceObject | Remove-NetIPAddress -AddressFamily "IPv4" -Confirm:$false
        }
        If (($InterfaceObject | Get-NetIPConfiguration).Ipv4DefaultGateway) {
            $InterfaceObject | Remove-NetRoute -AddressFamily "IPv4" -Confirm:$false
        }
        Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\services\Tcpip\Parameters\Interfaces\$($InterfaceObject.InterfaceGuid)" -Name EnableDHCP -Value 0
        Start-Sleep -Seconds 2
        
        if ($null -ne $DefaultGateway) {
            $InterfaceObject | New-NetIPAddress -IPAddress $IPAddress -AddressFamily "IPv4" -PrefixLength $NetPrefix -DefaultGateway $DefaultGateway | Out-Null
            Write-Verbose "interface $($InterfaceObject.InterfaceDescription) has the static ip $($IPAddress) now"
        }
        else {
            $InterfaceObject | New-NetIPAddress -IPAddress $IPAddress -AddressFamily "IPv4" -PrefixLength $NetPrefix | Out-Null
            Write-Verbose "interface $($InterfaceObject.InterfaceDescription) has the static ip $($IPAddress) now"
        }        
        if ($null -ne $DNSAddresses) {
            $InterfaceObject | Set-DnsClientServerAddress -ServerAddresses $DNSAddresses
        }
        if ($null -ne $NewName) {
            $InterfaceObject | Rename-NetAdapter -NewName $NewName
            Write-Verbose "interface $($InterfaceObject.InterfaceDescription) renamed to $($NewName)"
        }

        $InterfaceObject | Restart-NetAdapter
    }
    catch {
        throw "error setting $($InterfaceObject.Name) - $($PSItem.Exception.Message)"
    }
}

function Test-FileLock {
    <#
        .Description
        this function test if a file is in use and returns true if so
 
        .Parameter Path
        file path to the file
 
        .Example
        Test-FileLock -Path C:\WINDOWS\CCM\Logs\PolicyAgentProvider.log
 
        .NOTES
        https://stackoverflow.com/questions/24992681/powershell-check-if-a-file-is-locked
    #>


    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [string]
        $Path
    )

    $oFile = New-Object System.IO.FileInfo $Path
    if ((Test-Path -Path $Path) -eq $false) {
        return $false
    }
    try {
        $oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)

        if ($oStream) {
            $oStream.Close()
        }
        $false
    }
    catch {
        # file is locked by a process.
        return $true
    }
}

function Test-RegistryValue {
    <#
        .Description
        tests if a registry and its value
 
        .Parameter DownloadURL
        tests if a registry and its value
 
        .Parameter Key
        Registry key path
 
        .Parameter Value
        value
 
        .Example
        Test-RegistryValue -Key "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "RebootInProgress"
 
        .NOTES
 
    #>


    [OutputType('bool')]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Key,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Value
    )

    $ErrorActionPreference = 'Stop'

    if (Get-ItemProperty -Path $Key -Name $Value -ErrorAction Ignore) {
        $true
    }
}

function Test-RebootPending {
    <#
        .SYNOPSIS
        checks some if reboot is pending
         
        .DESCRIPTION
        checks some registry key and value for a pending reboot
         
        .EXAMPLE
        Test-RebootPending
 
        .NOTES
        https://adamtheautomator.com/pending-reboot-registry-windows/
 
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [switch]
        $DisplayReason
    )

    [bool]$PendingReboot = $false

    # Check for Keys
    $Keys = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending",
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress",
        "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending",
        "HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts",
        "HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttemps"
    )
    foreach ($Key in $Keys) {
        if (Get-Item -Path $Key -ErrorAction Ignore) {
            if ($DisplayReason) {
                Write-Host $Key
            }
            $PendingReboot = $true
        }
    }

    # Check for Values not null
    # if (($regVal = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations' -ErrorAction Ignore) -and $regVal.($Value)) {
    # if ($DisplayReason) {
    # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager > PendingFileRenameOperations not null"
    # }
    # $PendingReboot = $true
    # }
    # if (($regVal = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name 'PendingFileRenameOperations2' -ErrorAction Ignore) -and $regVal.($Value)) {
    # if ($DisplayReason) {
    # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager > PendingFileRenameOperations2 not null"
    # }
    # $PendingReboot = $true
    # }

    #Check for Values
    If ((Test-RegistryValue -Key "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "RebootInProgress") -eq $true) {
        if ($DisplayReason) {
            Write-Host "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing > RebootInProgress"
        }
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Key "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "PackagesPending") -eq $true) {
        if ($DisplayReason) {
            Write-Host "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing > PackagesPending"
        }
        $PendingReboot = $true
    }

    # If ((Test-RegistryValue -Key "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Value "PendingFileRenameOperations") -eq $true) {
    # if ($DisplayReason) {
    # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager > PendingFileRenameOperations"
    # }
    # $PendingReboot = $true
    # }

    # If ((Test-RegistryValue -Key "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Value "PendingFileRenameOperations2") -eq $true) {
    # if ($DisplayReason) {
    # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager > PendingFileRenameOperations2"
    # }
    # $PendingReboot = $true
    # }

    If ((Test-RegistryValue -Key "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" -Value "DVDRebootSignal") -eq $true) {
        if ($DisplayReason) {
            Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce > DVDRebootSignal"
        }
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Key "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon" -Value "JoinDomain") -eq $true) {
        if ($DisplayReason) {
            Write-Host "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon > JoinDomain"
        }
        $PendingReboot = $true
    }

    If ((Test-RegistryValue -Key "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon" -Value "AvoidSpnSet") -eq $true) {
        if ($DisplayReason) {
            Write-Host "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon > AvoidSpnSet"
        }
        $PendingReboot = $true
    }

    #region custom
    # Added "test-path" to each test that did not leverage a custom function from above since
    # an exception is thrown when Get-ItemProperty or Get-ChildItem are passed a nonexistant key path

    # Added test to check first if key exists, using "ErrorAction ignore" will incorrectly return $true
    # 'HKLM:\SOFTWARE\Microsoft\Updates' | Where-Object { Test-Path $PSItem -PathType Container } | ForEach-Object {
    # try {
    # $Value = (Get-ItemProperty -Path $PSItem -Name 'UpdateExeVolatile' | Select-Object -ExpandProperty UpdateExeVolatile) -ne 0
    # if ($Value) {
    # if ($DisplayReason) {
    # Write-Host "UpdateExeVolatile under HKLM:\SOFTWARE\Microsoft\Updates not equals 0"
    # }
    # $PendingReboot = $true
    # }
    # }
    # catch { " "}
    # }

    # Added test to check first if keys exists, if not each group will return $Null
    # May need to evaluate what it means if one or both of these keys do not exist
    $ComputerNameChangeTest = ( 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName' | Where-Object { test-path $PSItem } | ForEach-Object { (Get-ItemProperty -Path $PSItem ).ComputerName } ) -ne `
    ( 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName' | Where-Object { Test-Path $PSItem } | ForEach-Object { (Get-ItemProperty -Path $PSItem ).ComputerName } )
    if ($ComputerNameChangeTest) {
        if ($DisplayReason) {
            Write-Host "pending computername change"
        }
        $PendingReboot = $true
    }

    # Added test to check first if key exists
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending' | Where-Object { (Test-Path $PSItem) -and (Get-ChildItem -Path $PSItem) } | ForEach-Object { 
        if ($DisplayReason) {
            Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending exists and has childitems"
        }
        $PendingReboot = $true
        $PendingReboot | Out-Null # just for the script analysis stuff
    }
    #endregion

    return $PendingReboot    
}

function Start-FileDownload {
    <#
        .Description
        this function can be used to download files, but also checks if the destination has already the file
 
        .Parameter DownloadURL
        url of the source
 
        .Parameter FileOutPath
        path where the file should be saved, with extension
 
        .Parameter MaxAgeOfFile
        maximum file modification date
 
        .Example
        Start-FileDownload -DownloadURL "https://www.microsoft.com/en-us/download/confirmation.aspx?id=104131" -FileOutPath "$($Outpath)\Exchange-$($Version).iso"
 
        .NOTES
 
    #>


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

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

        [Parameter(Mandatory = $false)]
        [datetime]
        $MaxAgeOfFile = (Get-Date).AddHours(-2)
    )

    # verify folder
    try {
        $FileName = $FileOutPath.Split("\")[-1]
        $FolderName = $FileOutPath.replace($FileName, "")
        New-ItemIfNotExists -Path $FolderName -ItemType Directory
    }
    catch {
        throw "error creating dest folder - $($PSItem.Exception.Message)"
    }
    
    # download
    try {
        if ((Test-Path -Path $FileOutPath) -eq $true) {
            if ((Get-Item $FileOutPath).LastWriteTime -gt $MaxAgeOfFile) {
                Write-Verbose "found $($FileOutPath), will use it"
            } 
            else {
                Write-Verbose "found $($FileOutPath), removing the file because too old"
                Remove-Item -Path $FileOutPath -Recurse -Force | Out-Null
                
                Write-Verbose "downloading from $($DownloadURL) to $($FileOutPath)"
                $ProgressPreference = "SilentlyContinue"
                Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL -OutFile $FileOutPath
                $ProgressPreference = "Continue"
                Write-Verbose "download finished"
            }
        }
        else {
            Write-Verbose "downloading from $($DownloadURL) to $($FileOutPath)"
            $ProgressPreference = "SilentlyContinue"
            Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL -OutFile $FileOutPath
            $ProgressPreference = "Continue"
            Write-Verbose "download finished"
        }
    }
    catch {
        throw "error downloading - $($PSItem.Exception.Message)"
    }
}

function Confirm-LatestModuleVersionInstalled {
    <#
        .Description
        this function checks the module version against powershell gallery, if older then it will as to update
 
        .Parameter ModuleName
        name of the module
 
        .Parameter AutoRunUpdate
        automatically start an update for the module, if there is one
 
        .Example
        Confirm-LatestModuleVersionInstalled -Module $ModuleName
 
        .NOTES
        https://blog.it-koehler.com/en/Archive/3359
 
    #>


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

        [Parameter(Mandatory = $false)]
        [switch]
        $AutoRunUpdate
    )

    try {
        if ($null -eq (Get-Module -Name $ModuleName -ListAvailable)) {
            throw "module $($ModuleName) not installed"
        }

        # test connectivity
        try {
            $ConTestResult = Invoke-WebRequest -Uri "https://www.powershellgallery.com" -UseBasicParsing
            if ($null -ne $ConTestResult.content) {
                $RunCheckForNewerVersion = $true
            }
            else {
                $RunCheckForNewerVersion = $false
            }
        }
        catch {
            $RunCheckForNewerVersion = $false
        }

        if ($RunCheckForNewerVersion -eq $true) {
            try {
                #getting version of installed module
                $version = (Get-Module -Name $ModuleName -ListAvailable) | Sort-Object Version -Descending  | Select-Object Version -First 1
    
                #converting version to string
                $stringver = $version | Select-Object @{n = 'ModuleVersion'; e = { $PSItem.Version -as [string] } }
                $LocalVersionOfModule = $stringver | Select-Object Moduleversion -ExpandProperty Moduleversion
    
                #getting latest module version from ps gallery
                $psgalleryversion = Find-Module -Name $ModuleName -Repository PSGallery | Sort-Object Version -Descending | Select-Object Version -First 1
    
                #converting version to string
                $onlinever = $psgalleryversion | Select-Object @{n = 'OnlineVersion'; e = { $PSItem.Version -as [string] } }
                $OnlineVersionOfModule = $onlinever | Select-Object OnlineVersion -ExpandProperty OnlineVersion
            }
            catch {
                throw "error collecting versions - $($PSItem.Exception.Message)"
            }
    
            try {
                if ([version]"$($LocalVersionOfModule)" -ge [version]"$($OnlineVersionOfModule)") {
                    # Write-Output "using $($ModuleName) with version $($LocalVersionOfModule)"
                }
                else {
                    if ($AutoRunUpdate -eq $false) {
                        Write-Output "the installed version of $($ModuleName) is not the latest"
                        Write-Output "the one installed locally ($($LocalVersionOfModule)) is lower than the one on the PowerShellGallery ($($OnlineVersionOfModule))"
                
                        #ask for update to proceed
                        do {
                            $askyesno = (Read-Host "do you want to update Module $ModuleName (Y/N)").ToLower()
                        } while ($askyesno -notin @('y', 'n'))

                        if ($askyesno -eq 'y') {
                            $RunUpdate = $true
                        }
                    }

                    if ($RunUpdate -eq $true -or $AutoRunUpdate -eq $true) {
                        Write-Output "updating module $($ModuleName)"
                        Update-Module -Name $ModuleName -Force
                    }
                    else {
                        Write-Output "skipping update of module $($ModuleName)"
                    }
                }
                if ($null -ne (Get-Module -Name $ModuleName)) {
                    Remove-Module -Name $ModuleName -ErrorAction SilentlyContinue
                }
            }
            catch {
                throw "error updating module - $($PSItem.Exception.Message)"
            }
        }
        else {
            if ($AutoRunUpdate -eq $false) {
                do {
                    $askyesno = (Read-Host "could not connect to powershell gallery, still continue? (Y/N)").ToLower()
                } while ($askyesno -notin @('y', 'n'))
                if ($askyesno -eq 'n') {
                    throw "stopping script, because could not connect to powershell gallery to get latest version of the $($ModuleName)"
                }
            }
            else {
                Write-Output "could not connect to powershell gallery, skipping the update"
            }            
        }

        try {
            #getting version of installed module
            $version = (Get-Module -Name $ModuleName -ListAvailable) | Sort-Object Version -Descending  | Select-Object Version -First 1

            #converting version to string
            $stringver = $version | Select-Object @{n = 'ModuleVersion'; e = { $PSItem.Version -as [string] } }
            $LocalVersionOfModule = $stringver | Select-Object Moduleversion -ExpandProperty Moduleversion

            # output
            Write-Output "using $($ModuleName) with version $($LocalVersionOfModule)"
        }
        catch {
            throw "error collecting local version - $($PSItem.Exception.Message)"
        }
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Confirm-RunningAsAdministrator {
    <#
        .Description
        this function checks if the current powershell session is running as administrator
 
        .Example
        Confirm-LatestModuleVersionInstalled -Module $ModuleName
 
        .NOTES
 
    #>


    $user = [Security.Principal.WindowsIdentity]::GetCurrent()
    [bool]$Result = (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
    
    if ($Result -ne $true) {
        throw "this session is not running as administrator, please restart with administrative privileges"
    }
}

function New-ItemIfNotExists {
    <#
        .Description
        this function adds an if statement infront of the new-item function to test if the path exists
 
        .Parameter Path
        FilePath for the item
 
        .Parameter ItemType
        type of the item
 
        .Example
        New-ItemIfNotExists -Path $TempFolderForSQL -ItemType Directory
 
        .NOTES
 
    #>


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

        [Parameter(Mandatory = $true)]
        [ValidateSet(
            "File",
            "Directory",
            "SymbolicLink",
            "Junction",
            "Hardlink"
        )]
        [string]
        $ItemType
    )

    if (-NOT (Test-Path $Path)) {
        New-Item -Path $Path -ItemType $ItemType -Force | Out-Null
    }
}

function Confirm-DomainConnectivity {
    <#
        .Description
        this function checks if the specified domain can be pinged
 
        .Parameter DomainName
        full qualified domain name of the domain
 
        .Example
        Confirm-DomainConnectivity -DomainName "google.de"
 
        .NOTES
 
    #>


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

    $Test = Test-NetConnection -ComputerName $DomainName
    if ($Test.PingSucceeded -ne $true) {
        throw "could not ping $($DomainName)"
    }
    else {
        Write-Verbose "ping to $($DomainName) was successfull"
    }
}

function Confirm-Question {
    <#
        .Description
        this function can be used to ask yes|no questions, throws an error the the answer is not yes
 
        .Parameter Question
        question as string
 
        .Example
        Confirm-AskYesOrNo -Question "domain intune-center.de available?"
 
        .NOTES
 
    #>


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

    $RunCount = 0

    do {
        $askyesno = (Read-Host "$($Question) (y/n)").ToLower()
        $RunCount++

        if ($RunCount -eq "3") {
            Write-Output "please read the question carefully and then answer with 'y' or 'n'"
        }
        elseif ($RunCount -eq "5") {
            throw "question was answered incorrectly five times, i dont have time for this!"
        }
    } while ($askyesno -notin @('y', 'n'))

    if ($askyesno -ne "y") {
        throw "the anser was not 'y', stopping execution"
    }
}

function Initialize-PowerShellEnviroment {
    <#
        .Description
        installs neccesary components for the powershell gallery
 
        .Parameter DomainName
        installs neccesary components for the powershell gallery
 
        .Example
        Initialize-PowerShellEnviroment
 
        .NOTES
        https://www.recastsoftware.com/resources/enable-psgallery-in-a-configmgr-task-sequence-while-in-winpe/
    #>


    # Source : https://www.recastsoftware.com/resources/enable-psgallery-in-a-configmgr-task-sequence-while-in-winpe/

    # Setup LOCALAPPDATA Variable
    [System.Environment]::SetEnvironmentVariable('LOCALAPPDATA', "$env:SystemDrive\Windows\system32\config\systemprofile\AppData\Local")
    $WorkingDir = $env:TEMP
    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

    Set-ExecutionPolicy Unrestricted -Scope Process -Force 
    Import-Module 'PackageManagement'

    try {
        Install-PackageProvider -Name Nuget -RequiredVersion 2.8.5.201 -Force | Out-Null
    }
    catch {
        throw "Error installing the powershell provider 'nuget'"
    }

    # PackageManagement from PSGallery URL
    try {
        $PackageManagementVersion = "1.4.8.1"
        if (!(Get-Module -Name PackageManagement | Where-Object -Property Version -Like "*$($PackageManagementVersion)*")) {
            Write-Verbose "installing PackageManagementVersion via file with at least version $($PackageManagementVersion)"
            $PackageManagementURL = "https://psg-prod-eastus.azureedge.net/packages/packagemanagement.$($PackageManagementVersion).nupkg"
            Invoke-WebRequest -UseBasicParsing -Uri $PackageManagementURL -OutFile "$WorkingDir\packagemanagement.$($PackageManagementVersion).zip"
            $Null = New-Item -Path "$WorkingDir\$($PackageManagementVersion)" -ItemType Directory -Force
            Expand-Archive -Path "$WorkingDir\packagemanagement.$($PackageManagementVersion).zip" -DestinationPath "$WorkingDir\$($PackageManagementVersion)" -Force
            $Null = New-Item -Path "$env:ProgramFiles\WindowsPowerShell\Modules\PackageManagement" -ItemType Directory -ErrorAction SilentlyContinue
            Move-Item -Path "$WorkingDir\$($PackageManagementVersion)" -Destination "$env:ProgramFiles\WindowsPowerShell\Modules\PackageManagement\$($PackageManagementVersion)" -ErrorAction SilentlyContinue -Force
        }
    }
    catch {
        throw "Error installing the powershell module 'PackageManagement'"
    }

    # PowerShellGet from PSGallery URL
    try {
        $PowerShellGetVersion = "2.2.5"
        if (!(Get-Module -Name PowerShellGet | Where-Object -Property Version -Like "*$($PowerShellGetVersion)*")) {
            Write-Verbose "installing PowerShellGetVersion via file with at least version $($PowerShellGetVersion)"
            $PowerShellGetURL = "https://psg-prod-eastus.azureedge.net/packages/powershellget.$($PowerShellGetVersion).nupkg"
            Invoke-WebRequest -UseBasicParsing -Uri $PowerShellGetURL -OutFile "$WorkingDir\powershellget.$($PowerShellGetVersion).zip"
            $Null = New-Item -Path "$WorkingDir\$($PowerShellGetVersion)" -ItemType Directory -Force
            Expand-Archive -Path "$WorkingDir\powershellget.$($PowerShellGetVersion).zip" -DestinationPath "$WorkingDir\$($PowerShellGetVersion)" -Force
            $Null = New-Item -Path "$env:ProgramFiles\WindowsPowerShell\Modules\PowerShellGet" -ItemType Directory -ErrorAction SilentlyContinue
            Move-Item -Path "$WorkingDir\$($PowerShellGetVersion)" -Destination "$env:ProgramFiles\WindowsPowerShell\Modules\PowerShellGet\$($PowerShellGetVersion)" -ErrorAction SilentlyContinue -Force
        }
    }
    catch {
        throw "Error installing the powershell module 'PowerShellGet'"
    }

    # Import PowerShellGet & set psgallery
    Import-Module PowerShellGet
    # Register-PSRepository -Name "PSGallery" –SourceLocation "https://www.powershellgallery.com/api/v2/" -InstallationPolicy Trusted
    Set-PSRepository -Name PSGallery -InstallationPolicy Trusted | Out-Null
}

function Update-WindowsSystem {
    <#
        .SYNOPSIS
        starts windows update process
 
        .DESCRIPTION
        installs module PSWindowsUpdate and starts windows update process
 
        .PARAMETER AutoReboot
        should the vm reboot after applying the updates
 
        .EXAMPLE
        Update-WindowsSystem
 
        .NOTES
        installs ps module PSWindowsUpdate, therefore need internet access
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [bool]
        $AutoReboot = $false
    )
    
    # Define Options
    $ErrorActionPreference = 'Stop'
    $WUModuleName = "PSWindowsUpdate"

    # Install module
    try {
        Initialize-PowerShellEnviroment
        Write-Verbose "installing ps module $($WUModuleName)"
        Set-ExecutionPolicy Unrestricted -Scope Process -Force 
        Install-Module -Name $WUModuleName -Force -WarningAction SilentlyContinue
    }
    catch {
        Write-Output "Error installing the powershell module - $($PSItem.Exception.Message)"
        break
    }
    try {
        Write-Verbose "importing ps module $($WUModuleName)"    
        if ($null -eq (Get-Module -Name $WUModuleName)) {
            if ($null -ne (Get-Module -Name $WUModuleName -ListAvailable)) {
                Import-Module -Name $WUModuleName -Force -WarningAction SilentlyContinue
            }
            else {
                throw "module not installed"
            }
        }
    }
    catch {
        throw "error importing the powershell '$($WUModuleName)': $($PSItem.Exception.Message)"
    }

    # Get & Install Updates
    try {
        Write-Output "installing windows updates"
        Import-Module -Name $WUModuleName
        Set-WUSettings -IncludeRecommendedUpdates -Confirm:$false | Out-Null
        Get-WindowsUpdate -UpdateType Software -AcceptAll -MicrosoftUpdate -IgnoreReboot -Install | Out-Null
        Write-Output "finished installing windows updates"
        if ($AutoReboot -eq $true) {
            Restart-Computer -Force
        }
    }
    catch {
        Write-Output "error finding or installing updates - $($PSItem.Exception.Message)"
        break
    }
}

function Write-ToLogOrConsole {
    <#
        .SYNOPSIS
        writes messages to a log file or the console
 
        .DESCRIPTION
        writes messages to a log file under "$($env:ProgramData)\NTS\LogFiles\" or to th console
 
        .PARAMETER LogFileName
        name of the log file
 
        .PARAMETER LogFileFolderPath
        path of the folder where to put the log file
 
        .PARAMETER Console
        writes to console instead of log file
 
        .PARAMETER Severity
        severity of the message
 
        .PARAMETER Message
        message to write in the log file
 
        .PARAMETER RemoteMessage
        string array of message to write in the log file
 
        .EXAMPLE
        Write-ToLogOrConsole -LogFileName "createvm.log" -Severity Info -Message "vm created"
 
        .EXAMPLE
        Write-ToLogOrConsole -Console -Severity Info -Message "vm created"
 
        .NOTES
        https://adamtheautomator.com/powershell-log-function/
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false, ParameterSetName="LogFile")]
        [string]
        $LogFileName = "",

        [Parameter(Mandatory = $false, ParameterSetName="LogFile")]
        [string]
        $LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles",

        [Parameter(Mandatory = $false, ParameterSetName="Console")]
        [switch]
        $Console,
 
        [Parameter(Mandatory = $false, ParameterSetName="LogFile")]
        [Parameter(Mandatory = $false, ParameterSetName="Console")]
        [ValidateSet('Verbose', 'Info', 'Warning', 'Error')]
        [string]
        $Severity = 'Info',

        [Parameter(Mandatory = $false, ParameterSetName="LogFile")]
        [Parameter(Mandatory = $false, ParameterSetName="Console")]
        [ValidateNotNullOrEmpty()]
        [string]
        $Message = "",

        [Parameter(Mandatory = $false, ParameterSetName="LogFile")]
        [Parameter(Mandatory = $false, ParameterSetName="Console")]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $RemoteMessage = @("")
    )
    
    try {
        if ($Message -ne "" -and $RemoteMessage -ne "") {
            throw "you can only use `$Message or `$RemoteMessage"
        }

        if ($Message -ne "") {
            if ($Console) {
                if ($Severity -eq 'Verbose') {
                    Write-Verbose $Message
                }
                elseif ($Severity -eq 'Info') {
                    Write-Output $Message
                }
                elseif ($Severity -eq 'Warning') {
                    Write-Warning $Message
                }
                elseif ($Severity -eq 'Error') {
                    Write-Error $Message
                }
            }
            elseif ($LogFileName -ne "") {
                $LogFilePath = "$($LogFileFolderPath)\$($LogFileName)"
                New-ItemIfNotExists -Path $LogFileFolderPath -ItemType Directory
                New-ItemIfNotExists -Path $LogFilePath -ItemType File
                Add-Content -Value "[$(Get-Date -format "dd/MM/yyyy HH:mm:ss")] - [$($Severity)] - $($Message)" -Path $LogFilePath
                Write-Verbose $Message
            }
        }
        if ($RemoteMessage -ne @("")) {
            $RemoteMessage | ForEach-Object {
                if ($Console) {
                    if ($Severity -eq 'Verbose') {
                        Write-Verbose $PSItem
                    }
                    elseif ($Severity -eq 'Info') {
                        Write-Output $PSItem
                    }
                    elseif ($Severity -eq 'Warning') {
                        Write-Warning $PSItem
                    }
                    elseif ($Severity -eq 'Error') {
                        Write-Error $PSItem
                    }
                }
                elseif ($LogFileName -ne "") {
                    $LogFilePath = "$($LogFileFolderPath)\$($LogFileName)"
                    New-ItemIfNotExists -Path $LogFileFolderPath -ItemType Directory
                    New-ItemIfNotExists -Path $LogFilePath -ItemType File
                    Add-Content -Value "[$(Get-Date -format "dd/MM/yyyy HH:mm:ss")] - [$($Severity)] - $($PSItem)" -Path $LogFilePath
                    Write-Verbose $PSItem
                }
            }            
        }
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Confirm-LogFileParameters {
    <#
        .SYNOPSIS
        a function to verify the parameters for logging
 
        .PARAMETER LogFileName
        name of the log file
 
        .PARAMETER LogFileFolderPath
        path of the folder where to put the log file
 
        .EXAMPLE
        $LogParam = Confirm-LogFileParameters -LogFileName $LogFileName -LogFileFolderPath $LogFileFolderPath
 
        .NOTES
 
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $LogFileName = "",

        [Parameter(Mandatory = $false)]
        [string]
        $LogFileFolderPath = ""
    )

    $ErrorActionPreference = 'Stop'

    try {
        if ($LogFileName -eq "") {
            $LogParam = @{
                Console = $true
            }
        }
        elseif ($LogFileName -ne "") {
            if ($LogFileFolderPath -eq "") {
                $LogParam = @{
                    LogFileName = $LogFileName
                    LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles"
                }
            }
            else {
                $LogParam = @{
                    LogFileName = $LogFileName
                    LogFileFolderPath = $LogFileFolderPath
                }
            }
        }
        else {
            throw "`$LogFileName is not defined properly, was $($LogFileName)"
        }
        
        return $LogParam
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Wait-ProgressBar {
    <#
        .SYNOPSIS
        a function to display a waiting countdown with write-progress
 
        .PARAMETER SecondsToWait
        amount of seconds the countdown should wait
 
        .EXAMPLE
        Wait-ProgressBar -SecondsToWait 60
 
        .NOTES
 
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [int]
        $SecondsToWait
    )

    $Counter = 0
    
    do {
        $PercentComplete = ($Counter / $SecondsToWait) * 100
        if($PercentComplete -eq 0) { $PercentComplete = 1 }
    
        Write-Progress -Activity "Waiting" -Status "($($Counter)/$($SecondsToWait)) seconds" -PercentComplete $PercentComplete
        Start-Sleep -Seconds 1
        $Counter++
    } while ($Counter -le $SecondsToWait)
}