Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"dependencies": [
{
"name": "puppetlabs/stdlib",
"version_requirement": ">= 8.4.0 < 10.0.0"
"version_requirement": ">= 8.4.0 < 11.0.0"
},
{
"name": "puppetlabs/powershell",
Expand All @@ -32,7 +32,7 @@
"requirements": [
{
"name": "puppet",
"version_requirement": ">8.0.0 < 9.0.0"
"version_requirement": ">=8.0.0 < 9.0.0"
}
],
"tags": [
Expand Down
4 changes: 1 addition & 3 deletions spec/acceptance/sqlserver_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ def ensure_sqlserver_instance(inst_name, ensure_val = 'present')
},
}
MANIFEST
retry_on_error_matching(10, 5, %r{apply manifest failed}) do
apply_manifest(pp, catch_failures: true)
end
apply_sqlserver_manifest(pp)
end

context 'Testing sqlserver::config' do
Expand Down
4 changes: 2 additions & 2 deletions spec/acceptance/sqlserver_instance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def ensure_sqlserver_instance(features, inst_name, ensure_val = 'present', sysad
},
}
MANIFEST
idempotent_apply(pp)
idempotent_sqlserver_apply(pp)
end

# Return options for run_sql_query
Expand Down Expand Up @@ -167,7 +167,7 @@ def ensure_sqlserver_instance_with_deferred_values(inst_name)
}
MANIFEST

idempotent_apply(pp)
idempotent_sqlserver_apply(pp)
end

context 'Deferred values' do
Expand Down
6 changes: 3 additions & 3 deletions spec/acceptance/sqlserver_user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def ensure_sqlserver_database(db_name, _ensure_val = 'present')
apply_manifest(pp, catch_failures: true)

puts "validate that the database user '#{@db_user}' is successfully created:"
query = "USE #{db_name}; SELECT * FROM SYS.DATABASE_PRINCIPALS WHERE name = '#{@db_user}';"
query = "USE #{@db_name}; SELECT * FROM SYS.DATABASE_PRINCIPALS WHERE name = '#{@db_user}';"
run_sql_query(query: query, server: @hostname, expected_row_count: 1)
end

Expand All @@ -176,7 +176,7 @@ def ensure_sqlserver_database(db_name, _ensure_val = 'present')
apply_manifest(pp, catch_failures: true)

# validate that the database user '#{@db_user}' is successfully created:
query = "USE #{db_name}; SELECT * FROM SYS.DATABASE_PRINCIPALS WHERE name = '#{@db_user}';"
query = "USE #{@db_name}; SELECT * FROM SYS.DATABASE_PRINCIPALS WHERE name = '#{@db_user}';"
run_sql_query(query: query, server: @hostname, expected_row_count: 1)

pp = <<-MANIFEST
Expand All @@ -191,7 +191,7 @@ def ensure_sqlserver_database(db_name, _ensure_val = 'present')
MANIFEST
apply_manifest(pp, catch_failures: true)
# validate that the database user '#{@db_user}' should be deleted:
query = "USE #{db_name}; SELECT * FROM SYS.DATABASE_PRINCIPALS WHERE name = '#{@db_user}';"
query = "USE #{@db_name}; SELECT * FROM SYS.DATABASE_PRINCIPALS WHERE name = '#{@db_user}';"
run_sql_query(query: query, server: @hostname, expected_row_count: 0)
end
end
Expand Down
8 changes: 6 additions & 2 deletions spec/acceptance/z_last_sqlserver_features_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,12 @@ def remove_sql_instance
expect(r.stdout).to match(%r{Client Tools Backwards Compatibility})
expect(r.stdout).to match(%r{Client Tools SDK})
end
expect(r.stdout).to match(%r{Integration Services})
expect(r.stdout).to match(%r{Master Data Services})
# IS/MDS are excluded from `features` for 2019+ (see above), so they
# were never installed here either; matches the 'can install' context.
unless version.to_i >= 2019
expect(r.stdout).to match(%r{Integration Services})
expect(r.stdout).to match(%r{Master Data Services})
end
end
end
end
Expand Down
142 changes: 122 additions & 20 deletions spec/spec_helper_acceptance_local.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ def retry_on_error_matching(max_retry_count = 3, retry_wait_interval_secs = 5, e
include PuppetLitmus
c.before :suite do
# Install archive module dependency first
run_shell('puppet module install puppet/archive')
run_shell('puppet module install puppet/archive --force')
# Install stdlib, needed by many modules including puppet_agent
Helper.instance.run_shell('puppet module install puppetlabs-stdlib')
Helper.instance.run_shell('puppet module install puppetlabs-stdlib --force')

# Install OLEDB driver (required for Puppet types to connect to SQL Server)
puts 'Installing Microsoft OLE DB Driver for SQL Server via Puppet manifest...'
Expand All @@ -64,14 +64,13 @@ def retry_on_error_matching(max_retry_count = 3, retry_wait_interval_secs = 5, e

# Pre-install Puppet agent dependencies and helper modules
# We need mount_iso provider to work on Windows
Helper.instance.run_shell('puppet module install puppetlabs-mount_iso')

# Ensure puppetlabs-puppet_agent module is present before including class
# Use Ruby-side guard to avoid complex PowerShell quoting issues
modules_list = Helper.instance.run_shell('puppet module list')
unless modules_list.stdout.include?('puppetlabs-puppet_agent')
Helper.instance.run_shell('puppet module install puppetlabs-puppet_agent')
end
#
# These are force-(re)installed because the base VM image can ship with
# pre-baked copies of these modules that predate Puppet 8's removal of
# top-scope fact variables (e.g. $::osfamily), which breaks catalog
# compilation. Forcing ensures a compatible release is always used.
Helper.instance.run_shell('puppet module install puppetlabs-mount_iso --force')
Helper.instance.run_shell('puppet module install puppetlabs-puppet_agent --force')

# Rerun the setup, but with the agent's path
# This is a workaround for the module's helper not being in the load path
Expand Down Expand Up @@ -186,9 +185,7 @@ def base_install(sql_version)

def install_sqlserver(features)
# this method installs SQl server on a given host
puts "[SQL Server Install] Starting installation with features: #{features}"
user = Helper.instance.run_shell('$env:UserName').stdout.chomp
puts "[SQL Server Install] Installing for user: #{user}"

pp = <<-MANIFEST
sqlserver_instance{'MSSQLSERVER':
Expand All @@ -211,24 +208,122 @@ def install_sqlserver(features)
}
MANIFEST

puts '[SQL Server Install] Applying manifest with retry logic...'
retry_on_error_matching(10, 5, %r{apply manifest failed}) do
Helper.instance.apply_manifest(pp)
retry_on_error_matching(10, 5) do
apply_manifest_via_scheduled_task(pp)
end
puts '[SQL Server Install] Installation completed successfully'
rescue StandardError => e
puts "[SQL Server Install] FAILED: #{e.message}"
print_sql_setup_log
raise e
end

def apply_manifest_via_scheduled_task(pp, timeout_minutes: 20)
# SQL Server's own setup chainer calls into Windows DPAPI
# (System.Security.Cryptography.ProtectedData.Protect) to serialize secure
# config values such as sa_pwd. DPAPI requires a loaded Windows user
# profile, which a WinRM session (how Litmus/Bolt normally run
# `puppet apply`) does not have, so setup fails with "Access is denied:
# There was an error generating the XML document." Running the apply from
# a password-based scheduled task performs a real logon that loads the
# profile, working around this.
require 'base64'
manifest_b64 = Base64.strict_encode64(pp)

ps_script = <<-POWERSHELL
$manifestPath = 'C:\\Windows\\Temp\\sqlserver_install.pp'
$outPath = 'C:\\Windows\\Temp\\sqlserver_install.out.log'
$taskName = 'PuppetSqlServerInstall'

try {
[IO.File]::WriteAllBytes($manifestPath, [Convert]::FromBase64String('#{manifest_b64}'))
if (Test-Path $outPath) { Remove-Item $outPath -Force }

$user = "$env:COMPUTERNAME\\$env:USERNAME"
$password = $env:pass

Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false

# No -Trigger here: this task is started explicitly via Start-ScheduledTask
# below. Registering it with a time-based trigger too was racing with that
# explicit start, and once the trigger fired a few seconds later while the
# manually-started run was still installing SQL Server, Task Scheduler's
# default "do not start a new instance" policy rejected it and overwrote
# LastTaskResult with 0x800710E0 ("the operator or administrator has
# refused the request") -- clobbering the real result of the run.
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/c puppet apply --trace $manifestPath > $outPath 2>&1"
Register-ScheduledTask -TaskName $taskName -Action $action -User $user -Password $password -RunLevel Highest -Force | Out-Null

Start-ScheduledTask -TaskName $taskName

$limit = (Get-Date).AddMinutes(#{timeout_minutes})
do {
Start-Sleep -Seconds 10
$info = Get-ScheduledTaskInfo -TaskName $taskName
} while ($info.LastTaskResult -eq 267009 -and (Get-Date) -lt $limit)

$lastTaskResult = $info.LastTaskResult
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue

Write-Host '===== [Scheduled Task] puppet apply output ====='
if (Test-Path $outPath) { Get-Content $outPath | Write-Host } else { Write-Host '(no output captured)' }
Write-Host "===== [Scheduled Task] LastTaskResult: $lastTaskResult ====="
# LastTaskResult can be an arbitrary large HRESULT-like value, which
# overflows Int32 and corrupts the process exit code if passed to `exit`
# directly. Collapse it to a plain 0/1 here and let the diagnostics above
# carry the real value.
if ($lastTaskResult -eq 0) { exit 0 } else { exit 1 }
} catch {
Write-Host "[Scheduled Task] ERROR: $($_.Exception.Message)"
exit 1
}
POWERSHELL

encoded = Base64.strict_encode64(ps_script.encode('UTF-16LE'))
cmd = "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand #{encoded}"
r = Helper.instance.run_shell(cmd)
unless r.exit_code.zero?
puts r.stdout
raise "Scheduled-task puppet apply failed (exit code #{r.exit_code})\n#{r.stderr}"
end

r
end

# Any manifest that creates or adds features to a sqlserver_instance runs
# setup.exe with SAPWD set, which hits the same DPAPI/WinRM profile
# limitation apply_manifest_via_scheduled_task works around (see its
# comment). Acceptance specs that install their own instances (beyond the
# one from base_install) should apply through this instead of Litmus's
# plain apply_manifest. Removing/uninstalling an instance never sets SAPWD,
# so it doesn't need this and can use apply_manifest directly.
def apply_sqlserver_manifest(pp)
retry_on_error_matching(10, 5) do
apply_manifest_via_scheduled_task(pp)
end
end

# Equivalent to Litmus's idempotent_apply, but routes the state-changing
# first apply through apply_sqlserver_manifest. The second (idempotency
# check) apply is a plain WinRM apply_manifest: an already-converged
# sqlserver_instance is a no-op and never re-runs setup.exe.
def idempotent_sqlserver_apply(pp)
apply_sqlserver_manifest(pp)
apply_manifest(pp, catch_changes: true)
end

def print_sql_setup_log
puts '[SQL Server Install] Checking setup logs...'
log_cmd = 'Get-ChildItem -Path "C:\\Program Files\\Microsoft SQL Server" -Recurse ' \
'-Filter "Summary*.txt" -ErrorAction SilentlyContinue | ' \
'Select-Object -First 1 -ExpandProperty FullName'
log_check = Helper.instance.run_shell(log_cmd)
if log_check.exit_code == 0 && !log_check.stdout.strip.empty?
puts "[SQL Server Install] Setup log found at: #{log_check.stdout.strip}"
log_content = Helper.instance.run_shell("Get-Content '#{log_check.stdout.strip}' -Tail 50")
puts "[SQL Server Install] Last 50 lines of setup log:\n#{log_content.stdout}"
log_content = Helper.instance.run_shell("Get-Content '#{log_check.stdout.strip}' -Tail 80")
puts "[SQL Server Install] Last 80 lines of setup log:\n#{log_content.stdout}"
else
puts '[SQL Server Install] No setup log found.'
end
raise e
end

def ensure_oledb_installed
Expand Down Expand Up @@ -442,7 +537,14 @@ def validate_sql_install(opts = {}, &block)
' if ($s) { Get-Content $s.FullName }',
'}',
].join("\n")
sum_cmd = "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"#{ps_summary}\""
# Base64-encoded (like the other dynamic PowerShell blocks in this file)
# rather than passed via -Command "...": the raw $s/$s.FullName variables
# were being stripped by the local shell that invokes Bolt before the
# command ever reached the Windows target, since it treats "$s" as its own
# (unset, empty) variable.
require 'base64'
encoded_summary = Base64.strict_encode64(ps_summary.encode('UTF-16LE'))
sum_cmd = "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand #{encoded_summary}"
result = Helper.instance.run_shell(sum_cmd)
return unless block

Expand Down
Loading