salt-bootstrap/bootstrap-salt.ps1
2025-02-24 22:09:49 +00:00

703 lines
26 KiB
PowerShell

<#
.SYNOPSIS
A simple Powershell script to download and install a Salt minion on Windows.
.DESCRIPTION
The script will download the official Salt package from SaltProject. It will
install a specific package version and accept parameters for the master and
minion IDs. Finally, it can stop and set the Windows service to "manual" for
local testing.
.EXAMPLE
./bootstrap-salt.ps1
Runs without any parameters. Uses all the default values/settings. Will
install the latest version of Salt
.EXAMPLE
./bootstrap-salt.ps1 -Version 3006.7
Specifies a particular version of the installer.
.EXAMPLE
./bootstrap-salt.ps1 -RunService $false
Specifies the salt-minion service to stop and be set to manual. Useful for
testing locally from the command line with the --local switch
.EXAMPLE
./bootstrap-salt.ps1 -Minion minion-box -Master master-box
Specifies the minion and master ids in the minion config. Defaults to the
installer values of host name for the minion id and "salt" for the master.
.EXAMPLE
./bootstrap-salt.ps1 -Minion minion-box -Master master-box -Version 3006.7 -RunService $false
Specifies all the optional parameters in no particular order.
.NOTES
All of the parameters are optional. The default should be the latest
version. The architecture is dynamically determined by the script.
.LINK
Salt Bootstrap GitHub Project (script home) - https://github.com/saltstack/salt-bootstrap
Original Vagrant Provisioner Project - https://github.com/saltstack/salty-vagrant
Vagrant Project (utilizes this script) - https://github.com/mitchellh/vagrant
Salt Download Location - https://packages.broadcom.com/artifactory/saltproject-generic/windows/
Salt Manual Install Directions (Windows) - https://docs.saltproject.io/salt/install-guide/en/latest/topics/install-by-operating-system/windows.html
#>
#===============================================================================
# Bind Parameters
#===============================================================================
[CmdletBinding()]
param(
[Parameter(Mandatory=$false, ValueFromPipeline=$True)]
[Alias("v")]
# The version of the Salt minion to install. Default is "latest" which will
# install the latest version of Salt minion available. Doesn't support
# versions prior to "YYYY.M.R-B"
[String]$Version = "latest",
[Parameter(Mandatory=$false, ValueFromPipeline=$True)]
[Alias("s")]
# Boolean flag to start or stop the minion service. $true will start the
# minion service. $false will stop the minion service and set it to "manual".
# The installer starts it by default.
[Bool]$RunService = $true,
[Parameter(Mandatory=$false, ValueFromPipeline=$True)]
[Alias("m")]
# Name of the minion being installed on this host. Installer defaults to the
# host name.
[String]$Minion = "not-specified",
[Parameter(Mandatory=$false, ValueFromPipeline=$True)]
[Alias("a")]
#Name or IP of the master server. Installer defaults to "salt".
[String]$Master = "not-specified",
[Parameter(Mandatory=$false, ValueFromPipeline=$True)]
[Alias("r")]
# URL to the windows packages. Will look for the installer at the root of
# the URL/Version. Place a folder for each version of Salt in this directory
# and place the installer binary for each version in its folder.
# Default is "https://packages.broadcom.com/artifactory/saltproject-generic/windows/"
[String]$RepoUrl = "https://packages.broadcom.com/artifactory/saltproject-generic/windows/",
[Parameter(Mandatory=$false, ValueFromPipeline=$True)]
[Alias("c")]
# Vagrant only
# Vagrant files are placed in "C:\tmp". Copies Salt config files from
# Vagrant (C:\tmp) to Salt config locations and exits. Does not run the
# installer
[Switch]$ConfigureOnly,
[Parameter(Mandatory=$false)]
[Alias("h")]
# Displays help for this script.
[Switch] $Help,
[Parameter(Mandatory=$false)]
[Alias("e")]
# Displays the Version for this script.
[Switch] $ScriptVersion
)
# We'll check for help first because it really has no requirements
if ($help) {
# Get the full script name
$this_script = & {$myInvocation.ScriptName}
Get-Help $this_script -Detailed
exit 0
}
$__ScriptVersion = "2025.02.24"
$ScriptName = $myInvocation.MyCommand.Name
# We'll check for the Version next, because it also has no requirements
if ($ScriptVersion) {
Write-Host $__ScriptVersion
exit 0
}
#===============================================================================
# Script Preferences
#===============================================================================
# Powershell supports only TLS 1.0 by default. Add support for TLS 1.2
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]'Tls12'
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
#===============================================================================
# Script Functions
#===============================================================================
function Get-IsAdministrator
{
$Identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = New-Object System.Security.Principal.WindowsPrincipal($Identity)
$Principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Get-IsUacEnabled
{
(Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System).EnableLua -ne 0
}
function Get-MajorVersion {
# Parses a version string and returns the major version
#
# Args:
# Version (string): The Version to parse
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[String] $Version
)
return ( $Version -split "\." )[0]
}
function Get-AvailableVersions {
# Get available versions from a remote location specified in the Source
# Parameter
Write-Verbose "Getting version information from the repo"
Write-Verbose "base_url: $base_url"
$available_versions = [System.Collections.ArrayList]@()
if ( $base_url.StartsWith("http") -or $base_url.StartsWith("ftp") ) {
# We're dealing with HTTP, HTTPS, or FTP
$response = Invoke-WebRequest "$base_url" -UseBasicParsing
try {
$response = Invoke-WebRequest "$base_url" -UseBasicParsing
} catch {
Write-Host "Failed to get version information" -ForegroundColor Red
exit 1
}
if ( $response.StatusCode -ne 200 ) {
Write-Host "There was an error getting version information" -ForegroundColor Red
Write-Host "Error: $($response.StatusCode)" -ForegroundColor red
exit 1
}
$response.links | ForEach-Object {
if ( $_.href.Length -gt 8) {
Write-Host "The content at this location is unexpected" -ForegroundColor Red
Write-Host "Should be a list of directories where the name is a version of Salt" -ForegroundColor Red
exit 1
}
}
# Getting available versions from response
Write-Verbose "Getting available versions from response"
$filtered = $response.Links | Where-Object -Property href -NE "../"
$filtered | Select-Object -Property href | ForEach-Object {
$available_versions.Add($_.href.Trim("/")) | Out-Null
}
} elseif ( $base_url.StartsWith("\\") -or $base_url -match "^[A-Za-z]:\\" ) {
# We're dealing with a local directory or SMB source
Get-ChildItem -Path $base_url -Directory | ForEach-Object {
$available_versions.Add($_.Name) | Out-Null
}
} else {
Write-Host "Unknown Source Type" -ForegroundColor Red
Write-Host "Must be one of HTTP, HTTPS, FTP, SMB Share, Local Directory" -ForegroundColor Red
exit 1
}
Write-Verbose "Available versions:"
$available_versions | ForEach-Object {
Write-Verbose "- $_"
}
# Get the latest version, should be the last in the list
Write-Verbose "Getting latest available version"
$latest = $available_versions | Select-Object -Last 1
Write-Verbose "Latest available version: $latest"
# Create a versions table
# This will have the latest version available, the latest version available
# for each major version, and every version available. This makes the
# version lookup logic easier. The contents of the versions table can be
# found by running -Verbose
Write-Verbose "Populating the versions table"
$versions_table = [ordered]@{"latest"=$latest}
$available_versions | ForEach-Object {
$versions_table[$(Get-MajorVersion $_)] = $_
$versions_table[$_.ToLower()] = $_.ToLower()
}
Write-Verbose "Versions Table:"
$versions_table | Sort-Object Name | Out-String | ForEach-Object {
Write-Verbose "$_"
}
return $versions_table
}
function Get-HashFromArtifactory {
# This function uses the artifactory API to get the SHA265 Hash for the file
# If Source is NOT artifactory, the sha will not be checked
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[String] $SaltVersion,
[Parameter(Mandatory=$true)]
[String] $SaltFileName
)
if ( $api_url ) {
$full_url = "$api_url/$SaltVersion/$SaltFileName"
Write-Verbose "Querying Artifactory API for hash:"
Write-Verbose $full_url
try {
$response = Invoke-RestMethod $full_url -UseBasicParsing
return $response.checksums.sha256
} catch {
Write-Verbose "Artifactory API Not available or file not"
Write-Verbose "available at specified location"
Write-Verbose "Hash will not be checked"
return ""
}
Write-Verbose "No hash found for this file: $SaltFileName"
Write-Verbose "Hash will not be checked"
return ""
}
Write-Verbose "No artifactory API defined"
Write-Verbose "Hash will not be checked"
return ""
}
function Get-FileHash {
# Get-FileHash is a built-in cmdlet in powershell 5+ but we need to support
# powershell 3. This will overwrite the powershell 5 commandlet only for
# this script. But it will provide the missing cmdlet for powershell 3
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[String] $Path,
[Parameter(Mandatory=$false)]
[ValidateSet(
"SHA1",
"SHA256",
"SHA384",
"SHA512",
# https://serverfault.com/questions/820300/
# why-isnt-mactripledes-algorithm-output-in-powershell-stable
"MACTripleDES", # don't use
"MD5",
"RIPEMD160",
IgnoreCase=$true)]
[String] $Algorithm = "SHA256"
)
if ( !(Test-Path $Path) ) {
Write-Verbose "Invalid path for hashing: $Path"
return @{}
}
if ( (Get-Item -Path $Path) -isnot [System.IO.FileInfo]) {
Write-Verbose "Not a file for hashing: $Path"
return @{}
}
$Path = Resolve-Path -Path $Path
Switch ($Algorithm) {
SHA1 {
$hasher = [System.Security.Cryptography.SHA1CryptoServiceProvider]::Create()
}
SHA256 {
$hasher = [System.Security.Cryptography.SHA256]::Create()
}
SHA384 {
$hasher = [System.Security.Cryptography.SHA384]::Create()
}
SHA512 {
$hasher = [System.Security.Cryptography.SHA512]::Create()
}
MACTripleDES {
$hasher = [System.Security.Cryptography.MACTripleDES]::Create()
}
MD5 {
$hasher = [System.Security.Cryptography.MD5]::Create()
}
RIPEMD160 {
$hasher = [System.Security.Cryptography.RIPEMD160]::Create()
}
}
Write-Verbose "Hashing using $Algorithm algorithm"
try {
$data = [System.IO.File]::OpenRead($Path)
$hash = $hasher.ComputeHash($data)
$hash = [System.BitConverter]::ToString($hash) -replace "-",""
return @{
Path = $Path;
Algorithm = $Algorithm.ToUpper();
Hash = $hash
}
} catch {
Write-Verbose "Error hashing: $Path"
Write-Verbose "ERROR: $_"
return @{}
} finally {
if ($null -ne $data) {
$data.Close()
}
}
}
#===============================================================================
# Check for Elevated Privileges
#===============================================================================
if (!(Get-IsAdministrator)) {
if (Get-IsUacEnabled) {
# We are not running "as Administrator" - so relaunch as administrator
# Create a new process object that starts PowerShell
$newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell";
# Specify the current script path and name as a parameter`
$parameters = ""
foreach ($boundParam in $PSBoundParameters.GetEnumerator())
{
$parameters = "$parameters -{0} '{1}'" -f $boundParam.Key, $boundParam.Value
}
$newProcess.Arguments = $myInvocation.MyCommand.Definition, $parameters
# Specify the current working directory
$newProcess.WorkingDirectory = "$script_path"
# Indicate that the process should be elevated
$newProcess.Verb = "runas";
# Start the new process
[System.Diagnostics.Process]::Start($newProcess);
# Exit from the current, unelevated, process
exit
}
else {
throw "You must be administrator to run this script"
}
}
#===============================================================================
# Check for older versions
#===============================================================================
$majorVersion = Get-MajorVersion -Version $Version
if ($majorVersion -lt "3006") {
# This is an older version, use the old URL
Write-Host "Versions older than 3006 are not available" -ForegroundColor Red
exit 1
}
#===============================================================================
# Declare variables
#===============================================================================
$ConfDir = "$RootDir\conf"
$PkiDir = "$ConfDir\pki\minion"
$RootDir = "$env:ProgramData\Salt Project\Salt"
# Check for existing installation where RootDir is stored in the registry
$SaltRegKey = "HKLM:\SOFTWARE\Salt Project\Salt"
if (Test-Path -Path $SaltRegKey) {
if ($null -ne (Get-ItemProperty $SaltRegKey).root_dir) {
$RootDir = (Get-ItemProperty $SaltRegKey).root_dir
}
}
# Get repo and api URLs. An artifactory URL will have "artifactory" in it
$domain, $target = $RepoUrl -split "/artifactory/"
if ( $target ) {
# Create $base_url and $api_url
$base_url = "$domain/artifactory/$target"
$api_url = "$domain/artifactory/api/storage/$target"
} else {
# This is a non-artifactory url, there is no api
$base_url = $domain
$api_url = ""
}
#===============================================================================
# Verify Parameters
#===============================================================================
Write-Verbose "Running Script: $ScriptName"
Write-Verbose "Script Version: $__ScriptVersion"
Write-Verbose "Parameters passed in:"
Write-Verbose "version: $Version"
Write-Verbose "runservice: $RunService"
Write-Verbose "master: $Master"
Write-Verbose "minion: $Minion"
Write-Verbose "repourl: $base_url"
Write-Verbose "apiurl: $api_url"
Write-Verbose "ConfDir: $ConfDir"
Write-Verbose "RootDir: $RootDir"
if ($RunService) {
Write-Verbose "Windows service will be set to run"
[bool]$RunService = $True
} else {
Write-Verbose "Windows service will be stopped and set to manual"
[bool]$RunService = $False
}
#===============================================================================
# Copy Vagrant Files to their proper location.
#===============================================================================
$ConfiguredAnything = $False
# Vagrant files will be placed in C:\tmp
# Check if minion keys have been uploaded, copy to correct location
if (Test-Path C:\tmp\minion.pem) {
New-Item $PkiDir -ItemType Directory -Force | Out-Null
Copy-Item -Path C:\tmp\minion.pem -Destination $PkiDir -Force | Out-Null
Copy-Item -Path C:\tmp\minion.pub -Destination $PkiDir -Force | Out-Null
$ConfiguredAnything = $True
}
# Check if minion config has been uploaded
# This should be done before the installer is run so that it can be updated with
# id: and master: settings when the installer runs
if (Test-Path C:\tmp\minion) {
New-Item $ConfDir -ItemType Directory -Force | Out-Null
Copy-Item -Path C:\tmp\minion -Destination $ConfDir -Force | Out-Null
$ConfiguredAnything = $True
}
# Check if grains config has been uploaded
if (Test-Path C:\tmp\grains) {
New-Item $ConfDir -ItemType Directory -Force | Out-Null
Copy-Item -Path C:\tmp\grains -Destination $ConfDir -Force | Out-Null
$ConfiguredAnything = $True
}
if ( $ConfigureOnly ) {
if ( !$ConfiguredAnything ) {
Write-Host "No configuration or keys were copied over." -ForegroundColor yes
Write-Host "No configuration was done!" -ForegroundColor Yellow
} else {
Write-Host "Salt minion successfully configured" -ForegroundColor Green
}
# If we're only configuring, we want to end here
exit 0
}
#===============================================================================
# Detect architecture
#===============================================================================
if ([IntPtr]::Size -eq 4) { $arch = "x86" } else { $arch = "AMD64" }
#===============================================================================
# Getting version information from the repo
#===============================================================================
$versions = Get-AvailableVersions
#===============================================================================
# Validate passed version
#===============================================================================
Write-Verbose "Looking up version: $Version"
if ( $versions.Contains($Version.ToLower()) ) {
$Version = $versions[$Version.ToLower()]
Write-Verbose "Found version: $Version"
} else {
Write-Host "Version $Version is not available" -ForegroundColor Red
Write-Host "Available versions are:" -ForegroundColor Yellow
$versions
exit 1
}
#===============================================================================
# Get file url and sha256
#===============================================================================
$saltFileName = "Salt-Minion-$Version-Py3-$arch-Setup.exe"
$saltFileUrl = "$base_url/$Version/$saltFileName"
$saltSha256 = Get-HashFromArtifactory -SaltVersion $Version -SaltFileName $saltFileName
#===============================================================================
# Download minion setup file
#===============================================================================
Write-Host "===============================================================================" -ForegroundColor Yellow
Write-Host " Bootstrapping Salt Minion" -ForegroundColor Green
Write-Host " - version: $Version"
Write-Host " - file name: $saltFileName"
Write-Host " - file url : $saltFileUrl"
Write-Host " - file hash: $saltSha256"
Write-Host " - master: $Master"
Write-Host " - minion id: $Minion"
Write-Host " - start service: $RunService"
Write-Host "-------------------------------------------------------------------------------" -ForegroundColor Yellow
$localFile = "$env:TEMP\$saltFileName"
Write-Host "Downloading Installer: " -NoNewline
Write-Verbose ""
Write-Verbose "Salt File URL: $saltFileUrl"
Write-Verbose "Local File: $localFile"
# Remove existing local file
if ( Test-Path -Path $localFile ) { Remove-Item -Path $localFile -Force }
# Download the file
Invoke-WebRequest -Uri $saltFileUrl -OutFile $localFile
if ( Test-Path -Path $localFile ) {
Write-Host "Success" -ForegroundColor Green
} else {
Write-Host "Failed" -ForegroundColor Red
exit 1
}
# Compare the hash if there is a hash to compare
if ( $saltSha256 ) {
$localSha256 = (Get-FileHash -Path $localFile -Algorithm SHA256).Hash
Write-Host "Comparing Hash: " -NoNewline
Write-Verbose ""
Write-Verbose "Local Hash: $localSha256"
Write-Verbose "Remote Hash: $saltSha256"
if ( $localSha256 -eq $saltSha256 ) {
Write-Host "Success" -ForegroundColor Green
} else {
Write-Host "Failed" -ForegroundColor Red
exit 1
}
}
#===============================================================================
# Set the parameters for the installer
#===============================================================================
# Unless specified, use the installer defaults
# - id: <hostname>
# - master: salt
# - Start the service
$parameters = ""
if($Minion -ne "not-specified") {$parameters = "/minion-name=$Minion"}
if($Master -ne "not-specified") {$parameters = "$parameters /master=$Master"}
#===============================================================================
# Install minion silently
#===============================================================================
Write-Host "Installing Salt Minion (5 min timeout): " -NoNewline
Write-Verbose ""
Write-Verbose "Local File: $localFile"
Write-Verbose "Parameters: $parameters"
$process = Start-Process $localFile `
-WorkingDirectory $(Split-Path $localFile -Parent) `
-ArgumentList "/S /start-service=0 $parameters" `
-NoNewWindow -PassThru
# Sometimes the installer hangs... we'll wait 5 minutes and then kill it
Write-Verbose "Waiting for installer to finish"
$process | Wait-Process -Timeout 300 -ErrorAction SilentlyContinue
$process.Refresh()
if ( !$process.HasExited ) {
Write-Verbose "Installer Timeout"
Write-Host ""
Write-Host "Killing hung installer: " -NoNewline
$process | Stop-Process
$process.Refresh()
if ( $process.HasExited ) {
Write-Host "Success" -ForegroundColor Green
} else {
Write-Host "Failed" -ForegroundColor Red
exit 1
}
}
# Wait for salt-minion service to be registered to verify successful
# installation
$service = Get-Service salt-minion -ErrorAction SilentlyContinue
$tries = 0
$max_tries = 15 # We'll try for 30 seconds
Write-Verbose "Checking that the service is installed"
while ( ! $service ) {
# We'll keep trying to get a service object until we're successful, or we
# reach max_tries
if ( $tries -le $max_tries ) {
$service = Get-Service salt-minion -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
$tries += 1
} else {
# If the salt-minion service is still not running, something
# probably went wrong and user intervention is required - report
# failure.
Write-Host "Failed" -ForegroundColor Red
Write-Host "Timeout waiting for the salt-minion service to be installed"
exit 1
}
}
# If we get this far, the service was installed, we have a service object
Write-Host "Success" -ForegroundColor Green
#===============================================================================
# Configure the minion service
#===============================================================================
if( $RunService ) {
# Start the service
Write-Host "Starting Service: " -NoNewline
Write-Verbose ""
$tries = 0
# We'll try for 2 minutes, sometimes the minion takes that long to start as
# it compiles python code for the first time
$max_tries = 60
if ( $service.Status -ne "Running" ) {
while ( $service.Status -ne "Running" ) {
if ( $service.Status -eq "Stopped" ) {
Start-Service -Name "salt-minion" -ErrorAction SilentlyContinue
}
Start-Sleep -Seconds 2
Write-Verbose "Checking the service status"
$service.Refresh()
if ( $service.Status -eq "Running" ) {
Write-Host "Success" -ForegroundColor Green
} else {
if ( $tries -le $max_tries ) {
$tries += 1
} else {
# If the salt-minion service is still not running, something
# probably went wrong and user intervention is required - report
# failure.
Write-Host "Failed" -ForegroundColor Red
Write-Host "Timed out waiting for the salt-minion service to start"
exit 1
}
}
}
} else {
Write-Host "Success" -ForegroundColor Green
}
} else {
# Set the service to manual start
$service.Refresh()
if ( $service.StartType -ne "Manual" ) {
Write-Host "Setting Service Start Type to 'Manual': " -NoNewline
Set-Service "salt-minion" -StartupType "Manual"
$service.Refresh()
if ( $service.StartType -eq "Manual" ) {
Write-Host "Success" -ForegroundColor Green
} else {
Write-Host "Failed" -ForegroundColor Red
exit 1
}
}
# The installer should have installed the service stopped, but we'll make
# sure it is stopped here
if ( $service.Status -ne "Stopped" ) {
Write-Host "Stopping Service: " -NoNewline
Stop-Service "salt-minion"
$service.Refresh()
if ( $service.Status -eq "Stopped" ) {
Write-Host "Success" -ForegroundColor Green
} else {
Write-Host "Failed" -ForegroundColor Red
exit 1
}
}
}
#===============================================================================
# Script Complete
#===============================================================================
Write-Host "-------------------------------------------------------------------------------" -ForegroundColor Yellow
Write-Host "Salt Minion Installed Successfully" -ForegroundColor Green
Write-Host "===============================================================================" -ForegroundColor Yellow
exit 0