new-module -name Omnitruck -scriptblock { [Console]::OutputEncoding = New-Object -typename System.Text.ASCIIEncoding [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]'Tls,Tls11,Tls12' function Get-PlatformVersion { [version]$osVersion = (Get-Win32OS).version $platform_version = switch ($osVersion) { # Windows Server build numbers from: https://betawiki.net/wiki/Microsoft_Windows { $_ -ge [version]'10.0.20145' } { '2022'; break } { $_ -ge [version]'10.0.17609' } { '2019'; break } { $_ -ge [version]'10.0.0' } { '2016'; break } { $_ -ge [version]'6.3.0' } { '2012r2'; break } { $_ -ge [version]'6.2.0' } { '2012'; break } { $_ -ge [version]'6.1.0' } { '2008r2'; break } { $_ -ge [version]'6.0.0' } { '2008'; break } } if(Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Server\ServerLevels') { $levels = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Server\ServerLevels' if($levels.NanoServer -eq 1) { $platform_version += 'nano' } } return $platform_version } function Get-PlatformArchitecture { if ((Get-Win32OS).osarchitecture -match '64') { $architecture = 'x86_64' } else { $architecture = 'i386' } return $architecture } function Get-Win32OS { if(!$global:win32OS) { $global:win32OS = Get-WMIQuery win32_operatingsystem } $global:win32OS } function New-Uri { param ($baseuri, $newuri) try { $base = new-object System.Uri $baseuri new-object System.Uri $base, $newuri } catch [System.Management.Automation.MethodInvocationException]{ Write-Error "$($_.exception.message)" throw $_.exception } } function Get-WebContent { param ($uri, $filepath) try { if($PSVersionTable.PSEdition -eq 'Core') { Get-WebContentOnCore $uri $filepath } else { Get-WebContentOnFullNet $uri $filepath } } catch { $exception = $_.Exception Write-Host "There was an error: " do { Write-Host "`t$($exception.message)" $exception = $exception.innerexception } while ($exception) throw "Failed to download from $uri." } } function Get-WebContentOnFullNet { param ($uri, $filepath) $proxy = New-Object -TypeName System.Net.WebProxy $wc = new-object System.Net.WebClient $wc.Headers.Add("user-agent", "mixlib-install/3.12.30") $proxy.Address = $env:http_proxy $bypassList = $env:no_proxy if($bypassList -ne $null){ $bypassList = $bypassList.split(",") $proxy.BypassList = $byPassList } $wc.Proxy = $proxy if ([string]::IsNullOrEmpty($filepath)) { $wc.downloadstring($uri) } else { $wc.downloadfile($uri, $filepath) } } function Get-WebContentOnCore { param ($uri, $filepath) $handler = New-Object System.Net.Http.HttpClientHandler $client = New-Object System.Net.Http.HttpClient($handler) $client.DefaultRequestHeaders.UserAgent.ParseAdd("mixlib-install/3.12.30") $client.Timeout = New-Object System.TimeSpan(0, 30, 0) $cancelTokenSource = [System.Threading.CancellationTokenSource]::new() $responseMsg = $client.GetAsync([System.Uri]::new($uri), $cancelTokenSource.Token) $responseMsg.Wait() if (!$responseMsg.IsCanceled) { $response = $responseMsg.Result if ($response.IsSuccessStatusCode) { if ([string]::IsNullOrEmpty($filepath)) { $response.Content.ReadAsStringAsync().Result } else { $downloadedFileStream = [System.IO.FileStream]::new($filepath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write) $copyStreamOp = $response.Content.CopyToAsync($downloadedFileStream) $copyStreamOp.Wait() $downloadedFileStream.Close() if ($copyStreamOp.Exception -ne $null) { throw $copyStreamOp.Exception } } } } } function Test-ProjectPackage { [cmdletbinding()] param ($Path, $Algorithm = 'SHA256', $Hash) if (!$env:Valid_ProjectPackage){ Write-Verbose "Testing the $Algorithm hash for $path." $ActualHash = (Custom-GetFileHash -Algorithm $Algorithm -Path $Path).Hash.ToLower() Write-Verbose "`tDesired Hash - '$Hash'" Write-Verbose "`tActual Hash - '$ActualHash'" $env:Valid_ProjectPackage = $ActualHash -eq $Hash if (-not $env:Valid_ProjectPackage) { Write-Error "Failed to validate the downloaded installer. The expected $Algorithm hash was '$Hash' and the actual hash was '$ActualHash' for $path" } } return $env:Valid_ProjectPackage } function Custom-GetFileHash ($Path, $Algorithm) { function disposable($o){($o -is [IDisposable]) -and (($o | get-member | foreach-object {$_.name}) -contains 'Dispose')} function use($obj, [scriptblock]$sb){try {& $sb} catch [exception]{throw $_} finally {if (disposable $obj) {$obj.Dispose()}} } $Path = (resolve-path $Path).providerpath $hash = @{Algorithm = $Algorithm; Path = $Path} use ($c = Get-SHA256Converter) { use ($in = (gi $Path).OpenRead()) { $hash.Hash = ([BitConverter]::ToString($c.ComputeHash($in))).Replace("-", "").ToUpper() } } return $hash } function Get-SHA256Converter { if ($(Is-FIPS) -ge 1) { New-Object -TypeName Security.Cryptography.SHA256Cng } else { if($PSVersionTable.PSEdition -eq 'Core') { [System.Security.Cryptography.SHA256]::Create() } else { New-Object -TypeName Security.Cryptography.SHA256Managed } } } function Is-FIPS { if (!$env:fips){ $env:fips = (Get-ItemProperty HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy).Enabled } return $env:fips } function Get-WMIQuery { param ($class) if(Get-Command -Name Get-CimInstance -ErrorAction SilentlyContinue) { try{ $classObject = Get-CimInstance $class # If the Get-CimInstance command exists but fails due to security settings, try Get-WmiObject. } catch { $classObject = Get-WmiObject $class } } else { $classObject = Get-WmiObject $class } return $classObject } function Get-ProjectMetadata { <# .SYNOPSIS Get metadata for a Chef Software, Inc. project .DESCRIPTION Get metadata for project .EXAMPLE iex (new-object net.webclient).downloadstring('https://omnitruck.chef.io/install.ps1'); Get-ProjectMetadata -project chef -channel stable Gets the download url and SHA256 checksum for the latest stable release of Chef. .EXAMPLE iex (irm 'https://omnitruck.chef.io/install.ps1'); Get-ProjectMetadata -project chefdk -channel stable -version 0.8.0 Gets the download url and SHA256 checksum for ChefDK 0.8.0. #> [cmdletbinding()] param ( # Base url to retrieve metadata from. [uri]$base_server_uri = 'https://omnitruck.chef.io', [string] # Project to install [string] $project = 'chef', # Version of the application to install # This parameter is optional, if not supplied it will provide the latest version, # and if an iteration number is not specified, it will grab the latest available iteration. # Partial version numbers are also acceptable (using v=11 # will grab the latest 11.x client which matches the other flags). [string] $version, # Release channel to install from [validateset('current', 'stable', 'unstable')] [string] $channel = 'stable', # The following legacy switches are just aliases for the current channel [switch] $prerelease, [switch] $nightlies, [validateset('auto', 'i386', 'x86_64')] [string] $architecture = 'auto' ) # The following legacy switches are just aliases for the current channel if (($prerelease -eq $true)) { $channel = 'current'} if (($nightlies -eq $true)) { $channel = 'current'} # PowerShell is only on Windows ATM $platform = 'windows' Write-Verbose "Platform: $platform" $platform_version = Get-PlatformVersion Write-Verbose "Platform Version: $platform_version" if ($architecture -eq 'auto') { $architecture = Get-PlatformArchitecture } Write-Verbose "Architecture: $architecture" Write-Verbose "Project: $project" $metadata_base_url = "/$($channel)/$($project)/metadata" $metadata_array = ("?v=$($version)", "p=$platform", "pv=$platform_version", "m=$architecture") $metadata_base_url += [string]::join('&', $metadata_array) $metadata_url = new-uri $base_server_uri $metadata_base_url Write-Verbose "Downloading $project details from $metadata_url" $package_metadata = (Get-WebContent $metadata_url).trim() -split '\n' | foreach { $hash = @{} } {$key, $value = $_ -split '\s+'; $hash.Add($key, $value)} {$hash} Write-Verbose "Project details: " foreach ($key in $package_metadata.keys) { Write-Verbose "`t$key = $($package_metadata[$key])" } $package_metadata } function Install-Project { <# .SYNOPSIS Install a Chef Software, Inc. product .DESCRIPTION Install a Chef Software, Inc. product .EXAMPLE iex (new-object net.webclient).downloadstring('https://omnitruck.chef.io/install.ps1'); Install-Project -project chef -channel stable Installs the latest stable version of Chef. .EXAMPLE iex (irm 'https://omnitruck.chef.io/install.ps1'); Install-Project -project chefdk -channel current Installs the latest integration build of the Chef Development Kit #> [cmdletbinding(SupportsShouldProcess=$true)] param ( # Project to install [string] $project = 'chef', # Release channel to install from [validateset('current', 'stable', 'unstable')] [string] $channel = 'stable', # Version of the application to install # This parameter is optional, if not supplied it will provide the latest version, # and if an iteration number is not specified, it will grab the latest available iteration. # Partial version numbers are also acceptable (using v=11 # will grab the latest 11.x client which matches the other flags). [string] $version, # Full path for the downloaded installer. [string] $filename, # Full path to the location to download the installer [string] $download_directory = $env:temp, # The following legacy switches are just aliases for the current channel [switch] $prerelease, [switch] $nightlies, [validateset('auto', 'i386', 'x86_64')] [string] $architecture = 'auto', [validateset('auto', 'service', 'task')] [string] $daemon = 'auto', [string] $http_proxy, # Specify an alternate download url [string] $download_url_override, # SHA256 checksum to verify cached files (optional) [string] $checksum, # Set to 'once' to skip install if project is detected [string] $install_strategy ) if ((Test-Path "$env:systemdrive\opscode\$project\embedded") -and ($install_strategy -eq 'once')) { Write-Host "$project installation detected" Write-Host "install_strategy set to 'once'" Write-Host "Nothing to install" exit } # Set http_proxy as env var if(-not [string]::IsNullOrEmpty($http_proxy)) { $env:http_proxy = $http_proxy } $cached_installer_available = $false $verify_checksum = $true if (-not [string]::IsNullOrEmpty($download_url_override)) { $download_url = $download_url_override $sha256 = $checksum } else { $package_metadata = Get-ProjectMetadata -project $project -channel $channel -version $version -prerelease:$prerelease -nightlies:$nightlies -architecture $architecture $download_url = $package_metadata.url $sha256 = $package_metadata.sha256 } if (-not [string]::IsNullOrEmpty($filename)) { $download_directory = split-path $filename $filename = split-path $filename -leaf if ([string]::IsNullOrEmpty($download_directory)) { $download_directory = $pwd } } else { $filename = (([System.Uri]$download_url).AbsolutePath -split '/')[-1] } Write-Verbose "Download directory: $download_directory" Write-Verbose "Filename: $filename" if (-not (test-path $download_directory)) { mkdir $download_directory } $download_directory = (resolve-path $download_directory).providerpath $download_destination = join-path $download_directory $filename if ((test-path $download_destination)) { Write-Verbose "Found existing installer at $download_destination." if (-not [string]::IsNullOrEmpty($sha256)) { Write-Verbose "Checksum specified" $valid_checksum = Test-ProjectPackage -Path $download_destination -Algorithm 'SHA256' -Hash $sha256 if ($valid_checksum -eq $true) { Write-Verbose "Checksum verified, using existing installer." $cached_installer_available=$true # local file OK $verify_checksum = $false # no need to re-verify checksums } else { Write-Verbose "Checksum mismatch, ignoring existing installer." $cached_installer_available=$false # bad local file $verify_checksum = $false # re-verify checksums } } else { Write-Verbose "Checksum not specified, existing installer ignored." $cached_installer_available=$false # ignore local file $verify_checksum = $false # no checksum to compare } } if (-not ($cached_installer_available)) { if ($pscmdlet.ShouldProcess("$($download_url)", "Download $project")) { Write-Verbose "Downloading $project from $($download_url) to $download_destination." Get-WebContent $download_url -filepath $download_destination } } if ($pscmdlet.ShouldProcess("$download_destination", "Installing")) { if (($verify_checksum) -and (-not (Test-ProjectPackage -Path $download_destination -Algorithm 'SHA256' -Hash $sha256))) { throw "Failed to validate the downloaded installer for $project." } Write-Host "Installing $project from $download_destination" $installingProject = $True $installAttempts = 0 while ($installingProject) { $installAttempts++ $result = $false if($download_destination.EndsWith(".appx")) { $result = Install-ChefAppx $download_destination $project } else { $result = Install-ChefMsi $download_destination $daemon } if(!$result) { continue } $installingProject = $False } } } set-alias install -value Install-Project Function Install-ChefMsi($msi, $addlocal) { if ($addlocal -eq "service") { $p = Start-Process -FilePath "msiexec.exe" -ArgumentList "/qn /i $msi ADDLOCAL=`"ChefClientFeature,ChefServiceFeature`"" -Passthru -Wait -NoNewWindow } ElseIf ($addlocal -eq "task") { $p = Start-Process -FilePath "msiexec.exe" -ArgumentList "/qn /i $msi ADDLOCAL=`"ChefClientFeature,ChefSchTaskFeature`"" -Passthru -Wait -NoNewWindow } ElseIf ($addlocal -eq "auto") { $p = Start-Process -FilePath "msiexec.exe" -ArgumentList "/qn /i $msi" -Passthru -Wait -NoNewWindow } $p.WaitForExit() if ($p.ExitCode -eq 1618) { Write-Host "$((Get-Date).ToString()) - Another msi install is in progress (exit code 1618), retrying ($($installAttempts))..." return $false } elseif ($p.ExitCode -ne 0) { throw "msiexec was not successful. Received exit code $($p.ExitCode)" } return $true } Function Install-ChefAppx($appx, $project) { Add-AppxPackage -Path $appx -ErrorAction Stop $package = (Get-AppxPackage -Name $project).InstallLocation $installRoot = "$env:SystemDrive/opscode" $omnibusRoot = Join-Path $installRoot $project if(!(Test-Path $installRoot)) { New-Item -ItemType Directory -Path $installRoot } # Remove old version of chef if it is here if(Test-Path $omnibusRoot) { Remove-Item -Path $omnibusRoot -Recurse -Force } # copy the appx install to the omnibus root. There are serious # ACL related issues with running chef from the appx InstallLocation # Hoping this is temporary and we can eventually just symlink Copy-Item $package $omnibusRoot -Recurse return $true } export-modulemember -function 'Install-Project','Get-ProjectMetadata' -alias 'install' }