daisy_workflows/image_build/sqlserver/sql_install.ps1 (321 lines of code) (raw):
# Copyright 2017 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
$ErrorActionPreference = 'Stop'
function Format-ScratchDisk {
<#
.SYNOPSIS
Clears then formats the scratch disk and assigns as D:
#>
Write-Host 'Formatting scratch disk.'
Set-Disk -Number 1 -IsOffline $false
Initialize-Disk -Number 1 -PartitionStyle MBR
New-Partition -DiskNumber 1 -UseMaximumSize -DriveLetter D -IsActive |
Format-Volume -FileSystem 'NTFS' -Confirm:$false
Write-Host 'Formatting scratch disk complete.'
}
function Get-MetadataValue {
<#
.SYNOPSIS
Returns a value for a given metadata key.
.DESCRIPTION
Attempt to retrieve the value for a given metadata key.
Returns null if not found.
.PARAMETER $key
The metadata key to retrieve.
.PARAMETER $default
The value to return if the key is not found.
.RETURNS
The value for the key or null.
#>
param (
[parameter(Mandatory=$true)]
[string]$key,
[parameter(Mandatory=$false)]
[string]$default
)
# Returns the provided metadata value for a given key.
$url = "http://metadata.google.internal/computeMetadata/v1/instance/attributes/${key}"
try {
$client = New-Object Net.WebClient
$client.Headers.Add('Metadata-Flavor', 'Google')
return ($client.DownloadString($url)).Trim()
}
catch [System.Net.WebException] {
if ($default) {
return $default
}
else {
Write-Host "Failed to retrieve value for ${key}."
return $null
}
}
}
function Download-Sbomutil {
<#
.SYNOPSIS
Downloads sbomutil from GCE to the local components directory.
#>
$gs_path = Get-MetadataValue -key 'sbom-util-gcs-root'
if (!$gs_path) {
Write-Output "No metadata sbom-util-gcs-root set, skipping sbomutil download."
return
}
$gs_path = "${gs_path}/windows"
$latest = gsutil ls "${gs_path}" | Select -Last 1
if (!$latest) {
Write-Output "Could not determine sbomutil's latest release, skipping sbomutil download."
return
}
# The variable $latest already has a backslash at the end, as a result of gsutil ls.
Write-Output "Downloading sbomutil from $latest."
& 'gsutil' -m cp "${latest}sbomutil.exe" C:\sbomutil.exe
Write-Output 'Components download complete.'
}
function Generate-Sbom {
<#
.SYNOPSIS
Generates sbom and upload the result to a gcs bucket.
#>
$gs_path = Get-MetadataValue -key 'sbom-destination'
if (!$gs_path) {
Write-Output "No metadata sbom-destination set, skipping sbom generation."
return
}
if (!(Test-Path "C:\sbomutil.exe")) {
Write-Output "Could not find sbomutil tool, skipping sbom generation."
return
}
# Comp name is a short descriptor at the top of the sbom file for the software.
$comp_name = Get-MetadataValue -key 'img-family'
Write-Output "Generating sbom."
& "C:\sbomutil.exe" -archetype=windows-image -googet_path 'C:\ProgramData\GooGet' -extra_content="${script:sbom_dir}\" -comp_name="${comp_name}" -output image.sbom.json
& 'gsutil' -m cp image.sbom.json $gs_path
Write-Output "Sbom file uploaded to $gs_path."
}
function Install-WindowsUpdates {
<#
.SYNOPSIS
Check for updates, returns true if restart is required.
#>
# https://support.microsoft.com/en-us/help/4072698/windows-server-guidance-to-protect-against-the-speculative-execution
if (-not (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\QualityCompat')) {
New-Item -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\QualityCompat' -Type Directory | Out-Null
}
New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\QualityCompat' -Name 'cadca5fe-87d3-4b96-b7fb-a231484277cc' -Value 0 -PropertyType DWORD -Force | Out-Null
Write-Host 'Install-WindowsUpdates: Starting Windows update.'
# In 2008R2 the initial search can fail with error 0x80244010. Retrying the search again generally resolves the issueis.
$session = New-Object -ComObject 'Microsoft.Update.Session'
$query = 'IsInstalled=0'
$searcher = $session.CreateUpdateSearcher()
$i = 1
while ($i -lt 10) {
try {
Write-Host "Install-WindowsUpdates: Searching for updates, try $i."
$updates = $searcher.Search($query).Updates
# Skip Windows 7 optional language pack updates
if ($pn -like 'Windows 7*') {
if ($updates.Count -le 37 -and $updates.Count -ge 33) {
Write-Host 'Install-WindowsUpdates: Windows 7 detected. Skipping ~35 language pack updates.'
$query = 'IsInstalled=0 and AutoSelectOnWebsites=1'
continue
}
}
break
} catch {
Write-Host 'Install-WindowsUpdates: Update search failed.'
$i++
if ($i -ge 10) {
Write-Host 'Install-WindowsUpdates: Reseting update server'
Reset-WindowsUpdateServer | Out-Null
Write-Host 'Install-WindowsUpdates: Searching for updates one last time.'
$updates = $searcher.Search($query).Updates
}
}
}
if ($updates.Count -eq 0) {
Write-Host 'Install-WindowsUpdates: No updates required!'
return $false
}
# Windows 7 may enter a loop with a single update remaining
if ($pn -like 'Windows 7*') {
if ($updates.Count -eq 1) {
Write-Host 'Install-WindowsUpdates: Windows 7 detected. Single update remaining. Displaying and continuing install.'
foreach ($update in $updates) {
Write-Host ($update.Description)
}
return $false
}
}
foreach ($update in $updates) {
if (-not ($update.EulaAccepted)) {
Write-Host 'The following update required a EULA to be accepted:'
Write-Host '----------------------------------------------------'
Write-Host ($update.Description)
Write-Host '----------------------------------------------------'
Write-Host ($update.EulaText)
Write-Host '----------------------------------------------------'
$update.AcceptEula()
}
}
Write-Host "Install-WindowsUpdates: Downloading and installing $($updates.Count) updates."
foreach ($update in $updates) {
Write-Host "Install-WindowsUpdates: Update - Title:$($update.Title), Description:$($update.Description)"
}
$downloader = $session.CreateUpdateDownloader()
$downloader.Updates = $updates
$download_result = $downloader.Download()
Write-Host "Install-WindowsUpdates: Download complete. Result: $(Get-ResultCodeDescription $download_result.ResultCode). Installing updates."
$installer = $session.CreateUpdateInstaller()
$installer.Updates = $updates
$installer.AllowSourcePrompts = $false
$install_result = $installer.Install()
Write-Host "Install-WindowsUpdates: Update installation completed. Result: $(Get-ResultCodeDescription $install_result.ResultCode)"
return $true
}
function Get-ResultCodeDescription {
<#
.SYNOPSIS
Returns the description of the Windows Update download/install ResultCode.
.PARAMETER $ResultCode
The ResultCode to convert.
.RETURNS
The human readable description of the result code.
#>
param (
[Parameter(Mandatory=$true)] [int]$ResultCode
)
$Result = switch ($ResultCode) {
0 { 'Not Started' }
1 { 'In Progress' }
2 { 'Succeeded' }
3 { 'SucceededWithErrors' }
4 { 'Failed' }
5 { 'Aborted' }
default { "Unknown, ResultCode: $ResultCode" }
}
return $Result
}
function Install-SqlServer {
Write-Host 'Beginning SQL Server install.'
$sql_server_media = Get-MetadataValue -key 'sql-server-media'
$sql_server_config = Get-MetadataValue -key 'sql-server-config'
$gs_path = Get-MetadataValue -key 'daisy-sources-path'
$sql_install = 'C:\sql_server_install'
$sql_config_path = "${gs_path}/sql_config.ini"
$sql_config = 'D:\sql_config.ini'
& 'gsutil' -m cp $sql_config_path $sql_config
if ($sql_server_config -like '*2012*' -or $sql_server_config -like '*2014*') {
Write-Host 'Installing .Net 3.5'
Install-WindowsFeature Net-Framework-Core
}
if ($sql_server_media -like '*.iso') {
Write-Host 'Downloading SQL Server ISO'
$iso = "${script:sbom_dir}\sql_server.iso"
& 'gsutil' -m cp "${gs_path}/sql_installer.media" $iso
Write-Host 'Mount ISO'
$mount_result = Mount-DiskImage -ImagePath $iso -PassThru
$iso_drive = ($mount_result | Get-Volume).DriveLetter
Write-Host 'Copying ISO contents'
New-Item $sql_install -Type Directory
Copy-Item "${iso_drive}:\*" $sql_install -Recurse
Write-Host 'Unmounting ISO'
Dismount-DiskImage -ImagePath $iso
}
elseif ($sql_server_media -like '*.exe') {
Write-Host 'Downloading SQL Server exe'
$exe = "${script:sbom_dir}\sql_server.exe"
& 'gsutil' -m cp "${gs_path}/sql_installer.media" $exe
Start-Process $exe -ArgumentList @("/x:${sql_install}",'/u') -Wait
}
else {
throw "Install media not iso or exe: ${sql_server_media}"
}
Write-Host 'Opening port 1433 for SQL Server'
& netsh advfirewall firewall add rule name='SQL Server' dir=in action=allow protocol=TCP localport=1433
Write-Host 'Installing SQL Server'
$process = Start-Process "${sql_install}\setup.exe" -ArgumentList "/ConfigurationFile=${sql_config}" -PassThru -Wait
if ($process.ExitCode -ne 0) {
throw "SQL Server installer returned non-zero exit code. Exit Code: $($process.ExitCode)"
}
Write-Host 'Finished installing SQL Server'
}
function Install-SSMS {
$sql_server_config = Get-MetadataValue -key 'sql-server-config'
if ($sql_server_config -like '*core*') {
Write-Host "Not installing SSMS for config ${sql_server_config}"
return
}
Write-Host 'Installing SSMS'
$gs_path = Get-MetadataValue -key 'daisy-sources-path'
$ssms_exe = "${script:sbom_dir}\SSMS-Setup-ENU.exe"
& 'gsutil' -m cp "${gs_path}/SSMS-Setup-ENU.exe" $ssms_exe
$process = Start-Process $ssms_exe -ArgumentList @('/install','/quiet','/norestart') -Passthru -Wait
if ($process.ExitCode -ne 0) {
throw "SSMS installer returned non-zero exit code. Exit Code: $($process.ExitCode)"
}
Write-Host 'Finished installing SSMS'
}
function Enable-MicrosoftUpdate {
$service_manager = New-Object -ComObject 'Microsoft.Update.ServiceManager'
$service_manager.AddService2("7971f918-a847-4430-9279-4a52d1efe18d",7,"")
}
function Configure-Power {
<#
.SYNOPSIS
Change power plan to High-performance.
#>
Write-Host 'Changing power plan to High performance'
$power_plan = Get-CimInstance -Namespace 'root\cimv2\power' -ClassName 'win32_PowerPlan' -OperationTimeoutSec 5 -Filter "ElementName = 'High performance'" -ErrorAction SilentlyContinue
powercfg /setactive $power_plan.InstanceID.ToString().Replace("Microsoft:PowerPlan\{","").Replace("}","")
$active_plan = powercfg /getactivescheme
if ($active_plan -like '*High performance*') {
Write-Host 'Power plan updated successfully'
}
else {
throw 'Failed to update the power plan'
}
}
try {
if (!(Test-Path 'D:\')) {
# Any file which should be included in the sbom is stored in the sbom directory.
$script:sbom_dir = "D:\sbomcomponents"
$sysprep = 'c:\Windows\System32\Sysprep'
Remove-Item "${sysprep}\Panther\*" -Recurse -Force -ErrorAction Continue
Remove-Item "${sysprep}\Sysprep_succeeded.tag" -Recurse -Force -ErrorAction Continue
Format-ScratchDisk
Install-SqlServer
Install-SSMS
Download-Sbomutil
Generate-Sbom
Enable-MicrosoftUpdate
Configure-Power
}
$reboot_required = Install-WindowsUpdates
if ($reboot_required) {
Write-Host 'Reboot required.'
Restart-Computer
exit
}
if (-not (Test-Path 'C:\Program Files\Microsoft SQL Server')) {
throw 'SQL Server is not installed.'
}
Write-Host 'Launching sysprep.'
& 'C:\Program Files\Google\Compute Engine\sysprep\gcesysprep.bat' > "${log_path}\sysprep.txt" 2>&1
}
catch {
Write-Host 'Exception caught in script:'
Write-Host $_.InvocationInfo.PositionMessage
Write-Host "SQL build failed: $($_.Exception.Message)"
exit 1
}