DSCResources/POSHOrigin_vSphere_VM/Provisioners/Chef/Provision.ps1

[cmdletbinding()]
param(
    [parameter(mandatory)]
    $Options
)

begin {
    Write-Debug -Message 'Chef provisioner: beginning'
}

process {
    try {
        Write-Verbose -Message 'Configuring Chef client...'
        $provOptions = ConvertFrom-Json -InputObject $Options.Provisioners
        $chefOptions = ($provOptions | Where-Object {$_.name -eq 'chef'}).options

        $t = Get-VM -Id $Options.vm.Id -Verbose:$false -Debug:$false
        #$ip = $t.Guest.IPAddress | Where-Object { ($_ -notlike '169.*') -and ( $_ -notlike '*:*') } | Select-Object -First 1
        $ip = _GetGuestVMIPAddress -VM $t
        if ($null -ne $ip -and $ip -ne [string]::Empty) {

            $chefSvc = Invoke-Command -ComputerName $ip -Credential $Options.GuestCredentials -ScriptBlock { Get-Service -Name chef-client -ErrorAction SilentlyContinue } -Verbose:$false
            if (-Not $chefSvc) {
                # Invoke a local command on the target to install Chef
                $cmd = {
                    $VerbosePreference = $Using:VerbosePreference
                    Write-Verbose -Message 'Installing Chef client...'
                    try {
                        $options = $args[0]
                        $provOptions = $args[1]
                        $source = $provOptions.source
                        $sourceName = 'chef-client.msi'
                        $validatorKey = $provOptions.validatorKey
                        $validatorName = $validatorKey.split('/') | Select-Object -Last 1
                        $cert = $provOptions.cert
                        $certName = $cert.split('/') | Select-Object -Last 1
                        $runList = $provOptions.runList
                        $automateUrl = $provOptions.automateUrl
                        $automateToken = $provOptions.automateToken
                        $automateCert = $provOptions.automateCert
                        if ($automateCert) {
                            $automateCertName = $automateCert.split('/') | Select-Object -Last 1
                        }

                        # Ensure Chef node name is always lowercase
                        $fqdnlower = $provOptions.nodeName.ToLower()

                        # Copy Chef items locally
                        New-Item -Path "C:\Windows\Temp\ChefClient" -ItemType Directory -Force
                        Invoke-WebRequest -Uri $source -OutFile "c:\windows\temp\ChefClient\$sourceName"
                        Invoke-WebRequest -Uri $validatorKey -OutFile "c:\windows\temp\ChefClient\validator.pem"
                        Invoke-WebRequest -Uri $cert -OutFile "c:\windows\temp\ChefClient\$certName"
                        if ($automateCert) {
                            Invoke-WebRequest -Uri $automateCert -OutFile "C:\Windows\Temp\ChefClient\$automateCertName"
                        }

                        # Install Chef MSI
                        $params = @{
                            FilePath = 'msiexec'
                            ArgumentList = '/qn /i c:\windows\temp\ChefClient\' + $sourceName + ' ADDLOCAL="ChefClientFeature,ChefServiceFeature"'
                            Wait = $true
                        }
                        Start-Process @params

                        # Add Chef to env vars
                        If ($env:Path -notmatch 'C:\\opscode\\chef\\bin' -and $env:Path -notmatch 'c:\\opscode\\chef\\embedded\\bin') {
                            [Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\opscode\chef\bin;C:\opscode\chef\embedded\bin", [System.EnvironmentVariableTarget]::Machine)
                            $env:Path = $env:Path + ";C:\opscode\chef\bin;C:\opscode\chef\embedded\bin"
                        }

                        # Create knife.rb
                        $url = $provOptions.url
                        $validatorClientName = $validatorName.split('.')[0]
                        $knifeRB= @"
current_dir = File.dirname(__FILE__)
log_level :info
log_location STDOUT
node_name "$fqdnlower"
client_key "c:\\chef\\client.pem"
validation_client_name "$validatorClientName"
validation_key "c:\\chef\\validator.pem"
chef_server_url "$url"
cookbook_path ["C:\\chef_cookbooks"]
"@

                        if ($automateUrl) {
                        $clientRB = @"
chef_server_url "$url"
validation_client_name "$validatorClientName"
validation_key "c:\\chef\\validator.pem"
client_key "c:\\chef\\client.pem"
node_name '$fqdnlower'
data_collector.server_url "$automateUrl"
data_collector.token "$automateToken"
"@

                        } else {
                        $clientRB = @"
chef_server_url "$url"
validation_client_name "$validatorClientName"
validation_key "c:\\chef\\validator.pem"
client_key "c:\\chef\\client.pem"
node_name '$fqdnlower'
"@

                }
                        New-Item -Path "$HOME\.chef" -ItemType Directory -ErrorAction SilentlyContinue -Force
                        $knifeRB | Out-File -FilePath "$HOME\.chef\knife.rb" -Encoding ascii -Force
                        $clientRB | Out-File -FilePath 'c:\chef\client.rb' -Encoding ascii -Force

                        # Copy certs
                        New-Item -Path "$HOME\.chef\trusted_certs" -ItemType Directory -ErrorAction SilentlyContinue
                        New-Item -Path 'c:\chef\trusted_certs' -Type Directory -Force -ErrorAction SilentlyContinue
                        Copy-Item -Path "c:\windows\temp\ChefClient\$certName" -Destination 'c:\chef\trusted_certs' -Force
                        Copy-Item -Path "c:\windows\temp\ChefClient\$certName" -Destination "$HOME\.chef\trusted_certs" -Force
                        Copy-Item -Path "c:\windows\temp\ChefClient\validator.pem" -Destination 'c:\chef' -Force
                        if ($automateCert) {
                            Copy-Item -Path "c:\windows\temp\ChefClient\$automateCertName" -Destination 'c:\chef\trusted_certs' -Force
                        }

                        # Start Chef as service
                        Start-Process -FilePath 'chef-service-manager' -ArgumentList '-a install' -NoNewWindow -Wait
                        Start-Process -FilePath 'chef-service-manager' -ArgumentList '-a start' -NoNewWindow -Wait
                        Start-Process -FilePath 'chef-client' -NoNewWindow -Wait

                        # Cleanup
                        Remove-Item -Path "c:\chef\validator.pem" -Force
                        Remove-Item -Path 'c:\windows\temp\chefclient\' -Recurse -Force

                        Write-Verbose -Message 'Chef installed. Sleeping...'
                        Start-Sleep -Seconds 5
                        return $true
                    } catch {
                        Write-Error -Message 'There was a problem installing the Chef client'
                        Write-Error -Message "$($_.InvocationInfo.ScriptName)($($_.InvocationInfo.ScriptLineNumber)): $($_.InvocationInfo.Line)"
                        write-Error $_
                        return $false
                    }
                }
                $params = @{
                    ComputerName = $ip
                    Credential = $Options.GuestCredentials
                    ScriptBlock = $cmd
                    ArgumentList = @($Options, $chefOptions)
                }
                $chefInstallResult = Invoke-Command @params
            }

            # Chef is already installed or was just installed
            if ($chefSvc -or $chefInstallResult) {
            
                # Check automate settings if any, and update if needed
                $automateCmd = {
                    $VerbosePreference = $Using:VerbosePreference
                    $chefOptions = $args[0]
                    $automateUrl = $chefOptions.automateUrl
                    $automateToken = $chefOptions.automateToken
                    $automateCert = $chefOptions.automateCert
                    $automateCertName = $automateCert.split('/') | Select-Object -Last 1
                    $testExists = Test-Path "C:\chef\trusted_certs\$automateCertName"
                    if (!($testExists)) {
                        Invoke-WebRequest -Uri "$automateCert" -OutFile "C:\chef\trusted_certs\$automateCertName" | Out-Null
                    } else {
                        Invoke-WebRequest -Uri "$automateCert" -OutFile "C:\chef\$automateCertName" | Out-Null
                        $serverVersion = Get-Content "C:\chef\trusted_certs\$automateCertName"
                        $currentVersion = Get-Content "C:\chef\$automateCertName"
                        $compare = Compare-Object $serverVersion $currentVersion
                        Start-Sleep -Seconds 1
                        if ($compare) {
                            Write-Verbose -Message "Updated chef automate cert"
                            Move-Item -Path "C:\chef\$automateCertName" -Destination "C:\chef\trusted_certs\$automateCertName" -Force -Confirm:$false
                        } else {
                            Remove-Item -Path "C:\chef\$automateCertName" -Force -Confirm:$false
                        }
                    }
                    $clientRB = Get-Content C:\chef\client.rb -ErrorAction SilentlyContinue | Out-String
                        $autoUrl = "(.*)data_collector.server_url\s+'$automateUrl(.*)'"
                        $autoToken = "(.*)data_collector.token\s+'$automateToken(.*)'"
                    if ($clientRB -notmatch $autoUrl) {
                        if ($clientRB -like "*data_collector.server_url*") {
                            $clientRB = Get-Content C:\chef\client.rb -ErrorAction SilentlyContinue
                            $clientRB | foreach-Object {$_ -replace "^.*data_collector.server_url.*$","data_collector.server_url`t'$automateUrl'"} | set-content C:\chef\client.rb
                            Write-Verbose 'Updated chef client.rb for automate url'
                        } else {
                            Add-Content C:\chef\client.rb -Value "`r`ndata_collector.server_url`t'$automateUrl'"
                            Write-Verbose 'Created entry in client.rb for automate url'
                        }
                    }

                    $clientRB = Get-Content C:\chef\client.rb -ErrorAction SilentlyContinue | Out-String
                    if ($clientRB -notmatch $autoToken) {
                        if ($clientRB -like "*data_collector.token*") {
                            $clientRB = Get-Content C:\chef\client.rb -ErrorAction SilentlyContinue
                            $clientRB | foreach-Object {$_ -replace "^.*data_collector.token.*$","data_collector.token`t'$automateToken'"} | set-content C:\chef\client.rb
                            Write-Verbose 'Updated chef client.rb for automate token'
                        } else {
                            Add-Content C:\chef\client.rb -Value "`r`ndata_collector.token`t'$automateToken'"
                            Write-Verbose 'Created entry in client.rb for automate token'
                        }
                    }
                }
                $automateParams = @{
                    ComputerName = $ip
                    Credential = $Options.GuestCredentials
                    ScriptBlock = $automateCmd
                    ArgumentList = $chefOptions
                }

                if ($chefOptions.automateUrl) {
                    Invoke-Command @automateParams
                }

                # Get the node from Chef
                $getParams = @{
                    Method = 'GET'
                    OrgUri = $chefOptions.url
                    Path = "/nodes/$($chefOptions.NodeName)"
                    UserItem = (Split-Path -Path $chefOptions.clientKey -Leaf).Split('.')[0]
                    KeyPath = $chefOptions.clientKey
                }
                $chefNode = & "$PSScriptRoot\Helpers\_InvokeChefQuery.ps1" @getParams

                if ($chefNode) {
                    # Verify run list
                    if (@($chefNode.run_list).Count -ne @($chefOptions.runlist).Count) {
                        Write-Verbose -Message "Chef run list does not match"

                        # Update the run list on the node
                        $chefNode.run_List = @(@($chefOptions.runlist) | ForEach-Object {
                            if ($_.recipe) {
                                "recipe[$($_.recipe)]"
                            } elseif ($_.role) {
                                "role[$($_.role)]"
                            }
                        })

                        # Send the json to the Chef API
                        $newRunList = $chefNode.Run_List -join ','
                        Write-Verbose -Message "Assigning run list: $newRunList"
                        $putParams = @{
                            Method = 'PUT'
                            OrgUri = $chefOptions.url
                            Path = "/nodes/$($chefOptions.NodeName)"
                            UserItem = (Split-Path -Path $chefOptions.clientKey -Leaf).Split('.')[0]
                            KeyPath = $chefOptions.clientKey
                            data = $ChefNode | ConvertTo-Json
                        }
                        $putResult = & "$PSScriptRoot\Helpers\_InvokeChefQuery.ps1" @putParams
                    }

                    # Verify environment
                    if ($chefOptions.environment) {
                        if ($chefNode.chef_environment.ToLower() -ne $chefOptions.environment.ToLower()) {
                            $chefNode.chef_environment = $chefOptions.environment.ToLower()
                            Write-Verbose -Message "Changing environment to [$($chefNode.chef_environment.ToLower())]"
                            $putParams = @{
                                Method = 'PUT'
                                OrgUri = $chefOptions.url
                                Path = "/nodes/$($chefOptions.NodeName)"
                                UserItem = (Split-Path -Path $chefOptions.clientKey -Leaf).Split('.')[0]
                                KeyPath = $chefOptions.clientKey
                                data = $ChefNode | ConvertTo-Json
                            }
                            $putResult = & "$PSScriptRoot\Helpers\_InvokeChefQuery.ps1" @putParams
                        }
                    }

                    # Assign attributes if needed
                    # If we didn't specify any desired attributes, create an empty set
                    # so we can compare it against Chef
                    # Chef node attributes usually have an empty tags attributes by default
                    # so add that to the reference if isn't doesn't already exist
                    if (-Not $ChefOptions.attributes) {
                        $chefOptions | Add-Member -MemberType NoteProperty -Name attributes -Value @{tags = @()}
                    } else {
                        if (-Not $ChefOptions.attributes.tags) {
                            $chefOptions.attributes | Add-Member -MemberType NoteProperty -Name tags -Value @()
                        }
                    }
                    $refJson = $chefOptions.attributes | ConvertTo-Json
                    $diffJson = $chefNode.normal | ConvertTo-Json
                    if ($diffJson -ne $refJson) {
                        # Attributes don't match so update them
                        Write-Verbose -Message "Setting node attributes to `n $refJson"
                        $chefNode.normal = $chefOptions.attributes
                        $putParams = @{
                            Method = 'PUT'
                            OrgUri = $chefOptions.url
                            Path = "/nodes/$($chefOptions.NodeName)"
                            UserItem = (Split-Path -Path $chefOptions.clientKey -Leaf).Split('.')[0]
                            KeyPath = $chefOptions.clientKey
                            data = $chefNode | ConvertTo-Json
                        }
                        $putResult = & "$PSScriptRoot\Helpers\_InvokeChefQuery.ps1" @putParams
                    }
                } else {
                    Write-Error -Message "Unable to get find node $chefOptions.NodeName"
                }
            } else {
                Write-Error -Message 'There was a problem installing the Chef client. No validation of the Chef client will be done.'
            }
        } else {
           Write-Error -Message 'No valid IP address returned from VM view. Can not configure the Chef client'
        }
    } catch {
        Write-Error -Message 'There was a problem running the Chef provisioner'
        Write-Error -Message "$($_.InvocationInfo.ScriptName)($($_.InvocationInfo.ScriptLineNumber)): $($_.InvocationInfo.Line)"
        write-Error $_
        return $false
    }
}

end {
    Write-Debug -Message 'Chef provisioner: ending'
}