From 91805e1d82063b040da47897d1719ea8eb7648ed Mon Sep 17 00:00:00 2001 From: Alexander Olofsson <alexander.olofsson@liu.se> Date: Mon, 4 Sep 2023 09:53:50 +0200 Subject: [PATCH] Rework testing to properly stub WinRM --- app/models/wds_server.rb | 83 ++++++++++++----------- test/models/foreman_wds/wds_facet_test.rb | 8 ++- test/test_plugin_helper.rb | 45 ++++++++++++ 3 files changed, 94 insertions(+), 42 deletions(-) diff --git a/app/models/wds_server.rb b/app/models/wds_server.rb index aaf1cb4..56c360f 100644 --- a/app/models/wds_server.rb +++ b/app/models/wds_server.rb @@ -49,12 +49,9 @@ class WdsServer < ApplicationRecord objects = run_wql('SELECT * FROM MSFT_WdsClient', on_error: {})[:msft_wdsclient] objects = nil if objects&.empty? objects ||= begin - data = connection.shell(:powershell) do |s| - s.run('Get-WdsClient | ConvertTo-Json -Compress') - end.stdout - data = '[]' if data.empty? - - underscore_result([JSON.parse(data)].flatten) + clients = run_pwsh('Get-WdsClient').stdout + clients = '[]' if clients.empty? + underscore_result([JSON.parse(clients)].flatten) end objects @@ -82,18 +79,14 @@ class WdsServer < ApplicationRecord raise NotImplementedError, 'Not finished yet' ensure_unattend(host) - connection.shell(:powershell) do |sh| - sh.run("New-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}' -DeviceName '#{host.name}' -WdsClientUnattend '#{unattend_file(host)}' -BootImagePath 'boot\\#{wdsify_architecture(host.architecture)}\\images\\#{(host.wds_boot_image || boot_images.first).file_name}' -PxePromptPolicy 'NoPrompt'") - end + run_pwsh("New-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}' -DeviceName '#{host.name}' -WdsClientUnattend '#{unattend_file(host)}' -BootImagePath 'boot\\#{wdsify_architecture(host.architecture)}\\images\\#{(host.wds_boot_image || boot_images.first).file_name}' -PxePromptPolicy 'NoPrompt'") end def delete_client(host) raise NotImplementedError, 'Not finished yet' delete_unattend(host) - connection.shell(:powershell) do |sh| - sh.run("Remove-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}'") - end + run_pwsh("Remove-WdsClient -DeviceID '#{host.mac.upcase.delete ':'}'", json: false) end def all_images @@ -162,9 +155,10 @@ class WdsServer < ApplicationRecord def unattend_path cache.cache(:unattend_path) do - JSON.parse(connection.shell(:powershell) do |sh| - sh.run('Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\WDSServer\Providers\WDSTFTP -Name RootFolder | select RootFolder | ConvertTo-Json -Compress') - end, symbolize_names: true)[:RootFolder] + JSON.parse( + run_pwsh('Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\WDSServer\Providers\WDSTFTP -Name RootFolder | select RootFolder'), + symbolize_names: true + )[:RootFolder] end end @@ -187,6 +181,7 @@ class WdsServer < ApplicationRecord raise 'No provisioning interface available' unless iface raise NotImplementedException, 'TODO: Not implemented yet' + raise NotImplementedException, 'TODO: Not implemented yet' if SETTINGS[:wds_unattend_group] # TODO: render template, send as heredoc template = host.operatingsystem.provisioning_templates.find { |t| t.template_kind.name == 'wds_unattend' } @@ -198,33 +193,33 @@ class WdsServer < ApplicationRecord template_data = host.render_template template: template - connection.shell(:powershell) do |sh| - file_path = unattend_file(host) - - sh.run("$unattend_render = @'\n#{template_data}\n'@") - sh.run("New-Item -Path '#{file_path}' -ItemType 'file' -Value $unattend_render") - - source_image = host.wds_facet.install_image - target_image = target_image_for(host) - if SETTINGS[:wds_unattend_group] - raise NotImplementedException, 'TODO: Not implemented yet' - # New-WdsInstallImageGroup -Name #{SETTINGS[:wds_unattend_group]} - # Export-WdsInstallImage -ImageGroup <Group> ... - # Import-WdsInstallImage -ImageGroup #{SETTINGS[:wds_unattend_group]} -UnattendFile '#{file_path}' -OverwriteUnattend ... - else - sh.run("Copy-WdsInstallImage -ImageGroup '#{source_image.image_group}' -FileName '#{source_image.file_name}' -ImageName '#{source_image.image_name}' -NewFileName '#{target_image.file_name}' -NewImageName '#{target_image.image_name}'") - sh.run("Set-WdsInstallImage -ImageGroup '#{target_image.image_group}' -FileName '#{target_image.file_name}' -ImageName '#{target_image.image_name}' -DisplayOrder 99999 -UnattendFile '#{file_path}' -OverwriteUnattend") - end + file_path = unattend_file(host) + script = [] + script << "$unattend_render = @'\n#{template_data}\n'@" + script << "New-Item -Path '#{file_path}' -ItemType 'file' -Value $unattend_render" + + source_image = host.wds_facet.install_image + target_image = target_image_for(host) + + if SETTINGS[:wds_unattend_group] + # New-WdsInstallImageGroup -Name #{SETTINGS[:wds_unattend_group]} + # Export-WdsInstallImage -ImageGroup <Group> ... + # Import-WdsInstallImage -ImageGroup #{SETTINGS[:wds_unattend_group]} -UnattendFile '#{file_path}' -OverwriteUnattend ... + else + script << "Copy-WdsInstallImage -ImageGroup '#{source_image.image_group}' -FileName '#{source_image.file_name}' -ImageName '#{source_image.image_name}' -NewFileName '#{target_image.file_name}' -NewImageName '#{target_image.image_name}'" + script << "Set-WdsInstallImage -ImageGroup '#{target_image.image_group}' -FileName '#{target_image.file_name}' -ImageName '#{target_image.image_name}' -DisplayOrder 99999 -UnattendFile '#{file_path}' -OverwriteUnattend" end + + run_pwsh script.join("\n"), json: false end def delete_unattend(host) image = target_image_for(host) - connection.shell(:powershell) do |sh| - sh.run("Remove-WdsInstallImage -ImageGroup '#{image.image_group}' -ImageName '#{image.image_name}' -FileName '#{image.file_name}'") - sh.run("Remove-Item -Path '#{unattend_file(host)}'") - end.errcode.zero? + command = [] + command << "Remove-WdsInstallImage -ImageGroup '#{image.image_group}' -ImageName '#{image.image_name}' -FileName '#{image.file_name}'" + command << "Remove-Item -Path '#{unattend_file(host)}'" + run_pwsh(command.join("\n"), json: false).errcode.zero? end def ensure_client(_host) @@ -242,14 +237,12 @@ class WdsServer < ApplicationRecord objects = nil if objects.empty? unless objects - begin - result = connection.shell(:powershell) do |s| - s.run("Get-WDS#{type.to_s.capitalize}Image #{"-ImageName '#{name.sub("'", "`'")}'" if name} | ConvertTo-Json -Compress") - end + result = run_pwsh "Get-WDS#{type.to_s.capitalize}Image#{" -ImageName '#{name.sub("'", "`'")}'" if name}" + begin objects = underscore_result([JSON.parse(result.stdout)].flatten) rescue JSON::ParserError => e - ::Rails.logger.error "#{e.class}: #{e}\n#{result}" + ::Rails.logger.error "Failed to parse images - #{e.class}: #{e}, the data was;\n#{result.inspect}" raise e end end @@ -270,6 +263,14 @@ class WdsServer < ApplicationRecord end end + def run_pwsh(command, json: true) + cmd_arr = [command] + cmd_arr << '| ConvertTo-Json -Compress' if json + connection.shell(:powershell) do |s| + s.run cmd_arr.join(' ') + end + end + def run_wql(wql, on_error: :raise) connection.run_wql(wql) rescue StandardError diff --git a/test/models/foreman_wds/wds_facet_test.rb b/test/models/foreman_wds/wds_facet_test.rb index 4c8a17c..764dfcc 100644 --- a/test/models/foreman_wds/wds_facet_test.rb +++ b/test/models/foreman_wds/wds_facet_test.rb @@ -8,7 +8,7 @@ module ForemanWds FactoryBot.build(:wds_server) end let(:host) do - FactoryBot.build(:host, :managed, :with_wds_facet) do + FactoryBot.build(:host, :managed, :with_wds_facet) do |host| host.wds_facet.wds_server = wds_server end end @@ -23,6 +23,12 @@ module ForemanWds end context 'with WDS server' do + setup do + wds_server.stubs(:run_wql).returns({}) + wds_server.stubs(:run_pwsh).with('Get-WDSBootImage').returns(OpenStruct.new stdout: '[]') + wds_server.stubs(:run_pwsh).with("Get-WDSInstallImage -ImageName 'install.wim'").returns(OpenStruct.new stdout: '[]') + end + it 'does not error' do assert_nil host.wds_facet.boot_image assert_nil host.wds_facet.install_image diff --git a/test/test_plugin_helper.rb b/test/test_plugin_helper.rb index a236a6d..28acb8f 100644 --- a/test/test_plugin_helper.rb +++ b/test/test_plugin_helper.rb @@ -21,3 +21,48 @@ ActiveSupport::TestCase.file_fixture_path = File.join(__dir__, 'fixtures') # Add plugin to FactoryBot's paths FactoryBot.definition_file_paths << File.join(__dir__, 'factories') FactoryBot.reload + +class ActiveSupport::TestCase + setup :setup_winrm_stubs + + def stub_winrm_powershell(command = nil, &block) + ret = if block_given? + @winrm_shell_mock[:powershell].stubs(:run).with(&block) + else + @winrm_shell_mock[:powershell].stubs(:run).with(command) + end + class << ret + def returns_pwsh(value, **params) + returns OpenStruct.new(stdout: value, **params) + end + end + ret + end + + def stub_winrm_wql(query = nil) + if query + WinRM::Connection.any_instance.stubs(:run_wql).with(query) + else + WinRM::Connection.any_instance.stubs(:run_wql) + end + end + + private + + def setup_winrm_stubs + return if @winrm_mock + + @winrm_mock = true + require 'winrm' + + transport_mock = mock('winrm::http::transport') + WinRM::Connection.any_instance.stubs(:transport).returns(transport_mock) + + transport_mock.stubs(:send_request).raises(StandardError, 'Real WinRM connections are not allowed') + + @winrm_shell_mock = { + powershell: mock('winrm::shell::powershell') + } + WinRM::Connection.any_instance.stubs(:shell).with(:powershell).yields @winrm_shell_mock[:powershell] + end +end -- GitLab