cem_win_spec 0.1.2 → 0.1.4
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 +4 -4
- data/Gemfile.lock +11 -12
- data/cem_win_spec.gemspec +0 -1
- data/exe/cem-win-spec +20 -0
- data/lib/cem_win_spec/iap_tunnel.rb +11 -2
- data/lib/cem_win_spec/rspec_test_cmds.rb +12 -7
- data/lib/cem_win_spec/test_runner.rb +15 -10
- data/lib/cem_win_spec/version.rb +1 -1
- data/lib/cem_win_spec/win_exec/cmd/base_cmd.rb +96 -0
- data/lib/cem_win_spec/win_exec/{local_exec.rb → cmd/local_cmd.rb} +13 -13
- data/lib/cem_win_spec/win_exec/{winrm_exec.rb → cmd/winrm_cmd.rb} +21 -8
- data/lib/cem_win_spec/win_exec/connection_opts.rb +5 -5
- data/lib/cem_win_spec/win_exec/exec.rb +196 -0
- data/lib/cem_win_spec/win_exec/factory.rb +123 -0
- data/lib/cem_win_spec/win_exec.rb +2 -228
- data/lib/cem_win_spec.rb +12 -15
- metadata +8 -21
- data/lib/cem_win_spec/remote_command.rb +0 -47
- data/lib/cem_win_spec/win_exec/base_exec.rb +0 -42
@@ -0,0 +1,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../logging'
|
4
|
+
require_relative 'output'
|
5
|
+
|
6
|
+
module CemWinSpec
|
7
|
+
module WinExec
|
8
|
+
# Class for executing PowerShell commands over WinRM
|
9
|
+
class Exec
|
10
|
+
include CemWinSpec::Logging
|
11
|
+
|
12
|
+
HALTING_ERRORS = [
|
13
|
+
IapTunnelStartError,
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
attr_reader :title, :result, :reuse_tunnel, :ignore_exitcode
|
17
|
+
|
18
|
+
def initialize(title: 'Command', l_cmd: nil, r_cmd: nil, iap_tunnel: nil, ma_builder: nil, r_test_cmds: nil, &block)
|
19
|
+
add_title(title)
|
20
|
+
add_local_cmd(l_cmd) unless l_cmd.nil?
|
21
|
+
add_remote_cmd(r_cmd) unless r_cmd.nil?
|
22
|
+
add_iap_tunnel(iap_tunnel) unless iap_tunnel.nil?
|
23
|
+
add_ma_builder(ma_builder) unless ma_builder.nil?
|
24
|
+
add_rspec_test_cmds(r_test_cmds) unless r_test_cmds.nil?
|
25
|
+
add_command_block(&block) unless block.nil?
|
26
|
+
@reuse_tunnel = true
|
27
|
+
@ignore_exitcode = false
|
28
|
+
@result = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_title(value)
|
32
|
+
raise ArgumentError, 'title must be a string' unless value.is_a?(String)
|
33
|
+
|
34
|
+
@title = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_local_cmd(value)
|
38
|
+
raise ArgumentError, 'local_exec must implement the #run method' unless value.respond_to?(:run)
|
39
|
+
|
40
|
+
@local_cmd = value
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_remote_cmd(value)
|
44
|
+
raise ArgumentError, 'winrm_exec must implement the #run method' unless value.respond_to?(:run)
|
45
|
+
|
46
|
+
@remote_cmd = value
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_iap_tunnel(value)
|
50
|
+
unless value.respond_to?(:start) && value.respond_to?(:stop) && value.respond_to?(:with)
|
51
|
+
raise ArgumentError, 'iap_tunnel must implement the #start, #stop, and #with methods'
|
52
|
+
end
|
53
|
+
|
54
|
+
@iap_tunnel = value
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_ma_builder(value)
|
58
|
+
raise ArgumentError, 'ma_builder must implement the #build method' unless value.respond_to?(:build)
|
59
|
+
|
60
|
+
@ma_builder = value
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_rspec_test_cmds(value)
|
64
|
+
unless value.respond_to?(:cmd_standalone) && value.respond_to?(:cmd_parallel) &&
|
65
|
+
value.respond_to?(:prep_cmd) && value.respond_to?(:cleanup_cmd)
|
66
|
+
raise ArgumentError, 'rspec_test_cmds must implement the #cmd_standalone, #cmd_parallel, #prep_cmd, and #cleanup_cmd methods'
|
67
|
+
end
|
68
|
+
|
69
|
+
@rspec_test_cmds = value
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_command_block(&block)
|
73
|
+
raise ArgumentError, 'block must be a Proc' unless block.is_a?(Proc)
|
74
|
+
|
75
|
+
@block = block
|
76
|
+
end
|
77
|
+
|
78
|
+
def reuse_tunnel=(value)
|
79
|
+
raise ArgumentError, 'reuse_tunnel must be a boolean' unless [true, false].include?(value)
|
80
|
+
|
81
|
+
@reuse_tunnel = value
|
82
|
+
end
|
83
|
+
alias reuse_tunnel? reuse_tunnel
|
84
|
+
|
85
|
+
def ignore_exitcode=(value)
|
86
|
+
raise ArgumentError, 'ignore_exitcode must be a boolean' unless [true, false].include?(value)
|
87
|
+
|
88
|
+
@ignore_exitcode = value
|
89
|
+
end
|
90
|
+
alias ignore_exitcode? ignore_exitcode
|
91
|
+
|
92
|
+
def success?
|
93
|
+
@result.success? if @result.is_a?(Output)
|
94
|
+
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
# Proxy method calls for methods that don't exist here to various other objects.
|
99
|
+
# This allows doing things like: `win_exec.remote_exec('Get-Process')` and
|
100
|
+
# calling the `exec` method on the `winrm_exec` object. This is done by
|
101
|
+
# checking method name prefixes and calling the corresponding method on the
|
102
|
+
# appropriate object. The prefix is removed from the method name before
|
103
|
+
# calling the method on the object. The supported prefixes are:
|
104
|
+
# local_ - calls the method on the @local_cmd object
|
105
|
+
# remote_ - calls the method on the @remote_cmd object
|
106
|
+
# rspec_ - calls the method on the @rspec_test_cmds object
|
107
|
+
# module_archive_ - calls the method on the @ma_builder object
|
108
|
+
def method_missing(method, *args, **kwargs, &block)
|
109
|
+
if method.to_s.start_with?('local_') # proxy to local_exec
|
110
|
+
method = method.to_s.sub('local_', '').to_sym
|
111
|
+
@local_cmd.send(method, *args, **kwargs, &block)
|
112
|
+
elsif method.to_s.start_with?('remote_') # proxy to remote_exec
|
113
|
+
method = method.to_s.sub('remote_', '').to_sym
|
114
|
+
@remote_cmd.send(method, *args, **kwargs, &block)
|
115
|
+
elsif method.to_s.start_with?('rspec_') # proxy to rspec_test_cmds
|
116
|
+
method = method.to_s.sub('rspec_', '').to_sym
|
117
|
+
@rspec_test_cmds.send(method, *args, **kwargs, &block)
|
118
|
+
elsif method.to_s.start_with?('module_archive_') # proxy to ma_builder
|
119
|
+
method = method.to_s.sub('module_archive_', '').to_sym
|
120
|
+
@ma_builder.send(method, *args, **kwargs, &block)
|
121
|
+
else
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Proxy respond_to? for methods that don't exist here to various other objects.
|
127
|
+
# This allows doing things like: `win_exec.respond_to?(:remote_exec)` and
|
128
|
+
# checking if the `exec` method exists on the `winrm_exec` object. This is done by
|
129
|
+
# checking method name prefixes and calling the corresponding method on the
|
130
|
+
# appropriate object. The prefix is removed from the method name before
|
131
|
+
# calling the method on the object. The supported prefixes are:
|
132
|
+
# local_ - calls the method on the @local_cmd object
|
133
|
+
# remote_ - calls the method on the @remote_cmd object
|
134
|
+
# rspec_ - calls the method on the @rspec_test_cmds object
|
135
|
+
# module_archive_ - calls the method on the @ma_builder object
|
136
|
+
def respond_to_missing?(method, include_private = false)
|
137
|
+
if method.to_s.start_with?('local_')
|
138
|
+
@local_cmd.respond_to?(method.to_s.sub('local_', '').to_sym, include_private)
|
139
|
+
elsif method.to_s.start_with?('remote_')
|
140
|
+
@remote_cmd.respond_to?(method.to_s.sub('remote_', '').to_sym, include_private)
|
141
|
+
elsif method.to_s.start_with?('rspec_')
|
142
|
+
@rspec_test_cmds.respond_to?(method.to_s.sub('rspec_', '').to_sym, include_private)
|
143
|
+
elsif method.to_s.start_with?('module_archive_')
|
144
|
+
@ma_builder.respond_to?(method.to_s.sub('module_archive_', '').to_sym, include_private)
|
145
|
+
else
|
146
|
+
super
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def run(*args, **kwargs)
|
151
|
+
validate_instance_variables
|
152
|
+
logger.info "Running #{@title}"
|
153
|
+
result = run_with_tunnel { run_in_current_scope(*args, **kwargs) }
|
154
|
+
@result = Output.new(result)
|
155
|
+
return if @result.pending_threaded?
|
156
|
+
|
157
|
+
unless @result.success? || ignore_exitcode?
|
158
|
+
raise "Command failed with exit code #{@result.exitcode}: #{@result.stdout}\n#{@result.stderr}"
|
159
|
+
end
|
160
|
+
@result
|
161
|
+
rescue StandardError => e
|
162
|
+
raise if HALTING_ERRORS.include?(e.class)
|
163
|
+
|
164
|
+
logger.error "Error running #{@title}: #{e.message[0..100]}"
|
165
|
+
@result = Output.new(e)
|
166
|
+
@result
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def run_with_tunnel(&block)
|
172
|
+
if reuse_tunnel?
|
173
|
+
@iap_tunnel.start # ensure tunnel is running
|
174
|
+
block.call
|
175
|
+
else
|
176
|
+
@iap_tunnel.stop # ensure tunnel is stopped
|
177
|
+
@iap_tunnel.with do # start tunnel for this block
|
178
|
+
block.call
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def validate_instance_variables
|
184
|
+
%i[@title @local_cmd @remote_cmd @iap_tunnel @ma_builder @rspec_test_cmds @block].each do |var|
|
185
|
+
raise ArgumentError, "#{var} must be set" if instance_variable_get(var).nil?
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Runs the block in the current scope. This allows the block to access
|
190
|
+
# instance variables and methods from the current scope.
|
191
|
+
def run_in_current_scope(*args, **kwargs)
|
192
|
+
instance_exec(*args, **kwargs, &@block) # self is the "instance"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require_relative '../logging'
|
5
|
+
require_relative 'connection_opts'
|
6
|
+
require_relative 'cmd/local_cmd'
|
7
|
+
require_relative 'cmd/winrm_cmd'
|
8
|
+
|
9
|
+
module CemWinSpec
|
10
|
+
module WinExec
|
11
|
+
# Factory class for creating a WinExec object
|
12
|
+
class Factory
|
13
|
+
include CemWinSpec::Logging
|
14
|
+
|
15
|
+
attr_reader :current_local_exec, :current_remote_cmd, :current_conn_opts
|
16
|
+
|
17
|
+
def initialize(iap_tunnel, ma_builder, rspec_test_cmds, **opts)
|
18
|
+
@iap_tunnel = iap_tunnel
|
19
|
+
@ma_builder = ma_builder
|
20
|
+
@rspec_test_cmds = rspec_test_cmds
|
21
|
+
@current_local_cmd = LocalCmd.new(**opts)
|
22
|
+
@current_remote_cmd = nil
|
23
|
+
@current_conn_opts = nil
|
24
|
+
@init_opts = opts
|
25
|
+
end
|
26
|
+
|
27
|
+
# Build a WinExec object
|
28
|
+
# @param title [String] Title of the WinExec object
|
29
|
+
# @param merge [Boolean] Merge the current connection options with the new options, if applicable
|
30
|
+
# @param host [String] Hostname or IP address of the remote host
|
31
|
+
# @param port [Integer] Port of the remote host
|
32
|
+
# @param user [String] Username for the remote host
|
33
|
+
# @param pass [String] Password for the remote host
|
34
|
+
# @param opts [Hash] Additional options for the WinRM connection
|
35
|
+
# @return [Exec] An Exec object
|
36
|
+
def build(title, merge: true, user: nil, pass: nil, working_dir: nil, **opts, &block)
|
37
|
+
logger.debug "Building Wexec object for #{title}"
|
38
|
+
build_conn_opts(merge: merge, user: user, pass: pass, **opts)
|
39
|
+
logger.debug 'Created ConnectionOpts'
|
40
|
+
wexec = Exec.new
|
41
|
+
wexec.add_title title
|
42
|
+
wexec.add_local_cmd @current_local_cmd
|
43
|
+
wexec.add_remote_cmd get_remote_cmd(working_dir, **@init_opts.merge(opts))
|
44
|
+
wexec.add_iap_tunnel @iap_tunnel
|
45
|
+
wexec.add_ma_builder @ma_builder
|
46
|
+
wexec.add_rspec_test_cmds @rspec_test_cmds
|
47
|
+
wexec.add_command_block(&block)
|
48
|
+
wexec.reuse_tunnel = opts[:reuse_tunnel] if opts.key?(:reuse_tunnel)
|
49
|
+
wexec.ignore_exitcode = opts[:ignore_exitcode] if opts.key?(:ignore_exitcode)
|
50
|
+
logger.debug 'Created Wexec'
|
51
|
+
wexec
|
52
|
+
end
|
53
|
+
|
54
|
+
def local_threaded_results
|
55
|
+
logger.info 'Checking for deferred results...'
|
56
|
+
@current_local_exec.join_threads
|
57
|
+
@current_local_exec.threaded_results.each do |cmd, results|
|
58
|
+
logger.info "Deferred results for #{cmd}:"
|
59
|
+
Output.new(results).puts_combined
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def build_conn_opts(merge: true, user: nil, pass: nil, **opts)
|
66
|
+
if @current_conn_opts.nil?
|
67
|
+
logger.debug 'Creating new ConnectionOpts object'
|
68
|
+
@current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
69
|
+
return @current_conn_opts
|
70
|
+
end
|
71
|
+
|
72
|
+
if need_new_conn_opts?(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
73
|
+
if merge
|
74
|
+
logger.debug 'Merging ConnectionOpts with new options'
|
75
|
+
@current_conn_opts = @current_conn_opts.merge(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
76
|
+
else
|
77
|
+
logger.debug 'Creating new ConnectionOpts object with new options'
|
78
|
+
@current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
logger.debug 'Returning existing ConnectionOpts object'
|
82
|
+
end
|
83
|
+
@current_conn_opts
|
84
|
+
end
|
85
|
+
|
86
|
+
def need_new_conn_opts?(new_host: nil, new_port: nil, user: nil, pass: nil, **opts)
|
87
|
+
if new_host && new_host != @current_conn_opts.host
|
88
|
+
logger.debug "New host #{new_host} does not match current host #{@current_conn_opts.host}"
|
89
|
+
return true
|
90
|
+
end
|
91
|
+
if new_port && new_port != @current_conn_opts.port
|
92
|
+
logger.debug "New port #{new_port} does not match current port #{@current_conn_opts.port}"
|
93
|
+
return true
|
94
|
+
end
|
95
|
+
if user && user != @current_conn_opts.user
|
96
|
+
logger.debug "New user #{user} does not match current user #{@current_conn_opts.user}"
|
97
|
+
return true
|
98
|
+
end
|
99
|
+
if pass && Digest::SHA256.hexdigest(pass) != @current_conn_opts.pass
|
100
|
+
logger.debug 'New password does not match current password'
|
101
|
+
return true
|
102
|
+
end
|
103
|
+
if !opts.empty? && opts != @current_conn_opts.opts
|
104
|
+
logger.debug "New extra options #{opts} do not match current extra options #{@current_conn_opts.opts}"
|
105
|
+
return true
|
106
|
+
end
|
107
|
+
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_remote_cmd(working_dir = nil, **opts)
|
112
|
+
if @current_winrm_cmd.nil? || @current_winrm_cmd.conn_opts.digest != @current_conn_opts.digest
|
113
|
+
logger.debug 'Creating new WinRMExec object'
|
114
|
+
@current_remote_cmd = WinRMCmd.new(@current_conn_opts, **opts)
|
115
|
+
end
|
116
|
+
logger.debug "Setting working directory to #{working_dir}" unless working_dir.nil?
|
117
|
+
@current_remote_cmd.working_dir = working_dir
|
118
|
+
logger.debug 'Returning WinRMExec object'
|
119
|
+
@current_remote_cmd
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -1,234 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
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
3
|
module CemWinSpec
|
13
4
|
module WinExec
|
14
|
-
|
15
|
-
|
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
|
-
@result
|
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
|
5
|
+
require_relative 'win_exec/exec'
|
6
|
+
require_relative 'win_exec/factory'
|
233
7
|
end
|
234
8
|
end
|
data/lib/cem_win_spec.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'tty-spinner'
|
4
3
|
require_relative 'cem_win_spec/logging'
|
5
4
|
require_relative 'cem_win_spec/test_runner'
|
6
5
|
|
@@ -39,20 +38,19 @@ module CemWinSpec
|
|
39
38
|
logger.debug "Created TestRunner: #{runner}"
|
40
39
|
signal_handler(runner)
|
41
40
|
logger.debug 'Set up signal handler'
|
42
|
-
spinner = TTY::Spinner.new(format: :classic, interval: 1, hide_cursor: true, clear: true)
|
43
41
|
begin
|
44
|
-
sre_out = setup_remote_environment(runner
|
42
|
+
sre_out = setup_remote_environment(runner)
|
45
43
|
check_output!(sre_out, runner)
|
46
44
|
module_dir = sre_out.stdout.chomp
|
47
45
|
logger.debug "Module dir: #{module_dir}"
|
48
|
-
srr_out = setup_remote_ruby(runner, module_dir,
|
46
|
+
srr_out = setup_remote_ruby(runner, module_dir, **options)
|
49
47
|
check_output!(srr_out, runner)
|
50
48
|
case operation
|
51
49
|
when :spec
|
52
|
-
spec_out = run_spec(runner, module_dir,
|
50
|
+
spec_out = run_spec(runner, module_dir, **options)
|
53
51
|
check_output!(spec_out, runner)
|
54
52
|
when :clean_fixture_cache
|
55
|
-
clean_fixture_cache_out = clean_fixture_cache(runner, module_dir,
|
53
|
+
clean_fixture_cache_out = clean_fixture_cache(runner, module_dir, **options)
|
56
54
|
check_output!(clean_fixture_cache_out, runner)
|
57
55
|
else
|
58
56
|
raise ArgumentError, "Unknown operation: #{operation}"
|
@@ -64,39 +62,38 @@ module CemWinSpec
|
|
64
62
|
end
|
65
63
|
end
|
66
64
|
|
67
|
-
def self.setup_remote_environment(runner
|
65
|
+
def self.setup_remote_environment(runner)
|
68
66
|
runner.enable_long_paths.run
|
69
67
|
runner.enable_symlinks.run
|
70
68
|
runner.enable_symlinks.run
|
71
69
|
working_dir_out = runner.create_working_dir.run
|
72
70
|
working_dir = working_dir_out.stdout.chomp
|
73
71
|
logger.debug "Working dir: #{working_dir}"
|
74
|
-
runner.upload_module(working_dir: working_dir).run
|
72
|
+
runner.upload_module(operation_timeout: 300, receive_timeout: 310, working_dir: working_dir).run
|
75
73
|
end
|
76
74
|
|
77
|
-
def self.setup_remote_ruby(runner, module_dir,
|
75
|
+
def self.setup_remote_ruby(runner, module_dir, **opts)
|
78
76
|
runner.setup_ruby(operation_timeout: 300,
|
79
77
|
receive_timeout: 310,
|
80
78
|
working_dir: module_dir,
|
81
79
|
reuse_tunnel: false,
|
82
|
-
|
80
|
+
**opts).run
|
83
81
|
end
|
84
82
|
|
85
83
|
# Runs RSpec tests
|
86
|
-
def self.run_spec(runner, module_dir,
|
87
|
-
#runner.rspec_prep(working_dir: module_dir, reuse_tunnel: false, spinner: spinner).run
|
84
|
+
def self.run_spec(runner, module_dir, **opts)
|
88
85
|
runner.rspec_tests_parallel(operation_timeout: 300,
|
89
86
|
receive_timeout: 310,
|
90
87
|
working_dir: module_dir,
|
91
88
|
ignore_exitcode: true,
|
92
89
|
reuse_tunnel: false,
|
93
|
-
|
90
|
+
**opts).run
|
94
91
|
end
|
95
92
|
|
96
93
|
# Clean the remote fixture cache
|
97
94
|
# @param options [Hash] Options for the test runner
|
98
|
-
def self.clean_fixture_cache(runner, module_dir,
|
99
|
-
runner.clean_fixture_cache(working_dir: module_dir,
|
95
|
+
def self.clean_fixture_cache(runner, module_dir, **opts)
|
96
|
+
runner.clean_fixture_cache(working_dir: module_dir, **opts).run
|
100
97
|
end
|
101
98
|
|
102
99
|
def self.check_output!(output, runner = nil)
|