cem_win_spec 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +534 -0
- data/CODEOWNERS +2 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +115 -0
- data/LICENSE.txt +21 -0
- data/README.md +33 -0
- data/Rakefile +12 -0
- data/cem_win_spec.gemspec +37 -0
- data/exe/cem-win-spec +52 -0
- data/lib/cem_win_spec/fixture_cache.rb +121 -0
- data/lib/cem_win_spec/iap_tunnel.rb +174 -0
- data/lib/cem_win_spec/logging/formatter.rb +97 -0
- data/lib/cem_win_spec/logging.rb +170 -0
- data/lib/cem_win_spec/module_archive_builder.rb +84 -0
- data/lib/cem_win_spec/rake_tasks.rb +138 -0
- data/lib/cem_win_spec/remote_command.rb +47 -0
- data/lib/cem_win_spec/rspec_test_cmds.rb +51 -0
- data/lib/cem_win_spec/test_runner.rb +144 -0
- data/lib/cem_win_spec/version.rb +5 -0
- data/lib/cem_win_spec/win_exec/base_exec.rb +42 -0
- data/lib/cem_win_spec/win_exec/connection_opts.rb +169 -0
- data/lib/cem_win_spec/win_exec/local_exec.rb +74 -0
- data/lib/cem_win_spec/win_exec/output.rb +104 -0
- data/lib/cem_win_spec/win_exec/winrm_exec.rb +89 -0
- data/lib/cem_win_spec/win_exec.rb +234 -0
- data/lib/cem_win_spec.rb +79 -0
- data/sig/cem_win_spec.rbs +4 -0
- metadata +159 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require_relative 'base_exec'
|
|
5
|
+
|
|
6
|
+
module CemWinSpec
|
|
7
|
+
module WinExec
|
|
8
|
+
# Class for executing local shell commands
|
|
9
|
+
class LocalExec < BaseExec
|
|
10
|
+
attr_reader :thread_results
|
|
11
|
+
|
|
12
|
+
def initialize(working_dir = nil)
|
|
13
|
+
@ran_in_thread = false
|
|
14
|
+
@thread_results = {}
|
|
15
|
+
super(working_dir)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def available?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ran_in_thread?
|
|
23
|
+
@ran_in_thread
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def any_threads?
|
|
27
|
+
@ran_in_thread && thread_group.list.any?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def join_threads
|
|
31
|
+
thread_group.list.each(&:join)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Spawn a new process and detach it from the current process
|
|
35
|
+
# This is useful for running commands that should not block the current process
|
|
36
|
+
# and that you don't need stdout/stderr from.
|
|
37
|
+
# @param cmd [String] The command to execute
|
|
38
|
+
def bg_exec(cmd, *_args, **_kwargs)
|
|
39
|
+
puts_cmd(cmd)
|
|
40
|
+
Process.detach(spawn(cd_working_dir(cmd)))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Execute a command in a new thread. This works by creating a new thread in a thread group
|
|
44
|
+
# and then executing the command in that thread. The thread group is used to keep track of
|
|
45
|
+
# all the threads that have been created and to join / exit / kill them all at once. The
|
|
46
|
+
# command results are stored in a hash with the command as the key and the results as the
|
|
47
|
+
# value. The results are stored as an array of [stdout, stderr, status] where status is a
|
|
48
|
+
# Process::Status object.
|
|
49
|
+
# @param cmd [String] The command to execute
|
|
50
|
+
# @return [Array] An array of [:threaded, cmd]
|
|
51
|
+
def thread_exec(cmd, *_args, **_kwargs)
|
|
52
|
+
@ran_in_thread = true
|
|
53
|
+
th = Thread.new do
|
|
54
|
+
puts_cmd(cmd)
|
|
55
|
+
so, se, st = Open3.capture3(cd_working_dir(cmd))
|
|
56
|
+
@thread_results[cmd] = [so, se, st]
|
|
57
|
+
end
|
|
58
|
+
thread_group.add th
|
|
59
|
+
:threaded
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def exec(cmd, *_args, **_kwargs)
|
|
63
|
+
puts_cmd(cmd)
|
|
64
|
+
Open3.caputure3(cd_working_dir(cmd))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def thread_group
|
|
70
|
+
@thread_group ||= ThreadGroup.new
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'winrm'
|
|
4
|
+
require_relative '../logging'
|
|
5
|
+
|
|
6
|
+
module CemWinSpec
|
|
7
|
+
module WinExec
|
|
8
|
+
# Wrapper class for WinRM::Output
|
|
9
|
+
class Output
|
|
10
|
+
include CemWinSpec::Logging
|
|
11
|
+
|
|
12
|
+
attr_reader :stdout, :stderr, :exitcode
|
|
13
|
+
|
|
14
|
+
def initialize(output, quiet: false, line_prefix: " #$ ")
|
|
15
|
+
@raw_output = output
|
|
16
|
+
@quiet = quiet
|
|
17
|
+
@line_prefix = line_prefix
|
|
18
|
+
@pending_threaded = nil
|
|
19
|
+
set_vars_from_output(output)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def nil?
|
|
23
|
+
@raw_output.nil?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def success?
|
|
27
|
+
@exitcode&.zero?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def pending_threaded?
|
|
31
|
+
!@pending_threaded.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def puts_combined
|
|
35
|
+
puts_stream(:stdout)
|
|
36
|
+
puts_stream(:stderr)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def puts_stream(stream_name)
|
|
40
|
+
return if @quiet || @raw_output.nil?
|
|
41
|
+
raise "Invalid stream: #{stream_name}" unless %i[stdout stderr].include?(stream_name)
|
|
42
|
+
|
|
43
|
+
stream = send(stream_name)
|
|
44
|
+
return if stream.nil? || stream.empty?
|
|
45
|
+
|
|
46
|
+
out_array = stream.split(%r{\n|\r\n}).map do |c|
|
|
47
|
+
chomped_c = c&.chomp
|
|
48
|
+
if chomped_c.nil? || chomped_c.strip.empty?
|
|
49
|
+
nil
|
|
50
|
+
else
|
|
51
|
+
format_output_string(chomped_c)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
out_array.compact!
|
|
55
|
+
return if out_array.empty?
|
|
56
|
+
|
|
57
|
+
logger.info "#{stream_name.to_s.upcase}:\n#{out_array.join("\n")}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def format_output_string(str)
|
|
63
|
+
# Cut the number of spaces in half, replace tabs with double spaces
|
|
64
|
+
str.gsub!(/\s\s/, ' ')
|
|
65
|
+
str.gsub!(/\t/, ' ')
|
|
66
|
+
return "#{@line_prefix}#{str}" if str.length <= 115
|
|
67
|
+
|
|
68
|
+
str.scan(/.{1,115}/).map { |s| "#{@line_prefix}#{s}" }.join("\n")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def set_vars_from_output(out)
|
|
72
|
+
if out.is_a?(Array)
|
|
73
|
+
if out[0] == :threaded
|
|
74
|
+
@stdout = ''
|
|
75
|
+
@stderr = ''
|
|
76
|
+
@exitcode = 0
|
|
77
|
+
# Set pending threaded to the command we are waiting on
|
|
78
|
+
@pending_threaded = out[1]
|
|
79
|
+
elsif out.length == 3
|
|
80
|
+
@stdout = out[0]
|
|
81
|
+
@stderr = out[1]
|
|
82
|
+
@exitcode = out[2].to_i
|
|
83
|
+
else
|
|
84
|
+
raise "Invalid output array: #{out}"
|
|
85
|
+
end
|
|
86
|
+
elsif out.is_a? WinRM::Output
|
|
87
|
+
@stdout = out.stdout
|
|
88
|
+
@stderr = out.stderr
|
|
89
|
+
@exitcode = out.exitcode
|
|
90
|
+
elsif out.is_a? StandardError
|
|
91
|
+
@stdout = out.message
|
|
92
|
+
@stderr = out.backtrace.join("\n")
|
|
93
|
+
@exitcode = 1
|
|
94
|
+
elsif out.respond_to? :to_s
|
|
95
|
+
@stdout = out.to_s
|
|
96
|
+
@stderr = ''
|
|
97
|
+
@exitcode = 0
|
|
98
|
+
else
|
|
99
|
+
raise "Invalid output type: #{out.class}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'winrm'
|
|
4
|
+
require 'winrm-fs'
|
|
5
|
+
require_relative 'base_exec'
|
|
6
|
+
|
|
7
|
+
module CemWinSpec
|
|
8
|
+
module WinExec
|
|
9
|
+
# Class for executing PowerShell commands over WinRM
|
|
10
|
+
class WinRMExec < BaseExec
|
|
11
|
+
attr_reader :conn_opts
|
|
12
|
+
|
|
13
|
+
def initialize(conn_opts, working_dir = nil, quiet: false)
|
|
14
|
+
@conn_opts = conn_opts
|
|
15
|
+
@quiet = quiet
|
|
16
|
+
@conn = new_conn(@conn_opts.to_h)
|
|
17
|
+
super(working_dir)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def available?
|
|
21
|
+
@available
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def exec(cmd, *_args, **_kwargs)
|
|
25
|
+
puts_cmd(cmd)
|
|
26
|
+
shell = nil
|
|
27
|
+
output = nil
|
|
28
|
+
begin
|
|
29
|
+
shell = conn.shell(:powershell)
|
|
30
|
+
output = shell.run(cd_working_dir(cmd))
|
|
31
|
+
rescue WinRM::WinRMAuthorizationError => e
|
|
32
|
+
@available = false
|
|
33
|
+
raise e
|
|
34
|
+
ensure
|
|
35
|
+
shell&.close
|
|
36
|
+
end
|
|
37
|
+
raise 'Something went wrong, no output from command' if output.nil?
|
|
38
|
+
|
|
39
|
+
output
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create_dir(path)
|
|
43
|
+
result = file_manager.create_dir(path)
|
|
44
|
+
raise "Failed to create directory #{path}" unless result
|
|
45
|
+
|
|
46
|
+
logger.info "Created directory #{path}"
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete_file(path)
|
|
51
|
+
result = file_manager.delete(path)
|
|
52
|
+
raise "Failed to delete file #{path}" unless result
|
|
53
|
+
|
|
54
|
+
logger.info "Deleted file #{path}"
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def file_exists?(path)
|
|
59
|
+
file_manager.exists?(path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def temp_dir
|
|
63
|
+
file_manager.temp_dir
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def upload(local_path, remote_path)
|
|
67
|
+
file_manager.upload(local_path, remote_path) do |bytes_copied, total_bytes, lpath, rpath|
|
|
68
|
+
logger.debug "Copied #{bytes_copied} of #{total_bytes} bytes from #{lpath} to #{rpath}"
|
|
69
|
+
end
|
|
70
|
+
logger.info "Uploaded #{local_path} to #{remote_path}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
attr_reader :conn
|
|
76
|
+
|
|
77
|
+
def new_conn(hash_opts)
|
|
78
|
+
logger.debug "Creating connection to #{hash_opts[:endpoint]}"
|
|
79
|
+
new_conn = WinRM::Connection.new(hash_opts)
|
|
80
|
+
new_conn.logger = logger
|
|
81
|
+
new_conn
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def file_manager
|
|
85
|
+
@file_manager ||= WinRM::FS::FileManager.new(conn)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative 'iap_tunnel'
|
|
5
|
+
require_relative 'logging'
|
|
6
|
+
require_relative 'module_archive_builder'
|
|
7
|
+
require_relative 'win_exec/connection_opts'
|
|
8
|
+
require_relative 'win_exec/local_exec'
|
|
9
|
+
require_relative 'win_exec/winrm_exec'
|
|
10
|
+
require_relative 'win_exec/output'
|
|
11
|
+
|
|
12
|
+
module CemWinSpec
|
|
13
|
+
module WinExec
|
|
14
|
+
# Class for executing PowerShell commands over WinRM
|
|
15
|
+
class Exec
|
|
16
|
+
include CemWinSpec::Logging
|
|
17
|
+
|
|
18
|
+
attr_reader :title, :result, :reuse_tunnel, :ignore_exitcode
|
|
19
|
+
|
|
20
|
+
def initialize(title, local_exec, winrm_exec, iap_tunnel, ma_builder, rspec_test_cmds, &block)
|
|
21
|
+
@title = title
|
|
22
|
+
@local_exec = local_exec
|
|
23
|
+
@winrm_exec = winrm_exec
|
|
24
|
+
@iap_tunnel = iap_tunnel
|
|
25
|
+
@ma_builder = ma_builder
|
|
26
|
+
@rspec_test_cmds = rspec_test_cmds
|
|
27
|
+
@block = block
|
|
28
|
+
@reuse_tunnel = true
|
|
29
|
+
@ignore_exitcode = false
|
|
30
|
+
@spinner = nil
|
|
31
|
+
@result = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reuse_tunnel=(value)
|
|
35
|
+
raise ArgumentError, 'reuse_tunnel must be a boolean' unless [true, false].include?(value)
|
|
36
|
+
|
|
37
|
+
@reuse_tunnel = value
|
|
38
|
+
end
|
|
39
|
+
alias reuse_tunnel? reuse_tunnel
|
|
40
|
+
|
|
41
|
+
def ignore_exitcode=(value)
|
|
42
|
+
raise ArgumentError, 'ignore_exitcode must be a boolean' unless [true, false].include?(value)
|
|
43
|
+
|
|
44
|
+
@ignore_exitcode = value
|
|
45
|
+
end
|
|
46
|
+
alias ignore_exitcode? ignore_exitcode
|
|
47
|
+
|
|
48
|
+
def success?
|
|
49
|
+
@result.success? if @result.is_a?(Output)
|
|
50
|
+
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def spinner=(value)
|
|
55
|
+
raise ArgumentError, 'spinner must implement #auto_spin and #stop methods' unless value.respond_to?(:auto_spin) && value.respond_to?(:stop)
|
|
56
|
+
|
|
57
|
+
@spinner = value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Proxy method calls for methods that don't exist here to various other objects.
|
|
61
|
+
# This allows doing things like: `win_exec.remote_exec('Get-Process')` and
|
|
62
|
+
# calling the `exec` method on the `winrm_exec` object. This is done by
|
|
63
|
+
# checking method name prefixes and calling the corresponding method on the
|
|
64
|
+
# appropriate object. The prefix is removed from the method name before
|
|
65
|
+
# calling the method on the object. The supported prefixes are:
|
|
66
|
+
# local_ - calls the method on the @local_exec object
|
|
67
|
+
# remote_ - calls the method on the @winrm_exec object
|
|
68
|
+
# rspec_ - calls the method on the @rspec_test_cmds object
|
|
69
|
+
# module_archive_ - calls the method on the @ma_builder object
|
|
70
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
71
|
+
if method.to_s.start_with?('local_') # proxy to local_exec
|
|
72
|
+
method = method.to_s.sub('local_', '').to_sym
|
|
73
|
+
@local_exec.send(method, *args, **kwargs, &block)
|
|
74
|
+
elsif method.to_s.start_with?('remote_') # proxy to winrm_exec
|
|
75
|
+
method = method.to_s.sub('remote_', '').to_sym
|
|
76
|
+
@winrm_exec.send(method, *args, **kwargs, &block)
|
|
77
|
+
elsif method.to_s.start_with?('rspec_') # proxy to rspec_test_cmds
|
|
78
|
+
method = method.to_s.sub('rspec_', '').to_sym
|
|
79
|
+
@rspec_test_cmds.send(method, *args, **kwargs, &block)
|
|
80
|
+
elsif method.to_s.start_with?('module_archive_') # proxy to ma_builder
|
|
81
|
+
method = method.to_s.sub('module_archive_', '').to_sym
|
|
82
|
+
@ma_builder.send(method, *args, **kwargs, &block)
|
|
83
|
+
else
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Proxy respond_to? for methods that don't exist here to various other objects.
|
|
89
|
+
# This allows doing things like: `win_exec.respond_to?(:remote_exec)` and
|
|
90
|
+
# checking if the `exec` method exists on the `winrm_exec` object. This is done by
|
|
91
|
+
# checking method name prefixes and calling the corresponding method on the
|
|
92
|
+
# appropriate object. The prefix is removed from the method name before
|
|
93
|
+
# calling the method on the object. The supported prefixes are:
|
|
94
|
+
# local_ - calls the method on the @local_exec object
|
|
95
|
+
# remote_ - calls the method on the @winrm_exec object
|
|
96
|
+
# rspec_ - calls the method on the @rspec_test_cmds object
|
|
97
|
+
# module_archive_ - calls the method on the @ma_builder object
|
|
98
|
+
def respond_to_missing?(method, include_private = false)
|
|
99
|
+
if method.to_s.start_with?('local_')
|
|
100
|
+
@local_exec.respond_to?(method.to_s.sub('local_', '').to_sym, include_private)
|
|
101
|
+
elsif method.to_s.start_with?('remote_')
|
|
102
|
+
@winrm_exec.respond_to?(method.to_s.sub('remote_', '').to_sym, include_private)
|
|
103
|
+
elsif method.to_s.start_with?('rspec_')
|
|
104
|
+
@rspec_test_cmds.respond_to?(method.to_s.sub('rspec_', '').to_sym, include_private)
|
|
105
|
+
elsif method.to_s.start_with?('module_archive_')
|
|
106
|
+
@ma_builder.respond_to?(method.to_s.sub('module_archive_', '').to_sym, include_private)
|
|
107
|
+
else
|
|
108
|
+
super
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def run(*args, **kwargs)
|
|
113
|
+
logger.info "Running #{@title}"
|
|
114
|
+
@spinner&.auto_spin
|
|
115
|
+
result = if reuse_tunnel?
|
|
116
|
+
@iap_tunnel.start # ensure tunnel is running
|
|
117
|
+
run_in_current_scope(*args, **kwargs)
|
|
118
|
+
else
|
|
119
|
+
@iap_tunnel.stop # ensure tunnel is stopped
|
|
120
|
+
@iap_tunnel.with do # start tunnel for this block
|
|
121
|
+
run_in_current_scope(*args, **kwargs)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
@spinner&.stop
|
|
125
|
+
@result = Output.new(result)
|
|
126
|
+
return if @result.pending_threaded?
|
|
127
|
+
|
|
128
|
+
@result.puts_combined
|
|
129
|
+
unless @result.success? || ignore_exitcode?
|
|
130
|
+
raise "Command failed with exit code #{@result.exitcode}: #{@result.stdout}\n#{@result.stderr}"
|
|
131
|
+
end
|
|
132
|
+
@result
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
logger.error "Error running #{@title}: #{e.message[0..100]}"
|
|
135
|
+
@result = Output.new(e)
|
|
136
|
+
raise e unless ignore_exitcode?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def run_in_current_scope(*args, **kwargs)
|
|
142
|
+
instance_exec(*args, **kwargs, &@block)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def wrap_spinner
|
|
146
|
+
@spinner.auto_spin if @spinner
|
|
147
|
+
yield if block_given?
|
|
148
|
+
ensure
|
|
149
|
+
@spinner.stop if @spinner
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Factory class for creating a WinExec object
|
|
154
|
+
class Factory
|
|
155
|
+
include CemWinSpec::Logging
|
|
156
|
+
|
|
157
|
+
attr_reader :current_local_exec, :current_winrm_exec, :current_conn_opts
|
|
158
|
+
|
|
159
|
+
def initialize(iap_tunnel, ma_builder, rspec_test_cmds)
|
|
160
|
+
@iap_tunnel = iap_tunnel
|
|
161
|
+
@ma_builder = ma_builder
|
|
162
|
+
@rspec_test_cmds = rspec_test_cmds
|
|
163
|
+
@current_local_exec = LocalExec.new
|
|
164
|
+
@current_winrm_exec = nil
|
|
165
|
+
@current_conn_opts = nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Build a WinExec object
|
|
169
|
+
# @param title [String] Title of the WinExec object
|
|
170
|
+
# @param merge [Boolean] Merge the current connection options with the new options, if applicable
|
|
171
|
+
# @param host [String] Hostname or IP address of the remote host
|
|
172
|
+
# @param port [Integer] Port of the remote host
|
|
173
|
+
# @param user [String] Username for the remote host
|
|
174
|
+
# @param pass [String] Password for the remote host
|
|
175
|
+
# @param opts [Hash] Additional options for the WinRM connection
|
|
176
|
+
# @return [Exec] An Exec object
|
|
177
|
+
def build(title, merge: true, user: nil, pass: nil, working_dir: nil, **opts, &block)
|
|
178
|
+
logger.debug "Building Wexec object for #{title}"
|
|
179
|
+
build_conn_opts(merge: merge, user: user, pass: pass, **opts)
|
|
180
|
+
logger.debug 'Created ConnectionOpts'
|
|
181
|
+
wexec = Exec.new(title, @current_local_exec, get_winrm_exec(working_dir), @iap_tunnel, @ma_builder, @rspec_test_cmds, &block)
|
|
182
|
+
wexec.reuse_tunnel = opts[:reuse_tunnel] if opts.key?(:reuse_tunnel)
|
|
183
|
+
wexec.ignore_exitcode = opts[:ignore_exitcode] if opts.key?(:ignore_exitcode)
|
|
184
|
+
wexec.spinner = opts[:spinner] if opts.key?(:spinner)
|
|
185
|
+
logger.debug 'Created Wexec'
|
|
186
|
+
wexec
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def local_threaded_results
|
|
190
|
+
logger.info 'Checking for deferred results...'
|
|
191
|
+
@current_local_exec.join_threads
|
|
192
|
+
@current_local_exec.threaded_results.each do |cmd, results|
|
|
193
|
+
logger.info "Deferred results for #{cmd}:"
|
|
194
|
+
Output.new(results).puts_combined
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
def build_conn_opts(merge: true, user: nil, pass: nil, **opts)
|
|
201
|
+
if @current_conn_opts.nil?
|
|
202
|
+
logger.debug 'Creating new ConnectionOpts object'
|
|
203
|
+
@current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
|
204
|
+
return @current_conn_opts
|
|
205
|
+
end
|
|
206
|
+
opts_digest = Digest::SHA256.hexdigest(['localhost', @iap_tunnel.port, user, pass, opts].join(':'))
|
|
207
|
+
if opts_digest != @current_conn_opts.digest
|
|
208
|
+
if merge
|
|
209
|
+
logger.debug 'Merging ConnectionOpts with new options'
|
|
210
|
+
@current_conn_opts = @current_conn_opts.merge(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
|
211
|
+
else
|
|
212
|
+
logger.debug 'Creating new ConnectionOpts object with new options'
|
|
213
|
+
@current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
logger.debug 'Returning existing ConnectionOpts object'
|
|
217
|
+
end
|
|
218
|
+
@current_conn_opts
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_winrm_exec(working_dir = nil)
|
|
223
|
+
if @current_winrm_exec.nil? || @current_winrm_exec.conn_opts.digest != @current_conn_opts.digest
|
|
224
|
+
logger.debug 'Creating new WinRMExec object'
|
|
225
|
+
@current_winrm_exec = WinRMExec.new(@current_conn_opts)
|
|
226
|
+
end
|
|
227
|
+
logger.debug "Setting working directory to #{working_dir}" unless working_dir.nil?
|
|
228
|
+
@current_winrm_exec.working_dir = working_dir
|
|
229
|
+
logger.debug 'Returning WinRMExec object'
|
|
230
|
+
@current_winrm_exec
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
data/lib/cem_win_spec.rb
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tty-spinner'
|
|
4
|
+
require_relative 'cem_win_spec/logging'
|
|
5
|
+
require_relative 'cem_win_spec/test_runner'
|
|
6
|
+
|
|
7
|
+
module CemWinSpec
|
|
8
|
+
class << self
|
|
9
|
+
include CemWinSpec::Logging
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.signal_handler(runner)
|
|
13
|
+
Signal.trap('INT') do
|
|
14
|
+
puts 'Caught interrupt, killing tunnel and exiting'
|
|
15
|
+
runner.iap_tunnel.stop(wait: false, log: false)
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
Signal.trap('TERM') do
|
|
19
|
+
puts 'Caught interrupt, killing tunnel and exiting'
|
|
20
|
+
runner.iap_tunnel.stop(wait: false, log: false)
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Runs the tests
|
|
26
|
+
# @param options [Hash] Options for the test runner
|
|
27
|
+
# @option options [Boolean] :quiet (false) Whether to suppress output
|
|
28
|
+
# @option options [Boolean] :debug (false) Whether to enable debug output
|
|
29
|
+
# @option options [Boolean] :verbose (false) Whether to enable verbose output
|
|
30
|
+
# @option options [String] :log_level (nil) Explicitly set the log level
|
|
31
|
+
# @option options [String] :log_file (nil) Log output to file
|
|
32
|
+
# @option options [String] :log_format (nil) Set log format(text, json, github_action)
|
|
33
|
+
def self.run_tests(options = {})
|
|
34
|
+
raise 'Must be ran from the root of the project' unless File.exist?('Gemfile')
|
|
35
|
+
|
|
36
|
+
log_setup!(options)
|
|
37
|
+
logger.info 'Running tests'
|
|
38
|
+
logger.debug "Options: #{options}"
|
|
39
|
+
runner = TestRunner.new
|
|
40
|
+
logger.debug "Created TestRunner: #{runner}"
|
|
41
|
+
signal_handler(runner)
|
|
42
|
+
logger.debug 'Set up signal handler'
|
|
43
|
+
exitcode = 99
|
|
44
|
+
spinner = TTY::Spinner.new(format: :classic, interval: 1, hide_cursor: true, clear: true)
|
|
45
|
+
begin
|
|
46
|
+
runner.enable_long_paths.run
|
|
47
|
+
runner.enable_symlinks.run
|
|
48
|
+
working_dir_out = runner.create_working_dir.run
|
|
49
|
+
working_dir = working_dir_out&.stdout.chomp
|
|
50
|
+
logger.debug "Working dir: #{working_dir}"
|
|
51
|
+
module_dir = runner.upload_module(working_dir: working_dir).run&.stdout.chomp
|
|
52
|
+
logger.debug "Module dir: #{module_dir}"
|
|
53
|
+
runner.setup_ruby(operation_timeout: 300,
|
|
54
|
+
receive_timeout: 310,
|
|
55
|
+
working_dir: module_dir,
|
|
56
|
+
reuse_tunnel: false,
|
|
57
|
+
spinner: spinner).run
|
|
58
|
+
runner.rspec_prep(working_dir: module_dir, reuse_tunnel: false, spinner: spinner).run
|
|
59
|
+
spec_out = runner.rspec_tests_parallel(operation_timeout: 300,
|
|
60
|
+
receive_timeout: 310,
|
|
61
|
+
working_dir: module_dir,
|
|
62
|
+
ignore_exitcode: true,
|
|
63
|
+
reuse_tunnel: false,
|
|
64
|
+
spinner: spinner).run
|
|
65
|
+
# We currently don't have any local commands being run
|
|
66
|
+
# but we may in the future. If we do, we'll need to
|
|
67
|
+
# call local_threaded_results on the TestRunner
|
|
68
|
+
# runner.local_threaded_results
|
|
69
|
+
exitcode = spec_out&.exitcode
|
|
70
|
+
logger.info "Completed tests with exit code: #{exitcode}"
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
logger.fatal "Error: #{e.message}"
|
|
73
|
+
logger.debug e.backtrace.join("\n")
|
|
74
|
+
exitcode = 1
|
|
75
|
+
ensure
|
|
76
|
+
exit exitcode
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|