From b501b480cdd0da5a99ecd3d5b7b8c02427c9b403 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Mon, 10 Feb 2025 22:42:56 -0800 Subject: [PATCH 1/4] Add puppetcore support for Windows install tasks Now possible to run the install task specifying puppetcore collection: ``` /opt/puppetlabs/bolt/bin/bolt task run puppet_agent::install \ collection=puppetcore8 \ version=8.11.0 \ username=forge-key \ password=${PUPPET_FORGE_TOKEN} \ --targets 'winrm://HOST' \ --user Administrator \ --password ... ``` If the `windows_source` class parameter is explicitly given, then the task will use that. Also add additional logging as to where we are downloading the MSI from and the exception message if downloading fails. --- tasks/install_powershell.json | 8 +++++++ tasks/install_powershell.ps1 | 39 ++++++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/tasks/install_powershell.json b/tasks/install_powershell.json index 868a95c6..8d64afb0 100644 --- a/tasks/install_powershell.json +++ b/tasks/install_powershell.json @@ -42,6 +42,14 @@ "description": "The number of retries in case of network connectivity failures", "type": "Optional[Integer]", "default": 5 + }, + "username": { + "description": "The username to use when downloading from a source location requiring authentication", + "type": "Optional[String]" + }, + "password": { + "description": "The password to use when downloading from a source location requiring authentication", + "type": "Optional[String]" } }, "supports_noop": true diff --git a/tasks/install_powershell.ps1 b/tasks/install_powershell.ps1 index 679baa07..6aaf26ef 100644 --- a/tasks/install_powershell.ps1 +++ b/tasks/install_powershell.ps1 @@ -7,11 +7,21 @@ Param( [String]$install_options = 'REINSTALLMODE="amus"', [Bool]$stop_service = $False, [Int]$retry = 5, - [Bool]$_noop = $False + [Bool]$_noop = $False, + [String]$username = 'forge-key', + [String]$password ) # If an error is encountered, the script will stop instead of the default of "Continue" $ErrorActionPreference = "Stop" +try { + $os_version = (Get-WmiObject Win32_OperatingSystem).Version +} +catch [System.Management.Automation.CommandNotFoundException] { + $os_version = (Get-CimInstance -ClassName win32_OperatingSystem).Version +} +$major_os_version = ($os_version -split '\.')[0] + try { if ((Get-WmiObject Win32_OperatingSystem).OSArchitecture -match '^32') { $arch = "x86" @@ -27,9 +37,19 @@ catch [System.Management.Automation.CommandNotFoundException] { } } +$fips = 'false' +try { + if ((Get-ItemPropertyValue -Path 'HKLM:\System\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy' -Name Enabled) -ne 0) { + $fips = 'true' + } +} +catch { + Write-Output "Failed to lookup FIPS mode, assuming it is disabled" +} + function Test-PuppetInstalled { $rootPath = 'HKLM:\SOFTWARE\Puppet Labs\Puppet' - try { + try { if (Get-ItemProperty -Path $rootPath) { RETURN $true } } catch { @@ -98,12 +118,16 @@ if (Test-RunningServices) { # Change windows_source only if the collection is a nightly build, and the source was not explicitly specified. if (($collection -like '*nightly*') -And -Not ($PSBoundParameters.ContainsKey('windows_source'))) { $windows_source = 'https://nightlies.puppet.com/downloads' +} elseif (($collection -like '*puppetcore*') -And -Not ($PSBoundParameters.ContainsKey('windows_source'))) { + $windows_source = 'https://artifacts-puppetcore.puppet.com/v1/download' } if ($absolute_source) { $msi_source = "$absolute_source" } -else { +elseif ($collection -like '*puppetcore*') { + $msi_source = "${windows_source}?version=${version}&os_name=windows&os_version=${major_os_version}&os_arch=${arch}&fips=${fips}" +} else { $msi_source = "$windows_source/windows/${collection}/${msi_name}" } @@ -125,15 +149,19 @@ function Set-Tls12 { } function DownloadPuppet { - Write-Output "Downloading the Puppet Agent installer on $env:COMPUTERNAME..." + Write-Output "Downloading the Puppet Agent installer on $env:COMPUTERNAME from ${msi_source}" Set-Tls12 $webclient = New-Object system.net.webclient - + if ($password) { + $credentials = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${username}:${password}")) + $webclient.Headers.Add("Authorization", "Basic ${credentials}") + } try { $webclient.DownloadFile($msi_source,$msi_dest) } catch [System.Net.WebException] { + Write-Host "Download exception: $($_.Exception.Message)" For ($attempt_number = 1; $attempt_number -le $retry; $attempt_number++) { try { Write-Output "Retrying... [$attempt_number/$retry]" @@ -141,6 +169,7 @@ function DownloadPuppet { break } catch [System.Net.WebException] { + Write-Host "Download exception: $($_.Exception.Message)" if($attempt_number -eq $retry) { # If we can't find the msi, then we may not be configured correctly if($_.Exception.Response.StatusCode -eq [system.net.httpstatuscode]::NotFound) { From fceee1b721d68910f1e667e67d05d4c9b5558d6a Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Tue, 18 Mar 2025 12:50:35 -0700 Subject: [PATCH 2/4] Upgrade puppetcore* msi from artifacts-puppetcore.puppet.com When using the puppetcore collection on Windows, if we detect the installed version does not match, then upgrade the MSI. Due to a puppet bug, we cannot pass credentials in the `source` parameter. And `curl.exe` is not present in our puppet-agent packages. So use powershell to download. Co-authored-by: Kevin <114269618+klab-systems@users.noreply.github.com> --- REFERENCE.md | 23 ++++++++++++ manifests/osfamily/windows.pp | 11 +++++- manifests/prepare/package.pp | 61 ++++++++++++++++++++++++------- metadata.json | 4 ++ templates/download_puppet.ps1.epp | 19 ++++++++++ 5 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 templates/download_puppet.ps1.epp diff --git a/REFERENCE.md b/REFERENCE.md index 8f2a2319..fa6cb61e 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -624,6 +624,7 @@ working with a remote https repository. The following parameters are available in the `puppet_agent::prepare::package` class: * [`source`](#-puppet_agent--prepare--package--source) +* [`package_file_name`](#-puppet_agent--prepare--package--package_file_name) ##### `source` @@ -632,6 +633,16 @@ Data type: `Variant[String, Array]` The source file for the puppet-agent package. Can use any of the data types and protocols that the File resource's source attribute can. +##### `package_file_name` + +Data type: `Optional[String]` + +The destination file name for the puppet-agent package. If no destination +is given, then the basename component of the source will be used as the +destination filename. + +Default value: `undef` + ### `puppet_agent::prepare::puppet_config` Private class called from puppet_agent::prepare class. @@ -993,6 +1004,18 @@ Data type: `Optional[Integer]` The number of retries in case of network connectivity failures +##### `username` + +Data type: `Optional[String]` + +The username to use when downloading from a source location requiring authentication + +##### `password` + +Data type: `Optional[String]` + +The password to use when downloading from a source location requiring authentication + ### `install_shell` Install the Puppet agent package diff --git a/manifests/osfamily/windows.pp b/manifests/osfamily/windows.pp index 8114ddeb..98ed62ae 100644 --- a/manifests/osfamily/windows.pp +++ b/manifests/osfamily/windows.pp @@ -23,13 +23,22 @@ } else { if $puppet_agent::collection == 'PC1' { $source = "${puppet_agent::windows_source}/windows/${puppet_agent::package_name}-${puppet_agent::prepare::package_version}-${puppet_agent::arch}.msi" + } elsif $puppet_agent::collection =~ /core/ { + $source = 'https://artifacts-puppetcore.puppet.com/v1/download' } else { $source = "${puppet_agent::windows_source}/windows/${puppet_agent::collection}/${puppet_agent::package_name}-${puppet_agent::prepare::package_version}-${puppet_agent::arch}.msi" } } + $destination_name = if $puppet_agent::collection =~ /core/ { + "${puppet_agent::package_name}-${puppet_agent::prepare::package_version}-${puppet_agent::arch}.msi" + } else { + undef + } + class { 'puppet_agent::prepare::package': - source => $source, + source => $source, + destination_name => $destination_name, } contain puppet_agent::prepare::package diff --git a/manifests/prepare/package.pp b/manifests/prepare/package.pp index dee0afa2..e90b9a2d 100644 --- a/manifests/prepare/package.pp +++ b/manifests/prepare/package.pp @@ -5,8 +5,13 @@ # @param source # The source file for the puppet-agent package. Can use any of the data types # and protocols that the File resource's source attribute can. +# @param destination_name +# The destination file name for the puppet-agent package. If no destination +# is given, then the basename component of the source will be used as the +# destination name. class puppet_agent::prepare::package ( Variant[String, Array] $source, + Optional[String] $destination_name = undef ) { assert_private() @@ -14,12 +19,17 @@ ensure => directory, } - # In order for the 'basename' function to work correctly we need to change - # any \s to /s (even for windows UNC paths) so that it will correctly pull off - # the filename. Since this operation is only grabbing the base filename and not - # any part of the path this should be safe, since the source will simply remain - # what it was before and we can still pull off the filename. - $package_file_name = basename(regsubst($source, "\\\\", '/', 'G')) + if $destination_name { + $package_file_name = $destination_name + } else { + # In order for the 'basename' function to work correctly we need to change + # any \s to /s (even for windows UNC paths) so that it will correctly pull off + # the filename. Since this operation is only grabbing the base filename and not + # any part of the path this should be safe, since the source will simply remain + # what it was before and we can still pull off the filename. + $package_file_name = basename(regsubst($source, "\\\\", '/', 'G')) + } + if $facts['os']['family'] =~ /windows/ { $local_package_file_path = windows_native_path("${puppet_agent::params::local_packages_dir}/${package_file_name}") $mode = undef @@ -28,12 +38,37 @@ $mode = '0644' } - file { $local_package_file_path: - ensure => file, - owner => $puppet_agent::params::user, - group => $puppet_agent::params::group, - mode => $mode, - source => $source, - require => File[$puppet_agent::params::local_packages_dir], + if $puppet_agent::collection =~ /core/ and $facts['os']['family'] =~ /windows/ { + $download_username = getvar('puppet_agent::username', 'forge-key') + $download_password = unwrap(getvar('puppet_agent::password')) + + $_download_puppet = windows_native_path("${facts['env_temp_variable']}/download_puppet.ps1") + file { $_download_puppet: + ensure => file, + content => Sensitive(epp('puppet_agent/download_puppet.ps1.epp')), + } + + exec { 'Download Puppet Agent': + command => [ + "${facts['os']['windows']['system32']}\\WindowsPowerShell\\v1.0\\powershell.exe", + '-ExecutionPolicy', + 'Bypass', + '-NoProfile', + '-NoLogo', + '-NonInteractive', + $_download_puppet + ], + creates => $local_package_file_path, + require => File[$puppet_agent::params::local_packages_dir], + } + } else { + file { $local_package_file_path: + ensure => file, + owner => $puppet_agent::params::user, + group => $puppet_agent::params::group, + mode => $mode, + source => $source, + require => File[$puppet_agent::params::local_packages_dir], + } } } diff --git a/metadata.json b/metadata.json index d3e2f515..f49b300a 100644 --- a/metadata.json +++ b/metadata.json @@ -23,6 +23,10 @@ { "name": "puppetlabs-facts", "version_requirement": ">= 0.5.0 < 2.0.0" + }, + { + "name": "puppetlabs-powershell", + "version_requirement": ">= 6.0.2 < 7.0.0" } ], "operatingsystem_support": [ diff --git a/templates/download_puppet.ps1.epp b/templates/download_puppet.ps1.epp new file mode 100644 index 00000000..bf9ff707 --- /dev/null +++ b/templates/download_puppet.ps1.epp @@ -0,0 +1,19 @@ +$body = @{ + "version" = "<%= $puppet_agent::prepare::package_version %>" + "os_name" = "<%= $facts['os']['family'] %>" + "os_version" = "<%= $facts['os']['release']['major'] %>" + "os_arch" = "<%= $facts['os']['architecture'] %>" + "fips" = "<%= $facts['fips_enabled'] %>" +} +$username = "<%= $puppet_agent::prepare::package::download_username %>" +$password = ConvertTo-SecureString "<%= $puppet_agent::prepare::package::download_password %>" -AsPlainText -Force +$credential = New-Object System.Management.Automation.PSCredential($username, $password) +try { + Invoke-WebRequest -Uri "<%= $puppet_agent::prepare::package::source %>" ` + -Body $body ` + -Credential $credential ` + -OutFile "<%= $puppet_agent::prepare::package::local_package_file_path %>" +} catch [System.Net.WebException] { + Write-Host "Network-related error: $($_.Exception.Message)" + exit 1 +} From 901a94ae7d98d3f706a93c1838c7cb57aab6c9d4 Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Wed, 2 Apr 2025 16:44:35 -0700 Subject: [PATCH 3/4] Include review feedback Use '@api private' for private classes, so they are excluded from REFERENCES.md Use 'String[1]' for username parameter. If it is specified, then it should not be empty. Use 'Sensitive[String[1]]' for password parameter. Use ruby-style way of conditionally setting a variable. --- REFERENCE.md | 44 ++++++++--------------------------- manifests/init.pp | 4 +++- manifests/prepare/package.pp | 16 ++++--------- tasks/install_powershell.json | 4 ++-- tasks/install_shell.json | 4 ++-- 5 files changed, 22 insertions(+), 50 deletions(-) diff --git a/REFERENCE.md b/REFERENCE.md index fa6cb61e..e8b419ea 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -6,6 +6,8 @@ ### Classes +#### Public Classes + * [`puppet_agent`](#puppet_agent): Upgrades Puppet 4 and newer to the requested version. * [`puppet_agent::configure`](#puppet_agent--configure): Uses $puppet_agent::config to manage settings in puppet.conf. * [`puppet_agent::install`](#puppet_agent--install): This class is called from puppet_agent for install. @@ -22,10 +24,13 @@ * [`puppet_agent::osfamily::windows`](#puppet_agent--osfamily--windows): Determines the puppet-agent package location for Windows OSes. * [`puppet_agent::params`](#puppet_agent--params): Sets variables according to platform. * [`puppet_agent::prepare`](#puppet_agent--prepare): This class is called from puppet_agent to prepare for the upgrade. -* [`puppet_agent::prepare::package`](#puppet_agent--prepare--package): Ensures correct puppet-agent package is downloaded locally. * [`puppet_agent::prepare::puppet_config`](#puppet_agent--prepare--puppet_config): Private class called from puppet_agent::prepare class. * [`puppet_agent::service`](#puppet_agent--service): Ensures that managed services are running. +#### Private Classes + +* `puppet_agent::prepare::package`: Ensures correct puppet-agent package is downloaded locally. + ### Resource types * [`puppet_agent_end_run`](#puppet_agent_end_run): Stops the current Puppet run if a puppet-agent upgrade was performed. Used on platforms that manage the Puppet Agent upgrade with a package r @@ -614,35 +619,6 @@ The puppet-agent version to install. Default value: `undef` -### `puppet_agent::prepare::package` - -for installation. This is used on platforms without package managers capable of -working with a remote https repository. - -#### Parameters - -The following parameters are available in the `puppet_agent::prepare::package` class: - -* [`source`](#-puppet_agent--prepare--package--source) -* [`package_file_name`](#-puppet_agent--prepare--package--package_file_name) - -##### `source` - -Data type: `Variant[String, Array]` - -The source file for the puppet-agent package. Can use any of the data types -and protocols that the File resource's source attribute can. - -##### `package_file_name` - -Data type: `Optional[String]` - -The destination file name for the puppet-agent package. If no destination -is given, then the basename component of the source will be used as the -destination filename. - -Default value: `undef` - ### `puppet_agent::prepare::puppet_config` Private class called from puppet_agent::prepare class. @@ -1006,13 +982,13 @@ The number of retries in case of network connectivity failures ##### `username` -Data type: `Optional[String]` +Data type: `Optional[String[1]]` The username to use when downloading from a source location requiring authentication ##### `password` -Data type: `Optional[String]` +Data type: `Optional[Sensitive[String[1]]]` The password to use when downloading from a source location requiring authentication @@ -1086,13 +1062,13 @@ The number of retries in case of network connectivity failures ##### `username` -Data type: `Optional[String]` +Data type: `Optional[String[1]]` The username to use when downloading from a source location requiring authentication ##### `password` -Data type: `Optional[String]` +Data type: `Optional[Sensitive[String[1]]]` The password to use when downloading from a source location requiring authentication diff --git a/manifests/init.pp b/manifests/init.pp index 9fad7b6b..502f1953 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -3,7 +3,9 @@ # @param arch # The package architecture. Defaults to the architecture fact. # @param collection -# The Puppet Collection to track. Defaults to 'PC1'. +# The Puppet Collection to track. Defaults to 'PC1'. Valid values are puppet7, +# puppet8, puppet, puppet7-nightly, puppet8-nightly, puppet-nightly, +# puppetcore7, puppetcore8. # @param is_pe # Install from Puppet Enterprise repos. Enabled if communicating with a PE master. # @param manage_pki_dir diff --git a/manifests/prepare/package.pp b/manifests/prepare/package.pp index e90b9a2d..d834b9f0 100644 --- a/manifests/prepare/package.pp +++ b/manifests/prepare/package.pp @@ -2,16 +2,10 @@ # for installation. This is used on platforms without package managers capable of # working with a remote https repository. # -# @param source -# The source file for the puppet-agent package. Can use any of the data types -# and protocols that the File resource's source attribute can. -# @param destination_name -# The destination file name for the puppet-agent package. If no destination -# is given, then the basename component of the source will be used as the -# destination name. +# @api private class puppet_agent::prepare::package ( Variant[String, Array] $source, - Optional[String] $destination_name = undef + Optional[String[1]] $destination_name = undef ) { assert_private() @@ -19,15 +13,15 @@ ensure => directory, } - if $destination_name { - $package_file_name = $destination_name + $package_file_name = if $destination_name { + $destination_name } else { # In order for the 'basename' function to work correctly we need to change # any \s to /s (even for windows UNC paths) so that it will correctly pull off # the filename. Since this operation is only grabbing the base filename and not # any part of the path this should be safe, since the source will simply remain # what it was before and we can still pull off the filename. - $package_file_name = basename(regsubst($source, "\\\\", '/', 'G')) + basename(regsubst($source, "\\\\", '/', 'G')) } if $facts['os']['family'] =~ /windows/ { diff --git a/tasks/install_powershell.json b/tasks/install_powershell.json index 8d64afb0..65b0c8bc 100644 --- a/tasks/install_powershell.json +++ b/tasks/install_powershell.json @@ -45,11 +45,11 @@ }, "username": { "description": "The username to use when downloading from a source location requiring authentication", - "type": "Optional[String]" + "type": "Optional[String[1]]" }, "password": { "description": "The password to use when downloading from a source location requiring authentication", - "type": "Optional[String]" + "type": "Optional[Sensitive[String[1]]]" } }, "supports_noop": true diff --git a/tasks/install_shell.json b/tasks/install_shell.json index 0fc475e0..b31139ab 100644 --- a/tasks/install_shell.json +++ b/tasks/install_shell.json @@ -46,11 +46,11 @@ }, "username": { "description": "The username to use when downloading from a source location requiring authentication", - "type": "Optional[String]" + "type": "Optional[String[1]]" }, "password": { "description": "The password to use when downloading from a source location requiring authentication", - "type": "Optional[String]" + "type": "Optional[Sensitive[String[1]]]" } }, "files": ["facts/tasks/bash.sh"], From d00532fad223613a7a5f3d44d7b2d5bffd95a42b Mon Sep 17 00:00:00 2001 From: Josh Cooper Date: Thu, 17 Apr 2025 18:38:44 -0700 Subject: [PATCH 4/4] Add support for development MSI builds Dev builds have more than 3 dotted components --- manifests/prepare/package.pp | 1 + tasks/install_powershell.ps1 | 11 ++++++++--- templates/download_puppet.ps1.epp | 9 +++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/manifests/prepare/package.pp b/manifests/prepare/package.pp index d834b9f0..a1b1e549 100644 --- a/manifests/prepare/package.pp +++ b/manifests/prepare/package.pp @@ -35,6 +35,7 @@ if $puppet_agent::collection =~ /core/ and $facts['os']['family'] =~ /windows/ { $download_username = getvar('puppet_agent::username', 'forge-key') $download_password = unwrap(getvar('puppet_agent::password')) + $dev = count(split($puppet_agent::prepare::package_version, '\.')) > 3 $_download_puppet = windows_native_path("${facts['env_temp_variable']}/download_puppet.ps1") file { $_download_puppet: diff --git a/tasks/install_powershell.ps1 b/tasks/install_powershell.ps1 index 6aaf26ef..ce67f2ee 100644 --- a/tasks/install_powershell.ps1 +++ b/tasks/install_powershell.ps1 @@ -124,9 +124,14 @@ if (($collection -like '*nightly*') -And -Not ($PSBoundParameters.ContainsKey('w if ($absolute_source) { $msi_source = "$absolute_source" -} -elseif ($collection -like '*puppetcore*') { - $msi_source = "${windows_source}?version=${version}&os_name=windows&os_version=${major_os_version}&os_arch=${arch}&fips=${fips}" +} elseif ($collection -like '*puppetcore*') { + # dev param is case-sensitive, so don't use $True + if (($version -split '\.').count -gt 3) { + $dev = '&dev=true' + } else { + $dev = '' + } + $msi_source = "${windows_source}?version=${version}&os_name=windows&os_version=${major_os_version}&os_arch=${arch}&fips=${fips}${dev}" } else { $msi_source = "$windows_source/windows/${collection}/${msi_name}" } diff --git a/templates/download_puppet.ps1.epp b/templates/download_puppet.ps1.epp index bf9ff707..43a04c3d 100644 --- a/templates/download_puppet.ps1.epp +++ b/templates/download_puppet.ps1.epp @@ -1,9 +1,10 @@ $body = @{ - "version" = "<%= $puppet_agent::prepare::package_version %>" - "os_name" = "<%= $facts['os']['family'] %>" + "version" = "<%= $puppet_agent::prepare::package_version %>" + "dev" = "<%= $puppet_agent::prepare::package::dev %>" + "os_name" = "<%= $facts['os']['family'] %>" "os_version" = "<%= $facts['os']['release']['major'] %>" - "os_arch" = "<%= $facts['os']['architecture'] %>" - "fips" = "<%= $facts['fips_enabled'] %>" + "os_arch" = "<%= $facts['os']['architecture'] %>" + "fips" = "<%= $facts['fips_enabled'] %>" } $username = "<%= $puppet_agent::prepare::package::download_username %>" $password = ConvertTo-SecureString "<%= $puppet_agent::prepare::package::download_password %>" -AsPlainText -Force