cem_win_spec 0.1.1 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|