cem_win_spec 0.1.1 → 0.1.3
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 +10 -8
- data/exe/cem-win-spec +21 -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 +9 -9
- 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} +17 -8
- data/lib/cem_win_spec/win_exec/connection_opts.rb +5 -5
- data/lib/cem_win_spec/win_exec/exec.rb +213 -0
- data/lib/cem_win_spec/win_exec/factory.rb +124 -0
- data/lib/cem_win_spec/win_exec.rb +2 -228
- data/lib/cem_win_spec.rb +12 -10
- metadata +8 -7
- data/lib/cem_win_spec/remote_command.rb +0 -47
- data/lib/cem_win_spec/win_exec/base_exec.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c59006dbfceccc0e0581630b0091722aa31a5644440bb906572c8429a654c37a
|
4
|
+
data.tar.gz: 91d3e8e17c961d90e90dd5895376418279062ace8ee3b3327d24a511b7287a98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cddc71efc46c8a5a0872ee5670b6bdcff1b77904190e18e87f3fa728f2bdb8c533bef7382e8df918337b87811fc21ac04a4b0bd0f358774911a78279f9709968
|
7
|
+
data.tar.gz: d52050dd876973fadb74a801b198e88f56341ee125a96a6407594376d6155a3a02a7037576ec10f775346660a7d5fd34ee75e26966ef7011889c45ad968a9e02
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cem_win_spec (0.1.
|
4
|
+
cem_win_spec (0.1.3)
|
5
5
|
parallel_tests (~> 3.4)
|
6
6
|
puppet_forge (~> 4.1)
|
7
7
|
tty-spinner (~> 0.9)
|
@@ -15,21 +15,22 @@ GEM
|
|
15
15
|
builder (3.2.4)
|
16
16
|
coderay (1.1.3)
|
17
17
|
diff-lcs (1.5.0)
|
18
|
-
erubi (1.
|
19
|
-
faraday (2.
|
20
|
-
faraday-net_http (
|
18
|
+
erubi (1.12.0)
|
19
|
+
faraday (2.7.5)
|
20
|
+
faraday-net_http (>= 2.0, < 3.1)
|
21
21
|
ruby2_keywords (>= 0.0.4)
|
22
22
|
faraday-follow_redirects (0.3.0)
|
23
23
|
faraday (>= 1, < 3)
|
24
|
-
faraday-net_http (
|
24
|
+
faraday-net_http (3.0.2)
|
25
25
|
ffi (1.15.5)
|
26
26
|
gssapi (1.3.1)
|
27
27
|
ffi (>= 1.0.1)
|
28
|
-
gyoku (1.
|
28
|
+
gyoku (1.4.0)
|
29
29
|
builder (>= 2.1.2)
|
30
|
+
rexml (~> 3.0)
|
30
31
|
httpclient (2.8.3)
|
31
32
|
little-plugger (1.1.4)
|
32
|
-
logging (2.3.
|
33
|
+
logging (2.3.1)
|
33
34
|
little-plugger (~> 1.1)
|
34
35
|
multi_json (~> 1.14)
|
35
36
|
method_source (1.0.0)
|
@@ -81,7 +82,7 @@ GEM
|
|
81
82
|
ruby2_keywords (0.0.5)
|
82
83
|
rubyntlm (0.6.3)
|
83
84
|
rubyzip (2.3.2)
|
84
|
-
semantic_puppet (1.0
|
85
|
+
semantic_puppet (1.1.0)
|
85
86
|
tty-cursor (0.7.1)
|
86
87
|
tty-spinner (0.9.3)
|
87
88
|
tty-cursor (~> 0.7)
|
@@ -103,6 +104,7 @@ GEM
|
|
103
104
|
|
104
105
|
PLATFORMS
|
105
106
|
x86_64-darwin-19
|
107
|
+
x86_64-darwin-20
|
106
108
|
|
107
109
|
DEPENDENCIES
|
108
110
|
cem_win_spec!
|
data/exe/cem-win-spec
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require 'optparse'
|
5
5
|
require 'cem_win_spec'
|
6
|
+
require 'cem_win_spec/version'
|
6
7
|
|
7
8
|
# This is a wrapper script for the cem_win_spec gem. It is used to run the
|
8
9
|
# cem_win_spec gem from the command line. It is installed as part of the gem
|
@@ -18,6 +19,26 @@ parser = OptionParser.new do |opts|
|
|
18
19
|
exit 0
|
19
20
|
end
|
20
21
|
|
22
|
+
opts.on('-p', '--puppet-version [VERSION]', 'Puppet version to test against') do |version|
|
23
|
+
unless %w[7 8].include?(version)
|
24
|
+
warn "Unknown Puppet version: #{version}"
|
25
|
+
warn 'Valid versions are: 7, 8'
|
26
|
+
puts opts
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
options[:puppet_version] = version
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on('-r', '--ruby-version [VERSION]', 'Ruby version to test against') do |version|
|
33
|
+
unless %w[2 3].include?(version)
|
34
|
+
warn "Unknown Ruby version: #{version}"
|
35
|
+
warn 'Valid versions are: 2, 3'
|
36
|
+
puts opts
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
options[:ruby_version] = version
|
40
|
+
end
|
41
|
+
|
21
42
|
opts.on('-o', '--operation [OPERATION]', 'Operation to perform (spec, clean_fixture_cache)') do |operation|
|
22
43
|
unless %w[spec clean_fixture_cache].include?(operation)
|
23
44
|
warn "Unknown operation: #{operation}"
|
@@ -4,6 +4,8 @@ require 'io/console'
|
|
4
4
|
require_relative 'logging'
|
5
5
|
|
6
6
|
module CemWinSpec
|
7
|
+
class IapTunnelStartError < StandardError; end
|
8
|
+
|
7
9
|
# This class is used to create a tunnel to a GCP instance
|
8
10
|
class IapTunnel
|
9
11
|
include CemWinSpec::Logging
|
@@ -23,7 +25,9 @@ module CemWinSpec
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def running?
|
26
|
-
!@pid.nil?
|
28
|
+
!@pid.nil? && Process.getpgid(@pid)
|
29
|
+
rescue Errno::ESRCH
|
30
|
+
false
|
27
31
|
end
|
28
32
|
|
29
33
|
def with
|
@@ -41,7 +45,12 @@ module CemWinSpec
|
|
41
45
|
logger.info 'Starting IAP tunnel...'
|
42
46
|
logger.debug "Running command: #{tunnel_cmd}"
|
43
47
|
@pid = spawn(tunnel_cmd)
|
44
|
-
sleep(
|
48
|
+
sleep(3) # Give the tunnel a few seconds to start
|
49
|
+
Process.getpgid(@pid) # Check if the process starts successfully
|
50
|
+
logger.info "IAP tunnel started on port #{port}"
|
51
|
+
rescue Errno::ESRCH
|
52
|
+
@pid = nil
|
53
|
+
raise IapTunnelStartError, "Failed to start IAP tunnel on port #{port}: #{$ERROR_INFO}"
|
45
54
|
end
|
46
55
|
|
47
56
|
# This method stops the IAP tunnel
|
@@ -6,24 +6,25 @@ module CemWinSpec
|
|
6
6
|
class RspecTestCmds
|
7
7
|
DEFAULT_PATTERN = 'spec/{classes,defines}/**/*_spec.rb'
|
8
8
|
|
9
|
-
def initialize(pattern: DEFAULT_PATTERN)
|
9
|
+
def initialize(use_bundler: true, pattern: DEFAULT_PATTERN)
|
10
|
+
@use_bundler = use_bundler
|
10
11
|
@pattern = pattern
|
11
12
|
end
|
12
13
|
|
13
14
|
def cmd_standalone(*args)
|
14
|
-
"
|
15
|
+
prefix "rake 'cem:spec_standalone#{rake_args(nil, *args)}'"
|
15
16
|
end
|
16
17
|
|
17
18
|
def cmd_parallel(*args)
|
18
|
-
"
|
19
|
+
prefix "rake 'cem:parallel_spec_standalone#{rake_args(*args)}'"
|
19
20
|
end
|
20
21
|
|
21
22
|
def cmd_chunked(*files)
|
22
|
-
"
|
23
|
+
prefix "rake 'cem:parallel_spec_files#{rake_args(files.join(' '))}'"
|
23
24
|
end
|
24
25
|
|
25
26
|
def prep_cmd
|
26
|
-
'
|
27
|
+
prefix 'rake cem:win_spec_prep'
|
27
28
|
end
|
28
29
|
|
29
30
|
def cmds(*args)
|
@@ -31,7 +32,7 @@ module CemWinSpec
|
|
31
32
|
end
|
32
33
|
|
33
34
|
def cleanup_cmd
|
34
|
-
'
|
35
|
+
prefix 'rake cem:win_spec_clean'
|
35
36
|
end
|
36
37
|
|
37
38
|
def spec_files
|
@@ -40,12 +41,16 @@ module CemWinSpec
|
|
40
41
|
|
41
42
|
private
|
42
43
|
|
44
|
+
def prefix(cmd)
|
45
|
+
@use_bundler ? "bundle exec #{cmd}" : cmd
|
46
|
+
end
|
47
|
+
|
43
48
|
def rake_args(*args)
|
44
49
|
args.empty? ? '' : "[#{args.join(',')}]"
|
45
50
|
end
|
46
51
|
|
47
52
|
def rspec_cmd(file, *args)
|
48
|
-
"
|
53
|
+
prefix "rake 'cem:spec_standalone#{rake_args(file, *args)}'"
|
49
54
|
end
|
50
55
|
end
|
51
56
|
end
|
@@ -38,7 +38,7 @@ module CemWinSpec
|
|
38
38
|
|
39
39
|
def enable_long_paths(**opts)
|
40
40
|
new_command('Enable long paths', **opts) do
|
41
|
-
|
41
|
+
remote_run('Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1')
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
@@ -68,7 +68,7 @@ module CemWinSpec
|
|
68
68
|
module_archive_build { |a| remote_upload(a, remote_working_dir) }
|
69
69
|
module_dir = "#{remote_working_dir}\\#{File.basename(module_archive_path, '.tar.gz')}"
|
70
70
|
logger.debug "Module uploaded to #{module_dir}.tar.gz, extracting..."
|
71
|
-
|
71
|
+
remote_run("tar -xzf #{module_dir}.tar.gz -C #{remote_working_dir}")
|
72
72
|
logger.debug "Module extracted to #{module_dir}"
|
73
73
|
module_dir
|
74
74
|
end
|
@@ -76,33 +76,33 @@ module CemWinSpec
|
|
76
76
|
|
77
77
|
def setup_ruby(**opts)
|
78
78
|
@setup_ruby ||= new_command('Set up ruby', **opts) do
|
79
|
-
|
80
|
-
|
79
|
+
remote_run('bundle config disable_platform_warnings true')
|
80
|
+
remote_run('bundle install')
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
84
84
|
def rspec_prep(**opts)
|
85
85
|
@rspec_prep ||= new_command('Prepare rspec tests', **opts) do
|
86
|
-
|
86
|
+
remote_run('bundle exec rake cem_win_spec:prep --trace')
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
90
90
|
def rspec_tests(**opts)
|
91
91
|
@rspec_tests ||= new_command('Run rspec tests', **opts) do
|
92
|
-
|
92
|
+
remote_run(rspec_cmd_standalone('false', 'progress', 'true'))
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
96
|
def rspec_tests_parallel(**opts)
|
97
97
|
@rspec_tests_parallel ||= new_command('Run rspec tests in parallel', **opts) do
|
98
|
-
|
98
|
+
remote_run('bundle exec rake cem_win_spec:parallel_spec')
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
102
|
def clean_up
|
103
103
|
@clean_up ||= new_command('Cleanup') do |working_dir|
|
104
104
|
if remote_available?
|
105
|
-
|
105
|
+
remote_run(cleanup_cmd, quiet: true)
|
106
106
|
else
|
107
107
|
logger.warn 'Cleanup not available'
|
108
108
|
end
|
@@ -111,7 +111,7 @@ module CemWinSpec
|
|
111
111
|
|
112
112
|
def clean_fixture_cache(**opts)
|
113
113
|
@clean_cache ||= new_command('Clean fixture cache', **opts) do
|
114
|
-
|
114
|
+
remote_run('bundle exec rake cem_win_spec:clean_fixture_cache')
|
115
115
|
end
|
116
116
|
end
|
117
117
|
|
data/lib/cem_win_spec/version.rb
CHANGED
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../logging'
|
4
|
+
|
5
|
+
module CemWinSpec
|
6
|
+
module WinExec
|
7
|
+
class BaseCmd
|
8
|
+
include CemWinSpec::Logging
|
9
|
+
|
10
|
+
COMMAND_SEPARATOR = '; '
|
11
|
+
PUPPET_VER_TO_RUBY_VER = {
|
12
|
+
'7' => '278',
|
13
|
+
'8' => '322',
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
attr_accessor :working_dir, :env_vars, :puppet_version
|
17
|
+
|
18
|
+
def initialize(working_dir = nil, puppet_version: nil, **env_vars)
|
19
|
+
@working_dir = working_dir
|
20
|
+
self.puppet_version = puppet_version
|
21
|
+
self.env_vars = env_vars
|
22
|
+
end
|
23
|
+
|
24
|
+
# Sets the puppet version that will be used to execute the command
|
25
|
+
# @param value [String, nil] the puppet version
|
26
|
+
def puppet_version=(value)
|
27
|
+
if value.nil?
|
28
|
+
@puppet_version = nil
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
ver = value.to_s
|
33
|
+
raise ArgumentError, 'puppet_version only supports major versions 7 and 8' unless ver.match?(/^(7|8).*/)
|
34
|
+
unless ver.match?(/^(7|8)(\.\d+)?\.\d+$/)
|
35
|
+
ver = "#{ver}.0"
|
36
|
+
end
|
37
|
+
|
38
|
+
@puppet_version = ver
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets the environment variables that will be used to execute the command
|
42
|
+
# @param value [Hash] the environment variables
|
43
|
+
def env_vars=(value)
|
44
|
+
raise ArgumentError, 'env_vars must be a hash' unless value.is_a?(Hash)
|
45
|
+
|
46
|
+
value['PUPPET_GEM_VERSION'] = "~> #{puppet_version}" if puppet_version
|
47
|
+
value['FACTER_GEM_VERSION'] = 'https://github.com/puppetlabs/facter#main' if puppet_version
|
48
|
+
@env_vars = value
|
49
|
+
end
|
50
|
+
|
51
|
+
def available?
|
52
|
+
raise NotImplementedError
|
53
|
+
end
|
54
|
+
|
55
|
+
def run(cmd, *_args, **_kwargs)
|
56
|
+
raise NotImplementedError
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the ruby version that will be used to execute the command
|
60
|
+
# The ruby version is determined by the puppet version that is set
|
61
|
+
# @return [String, nil] the ruby version or nil if the puppet version is not set
|
62
|
+
def ruby_version
|
63
|
+
return nil unless puppet_version
|
64
|
+
|
65
|
+
PUPPET_VER_TO_RUBY_VER[puppet_version.split('.')[0]]
|
66
|
+
end
|
67
|
+
|
68
|
+
def command(cmd)
|
69
|
+
cmd = [cmd]
|
70
|
+
cmd.unshift(change_ruby_version_cmd) if ruby_version # executes third
|
71
|
+
env_vars.each { |k, v| cmd.unshift(set_env_var_cmd(k, v)) } if env_vars.any? # executes second
|
72
|
+
cmd.unshift(change_working_dir_cmd(working_dir)) if working_dir # executes first
|
73
|
+
cmd.join(COMMAND_SEPARATOR)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def log_command(cmd)
|
79
|
+
cmd = command(cmd)
|
80
|
+
logger.debug "Executing command:\n#{cmd.split(%r{\n|\r\n|;\s*}).map { |c| " #> #{c}" }.join("\n")}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def change_ruby_version_cmd
|
84
|
+
"uru #{ruby_version}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def set_env_var_cmd(key, value)
|
88
|
+
"set #{key}=\"#{value}\""
|
89
|
+
end
|
90
|
+
|
91
|
+
def change_working_dir_cmd(dir)
|
92
|
+
"cd #{dir}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -1,18 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'open3'
|
4
|
-
require_relative '
|
4
|
+
require_relative 'base_cmd'
|
5
5
|
|
6
6
|
module CemWinSpec
|
7
7
|
module WinExec
|
8
8
|
# Class for executing local shell commands
|
9
|
-
class
|
9
|
+
class LocalCmd < BaseCmd
|
10
10
|
attr_reader :thread_results
|
11
11
|
|
12
|
-
def initialize(working_dir = nil)
|
12
|
+
def initialize(working_dir = nil, puppet_version: nil, **env_vars)
|
13
13
|
@ran_in_thread = false
|
14
14
|
@thread_results = {}
|
15
|
-
super(working_dir)
|
15
|
+
super(working_dir, puppet_version: puppet_version, **env_vars)
|
16
16
|
end
|
17
17
|
|
18
18
|
def available?
|
@@ -35,9 +35,9 @@ module CemWinSpec
|
|
35
35
|
# This is useful for running commands that should not block the current process
|
36
36
|
# and that you don't need stdout/stderr from.
|
37
37
|
# @param cmd [String] The command to execute
|
38
|
-
def
|
39
|
-
|
40
|
-
Process.detach(spawn(
|
38
|
+
def bg_run(cmd, *_args, **_kwargs)
|
39
|
+
log_command(cmd)
|
40
|
+
Process.detach(spawn(command(cmd)))
|
41
41
|
end
|
42
42
|
|
43
43
|
# Execute a command in a new thread. This works by creating a new thread in a thread group
|
@@ -48,20 +48,20 @@ module CemWinSpec
|
|
48
48
|
# Process::Status object.
|
49
49
|
# @param cmd [String] The command to execute
|
50
50
|
# @return [Array] An array of [:threaded, cmd]
|
51
|
-
def
|
51
|
+
def thread_run(cmd, *_args, **_kwargs)
|
52
52
|
@ran_in_thread = true
|
53
53
|
th = Thread.new do
|
54
|
-
|
55
|
-
so, se, st = Open3.capture3(
|
54
|
+
log_command(cmd)
|
55
|
+
so, se, st = Open3.capture3(command(cmd))
|
56
56
|
@thread_results[cmd] = [so, se, st]
|
57
57
|
end
|
58
58
|
thread_group.add th
|
59
59
|
:threaded
|
60
60
|
end
|
61
61
|
|
62
|
-
def
|
63
|
-
|
64
|
-
Open3.capture3(
|
62
|
+
def run(cmd, *_args, **_kwargs)
|
63
|
+
log_command(cmd)
|
64
|
+
Open3.capture3(command(cmd))
|
65
65
|
end
|
66
66
|
|
67
67
|
private
|
@@ -2,32 +2,32 @@
|
|
2
2
|
|
3
3
|
require 'winrm'
|
4
4
|
require 'winrm-fs'
|
5
|
-
require_relative '
|
5
|
+
require_relative 'base_cmd'
|
6
6
|
|
7
7
|
module CemWinSpec
|
8
8
|
module WinExec
|
9
9
|
# Class for executing PowerShell commands over WinRM
|
10
|
-
class
|
10
|
+
class WinRMCmd < BaseCmd
|
11
11
|
attr_reader :conn_opts
|
12
12
|
|
13
|
-
def initialize(conn_opts, working_dir = nil, quiet: false)
|
13
|
+
def initialize(conn_opts, working_dir = nil, quiet: false, puppet_version: nil, **env_vars)
|
14
14
|
@conn_opts = conn_opts
|
15
15
|
@quiet = quiet
|
16
16
|
@conn = new_conn(@conn_opts.to_h)
|
17
|
-
super(working_dir)
|
17
|
+
super(working_dir, puppet_version: puppet_version, **env_vars)
|
18
18
|
end
|
19
19
|
|
20
20
|
def available?
|
21
21
|
@available
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
25
|
-
|
24
|
+
def run(cmd, *_args, **_kwargs)
|
25
|
+
log_command(cmd)
|
26
26
|
shell = nil
|
27
27
|
output = nil
|
28
28
|
begin
|
29
29
|
shell = conn.shell(:powershell)
|
30
|
-
output = shell.run(
|
30
|
+
output = shell.run(command(cmd))
|
31
31
|
rescue WinRM::WinRMAuthorizationError => e
|
32
32
|
@available = false
|
33
33
|
raise e
|
@@ -77,10 +77,19 @@ module CemWinSpec
|
|
77
77
|
def new_conn(hash_opts)
|
78
78
|
logger.debug "Creating connection to #{hash_opts[:endpoint]}"
|
79
79
|
new_conn = WinRM::Connection.new(hash_opts)
|
80
|
-
new_conn.logger =
|
80
|
+
new_conn.logger = new_conn_logger
|
81
81
|
new_conn
|
82
82
|
end
|
83
83
|
|
84
|
+
def new_conn_logger
|
85
|
+
conn_logger = logger.dup
|
86
|
+
conn_logger.level = Logger::ERROR if @quiet
|
87
|
+
conn_logger.level = Logger::DEBUG if ENV['WINRM_DEBUG'] == 'true'
|
88
|
+
conn_logger.level = Logger::INFO if conn_logger.level == Logger::DEBUG && ENV.fetch('WINRM_DEBUG', 'false') != 'true'
|
89
|
+
logger.debug "WinRM connection logger level set to #{conn_logger.level}"
|
90
|
+
conn_logger
|
91
|
+
end
|
92
|
+
|
84
93
|
def file_manager
|
85
94
|
@file_manager ||= WinRM::FS::FileManager.new(conn)
|
86
95
|
end
|
@@ -49,7 +49,7 @@ module CemWinSpec
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def pass
|
52
|
-
|
52
|
+
Digest::SHA256.hexdigest(pt_pass) unless pt_pass.nil?
|
53
53
|
end
|
54
54
|
|
55
55
|
def endpoint
|
@@ -57,7 +57,7 @@ module CemWinSpec
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def opts
|
60
|
-
@opts ||=
|
60
|
+
@opts ||= create_new_opts(@kwargs)
|
61
61
|
end
|
62
62
|
|
63
63
|
def ==(other)
|
@@ -75,14 +75,14 @@ module CemWinSpec
|
|
75
75
|
end
|
76
76
|
|
77
77
|
def digest
|
78
|
-
Digest::SHA256.hexdigest([host, port, user,
|
78
|
+
Digest::SHA256.hexdigest([host, port, user, pt_pass, opts.values].join(':'))
|
79
79
|
end
|
80
80
|
|
81
81
|
# Merge new options into existing options and return a new ConnectionOpts object
|
82
82
|
# @param [Hash] new_opts
|
83
83
|
# @return [ConnectionOpts]
|
84
84
|
def merge(**new_opts)
|
85
|
-
self.class.new(opts_merge(**new_opts))
|
85
|
+
self.class.new(**opts_merge(**new_opts))
|
86
86
|
end
|
87
87
|
|
88
88
|
def merge!(**new_opts)
|
@@ -139,7 +139,7 @@ module CemWinSpec
|
|
139
139
|
password
|
140
140
|
end
|
141
141
|
|
142
|
-
def
|
142
|
+
def create_new_opts(nopts = {})
|
143
143
|
logger.debug 'Creating new connection options opts'
|
144
144
|
new_opts_h = CONN_DEFAULTS.dup.merge(nopts)
|
145
145
|
new_opts_h[:transport] = new_opts_h[:transport].to_sym
|
@@ -0,0 +1,213 @@
|
|
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
|
+
@spinner = nil
|
29
|
+
@result = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_title(value)
|
33
|
+
raise ArgumentError, 'title must be a string' unless value.is_a?(String)
|
34
|
+
|
35
|
+
@title = value
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_local_cmd(value)
|
39
|
+
raise ArgumentError, 'local_exec must implement the #run method' unless value.respond_to?(:run)
|
40
|
+
|
41
|
+
@local_cmd = value
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_remote_cmd(value)
|
45
|
+
raise ArgumentError, 'winrm_exec must implement the #run method' unless value.respond_to?(:run)
|
46
|
+
|
47
|
+
@remote_cmd = value
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_iap_tunnel(value)
|
51
|
+
unless value.respond_to?(:start) && value.respond_to?(:stop) && value.respond_to?(:with)
|
52
|
+
raise ArgumentError, 'iap_tunnel must implement the #start, #stop, and #with methods'
|
53
|
+
end
|
54
|
+
|
55
|
+
@iap_tunnel = value
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_ma_builder(value)
|
59
|
+
raise ArgumentError, 'ma_builder must implement the #build method' unless value.respond_to?(:build)
|
60
|
+
|
61
|
+
@ma_builder = value
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_rspec_test_cmds(value)
|
65
|
+
unless value.respond_to?(:cmd_standalone) && value.respond_to?(:cmd_parallel) &&
|
66
|
+
value.respond_to?(:prep_cmd) && value.respond_to?(:cleanup_cmd)
|
67
|
+
raise ArgumentError, 'rspec_test_cmds must implement the #cmd_standalone, #cmd_parallel, #prep_cmd, and #cleanup_cmd methods'
|
68
|
+
end
|
69
|
+
|
70
|
+
@rspec_test_cmds = value
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_command_block(&block)
|
74
|
+
raise ArgumentError, 'block must be a Proc' unless block.is_a?(Proc)
|
75
|
+
|
76
|
+
@block = block
|
77
|
+
end
|
78
|
+
|
79
|
+
def reuse_tunnel=(value)
|
80
|
+
raise ArgumentError, 'reuse_tunnel must be a boolean' unless [true, false].include?(value)
|
81
|
+
|
82
|
+
@reuse_tunnel = value
|
83
|
+
end
|
84
|
+
alias reuse_tunnel? reuse_tunnel
|
85
|
+
|
86
|
+
def ignore_exitcode=(value)
|
87
|
+
raise ArgumentError, 'ignore_exitcode must be a boolean' unless [true, false].include?(value)
|
88
|
+
|
89
|
+
@ignore_exitcode = value
|
90
|
+
end
|
91
|
+
alias ignore_exitcode? ignore_exitcode
|
92
|
+
|
93
|
+
def success?
|
94
|
+
@result.success? if @result.is_a?(Output)
|
95
|
+
|
96
|
+
false
|
97
|
+
end
|
98
|
+
|
99
|
+
def spinner=(value)
|
100
|
+
raise ArgumentError, 'spinner must implement #auto_spin and #stop methods' unless value.respond_to?(:auto_spin) && value.respond_to?(:stop)
|
101
|
+
|
102
|
+
@spinner = value
|
103
|
+
end
|
104
|
+
|
105
|
+
# Proxy method calls for methods that don't exist here to various other objects.
|
106
|
+
# This allows doing things like: `win_exec.remote_exec('Get-Process')` and
|
107
|
+
# calling the `exec` method on the `winrm_exec` object. This is done by
|
108
|
+
# checking method name prefixes and calling the corresponding method on the
|
109
|
+
# appropriate object. The prefix is removed from the method name before
|
110
|
+
# calling the method on the object. The supported prefixes are:
|
111
|
+
# local_ - calls the method on the @local_cmd object
|
112
|
+
# remote_ - calls the method on the @remote_cmd object
|
113
|
+
# rspec_ - calls the method on the @rspec_test_cmds object
|
114
|
+
# module_archive_ - calls the method on the @ma_builder object
|
115
|
+
def method_missing(method, *args, **kwargs, &block)
|
116
|
+
if method.to_s.start_with?('local_') # proxy to local_exec
|
117
|
+
method = method.to_s.sub('local_', '').to_sym
|
118
|
+
@local_cmd.send(method, *args, **kwargs, &block)
|
119
|
+
elsif method.to_s.start_with?('remote_') # proxy to remote_exec
|
120
|
+
method = method.to_s.sub('remote_', '').to_sym
|
121
|
+
@remote_cmd.send(method, *args, **kwargs, &block)
|
122
|
+
elsif method.to_s.start_with?('rspec_') # proxy to rspec_test_cmds
|
123
|
+
method = method.to_s.sub('rspec_', '').to_sym
|
124
|
+
@rspec_test_cmds.send(method, *args, **kwargs, &block)
|
125
|
+
elsif method.to_s.start_with?('module_archive_') # proxy to ma_builder
|
126
|
+
method = method.to_s.sub('module_archive_', '').to_sym
|
127
|
+
@ma_builder.send(method, *args, **kwargs, &block)
|
128
|
+
else
|
129
|
+
super
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Proxy respond_to? for methods that don't exist here to various other objects.
|
134
|
+
# This allows doing things like: `win_exec.respond_to?(:remote_exec)` and
|
135
|
+
# checking if the `exec` method exists on the `winrm_exec` object. This is done by
|
136
|
+
# checking method name prefixes and calling the corresponding method on the
|
137
|
+
# appropriate object. The prefix is removed from the method name before
|
138
|
+
# calling the method on the object. The supported prefixes are:
|
139
|
+
# local_ - calls the method on the @local_cmd object
|
140
|
+
# remote_ - calls the method on the @remote_cmd object
|
141
|
+
# rspec_ - calls the method on the @rspec_test_cmds object
|
142
|
+
# module_archive_ - calls the method on the @ma_builder object
|
143
|
+
def respond_to_missing?(method, include_private = false)
|
144
|
+
if method.to_s.start_with?('local_')
|
145
|
+
@local_cmd.respond_to?(method.to_s.sub('local_', '').to_sym, include_private)
|
146
|
+
elsif method.to_s.start_with?('remote_')
|
147
|
+
@remote_cmd.respond_to?(method.to_s.sub('remote_', '').to_sym, include_private)
|
148
|
+
elsif method.to_s.start_with?('rspec_')
|
149
|
+
@rspec_test_cmds.respond_to?(method.to_s.sub('rspec_', '').to_sym, include_private)
|
150
|
+
elsif method.to_s.start_with?('module_archive_')
|
151
|
+
@ma_builder.respond_to?(method.to_s.sub('module_archive_', '').to_sym, include_private)
|
152
|
+
else
|
153
|
+
super
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def run(*args, **kwargs)
|
158
|
+
validate_instance_variables
|
159
|
+
logger.info "Running #{@title}"
|
160
|
+
@spinner&.auto_spin
|
161
|
+
result = run_with_tunnel { run_in_current_scope(*args, **kwargs) }
|
162
|
+
@spinner&.stop
|
163
|
+
@result = Output.new(result)
|
164
|
+
return if @result.pending_threaded?
|
165
|
+
|
166
|
+
@result.puts_combined
|
167
|
+
unless @result.success? || ignore_exitcode?
|
168
|
+
raise "Command failed with exit code #{@result.exitcode}: #{@result.stdout}\n#{@result.stderr}"
|
169
|
+
end
|
170
|
+
@result
|
171
|
+
rescue StandardError => e
|
172
|
+
raise if HALTING_ERRORS.include?(e.class)
|
173
|
+
|
174
|
+
logger.error "Error running #{@title}: #{e.message[0..100]}"
|
175
|
+
@result = Output.new(e)
|
176
|
+
@result
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def run_with_tunnel(&block)
|
182
|
+
if reuse_tunnel?
|
183
|
+
@iap_tunnel.start # ensure tunnel is running
|
184
|
+
block.call
|
185
|
+
else
|
186
|
+
@iap_tunnel.stop # ensure tunnel is stopped
|
187
|
+
@iap_tunnel.with do # start tunnel for this block
|
188
|
+
block.call
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def validate_instance_variables
|
194
|
+
%i[@title @local_cmd @remote_cmd @iap_tunnel @ma_builder @rspec_test_cmds @block].each do |var|
|
195
|
+
raise ArgumentError, "#{var} must be set" if instance_variable_get(var).nil?
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Runs the block in the current scope. This allows the block to access
|
200
|
+
# instance variables and methods from the current scope.
|
201
|
+
def run_in_current_scope(*args, **kwargs)
|
202
|
+
instance_exec(*args, **kwargs, &@block) # self is the "instance"
|
203
|
+
end
|
204
|
+
|
205
|
+
def wrap_spinner
|
206
|
+
@spinner.auto_spin if @spinner
|
207
|
+
yield if block_given?
|
208
|
+
ensure
|
209
|
+
@spinner.stop if @spinner
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,124 @@
|
|
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
|
+
wexec.spinner = opts[:spinner] if opts.key?(:spinner)
|
51
|
+
logger.debug 'Created Wexec'
|
52
|
+
wexec
|
53
|
+
end
|
54
|
+
|
55
|
+
def local_threaded_results
|
56
|
+
logger.info 'Checking for deferred results...'
|
57
|
+
@current_local_exec.join_threads
|
58
|
+
@current_local_exec.threaded_results.each do |cmd, results|
|
59
|
+
logger.info "Deferred results for #{cmd}:"
|
60
|
+
Output.new(results).puts_combined
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def build_conn_opts(merge: true, user: nil, pass: nil, **opts)
|
67
|
+
if @current_conn_opts.nil?
|
68
|
+
logger.debug 'Creating new ConnectionOpts object'
|
69
|
+
@current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
70
|
+
return @current_conn_opts
|
71
|
+
end
|
72
|
+
|
73
|
+
if need_new_conn_opts?(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
74
|
+
if merge
|
75
|
+
logger.debug 'Merging ConnectionOpts with new options'
|
76
|
+
@current_conn_opts = @current_conn_opts.merge(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
77
|
+
else
|
78
|
+
logger.debug 'Creating new ConnectionOpts object with new options'
|
79
|
+
@current_conn_opts = ConnectionOpts.new(new_host: 'localhost', new_port: @iap_tunnel.port, user: user, pass: pass, **opts)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
logger.debug 'Returning existing ConnectionOpts object'
|
83
|
+
end
|
84
|
+
@current_conn_opts
|
85
|
+
end
|
86
|
+
|
87
|
+
def need_new_conn_opts?(new_host: nil, new_port: nil, user: nil, pass: nil, **opts)
|
88
|
+
if new_host && new_host != @current_conn_opts.host
|
89
|
+
logger.debug "New host #{new_host} does not match current host #{@current_conn_opts.host}"
|
90
|
+
return true
|
91
|
+
end
|
92
|
+
if new_port && new_port != @current_conn_opts.port
|
93
|
+
logger.debug "New port #{new_port} does not match current port #{@current_conn_opts.port}"
|
94
|
+
return true
|
95
|
+
end
|
96
|
+
if user && user != @current_conn_opts.user
|
97
|
+
logger.debug "New user #{user} does not match current user #{@current_conn_opts.user}"
|
98
|
+
return true
|
99
|
+
end
|
100
|
+
if pass && Digest::SHA256.hexdigest(pass) != @current_conn_opts.pass
|
101
|
+
logger.debug 'New password does not match current password'
|
102
|
+
return true
|
103
|
+
end
|
104
|
+
if !opts.empty? && opts != @current_conn_opts.opts
|
105
|
+
logger.debug "New extra options #{opts} do not match current extra options #{@current_conn_opts.opts}"
|
106
|
+
return true
|
107
|
+
end
|
108
|
+
|
109
|
+
false
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_remote_cmd(working_dir = nil, **opts)
|
113
|
+
if @current_winrm_cmd.nil? || @current_winrm_cmd.conn_opts.digest != @current_conn_opts.digest
|
114
|
+
logger.debug 'Creating new WinRMExec object'
|
115
|
+
@current_remote_cmd = WinRMCmd.new(@current_conn_opts, **opts)
|
116
|
+
end
|
117
|
+
logger.debug "Setting working directory to #{working_dir}" unless working_dir.nil?
|
118
|
+
@current_remote_cmd.working_dir = working_dir
|
119
|
+
logger.debug 'Returning WinRMExec object'
|
120
|
+
@current_remote_cmd
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
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
@@ -45,14 +45,14 @@ module CemWinSpec
|
|
45
45
|
check_output!(sre_out, runner)
|
46
46
|
module_dir = sre_out.stdout.chomp
|
47
47
|
logger.debug "Module dir: #{module_dir}"
|
48
|
-
srr_out = setup_remote_ruby(runner, module_dir, spinner)
|
48
|
+
srr_out = setup_remote_ruby(runner, module_dir, spinner, **options)
|
49
49
|
check_output!(srr_out, runner)
|
50
50
|
case operation
|
51
51
|
when :spec
|
52
|
-
spec_out = run_spec(runner, module_dir, spinner)
|
52
|
+
spec_out = run_spec(runner, module_dir, spinner, **options)
|
53
53
|
check_output!(spec_out, runner)
|
54
54
|
when :clean_fixture_cache
|
55
|
-
clean_fixture_cache_out = clean_fixture_cache(runner, module_dir, spinner)
|
55
|
+
clean_fixture_cache_out = clean_fixture_cache(runner, module_dir, spinner, **options)
|
56
56
|
check_output!(clean_fixture_cache_out, runner)
|
57
57
|
else
|
58
58
|
raise ArgumentError, "Unknown operation: #{operation}"
|
@@ -71,32 +71,34 @@ module CemWinSpec
|
|
71
71
|
working_dir_out = runner.create_working_dir.run
|
72
72
|
working_dir = working_dir_out.stdout.chomp
|
73
73
|
logger.debug "Working dir: #{working_dir}"
|
74
|
-
runner.upload_module(working_dir: working_dir).run
|
74
|
+
runner.upload_module(operation_timeout: 300, receive_timeout: 310, working_dir: working_dir).run
|
75
75
|
end
|
76
76
|
|
77
|
-
def self.setup_remote_ruby(runner, module_dir, spinner)
|
77
|
+
def self.setup_remote_ruby(runner, module_dir, spinner, **opts)
|
78
78
|
runner.setup_ruby(operation_timeout: 300,
|
79
79
|
receive_timeout: 310,
|
80
80
|
working_dir: module_dir,
|
81
81
|
reuse_tunnel: false,
|
82
|
-
spinner: spinner
|
82
|
+
spinner: spinner,
|
83
|
+
**opts).run
|
83
84
|
end
|
84
85
|
|
85
86
|
# Runs RSpec tests
|
86
|
-
def self.run_spec(runner, module_dir, spinner)
|
87
|
+
def self.run_spec(runner, module_dir, spinner, **opts)
|
87
88
|
#runner.rspec_prep(working_dir: module_dir, reuse_tunnel: false, spinner: spinner).run
|
88
89
|
runner.rspec_tests_parallel(operation_timeout: 300,
|
89
90
|
receive_timeout: 310,
|
90
91
|
working_dir: module_dir,
|
91
92
|
ignore_exitcode: true,
|
92
93
|
reuse_tunnel: false,
|
93
|
-
spinner: spinner
|
94
|
+
spinner: spinner,
|
95
|
+
**opts).run
|
94
96
|
end
|
95
97
|
|
96
98
|
# Clean the remote fixture cache
|
97
99
|
# @param options [Hash] Options for the test runner
|
98
|
-
def self.clean_fixture_cache(runner, module_dir, spinner)
|
99
|
-
runner.clean_fixture_cache(working_dir: module_dir, spinner: spinner).run
|
100
|
+
def self.clean_fixture_cache(runner, module_dir, spinner, **opts)
|
101
|
+
runner.clean_fixture_cache(working_dir: module_dir, spinner: spinner, **opts).run
|
100
102
|
end
|
101
103
|
|
102
104
|
def self.check_output!(output, runner = nil)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cem_win_spec
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Heston Snodgrass
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-07-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: winrm
|
@@ -119,16 +119,17 @@ files:
|
|
119
119
|
- lib/cem_win_spec/logging/formatter.rb
|
120
120
|
- lib/cem_win_spec/module_archive_builder.rb
|
121
121
|
- lib/cem_win_spec/rake_tasks.rb
|
122
|
-
- lib/cem_win_spec/remote_command.rb
|
123
122
|
- lib/cem_win_spec/rspec_test_cmds.rb
|
124
123
|
- lib/cem_win_spec/test_runner.rb
|
125
124
|
- lib/cem_win_spec/version.rb
|
126
125
|
- lib/cem_win_spec/win_exec.rb
|
127
|
-
- lib/cem_win_spec/win_exec/
|
126
|
+
- lib/cem_win_spec/win_exec/cmd/base_cmd.rb
|
127
|
+
- lib/cem_win_spec/win_exec/cmd/local_cmd.rb
|
128
|
+
- lib/cem_win_spec/win_exec/cmd/winrm_cmd.rb
|
128
129
|
- lib/cem_win_spec/win_exec/connection_opts.rb
|
129
|
-
- lib/cem_win_spec/win_exec/
|
130
|
+
- lib/cem_win_spec/win_exec/exec.rb
|
131
|
+
- lib/cem_win_spec/win_exec/factory.rb
|
130
132
|
- lib/cem_win_spec/win_exec/output.rb
|
131
|
-
- lib/cem_win_spec/win_exec/winrm_exec.rb
|
132
133
|
- sig/cem_win_spec.rbs
|
133
134
|
homepage: https://github.com/hsnodgrass/cem_win_spec
|
134
135
|
licenses:
|
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
153
|
- !ruby/object:Gem::Version
|
153
154
|
version: '0'
|
154
155
|
requirements: []
|
155
|
-
rubygems_version: 3.
|
156
|
+
rubygems_version: 3.4.6
|
156
157
|
signing_key:
|
157
158
|
specification_version: 4
|
158
159
|
summary: Write a short summary, because RubyGems requires one.
|
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'iap_tunnel'
|
4
|
-
require_relative 'win_exec'
|
5
|
-
|
6
|
-
module CemWinSpec
|
7
|
-
# Class for running a command on a remote Windows host
|
8
|
-
class RemoteCommand
|
9
|
-
attr_reader :title, :result
|
10
|
-
|
11
|
-
def initialize(title, iap_tunnel: nil, reuse_tunnel: true, winrm_opts: {}, &block)
|
12
|
-
@title = title
|
13
|
-
@iap_tunnel = iap_tunnel || IapTunnel.new
|
14
|
-
@reuse_tunnel = reuse_tunnel
|
15
|
-
@winrm_opts = winrm_opts
|
16
|
-
@block = block
|
17
|
-
@win_exec = WinExec.new('localhost', @iap_tunnel.port, winrm_opts: winrm_opts)
|
18
|
-
@result = nil
|
19
|
-
end
|
20
|
-
|
21
|
-
def ran?
|
22
|
-
!@result.nil?
|
23
|
-
end
|
24
|
-
|
25
|
-
def success?
|
26
|
-
@result.is_a? WinRM::Output
|
27
|
-
end
|
28
|
-
|
29
|
-
def run
|
30
|
-
puts "Running #{@title}"
|
31
|
-
@result = if @reuse_tunnel
|
32
|
-
@iap_tunnel.start # ensure tunnel is running
|
33
|
-
@block.call @win_exec
|
34
|
-
else
|
35
|
-
@iap_tunnel.stop # ensure tunnel is stopped
|
36
|
-
@iap_tunnel.with do # start tunnel for this block
|
37
|
-
@block.call @win_exec
|
38
|
-
end
|
39
|
-
end
|
40
|
-
@result
|
41
|
-
rescue StandardError => e
|
42
|
-
puts "Error running #{@title}: #{e}"
|
43
|
-
@result = e
|
44
|
-
raise @result
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -1,42 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative '../logging'
|
4
|
-
|
5
|
-
module CemWinSpec
|
6
|
-
module WinExec
|
7
|
-
class BaseExec
|
8
|
-
include CemWinSpec::Logging
|
9
|
-
|
10
|
-
attr_accessor :working_dir
|
11
|
-
|
12
|
-
def initialize(working_dir = nil)
|
13
|
-
@working_dir = working_dir
|
14
|
-
end
|
15
|
-
|
16
|
-
def available?
|
17
|
-
raise NotImplementedError
|
18
|
-
end
|
19
|
-
|
20
|
-
def exec(cmd, *_args, **_kwargs)
|
21
|
-
raise NotImplementedError
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def cd_working_dir(cmd)
|
27
|
-
return cmd if working_dir.nil?
|
28
|
-
|
29
|
-
"cd #{working_dir}; #{cmd}"
|
30
|
-
end
|
31
|
-
|
32
|
-
def cmd_prefix_msg
|
33
|
-
prefix = 'Executing command'
|
34
|
-
working_dir.nil? ? "#{prefix}:\n" : "#{prefix} in #{working_dir}:\n"
|
35
|
-
end
|
36
|
-
|
37
|
-
def puts_cmd(cmd)
|
38
|
-
logger.debug "#{cmd_prefix_msg}#{cmd.split(%r{\n|\r\n|;\s*}).map { |c| " #> #{c}" }.join("\n")}"
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|