#!/usr/bin/env pwsh # # This script is meant for quick & easy install on Windows via an elevated command prompt: # # PS>Invoke-WebRequest -Uri get.mirantis.com/install.ps1 # PS>Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process # PS>.\install.ps1 # # To obtain explicit versions of the script, append version number after replacing # '.' and '-' characters in version with '_': # # PS>Invoke-WebRequest -Uri https://get.mirantis.com/install_1_0_5.ps1 -o install.ps1 # # For more usage information run: # # PS>Get-Help .\install.ps1 # # Copyright Mirantis Inc # # Script build information: # COMMIT_SHA=b94904e75a8c212b27c73f088cf5556bdd50d483 # COMMIT_SHA_PVAL=b94904e # SEMVER_VERSION=1.0.24 # PUBLISH_STRING=stable # <# .SYNOPSIS Installs required binaries for UCP install .DESCRIPTION The install.ps1 script installs DockerEE and containerd binaries and services to the local machine. The script has inbuilt-defaults for everything and can be run without specifying any values. Script parameters and env variables can be used to overrule the default values. Parameter values take precedence over env variables. Both take precedence over inbuilt default values. The script needs to be executed from an elevated command prompt. Should you want to change the default daemon configs, you may want to have the alternative configurations and the related collateral in-place before executing the script. For example if you would like to enable TLS with docker, please make sure that the certificates are stored appropriately and the daemon configuration file is written before invoking the script. .PARAMETER DownloadUrl [Alternately specified by $Env:DOWNLOAD_URL] Specify an alternative repository to download container runtime packages. Please consult the Mirantis product installation documentation for air-gapped installs to learn more about setting up a repository mirror. .PARAMETER Channel [Alternately specified by $Env:CHANNEL] Specifies the channel to be used for picking the binaries. Examples of channels are: stable, test etc. Stable is used as the default channel. .PARAMETER DockerVersion [Alternately specified by $Env:DOCKER_VERSION] Specifies the version number for the DockerEE binaries to install. Latest is used as the default version. .PARAMETER ContainerdVersion [Alternately specified by $Env:CONTAINERD_VERSION] Specifies the version number for the containerd binaries to install Latest is used as the default version. .PARAMETER DryRun If specified, list different steps that would be used without actually invoking them. .PARAMETER Uninstall If specified, uninstalls all packages. This entails unregistering the corresponding services and removing paths for the package from the registry. All other script parameters (except DryRun and DestPath) are ignored if this switch is specified. Common parameters such as -Verbose are still honored. .PARAMETER Ver Print version info for the script and exit .PARAMETER NoServiceStarts If specified, services are not started on successful install. By default, all services installed by the script are left in a running state before exit. .PARAMETER DestPath Path to the directory under which binaries will be installed. By default, this path is %PROGRAMFILES% .PARAMETER OfflinePackagesPath The folder for airgap/offline scenarios. For use when the offline or DownloadOnly parameters are specified. Used to either save the downloaded packages for later offline use or for pointing to previously downloaded packages for offline install. .PARAMETER Offline Install packages in offline/airgap mode. By default the current directory will be used to look for previously downloaded packages. That can be overridden by using the OfflinePackagesPath parameter. .PARAMETER DownloadOnly Download and save packages for later offline/airgap install .PARAMETER EngineOnly Skip all steps except those related to Docker EE engine .PARAMETER ShowVersions Display a list of all available versions for each package with the specified Download URL and Channel. Can be used to determine the version number to use when you want to specify an explicit version number at install time. .INPUTS None. You cannot pipe objects to install.ps1. .OUTPUTS None. install.ps1 does not generate any output. .EXAMPLE PS> .\install.ps1 .EXAMPLE PS> .\install.ps1 -Verbose .NOTES 1. In scenarios where you have existing installed software that has its own copies of OpenSSL libraries, you may run into the following error: OpenSSL error: error:0F06D065:common libcrypto routines:FIPS_mode_set:fips mode not supported This is often hit if you have ming/mingw64 as a part of your PATH env variable. To work around this, ensure that the offending software is not on the PATH and run the script again. 2. The script supports airgap functionality by providing access to download packages while online as well as to install those selfsame packages while offline. For downloads, please ensure that the script has access to the internet. Use the -DownloadOnly parameter. By default the script will use the current directory to store the packages after download. This can be changed by specifying the path explicitly with the -OfflinePackagesPath parameter. For offline/airgap install, please use the -Offline parameter. By default the script will look for pacakage in the current directory. This can be changed by specifying the -OfflinePackagesPath parameter. While downloading using -DownloadOnly parameter, make sure that the download path is accessible to the script, especially if you run the script without administrative rights. #> # The following is required so that the script can be invoked with named # parameters (e.g. -ContainerdVersion 1.3.4...). If a parameter is used; # its type is checked by powershell - we give a higher precedence to the # parameters specified this way vs. the same value specified by env vars. # Parameters gotten at invocation time. Some of these values are "merged" # with values specified by env vars - see reconcileParams. Others are # used as-is. [CmdletBinding(PositionalBinding=$FALSE)] param ( [string]$DownloadUrl, # Pointer to CDN for the package zip files. [string]$Channel, # [string]$DockerVersion, # [string]$ContainerdVersion, # [switch]$DryRun, # Gives an overview of what would happen on invoking the script. Is # idempotent - can be run repeatedly without impacting system state. [switch]$Uninstall, # Uninstall all installed packages [switch]$Ver, # Print version Info and exit [switch]$NoServiceStarts, # Do not start the services at the end. Useful if certificates need to be # used or config needs to be built before starting one or more daemons. # For example, script invoker may want to build docker config file before # starting the dockerd service. [string]$DestPath, # A folder to which all installs will be done. Currently there is no way # to specify the name of the leaf folder and they are hardcoded. [string]$OfflinePackagesPath, # A folder for airgap/offline scenarios. If specified, will be used to save # and retrieve packages for this scenario. [switch]$Offline, # Install packages in offline/airgap mode [switch]$DownloadOnly, # Download and save packages for later offline/airgap install [switch]$EngineOnly, # Skip everything except DockerEE [switch]$ShowVersions # Display a list of available versions on specified [or default] channel ) $ErrorActionPreference="Stop" if (-not $DryRun) { $global:ProgressPreference = 'SilentlyContinue' } # Updated manually New-Variable -Name 'SCRIPT_SEMVER_VERSION' -Value '1.0.24' -Option Constant # Leave the value blank except when intending to make an external release # in which case set the value to a small phrase about the release e.g. # 'Beta Refresh for UCP 3.3.0' New-Variable -Name 'PUBLISH_STRING' -Value 'stable' -Option Constant # Updated automatically by the CI pipeline New-Variable -Name 'SCRIPT_COMMIT_SHA' -Value 'b94904e' -Option Constant # Internal values[constants/variables] $script:intgenUrl='' # The effective Url for the package - see function genUrlFromVersionAndChannel $script:intDownloadUrl='' $script:intChannel='' $script:intDockerVersion='' $script:intContainerdVersion='' $script:intDestPath='' $script:intOfflinePackagesPath='' New-Variable -Name 'DOCKER_PKG_NAME' -Value 'docker' -Option Constant New-Variable -Name 'CONTAINERD_PKG_NAME' -Value 'containerd' -Option Constant New-Variable -Name 'DOCKER_SVC_NAME' -Value 'docker' -Option Constant New-Variable -Name 'CONTAINERD_SVC_NAME' -Value 'containerd' -Option Constant New-Variable -Name 'CONTAINERS_FEATURE_NOT_INSTALLED' -Value @" Installing the containers feature. It is a prerequisite for containers on Windows and requires a reboot. "@ -Option Constant New-Variable -Name 'DEFAULT_DOWNLOAD_URL' -Value "https://repos.mirantis.com" -Option Constant New-Variable -Name 'DEFAULT_CHANNEL' -Value 'stable' -Option Constant New-Variable -Name 'DEFAULT_DOCKER_VERSION' -Value 'latest' -Option Constant New-Variable -Name 'DEFAULT_CONTAINERD_VERSION' -Value 'latest' -Option Constant New-Variable -Name 'DEFAULT_DEST_PATH' -Value "$env:ProgramFiles" -Option Constant New-Variable -Name 'dockerExists' -Scope 'Script' -Value $FALSE New-Variable -Name 'containerdExists' -Scope 'Script' -Value $FALSE New-Variable -Name 'mustLogoff' -Scope 'Script' -Value $FALSE New-Variable -Name 'initDockerVer' -Scope 'Script' -Value '' New-Variable -Name 'initContainerdVer' -Scope 'Script' -Value '' New-Variable -Name 'finalDockerVer' -Scope 'Script' -Value '' New-Variable -Name 'finalContainerdVer' -Scope 'Script' -Value '' New-Variable -Name 'EXIT_REBOOT_MESSAGE' -Value "Your machine needs to be rebooted now. Installed packages will not work without reboot." -Option Constant New-Variable -Name 'EXIT_LOGOFF_MESSAGE' -Value "The system-wide PATH has been updated. To use docker.exe and other CLI tools, please logoff and logon to update your PATH." -Option Constant # PortNumber, Type, In/Out/Inout # Name of the generated rule will be "docker_[PortNumber]" $allPorts = (2376, "tcp", "in"), ` (2377, "tcp", "in"), ` (4789, "udp", "inout"), ` (6443, "tcp", "inout"), ` # kube (7946, "tcp", "inout"), ` (7946, "udp", "inout"), ` (10250, "tcp", "in"), ` # kubelet HTTPS port (12376, "udp", "inout") # To ensure that we can overwrite the package binaries # successfully, we need to stop the corresponding # service. Sometimes this can impact to network and # cause failure in download. To guard against it, we # download the packages first and saved their downloaded # location for user later on. $downloadedPackageLocation = @{} function Default { param( [string]$cmdlineValue, [string]$envvarValue, [string]$defaultValue ) $value="" if (![string]::IsNullOrWhiteSpace($cmdlineValue)) { $value=$cmdlineValue.Trim() return $value } if (![string]::IsNullOrWhiteSpace($envvarValue)) { $value=$envvarValue.Trim() return $value } # Use the default value no matter what it is $value="$defaultValue" return $value } function updateServicesInstallStatus { blank verboseLog "Checking state of existing services" if (Get-Service $DOCKER_SVC_NAME -ErrorAction SilentlyContinue) { verboseLog "Service $DOCKER_SVC_NAME exists" $script:dockerExists=$TRUE } else { verboseLog "Service $DOCKER_SVC_NAME does not exist" } if ($EngineOnly) { return } if (Get-Service $CONTAINERD_SVC_NAME -ErrorAction SilentlyContinue) { verboseLog "Service $CONTAINERD_SVC_NAME exists" $script:containerdExists=$TRUE } else { verboseLog "Service $CONTAINERD_SVC_NAME does not exist" } } function getServicesInstallStatus { updateServicesInstallStatus } function versionGte { param($verToCheck, $baseVersion) $verToCheck = $verToCheck.split("-")[0] if ([System.Version]$verToCheck -ge [System.Version]$baseVersion) { return $true } return $false } function deleteDeprecatedPlugins { $deprecatedPlugins = "docker-app.exe", "docker-cluster.exe", "docker-registry.exe" $versionRemoved = "20.10.15" $isCurrentVerGreater = $false if ($script:intDockerVersion -notcontains "latest") { $isCurrentVerGreater = versionGte $script:intDockerVersion $versionRemoved } if ($script:intDockerVersion.Contains("latest") -or $script:intDockerVersion.Contains("nightly") -or $isCurrentVerGreater) { Foreach ($plugin in $deprecatedPlugins) { Remove-Item "$env:ProgramFiles\Docker\cli-plugins\$plugin" -Force -ErrorAction SilentlyContinue } } } function ensureExistingServicesStarted { blank if (-not $EngineOnly) { if ($script:containerdExists) { if (-not $DryRun) { Start-Service -Name $CONTAINERD_SVC_NAME } verboseLog "Started service $CONTAINERD_SVC_NAME" } } if ($script:dockerExists) { if (-not $DryRun) { deleteDeprecatedPlugins Start-Service -Name $DOCKER_SVC_NAME } verboseLog "Started service $DOCKER_SVC_NAME" } verboseLog "Ensured all services are started" } function ensureExistingServicesStopped { blank if ($script:dockerExists) { if (-not $DryRun) { Stop-Service -Name $DOCKER_SVC_NAME -Force } verboseLog "Stopped service $DOCKER_SVC_NAME" } if ($EngineOnly) { verboseLog "Ensured any installed services are in a stopped state" return } if ($script:containerdExists) { if (-not $DryRun) { Stop-Service -Name $CONTAINERD_SVC_NAME -Force } verboseLog "Stopped service $CONTAINERD_SVC_NAME" } verboseLog "Ensured any installed services are in a stopped state" } function blank { Write-Verbose "" } function verboseLog { Write-Verbose "$args" } function infoLog { Write-Information "$args" } function errorLog { Write-Error "$args" } function VerboseWithCheck { param($pfx, $sfx) if (![string]::IsNullOrWhiteSpace($sfx)) { verboseLog "$pfx`:$sfx" } else { verboseLog "$pfx`:[unspecified]" } } function downloadPackageToInstall { param($pkgname, $channel, $version) verboseLog "Downloading binaries for $pkgName" blank $downloadedPackageLocation["$pkgname"] = "" genUrlFromVersionAndChannel "$pkgname" "$channel" "$version" $downloadUrl = $script:genUrl # Trim trailing / $downloadUrl = $downloadUrl.TrimEnd('/') if ($Offline) { # Offline install - files already available. # Verify and exit if offline files missing. $tempZipFilePath = Join-Path -Path "$script:intOfflinePackagesPath" -ChildPath "$pkgname.zip" if (-not (Test-Path $tempZipFilePath -PathType leaf)) { errorLog "Offline install: zip file $pkgname.zip not found at expected path $tempZipFilePath" exit } } else { if ($DownloadOnly) { $tempZipFilePath = Join-Path -Path "$script:intOfflinePackagesPath" -ChildPath "$pkgname.zip" $tempDockerZapFilePath = Join-Path -Path "$script:intOfflinePackagesPath" -ChildPath "docker-ci-zap.exe" if (-not (Test-Path $tempDockerZapFilePath -PathType leaf)) { Invoke-WebRequest -Uri "https://github.com/moby/docker-ci-zap/raw/master/docker-ci-zap.exe" -OutFile $tempDockerZapFilePath } } else { [string] $tempname = [System.Guid]::NewGuid() $tempZipFilePath = Join-Path -Path "$script:intDestPath" -ChildPath "$tempname.zip" } "Downloading $pkgname zip into $tempZipFilePath from: $downloadUrl - this may take some time" Invoke-WebRequest "$downloadUrl" -UseBasicParsing -OutFile "$tempZipFilePath" "Download of package $pkgname finished" } verboseLog "Downloaded binaries for $pkgName to $tempZipFilePath" $downloadedPackageLocation["$pkgname"] = "$tempZipFilePath" } function downloadAllPackagesForInstall { downloadPackageToInstall "Docker" "$script:intChannel" "$script:intDockerVersion" if ($EngineOnly) { return } downloadPackageToInstall "Containerd" "$script:intChannel" "$script:intContainerdVersion" } function genUrlFromVersionAndChannel { # All parameters below will always have a value due to defaults param($pkgname, $channel, $version) $script:genUrl = "$script:intDownloadUrl`/win`/static`/$channel`/x86_64`/$pkgname`-$version`.zip" # AWS is case sensitive and we use all lowers - ensure that. $script:genUrl = $script:genUrl.ToLower() verboseLog "genUrl: $pkgname $channel $version $script:genUrl" } function openPortWorker { param ($cmd) Invoke-Expression $cmd|Out-Null if ($LASTEXITCODE -ne 0) { Write-Warning "$cmd failed with $LASTEXITCODE. Please try to open firewall port manually" } else { verboseLog "Opened port successfully" } } # Failures opening ports are non-fatal but must be detected and logged function openPorts { foreach ($curPort in $allPorts) { $curPortNumber = $curPort[0] $curPortProtocol = $curPort[1] $curPortDirection = $curPort[2] if (-not $DryRun) { if ($curPortDirection -eq "in" -or $curPortDirection -eq "inout") { verboseLog "Opening IN port $curPortNumber[$curPortDirection] for $curPortProtocol" $cmd = "netsh advfirewall firewall add rule name=`"docker`_$curPortNumber`_in`" dir=in action=allow protocol=$curPortProtocol localport=$curPortNumber" openPortWorker "$cmd" } if ($curPortDirection -eq "out" -or $curPortDirection -eq "inout") { verboseLog "Opening OUT port $curPortNumber[$curPortDirection] for $curPortProtocol" $cmd = "netsh advfirewall firewall add rule name=`"docker`_$curPortNumber`_out`" dir=out action=allow protocol=$curPortProtocol localport=$curPortNumber" openPortWorker "$cmd" } } else { verboseLog "Opening port $curPortNumber[$curPortDirection] for $curPortProtocol" } } } function updatedDryRunPkgVer { param ($pkgname, $tempZipFilePath) $pkgname = $pkgname.ToLower() $parent = [System.IO.Path]::GetTempPath() [string] $name = [System.Guid]::NewGuid() $tempDir = Join-Path $parent $name New-Item -ItemType Directory -Path $tempDir|Out-Null Expand-Archive -Path "$tempZipFilePath" -DestinationPath "$tempDir" -Force # The following is intentionally fatal - we do want # to detect any packaging issues etc. immediately. if ($pkgname -eq "docker") { $tmp = & "$tempDir`\Docker`\docker.exe" -v|Out-String if ($LASTEXITCODE -ne 0) { errorLog "$cmd returned $LASTEXITCODE" exit } $tmp = $tmp.Trim().split(' ',3) $script:finalDockerVer = $tmp[2] } elseif ($pkgname -eq "containerd") { $tmp = & "$tempDir`\Containerd`\ctr.exe" -v|Out-String if ($LASTEXITCODE -ne 0) { errorLog "$cmd returned $LASTEXITCODE" exit } $tmp = $tmp.Trim().split(' ',3) $script:finalContainerdVer = $tmp[2].Substring(1) } Remove-Item -Recurse -Force "$tempDir" } function workerFunc { param($pkgname) $tempZipFilePath = $downloadedPackageLocation["$pkgname"] "Using preloaded zip File $tempZipFilePath for installing package $pkgname" if (-not $DryRun) { verboseLog "Expanding archive $tempZipFilePath into $script:intDestPath" Expand-Archive -Path "$tempZipFilePath" -DestinationPath "$script:intDestPath" -Force } else { updatedDryRunPkgVer $pkgname $tempZipFilePath } if (-not $Offline) { verboseLog "Removing temporary Zip File $tempZipFilePath" Remove-Item -Force "$tempZipFilePath" } } function ensurePathNotExist { param ($pathtoremove) $pathtoremove = $pathtoremove.ToLower().Trim().Trim('"').TrimStart('"') verboseLog "Ensure $pathtoremove is removed from PATH, if present" $newPath = "" $notChanged = $TRUE $oldpath = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).path ForEach ($curPathOrig in $oldpath.split(";")) { $curPath = $curPathOrig.ToLower().Trim().Trim('"').TrimStart('"') if ($curPath -ne $pathtoremove) { # Add it to new path $newPath += $curPathOrig + ';' } else { # Do not add - set a flag. $notChanged = $FALSE } } if (-not $notChanged) { verboseLog "Removing $pathtoremove from PATH" if (-not $DryRun) { Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $newpath } } else { verboseLog "$pathtoremove not found in PATH" } } function checkPathExists { param ($oldpath, $pathToCheck) $oldpath = $oldpath.ToLower().Trim() $pathToCheck = $pathToCheck.ToLower().Trim() # Present at the very end without any separator if ($oldpath.endsWith($pathToCheck)) { return $TRUE } if ($oldpath.contains("$pathToCheck`;")) { return $TRUE } if ($oldpath.contains("$pathToCheck`"")) { return $TRUE } return $FALSE } # In case a service already existed, unregister and register it # so that we always have a consistent state at the end. # Verified (code and testing) that --unregister-service does not # cause any existing windows events to be lost. function processPackage { param($pkgname, $pkgSvcBinary, $pkgSvcExists) blank verboseLog "Processing package $pkgName" blank verboseLog "Installing binaries for $pkgName" workerFunc $pkgname verboseLog "$pkgname package installed" blank blank $svcname = $pkgname.ToLower() verboseLog "Installing $svcname service" $pkgDirPath = Join-Path "$intDestPath" -ChildPath "$pkgname" $pkgBinPath = Join-Path "$pkgDirPath" -ChildPath "$pkgSvcBinary" if ($pkgSvcExists) { verboseLog "Unregistering the existing $svcname service" if (-not $DryRun) { & "$pkgBinPath" --unregister-service } } verboseLog "Invoking $pkgBinPath to register $svcname service" if (-not $DryRun) { & "$pkgBinPath" --register-service if ($LASTEXITCODE -ne 0) { errorLog "Failed to register $svcname service - exit code $LASTEXITCODE" exit } } if (-not $DryRun) { Set-Service docker -StartupType Automatic } verboseLog "Service $svcname registered and set to automatic" # Make sure the binary location is in the path $oldpath = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).path $pathExists = checkPathExists "$oldpath" "$pkgDirPath" if (-not $pathExists) { verboseLog "Adding $pkgDirPath to system PATH" if (-not $DryRun) { $newpath = "$oldpath`;$pkgDirPath" Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $newpath } } else { verboseLog "Skipping PATH modification: $pkgDirPath already exists in system PATH" } # Modify the current PATH also - so that calls to ctr and docker.exe work # OK to do so for dryrun as well since this only impacts the current process. $pathExists = checkPathExists "$Env:Path" "$pkgDirPath" if (-not $pathExists) { $Env:Path += "`;$pkgDirPath" } blank } # In case a service already existed, unregister and register it # so that we always have a consistent state at the end. # Verified (code and testing) that --unregister-service does not # cause any existing windows events to be lost. function uninstallPackage { param($pkgname, $pkgSvcBinary, $pkgSvcExists) blank verboseLog "Uninstalling package $pkgName" blank blank $svcname = $pkgname.ToLower() verboseLog "Uninstalling $svcname service" $pkgDirPath = Join-Path "$intDestPath" -ChildPath "$pkgname" $pkgBinPath = Join-Path "$pkgDirPath" -ChildPath "$pkgSvcBinary" if ($pkgSvcExists) { verboseLog "Unregistering the existing $svcname service" if (-not $DryRun) { & "$pkgBinPath" --unregister-service } } # We need to "zap" the docker folder instead of deleting it. if($Offline) { # For offline, we expect/recommend docker-ci-zap to be present in $script:intOfflinePackagesPath $tempDockerZapFilePath = Join-Path -Path "$script:intOfflinePackagesPath" -ChildPath "docker-ci-zap.exe" if (-not (Test-Path $tempDockerZapFilePath -PathType leaf)) { errorLog "Docker Zap util is unavailable at $tempDockerZapFilePath" exit } & $tempDockerZapFilePath -folder $pkgDirPath } else { Invoke-WebRequest -Uri "https://github.com/moby/docker-ci-zap/raw/master/docker-ci-zap.exe" -OutFile "$env:TEMP\docker-ci-zap.exe" & $env:TEMP\docker-ci-zap.exe -folder $pkgDirPath # Remove the util Remove-Item $env:TEMP\docker-ci-zap.exe } # Make sure the binary location is removed from the path ensurePathNotExist "$pkgDirPath" } function reconcileParams { verboseLog "Reconciling parameters for the script" # Binaries CDN location - a default value must be specified $script:intDownloadUrl = Default ` "$DownloadUrl" ` "$env:DOWNLOAD_URL" ` $DEFAULT_DOWNLOAD_URL verboseLog "Using Docker Url: $script:intDownloadUrl" # Channel name (e.g. test, stable etc.) $script:intChannel = Default ` "$Channel" ` "$env:CHANNEL" ` $DEFAULT_CHANNEL VerboseWithCheck "Using Channel" "$script:intChannel" # Docker binaries version # For offline, we shouldn't care about latest. if(-not $Offline) { $DEFAULT_DOCKER_VERSION = getNumericallyHigherVersion("docker") } $script:intDockerVersion = Default ` "$DockerVersion" ` "$env:DOCKER_VERSION" ` $DEFAULT_DOCKER_VERSION VerboseWithCheck "Using Docker Version" "$script:intDockerVersion" # Containerd binaries version # For offline, we shouldn't care about latest. if(-not $Offline) { $DEFAULT_CONTAINERD_VERSION = getNumericallyHigherVersion("containerd") } $script:intContainerdVersion = Default ` "$ContainerdVersion" ` "$env:CONTAINERD_VERSION" ` $DEFAULT_CONTAINERD_VERSION VerboseWithCheck "Using Containerd Version" "$script:intContainerdVersion" # Destination path for installing the binaries $script:intDestPath = Default ` "$DestPath" ` "" ` $DEFAULT_DEST_PATH verboseLog "Using Destination Path: $script:intDestPath" $defLocation = Get-Location # Path for saving/loading for airgap installs $script:intOfflinePackagesPath = Default ` "$OfflinePackagesPath" ` "" ` $defLocation verboseLog "Using Offline Packages Path: $script:intOfflinePackagesPath" } function getNumericallyHigherVersion { param( [string] $packageName ) $url = "$script:intDownloadUrl`/win`/static`/$script:intChannel`/x86_64`/" $resp = Invoke-WebRequest -Uri $url -UseBasicParsing $tmpPkg = $resp.Links | Select -ExpandProperty href | findstr $packageName-[0-9] # Serializing for System.Version[] casting if ($packageName -eq "containerd") { $tmpPkgs = ($tmpPkg -replace "$packageName-" , '' -replace ".zip" , '' -replace "-beta.", ".1000" -replace "-rc.", ".2000") } else { # Docker-type convention $tmpPkgs = ($tmpPkg -replace "$packageName-" , '' -replace ".zip" , '' -replace "-dev" , ".1000" -replace "-nightly", ".2000" -replace "-beta", ".3000" -replace "-tp", ".4000" -replace "-rc", ".5000") } # A default sort would be lexicographic order which would place 1.3.10 before 1.3.9 for ascending. # We will need to type cast it to System.Version to take care of this issue while selecting a numerically higher version. $candidateVer = ( [System.Version[]] $tmpPkgs | Sort-Object | Select-Object -last 1) # Deserializing back to original filename if ($packageName -eq "containerd") { $c = $candidateVer.toString() -replace ".1000", "-beta." -replace ".2000", "-rc." } else { # Docker-type convention $c = $candidateVer.toString() -replace ".1000" , "-dev" -replace ".2000", "-nightly" -replace ".3000", "-beta" -replace ".4000", "-tp" -replace ".5000", "-rc" } # Check if main release is available for tp/rc if( $c -like "*rc*" -or $c -like "*tp*" -or $c -like "*dev*" -or $c -like "*nightly*" -or $c -like "*beta*") { $mainRel = $c.split("-")[0] Foreach ($i in $tmpPkgs) { if ($i -eq $mainRel) { return $i } } } return $c } # Because we only ever use the versions for informational messages, # we treat errors here as non-fatal. function getDockerVer { if (Get-Command "docker.exe" -ErrorAction SilentlyContinue) { $tmp = docker -v|Out-String if ($LASTEXITCODE -eq 0) { $tmp = $tmp.Trim().split(' ',3) return $tmp[2] } else { verboseLog "docker -v returned $LASTEXITCODE" } } else { verboseLog "docker not in PATH" } return '' } function getContainerdVer { if (Get-Command "ctr.exe" -ErrorAction SilentlyContinue) { $tmp = ctr -v|Out-String if ($LASTEXITCODE -eq 0) { $tmp = $tmp.Trim().split(' ',3) return $tmp[2].Substring(1) } else { verboseLog "ctr version returned $LASTEXITCODE" } } else { verboseLog "ctr not in PATH" } return '' } function initState { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 reconcileParams getServicesInstallStatus if ($script:dockerExists) { $script:initDockerVer = getDockerVer } if ($EngineOnly) { return } if ($script:containerdExists) { $script:initContainerdVer = getContainerdVer } } function processAllPackagesForInstall { processPackage "Docker" "dockerd.exe" $script:dockerExists $script:dockerExists = $TRUE if ($EngineOnly) { return } processPackage "Containerd" "containerd.exe" $script:containerdExists $script:containerdExists = $TRUE } function processAllPackagesForUninstall { if ($script:dockerExists) { uninstallPackage "Docker" "dockerd.exe" $script:dockerExists if (![string]::IsNullOrWhiteSpace($script:initDockerVer)) { "Uninstalled package Docker $script:initDockerVer" } else { "Uninstalled package Docker" } $script:dockerExists = $FALSE } else { "Uninstall - package Docker does not exist" } if ($EngineOnly) { return } if ($script:containerdExists) { uninstallPackage "Containerd" "containerd.exe" $script:containerdExists if (![string]::IsNullOrWhiteSpace($script:initContainerdVer)) { "Uninstalled package Containerd $script:initContainerdVer" } else { "Uninstalled package Containerd" } $script:containerdExists = $FALSE } else { "Uninstall - package Containerd does not exist" } } function printScriptVer { "install.ps1 version 1.0.24 build b94904e75a8c212b27c73f088cf5556bdd50d483" if (![string]::IsNullOrWhiteSpace($PUBLISH_STRING)) { "For $PUBLISH_STRING" } } function printPackageVersions { reconcileParams $indexUrl = "$script:intDownloadUrl`/win`/static`/$script:intChannel`/index.json" $json=(Invoke-WebRequest -Uri $indexUrl).Content|ConvertFrom-Json forEach ($curPkg in ("docker", "containerd")) { $omitlatest=@($json.x86_64.$curPkg) -notmatch 'latest' [array]::Reverse($omitlatest) Write-Host "$curPkg`:" ($omitlatest -join ", ") } } function installOrUninstall { if ($Ver) { printScriptVer exit } if ($ShowVersions) { printPackageVersions exit } $rebootReminder = $FALSE if (-not $DryRun -and -not $DownloadOnly) { if (-not ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))) { # Possible TODO: Addressing the need to have a non-admin access docker client # This requires: # 1. Specifying a security group whose users can access docker client even if they are not admins # 2. Creation of this security group and adding a user to it # Both of the above need admits so we cannot do that automatically at this stage. # We could possibly provide guidance to what an adinistrator user of this script needs to do. errorLog "Installation of Docker EE requires administrator rights on Windows" "By adding the group option to the Docker Daemon config file, it is possible to execute Docker commands as non-admin." "Specifying the Security group for allowing non-admins access to Docker:https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-docker/configure-docker-daemon#set-docker-security-group" "To create SG and add user to it, execute as an admin: net localgroup /add && net localgroup /add" exit } } if (-not $DownloadOnly) { # The script should work as long as its prerequisites # [Containers feature] is installed. # Make sure containers feature is enabled on the host if (-not $Uninstall) { if ((get-windowsoptionalfeature -Online -FeatureName containers).State -ne 'Enabled') { blank blank if (-not $DryRun) { "$CONTAINERS_FEATURE_NOT_INSTALLED" Invoke-Expression "Enable-WindowsOptionalFeature -Online -FeatureName containers -NoRestart -WarningAction SilentlyContinue -All"|Out-Null $rebootReminder = $TRUE } else { "Containers feature is not installed and is required for Docker EE" "Dry run is ON - proceeding as if the feature was installed without actually installing it" } blank blank } else { verboseLog "Verified that the feature Containers is installed" } } } blank initState # Stopping services can impact network connectivity. # This is pronounced when dockerd is stopped and we # have reports of downloads failing as a result. So # we download before stopping the service(s). if (-not $Uninstall) { downloadAllPackagesForInstall if ($DownloadOnly) { exit } if (-not $DryRun -and -not $EngineOnly) { md -Force c:\k\cni\config | Out-Null } } # Stop existing services so that we could overwrite the binaries ensureExistingServicesStopped if ($Uninstall) { # Uninstall services processAllPackagesForUninstall } else { # Install services processAllPackagesForInstall # For DryRun, the version numbers have already been updated if (-not $DryRun) { $script:finalDockerVer = getDockerVer $script:finalContainerdVer = getContainerdVer } if (-not $EngineOnly) { openPorts } # Unless we are asked to leave the services in a stopped state, we need to start them. if (-not $rebootReminder -and -not $NoServiceStarts) { ensureExistingServicesStarted } blank # Before exiting, emit a message about different packages installed/upgraded # and their versions number(s) [pre-install and post-install]. if ($script:finalDockerVer -ne '') { if ($script:initDockerVer -ne '' -and $script:initDockerVer -ne $script:finalDockerVer) { "Updated Docker from $script:initDockerVer to $script:finalDockerVer" } else { "Installed Docker $script:finalDockerVer" } } if (-not $EngineOnly) { if ($script:finalContainerdVer -ne '') { if ($script:initContainerdVer -ne '' -and $script:initContainerdVer -ne $script:finalContainerdVer) { "Updated Containerd from $script:initContainerdVer to $script:finalContainerdVer" } else { "Installed Containerd $script:finalContainerdVer" } } } blank "Install/upgrade completed" # Reboot > Logoff. So show only reboot message even if logoff is also set. if ($rebootReminder) { Write-Warning $EXIT_REBOOT_MESSAGE } elseif ($script:mustLogoff) { Write-Warning $EXIT_LOGOFF_MESSAGE } } } installOrUninstall # SIG # Begin signature block # MIInFAYJKoZIhvcNAQcCoIInBTCCJwECAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA6yzGMav+xYLzz # 36eqUNO5TFTLuGtkbLXEMsNEdoaYf6CCIKAwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqG # SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy # RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg # Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXH # JQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMf # UBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w # 1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRk # tFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYb # qMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUm # cJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP6 # 5x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzK # QtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo # 80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjB # Jgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXche # MBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB # /wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU # 7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG # CCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29j # c3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDig # NqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v # dEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI # hvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd # 4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiC # qBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl # /Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeC # RK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYT # gAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/ # a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37 # xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmL # NriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0 # YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJ # RyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIG # sDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw # HhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0 # ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjAN # BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zr # PYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHM # gQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8Irg # nQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyC # EUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0 # p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQa # khCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0 # XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960I # HnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2 # FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBH # X8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q2 # 7IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD # VR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1k # TN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcD # AzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj # ZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t # L0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmww # HAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIB # ADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6j # fCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmI # moqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtf # JqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrx # oj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3 # LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx # 4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9 # Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+I # Cw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug # 0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5 # Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQ # BUSv85SdCDmmv9s/X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 # ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAw # MDAwMFoXDTM0MTAxMzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp # Z2lDZXJ0LCBJbmMuMSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X # 5dLnXaEOCdwvSKOXejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uU # UI8cIOrHmjsvlmbjaedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa # 2mq62DvKXd4ZGIX7ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgt # XkV1lnX+3RChG4PBuOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60 # pCFkcOvV5aDaY7Mu6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17 # cz4y7lI0+9S769SgLDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BY # QfvYsSzhUa+0rRUGFOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9 # c33u3Qr/eTQQfqZcClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw # 9/sqhux7UjipmAmhcbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2c # kpMEtGlwJw1Pt7U20clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhR # B8qUt+JQofM604qDy0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYD # VR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgG # BmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxq # II+eyG8wHQYDVR0OBBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEw # T6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH # NFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGD # MIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYB # BQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 # ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQEL # BQADggIBAIEa1t6gqbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF # 7SaCinEvGN1Ott5s1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrC # QDifXcigLiV4JZ0qBXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFc # jGnRuSvExnvPnPp44pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8 # wWkZus8W8oM3NG6wQSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbF # KNOt50MAcN7MmJ4ZiQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP # 4xeR0arAVeOGv6wnLEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VP # NTwAvb6cKmx5AdzaROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvr # moI1VygWy2nyMpqy0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2 # obhDLN9OTH0eaHDAdwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJ # uEbTbDJ8WC9nR2XlG3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIG3zCCBMeg # AwIBAgIQBQUMKnNoy0s6kQmUIdAhXjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG # EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0 # IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0Ex # MB4XDTIzMDYyMDAwMDAwMFoXDTI0MDYyNTIzNTk1OVowZzELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCkNhbGlmb3JuaWExETAPBgNVBAcTCENhbXBiZWxsMRcwFQYDVQQK # Ew5NaXJhbnRpcywgSW5jLjEXMBUGA1UEAxMOTWlyYW50aXMsIEluYy4wggGiMA0G # CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDbhBuUNlTCWnDuAowew/RKhCDueDLv # 4Z8b3IOXK+9O1gMC4AeZVBkqRTBrSxqgw3RixwGnjRbGPIqJ4w7Mj8Zclj+kQ3/8 # 32/uGEIaPEEAkO8QfPWum93/purafIByEQDUjgduI9ipIzQgAhAILGTp6auMHAXK # BqFmtv0o05tj8Wt/cglKVNM8HQYdOrl135bSTLTMhijDJKwWagD/f+g26UHDNxyw # nkdDjTH+8mCJ4ZRY8EUM46Nm1wKJK4KOyL/5LdXewJsbRbY1aWHpUi3z9WO3os8n # VK+d8QQQGCNOm800490o2891JuHurfzwWuVxeYP7mSsw2IqU2W2/rsnl7n3ep3lK # QfzQw7vRBE+mqVf0o6K7wSP6kKcg4HgKTZWzZka/6eSFDrnp7ijtWsHjjYQNaKi6 # gBUlE6ngK3arJWEa7mPL9ztHZfYXGREJ6Fi5TSlp5uaxBdXpzYSTBr+E8upB6djy # IREBIHWfWAXf+ANgFoDYjM0gNdTRQh2LMnUCAwEAAaOCAgMwggH/MB8GA1UdIwQY # MBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBQTtxRvYN+SvugFD1Oe # ePowmn4nmDAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUG # A1UdHwSBrTCBqjBToFGgT4ZNaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lD # ZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmww # U6BRoE+GTWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH # NENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMD4GA1UdIAQ3MDUw # MwYGZ4EMAQQBMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29t # L0NQUzCBlAYIKwYBBQUHAQEEgYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3Nw # LmRpZ2ljZXJ0LmNvbTBcBggrBgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNl # cnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0 # MjAyMUNBMS5jcnQwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAgEAR2ojEEpK # hFCUXasKA+r36Oo1y5QUFnTnBuHnUko/lRiIJgZuWQYEeWqzww5sMFmTg78tjOSQ # NSNyQN5L4xNWOe13ZBecTg/zIxrcRCeRBNIgV7pzNlJXkRjoLc+plRHps286Vrm/ # MJEzNFRoXjqRtVqtaDqoiPg1Rt0ZlywE5vdAXe/ANRby9HidN+38UmOPDJJsOkqz # ab19a2nTPadseUWtgKrMBYn7ENBX6udUYC8WFV+/L2nloiCyh0qtp3ZJPn6Ht44w # H7EcIFwnCTeOM9sCrjW2O6FFVjUOduvXHl705cD99VI0CwqCjEyFfzcWwXCXKaeM # LyzVt/mOgvOxs0G/4YT4mEffv6cqSCXtlf9GbBLRzzFV6zOeknHAAQW04xxkhBqq # bm4Z6lAbR5PMYFLzKDJC3LqzNp48tzvwu5S0hgYSq/quJEBGtRn4U1wD7L1mM8xZ # SUZe5P1NqeldnUN4T1kUE3PYSNGZJDXD1uEhklJWgAwDzECyqkx4e/aHAogpix5z # EO0OMGJlVc73h7RWRyjT6TGrN75vR9HyiigiYP7SIMv62rxThLNloM1G2BpnEclX # 2XMLeOzEM/eneGpCGLPveMryaOmXyglpZ/Od/00E+b7FZ2dY4YDzNtf0jf49X3bC # 58lXs87hyT0wPwyGiPEcjZtLFOrPDe6hZWoxggXKMIIFxgIBATB9MGkxCzAJBgNV # BAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNl # cnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBD # QTECEAUFDCpzaMtLOpEJlCHQIV4wDQYJYIZIAWUDBAIBBQCgfDAQBgorBgEEAYI3 # AgEMMQIwADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgEL # MQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg5c8fxk9v2TEkBJiawsL4 # 5+QTpoRyaHsQ5ZRfnHcz5pUwDQYJKoZIhvcNAQEBBQAEggGARrvFKSByNLxQrgHc # bHRIvzS7oLn7KMwL2YUQehQWLQ8gtisTskYJAAnvQzW7s44yw2AvTrjJLFsI/EFz # ss/lhGHfA9NEqqqAjFwhs0CIdlGtfAdsM4LG6KcpRRvOgd1P6GwpO3BJrTpGCzu5 # tqvESIHuI3GfmaC26UfFQLrBKtKCKMIXZvrc6frPgvSqM0faZFK/42W1YijPYHTf # G0lk/kEHoERZjViTp9ZKjLkWCv0t89oLWJukzMRt8/ZvxgZI0fYRc2vZ0bzMQb1m # Lqe0bmt5qC3SjMXsVUqGf6+r2bYWBMFO9LMlUhnxTjOZwRBGJhN0niChvi6GFL4j # Nm9Ru6HiRthS2xslV9oaeB3ZMfjzwPWSZCZPfC1qWARBXHFdUQgGcriU3Md1kKvF # Iu25XzHL9emhn5wvi8VvWpWO8KRyjdGi9uX3qUXCnVLRiNDTgPOw/fwKJrhBooxX # tRGt3PXpyP2FrlMmC5IuTKlUHD80KSJQFyl5GH8MzS3jkVrfoYIDIDCCAxwGCSqG # SIb3DQEJBjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp # Z2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQw # OTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQQIQBUSv85SdCDmmv9s/X+VhFjANBglg # hkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcN # AQkFMQ8XDTIzMTExMzE3NDI1OVowLwYJKoZIhvcNAQkEMSIEIDNaVgJD4nP9IFWV # qdBFwxrUAELZ1648nHfjJCPb9J+WMA0GCSqGSIb3DQEBAQUABIICAE+kkCyq/qeu # sJ5nPTr1YtXx3611KG7rtBJkRpFGdsfqWeDg0PjrbkHfGQkidAxRdfPDrpcB/gRx # 4PvTseEkBACwsTmqj9yjcY+39ebq1vMhm0CsIX7h6BgLrPaC7bsbKYAQayTmkHHe # Jql0c25wX1ASIz6uuyvTrzPmYmfmasbfZ+aIJvOUplf9LUiQ0RXgKVWx5QHuD7d2 # wYlzsu3Qr94L3/jWLfuGNgNCTKyKw6vk3E6tmNRTmlUcOvl1LSjr98othIy0D1Ih # gNFg6BvuKVvc5vIypJrze0QFwgZDjB+5d/prd3yzXy3IUzW2ufTRhUerFnPYxqZY # SyIiRHKofZN/j0jcK855vpE2CrKokRWIYV60LXeybuEY1zJGkYVlnPN+1GrAxmaj # CXbywDXEF4GSYhV31iwkD57WJk9mmloN6jSpbxELHgoM+Oz6qrFbMyPvhO9ro3mj # cgUmtyY+K987+fLkI8GgCmn+9lF4HMDuEE3JCJKCIevHnI0nRZehmWMEF3e7AAEl # r1pzbOrafSBC/sDERl6Hs2H3JBYHX50u2Zr9ZQ7BhIvLfFWzU9JJubJBvCqHQiz9 # vmwIzoPtey1VP3TXu2vLHkHKzC5HkbBWKMr//0Q6Ps5YOQJOg0P4prmV8asO6OKI # 8ndRksy6sc5rQlrv2kkKHu56VoRL0JLC # SIG # End signature block