Public/Start-cChocoEx.ps1

<#
.SYNOPSIS
Bootstraps the cChoco PowerShell DSC Module
 
.DESCRIPTION
Bootstraps the cChoco PowerShell DSC Module
#>

function Start-cChocoEx {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string]
        $SettingsURI,
        # Chocolatey Installation Directory
        [Parameter()]
        [string]
        $InstallDir = "$env:ProgramData\chocolatey",
        # Chocolatey Installation Script URL
        [Parameter()]
        [string]
        $ChocoInstallScriptUrl = 'https://chocolatey.org/install.ps1',
        # URL to chocolatey nupkg
        [Parameter()]
        [string]
        $ChocoDownloadUrl,
        # URL to cChoco sources configuration file
        [Parameter()]
        [string]
        $SourcesConfig,
        # URL to cCHoco packages
        [Parameter()]
        [array]
        $PackageConfig,
        # URL to cChoco Chocolatey configuration file
        [Parameter()]
        [string]
        $ChocoConfig,
        # URL to cChoco Chocolatey features configuration file
        [Parameter()]
        [string]
        $FeatureConfig,
        # Do not cache configuration files
        [Parameter()]
        [switch]
        $NoCache,
        # Wipe locally cached psd1 configurations
        [Parameter()]
        [switch]
        $WipeCache,
        # RandomDelay
        [Parameter()]
        [switch]
        $RandomDelay,
        # Loop the Function
        [Parameter()]
        [Switch]
        $Loop,
        # Loop Delay in Minutes
        [Parameter()]
        [int]
        $LoopDelay = 60,
        # Legacy Migration Automation
        [Parameter()]
        [Switch]
        $MigrateLegacyConfigurations,
        # OverrideMaintenanceWindow
        [Parameter()]
        [switch]
        $OverrideMaintenanceWindow,
        # Enable Desktop Notifications
        [Parameter()]
        [Switch]
        $EnableNotifications,
        # Set machine enviroment variables
        [Parameter()]
        [switch]
        $SetcChocoExEnvironment
    )

    #Ensure Running as Administrator
    if (-Not (Test-IsAdmin)) {
        Write-Warning "This function requires elevated access, please reopen PowerShell as an Administrator"
        Break
    }   
    
    #Enable TLS 1.2
    #https://docs.microsoft.com/en-us/dotnet/api/system.net.securityprotocoltype?view=net-5.0
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072

    #Set Notifications Variable
    $Global:EnableNotifications = $EnableNotifications

    #Default Maintenance Windows Active and Enabled
    $Global:MaintenanceWindowEnabled = $true 
    $Global:MaintenanceWindowActive = $true


    #Validate Current Execution Policy
    $CurrentExecutionPolicy = Get-ExecutionPolicy
    try {
        $null = Set-ExecutionPolicy Bypass -Scope CurrentUser
    }
    catch {
        Write-Log -Severity 'Warning' -Message "Error Changing Execution Policy"
    }

    #Exclude Machines Set to Exclude Ring
    if ((Get-cChocoExRing) -eq 'Exclude') {  
        Write-Log -Severity 'Information' -Message 'This machine is set to the Exclude Ring. cChocoEx Stopped'
        Break
    }

    #Ensure cChocoExBootStrapTask is not running
    $PendingFile = Join-Path $env:cChocoExDataFolder '.cChocoExPending'
    if (Test-Path -Path $PendingFile) {
        #Autoremove is older than 24 hours
        if ((Get-Item -Path $PendingFile).CreationTime -lt (Get-Date).AddDays(-1)) {
            Write-Log -Severity 'Warning' -Message 'Stale cChocoEx pending file found, removing'
            Remove-Item -Path $PendingFile -Force
        }
    }
    if (Test-Path -Path $PendingFile) {
        Write-Log -Severity 'Warning' -Message 'cChocoEx pending completion, please wait until it finishes to invoke again'
        break
    }
    Set-Content -Path $PendingFile -Value '' -Force

    #Ensure choco.exe is not active
    $i = 0
    do {
        $IsChocoActive = Test-IsChocoActive
        if ($i -eq 1) {
            Write-Log -Severity 'Information' -Message 'Choco.exe is active, waiting up to 300 seconds'
        }
        if ($i -gt 0) {
            Start-Sleep -Seconds 1        
        }
        $i++
    } until (
        ($IsChocoActive -eq $False) -or ($i -gt 300)
    )
    if (Test-IsChocoActive) {
        Write-Log -Severity 'Information' -Message 'Choco.exe is active, cChocoEx Stopped'
        Break
    }

    #Log Start
    try {
        Write-Log -Severity 'Information' -Message 'cChocoEx Started'
        Write-EventLog -LogName 'Application' -Source 'cChocoEx' -EventId 4000 -EntryType Information -Message 'cChocoEx Started'
    }
    catch {
        Write-Warning "Error Starting Log, wiping and retrying"
        Write-Log -Severity 'Information' -Message 'cChoco Bootstrap Started' -New
        Write-EventLog -LogName 'Application' -Source 'cChocoEx' -EventId 4000 -EntryType Information -Message 'cChocoEx Started'
    }
    
    #Register and Start cChocoEx Task
    Register-cChocoExTask

    #Update Media Folder
    $null = Copy-Item -Path (Join-Path -Path ($PSScriptRoot | Split-Path) -ChildPath 'Media\*') -Destination $cChocoExMediaFolder -Recurse -Force

    #Ensure cChoco Module Is Present and Available
    if (-not($ModuleBase)) {
        Write-Log -Severity 'Error' -Message 'Required Module cChoco Not Found'
        Break
    }

    #Migrate Legacy Configuration Files
    if ($MigrateLegacyConfigurations) {
        Move-LegacyConfigurations
    }

    #Evaluate Mainteance Window Override
    if ($OverrideMaintenanceWindow) {
        $Global:OverrideMaintenanceWindow = $True
        Write-Log -Severity 'Information' -Message 'Global OverrideMaintenanceWindow Enabled'
    }
    else {
        $Global:OverrideMaintenanceWindow = $False
    }

    #Ensure Notification Prerequisites are Installed and Imported
    if ($Global:EnableNotifications) {
        $OSMajorVersion = (Get-CimInstance -ClassName Win32_OperatingSystem -Property Version).Version.Split('.')[0]
        if ([int]$OSMajorVersion -lt 10) {
            Write-Log -Severity 'Warning' -Message 'Notifications Require Windows 10 or Server 2016 and Greater'
            $Global:EnableNotifications = $false
        }
        else {
            Install-BurntToast
            Install-RunAsUser    
        }
    }
    
    #####################################
    # Gather Environment Variables
    #####################################
        
    #ChocoInstallScriptUrl
    if ($env:ChocoInstallScriptUrl) {
        Write-Log -Severity 'Information' -Message "Environment Variable `$env:ChocoInstallScriptUrl: $env:ChocoInstallScriptUrl"
        $ChocoInstallScriptUrl = $env:ChocoInstallScriptUrl
    }
    #ChocoDownloadUrl
    if ($env:ChocoDownloadUrl -and ([string]::IsNullOrEmpty($ChocoDownloadUrl))) {
        Write-Log -Severity 'Information' -Message "Environment Variable `$env:ChocoDownloadUrl: $env:ChocoDownloadUrl"
        $ChocoDownloadUrl = $env:ChocoDownloadUrl
    }
    #cChocoExChocoConfig
    if ($env:cChocoExChocoConfig -and ([string]::IsNullOrEmpty($ChocoConfig))) {
        Write-Log -Severity 'Information' -Message "Environment Variable `$env:cChocoExChocoConfig: $env:cChocoExChocoConfig"
        $ChocoConfig = $env:cChocoExChocoConfig
    }
    #cChocoExSourcesConfig
    if ($env:cChocoExSourcesConfig -and ([string]::IsNullOrEmpty($SourcesConfig))) {
        Write-Log -Severity 'Information' -Message "Environment Variable `$env:cChocoExSourcesConfig: $env:cChocoExSourcesConfig"
        $SourcesConfig = $env:cChocoExSourcesConfig
    }
    #cChocoExPackageConfig
    if ($env:cChocoExPackageConfig -and ([string]::IsNullOrEmpty($PackageConfig))) {
        Write-Log -Severity 'Information' -Message "Environment Variable `$env:cChocoExPackageConfig: $env:cChocoExPackageConfig"
        $PackageConfig = $env:cChocoExPackageConfig -join ','
    }
    #cChocoExFeatureConfig
    if ($env:cChocoExFeatureConfig -and ([string]::IsNullOrEmpty($FeatureConfig))) {
        Write-Log -Severity 'Information' -Message "Environment Variable `$env:cChocoExFeatureConfig: $env:cChocoExFeatureConfig"
        $FeatureConfig = $env:cChocoExFeatureConfig
    }

    #####################################
    # Set Environment Variables
    #####################################
    if ($SetcChocoExEnvironment) {
        #ChocoInstallScriptUrl
        if ($ChocoInstallScriptUrl) {
            Write-Log -Severity 'Information' -Message "Setting Environment Variable `$env:ChocoInstallScriptUrl: $ChocoInstallScriptUrl"
            [Environment]::SetEnvironmentVariable('ChocoInstallScriptUrl', $ChocoInstallScriptUrl, 'Machine')
            $env:ChocoInstallScriptUrl = $ChocoInstallScriptUrl  
        }
        #ChocoDownloadUrl
        if ($ChocoDownloadUrl) {
            Write-Log -Severity 'Information' -Message "Setting Environment Variable `$env:ChocoDownloadUrl: $ChocoDownloadUrl"
            [Environment]::SetEnvironmentVariable('ChocoDownloadUrl', $ChocoDownloadUrl, 'Machine')  
            $env:ChocoDownloadUrl = $ChocoDownloadUrl     
        }
        #cChocoExChocoConfig
        if ($ChocoConfig) {
            Write-Log -Severity 'Information' -Message "Setting Environment Variable `$env:ChocoConfig: $ChocoConfig"
            [Environment]::SetEnvironmentVariable('cChocoExChocoConfig', $ChocoConfig, 'Machine')
            $env:cChocoExChocoConfig = $ChocoConfig
        }
        #cChocoExSourcesConfig
        if ($SourcesConfig) {
            Write-Log -Severity 'Information' -Message "Setting Environment Variable `$env:SourcesConfig: $SourcesConfig"
            [Environment]::SetEnvironmentVariable('cChocoExSourcesConfig', $SourcesConfig, 'Machine')
            $env:cChocoExSourcesConfig = $SourcesConfig
        }
        #cChocoExPackageConfig
        if ($PackageConfig) {
            $PackageConfigString = $PackageConfig -join ','
            Write-Log -Severity 'Information' -Message "Setting Environment Variable `$env:cChocoExPackageConfig: $PackageConfigString"
            [Environment]::SetEnvironmentVariable('cChocoExPackageConfig', $PackageConfigString, 'Machine')
            $env:cChocoExPackageConfig = $PackageConfigString
        }
        #cChocoExFeatureConfig
        if ($FeatureConfig) {
            Write-Log -Severity 'Information' -Message "Setting Environment Variable `$env:FeatureConfig: $FeatureConfig"
            [Environment]::SetEnvironmentVariable('cChocoExFeatureConfig', $FeatureConfig, 'Machine')
            $env:cChocoExFeatureConfig = $FeatureConfig
        }
        #cChocoExBootStrapUri
        if ($env:cChocoExBootStrapUri) {
            Write-Log -Severity 'Information' -Message "Setting Environment Variable `$env:cChocoExBootStrapUri: $env:cChocoExBootStrapUri"
            [Environment]::SetEnvironmentVariable('cChocoExBootStrapUri', $env:cChocoExBootStrapUri, 'Machine')
        }
    }

    #####################################
    # cChocoInstaller
    #####################################
    $Configuration = @{
        InstallDir            = $InstallDir
        ChocoInstallScriptUrl = $ChocoInstallScriptUrl
    }

    #Set Enviromental Variable for chocolatey url to nupkg
    if ($ChocoDownloadUrl) {
        $env:chocolateyDownloadUrl = $ChocoDownloadUrl
    }
    Start-cChocoInstaller -Configuration $Configuration

    #Ensure Chocolatey Config is Valid
    #https://github.com/chocolatey/choco/issues/1047
    if (-not(Test-ChocolateyConfig)) {
        $Reset = Reset-ChocolateyConfig
        Write-Log -Severity 'Information' -Message $Reset.Config
        Write-Log -Severity 'Information' -Message "Chocolatey Config Reset: $($Reset.Reset)"
    }
    
    #Evaluate Random Delay Switch
    if ($RandomDelay) {
        $RandomSeconds = Get-Random -Minimum 0 -Maximum 1800
        Write-Log -Severity 'Information' -Message "Random Delay Enabled"
        Write-Log -Severity 'Information' -Message "Delay: $RandomSeconds`s"
        Start-Sleep -Seconds $RandomSeconds
    }

    #Settings
    if ($SettingsURI) {
        $Destination = (Join-Path $cChocoExTMPConfigurationFolder "bootstrap-cchoco.psd1")

        try {
            Write-Log -Severity 'Information' -Message "Downloading SettingsURI File"
            Write-Log -Severity 'Information' -Message "Source: $SettingsURI"
            Write-Log -Severity 'Information' -Message "Destination: $Destination"

            switch (Test-PathEx -Path $SettingsURI) {
                'URL' { Invoke-WebRequest -Uri $SettingsURI -UseBasicParsing -OutFile $Destination }
                'FileSystem' { Copy-Item -Path $SettingsURI -Destination $Destination -Force }
            }        
        }
        catch {
            Write-Log -Severity 'Warning' -Message $_.Exception.Message
        }
        $SettingsFile = Import-PowerShellDataFile -Path $Destination
        $Settings = $SettingsFile | ForEach-Object { $_.Keys | ForEach-Object { $SettingsFile.$_ } } 
    
        #Variables
        $InstallDir = $Settings.InstallDir
        $ChocoInstallScriptUrl = $Settings.ChocoInstallScriptUrl
        $SourcesConfig = $Settings.SourcesConfig
        $PackageConfig = $Settings.PackageConfig
        $ChocoConfig = $Settings.ChocoConfig
        $FeatureConfig = $Settings.FeatureConfig
    }

    Write-Log -Severity 'Information' -Message "cChocoEx Settings"
    Write-Log -Severity 'Information' -Message "SettingsURI: $SettingsURI"
    Write-Log -Severity 'Information' -Message "InstallDir: $InstallDir"
    Write-Log -Severity 'Information' -Message "ChocoInstallScriptUrl: $ChocoInstallScriptUrl"
    Write-Log -Severity 'Information' -Message "SourcesConfig: $SourcesConfig"
    Write-Log -Severity 'Information' -Message "PackageConfig: $PackageConfig"
    Write-Log -Severity 'Information' -Message "ChocoConfig: $ChocoConfig"
    Write-Log -Severity 'Information' -Message "FeatureConfig: $FeatureConfig"

    if ($WipeCache) {
        Write-Log -Severity 'Information' -Message 'WipeCache Enabled. Wiping any previously downloaded psd1 configuration files'
        Get-ChildItem -Path $cChocoExConfigurationFolder -Filter *.psd1 | Remove-Item -Recurse -Force
    }
    #Preclear any previously downloaded NoCache configuration files
    if ($NoCache) {
        Write-Log -Severity 'Information' -Message 'NoCache Enabled. Wiping any previously downloaded NoCache configuration files from temp'
        Get-ChildItem -Path $cChocoExTMPConfigurationFolder -Filter *.psd1 | Remove-Item -Recurse -Force
    }

    #Copy Config Config?
    $Global:ChocoConfigDestination = (Join-Path $cChocoExConfigurationFolder "config.psd1")
    if ($ChocoConfig) {
        if ($NoCache) {
            $Global:ChocoConfigDestination = (Join-Path $cChocoExTMPConfigurationFolder "config.psd1")
        }
        try {
            Write-Log -Severity 'Information' -Message "Downloading Choco Config File"
            Write-Log -Severity 'Information' -Message "Source: $ChocoConfig"
            Write-Log -Severity 'Information' -Message "Destination: $ChocoConfigDestination"

            switch (Test-PathEx -Path $ChocoConfig) {
                'URL' { Invoke-WebRequest -Uri $ChocoConfig -UseBasicParsing -OutFile $ChocoConfigDestination }
                'FileSystem' { Copy-Item -Path $ChocoConfig -Destination $ChocoConfigDestination -Force }
            } 
            Write-Log -Severity 'Information' -Message 'Chocolatey Config File Set.'   
        }
        catch {
            Write-Log -Severity 'Warning' -Message $_.Exception.Message
        }
    }

    #Copy Sources Config
    $Global:SourcesConfigDestination = (Join-Path $cChocoExConfigurationFolder "sources.psd1")
    if ($SourcesConfig) {
        if ($NoCache) {
            $Global:SourcesConfigDestination = (Join-Path $cChocoExTMPConfigurationFolder "sources.psd1")
        }
        try {
            Write-Log -Severity 'Information' -Message "Downloading Source Config File"
            Write-Log -Severity 'Information' -Message "Source: $SourcesConfig"
            Write-Log -Severity 'Information' -Message "Destination: $SourcesConfigDestination"

            switch (Test-PathEx -Path $SourcesConfig) {
                'URL' { Invoke-WebRequest -Uri $SourcesConfig -UseBasicParsing -OutFile $SourcesConfigDestination }
                'FileSystem' { Copy-Item -Path $SourcesConfig -Destination $SourcesConfigDestination -Force }
            }
            Write-Log -Severity 'Information' -Message 'Chocolatey Sources File Set.'
        }
        catch {
            Write-Log -Severity 'Warning' -Message $_.Exception.Message
        }
    }

    #Copy Features Config
    $Global:FeatureConfigDestination = (Join-Path $cChocoExConfigurationFolder "features.psd1")
    if ($FeatureConfig) {
        if ($NoCache) {
            $Global:FeatureConfigDestination = (Join-Path $cChocoExTMPConfigurationFolder "features.psd1")
        }
        try {
            Write-Log -Severity 'Information' -Message "Downloading Feature Config File"
            Write-Log -Severity 'Information' -Message "Source: $FeatureConfig"
            Write-Log -Severity 'Information' -Message "Destination: $FeatureConfigDestination"

            switch (Test-PathEx -Path $FeatureConfig) {
                'URL' { Invoke-WebRequest -Uri $FeatureConfig -UseBasicParsing -OutFile $FeatureConfigDestination }
                'FileSystem' { Copy-Item -Path $FeatureConfig -Destination $FeatureConfigDestination -Force }
            }
            Write-Log -Severity 'Information' -Message 'Chocolatey Feature File Set.'
        }
        catch {
            Write-Log -Severity 'Warning' -Message $_.Exception.Message
        }
    }

    #Copy Package Config
    $Global:PackageConfigDestination = $cChocoExConfigurationFolder
    if ($PackageConfig) {
        if ($NoCache) {
            $Global:PackageConfigDestination = $cChocoExTMPConfigurationFolder
        }
        $PackageConfig | ForEach-Object {
            $Path = $_
            $Destination = (Join-Path $PackageConfigDestination ($_ | Split-Path -Leaf))

            try {
                Write-Log -Severity 'Information' -Message "Downloading Package Config File"
                Write-Log -Severity 'Information' -Message "Source: $Path"
                Write-Log -Severity 'Information' -Message "Destination: $Destination"

                switch (Test-PathEx -Path $_) {
                    'URL' { Invoke-WebRequest -Uri $Path -UseBasicParsing -OutFile $Destination }
                    'FileSystem' { Copy-Item -Path $Path -Destination $Destination -Force }
                }
                Write-Log -Severity 'Information' -Message 'Chocolatey Package File Set.'
            }
            catch {
                Write-Log -Severity 'Warning' -Message $_.Exception.Message
            }
        }
    }

    #####################################
    # cChocoConfig
    #####################################
    if (Test-Path $ChocoConfigDestination ) {
        $ConfigImport = $null
        $ConfigImport = Import-PowerShellDataFile $ChocoConfigDestination
        Start-cChocoConfig -ConfigImport $ConfigImport
    }
    else {
        Write-Log -Severity 'Information' -Message "File not found, configuration will not be modified"
    }
    #####################################
    # cChocoFeature
    #####################################
    if (Test-Path $FeatureConfigDestination ) {
        $ConfigImport = $null
        $ConfigImport = Import-PowerShellDataFile $FeatureConfigDestination
        Start-cChocoFeature -ConfigImport $ConfigImport
    }
    else {
        Write-Log -Severity 'Information' -Message "File not found, features will not be modified"
    }
    #####################################
    # cChocoSource
    #####################################
    if (Test-Path $SourcesConfigDestination ) {
        $ConfigImport = $null
        $ConfigImport = Import-PowerShellDataFile $SourcesConfigDestination
        Start-cChocoSource -ConfigImport $ConfigImport
    }
    else {
        Write-Log -Severity 'Information' -Message "File not found, sources will not be modified"
    }
    #####################################
    # cChocoPackageInstall
    #####################################
    [array]$Configurations = $null
    Get-ChildItem -Path $PackageConfigDestination -Filter *.psd1 | Where-Object { $_.Name -notmatch "sources.psd1|config.psd1|features.psd1" } | ForEach-Object {
        $ConfigImport = $null
        $ConfigImport = Import-PowerShellDataFile $_.FullName 
        $Configurations += $ConfigImport | ForEach-Object { $_.Keys | ForEach-Object { $ConfigImport.$_ } }
    }

    if ($Configurations ) {
        Start-cChocoPackageInstall -Configurations $Configurations
    }
    else {
        Write-Log -Severity 'Information' -Message "File not found, packages will not be modified"
    }
    
    #Cleanup
    #Preclear any previously downloaded NoCache configuration files
    if ($NoCache) {
        Write-Log -Severity "Information" -Message "Preclear any previously downloaded NoCache configuration files"
        Get-ChildItem -Path $cChocoExTMPConfigurationFolder -Filter *.psd1 | Remove-Item -Recurse -Force
    }

    #Register cChocoEx BootStrap Task if Enabled
    if ($Loop) {
        Write-Log -Severity "Information" -Message "Function Looping Enabled"
        Write-Log -Severity "Information" -Message "Looping Delay: $LoopDelay Minutes"
        Register-cChocoExBootStrapTask -LoopDelay $LoopDelay
    }
    #Clear Pending file
    if (Test-Path -Path $PendingFile) {
        Remove-Item -Path $PendingFile -Force    
    }
    $null = Set-ExecutionPolicy $CurrentExecutionPolicy -Scope CurrentUser -ErrorAction SilentlyContinue
    Write-EventLog -LogName 'Application' -Source 'cChocoEx' -EventId 4001 -EntryType Information -Message 'cChocoEx Finished'
    RotateLog
}