cem_win_spec 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +534 -0
- data/CODEOWNERS +2 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +115 -0
- data/LICENSE.txt +21 -0
- data/README.md +33 -0
- data/Rakefile +12 -0
- data/cem_win_spec.gemspec +37 -0
- data/exe/cem-win-spec +52 -0
- data/lib/cem_win_spec/fixture_cache.rb +121 -0
- data/lib/cem_win_spec/iap_tunnel.rb +174 -0
- data/lib/cem_win_spec/logging/formatter.rb +97 -0
- data/lib/cem_win_spec/logging.rb +170 -0
- data/lib/cem_win_spec/module_archive_builder.rb +84 -0
- data/lib/cem_win_spec/rake_tasks.rb +138 -0
- data/lib/cem_win_spec/remote_command.rb +47 -0
- data/lib/cem_win_spec/rspec_test_cmds.rb +51 -0
- data/lib/cem_win_spec/test_runner.rb +144 -0
- data/lib/cem_win_spec/version.rb +5 -0
- data/lib/cem_win_spec/win_exec/base_exec.rb +42 -0
- data/lib/cem_win_spec/win_exec/connection_opts.rb +169 -0
- data/lib/cem_win_spec/win_exec/local_exec.rb +74 -0
- data/lib/cem_win_spec/win_exec/output.rb +104 -0
- data/lib/cem_win_spec/win_exec/winrm_exec.rb +89 -0
- data/lib/cem_win_spec/win_exec.rb +234 -0
- data/lib/cem_win_spec.rb +79 -0
- data/sig/cem_win_spec.rbs +4 -0
- metadata +159 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require_relative 'logging'
|
|
6
|
+
|
|
7
|
+
module CemWinSpec
|
|
8
|
+
class ModuleArchiveBuilder
|
|
9
|
+
include CemWinSpec::Logging
|
|
10
|
+
|
|
11
|
+
FILE_ALLOWLIST = %w[
|
|
12
|
+
data
|
|
13
|
+
files
|
|
14
|
+
lib
|
|
15
|
+
manifests
|
|
16
|
+
plans
|
|
17
|
+
spec
|
|
18
|
+
tasks
|
|
19
|
+
types
|
|
20
|
+
.fixtures.yml
|
|
21
|
+
.gitignore
|
|
22
|
+
.rspec
|
|
23
|
+
.rubocop.yml
|
|
24
|
+
.yardopts
|
|
25
|
+
hiera.yaml
|
|
26
|
+
Gemfile
|
|
27
|
+
metadata.json
|
|
28
|
+
Rakefile
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
attr_reader :module_dir, :allowlist, :tempdir, :archive
|
|
32
|
+
|
|
33
|
+
def initialize(module_dir = Dir.pwd, allowlist: FILE_ALLOWLIST)
|
|
34
|
+
@module_dir = module_dir
|
|
35
|
+
@module_name = File.basename(module_dir)
|
|
36
|
+
@allowlist = allowlist
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias path archive
|
|
40
|
+
|
|
41
|
+
def build
|
|
42
|
+
create_tempdir
|
|
43
|
+
copy_module_to_tempdir
|
|
44
|
+
remove_unwanted_files
|
|
45
|
+
create_archive
|
|
46
|
+
if block_given?
|
|
47
|
+
begin
|
|
48
|
+
yield archive
|
|
49
|
+
ensure
|
|
50
|
+
FileUtils.rm_rf(archive) if archive && File.exist?(archive)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
ensure
|
|
54
|
+
FileUtils.rm_rf(tempdir) if tempdir && File.exist?(tempdir)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def create_tempdir
|
|
60
|
+
@tempdir = Dir.mktmpdir
|
|
61
|
+
logger.debug "Created tempdir #{tempdir}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def copy_module_to_tempdir
|
|
65
|
+
FileUtils.cp_r(module_dir, tempdir)
|
|
66
|
+
logger.debug "Copied #{module_dir} to #{tempdir}"
|
|
67
|
+
@temp_module_dir = File.join(tempdir, @module_name)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def remove_unwanted_files
|
|
71
|
+
to_remove = Dir.glob("*", File::FNM_DOTMATCH, base: @temp_module_dir).reject { |f| allowlist.include?(f) || %w[. ..].include?(f) }
|
|
72
|
+
to_remove.each do |file|
|
|
73
|
+
FileUtils.rm_rf(File.join(@temp_module_dir, file))
|
|
74
|
+
logger.debug "Removed #{file} from #{@temp_module_dir}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def create_archive
|
|
79
|
+
@archive = "#{@module_name}.tar.gz"
|
|
80
|
+
`tar -czf #{archive} -C #{tempdir} #{@module_name}`
|
|
81
|
+
logger.info "Created module archive #{archive}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rake'
|
|
4
|
+
require 'rake/file_list'
|
|
5
|
+
require 'rake/tasklib'
|
|
6
|
+
require 'parallel_tests'
|
|
7
|
+
require_relative 'fixture_cache'
|
|
8
|
+
|
|
9
|
+
# Provides custom rake tasks to run tests on a remote Windows host.
|
|
10
|
+
# Add the following to your Rakefile to use:
|
|
11
|
+
# require 'cem_win_spec/rake_tasks'
|
|
12
|
+
# CemWinSpecRakeTasks::All.new
|
|
13
|
+
module CemWinSpecRakeTasks
|
|
14
|
+
# Class for defining all custom rake tasks
|
|
15
|
+
class All
|
|
16
|
+
def initialize(**opts)
|
|
17
|
+
@namespace = opts[:namespace] || :cem_win_spec
|
|
18
|
+
@data_dir = opts[:data_dir]
|
|
19
|
+
@hiera_conf = opts[:hiera_conf]
|
|
20
|
+
@pattern = opts[:pattern]
|
|
21
|
+
@fail_fast = opts[:fail_fast]
|
|
22
|
+
@format = opts[:format]
|
|
23
|
+
define_tasks
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def define_tasks
|
|
29
|
+
define_prep_task
|
|
30
|
+
define_test_task
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def define_prep_task
|
|
34
|
+
prep_task = Prep.new(@namespace)
|
|
35
|
+
prep_task.data_dir = @data_dir if @data_dir
|
|
36
|
+
prep_task.hiera_conf = @hiera_conf if @hiera_conf
|
|
37
|
+
prep_task.define_task
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def define_test_task
|
|
41
|
+
test_task = Test.new(@namespace)
|
|
42
|
+
test_task.pattern = @pattern if @pattern
|
|
43
|
+
test_task.fail_fast = @fail_fast if @fail_fast
|
|
44
|
+
test_task.format = @format if @format
|
|
45
|
+
test_task.define_task
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Class for defining the cem_win_spec:prep task
|
|
50
|
+
class Prep
|
|
51
|
+
attr_accessor :data_dir, :hiera_conf
|
|
52
|
+
|
|
53
|
+
def initialize(namespace = :cem_win_spec)
|
|
54
|
+
@namespace = namespace
|
|
55
|
+
@data_dir = 'data'
|
|
56
|
+
@hiera_conf = 'hiera.yaml'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def define_task
|
|
60
|
+
namespace @namespace do
|
|
61
|
+
desc 'Prepare spec data'
|
|
62
|
+
task :prep do
|
|
63
|
+
if Gem.win_platform?
|
|
64
|
+
prep_data
|
|
65
|
+
prep_modules
|
|
66
|
+
else
|
|
67
|
+
puts 'Not on Windows, skipping prep'
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def fixtures(rel_path = nil)
|
|
76
|
+
fixtures_dir = File.expand_path(File.join('spec', 'fixtures'))
|
|
77
|
+
rel_path ? File.join(fixtures_dir, rel_path) : fixtures_dir
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def prep_data
|
|
81
|
+
puts 'Preparing spec data...'
|
|
82
|
+
data_fix = fixtures(data_dir)
|
|
83
|
+
if File.directory?(data_fix)
|
|
84
|
+
puts "Removing old #{data_fix} directory..."
|
|
85
|
+
::FileUtils.remove_entry_secure(data_fix)
|
|
86
|
+
end
|
|
87
|
+
puts "Copying Hiera data to #{data_fix}..."
|
|
88
|
+
::FileUtils.cp_r(data_dir, data_fix)
|
|
89
|
+
unless Dir.glob("#{data_fix}/**/*").map { |f| f.delete_prefix('spec/fixtures/') } == Dir.glob("#{data_dir}/**/*")
|
|
90
|
+
raise 'Spec data copy failed!'
|
|
91
|
+
end
|
|
92
|
+
hiera_fix = fixtures(hiera_conf)
|
|
93
|
+
puts "Copying Hiera config to #{hiera_fix}..."
|
|
94
|
+
::FileUtils.cp(hiera_conf, hiera_fix)
|
|
95
|
+
unless File.exist?(hiera_fix)
|
|
96
|
+
raise 'Hiera config copy failed!'
|
|
97
|
+
end
|
|
98
|
+
puts 'Spec data prepared successfully'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def prep_modules
|
|
102
|
+
puts 'Preparing module fixtures...'
|
|
103
|
+
mod_fix = fixtures('modules')
|
|
104
|
+
cache = CemWinSpec::FixtureCache.new
|
|
105
|
+
cache.copy_fixtures_to(mod_fix)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Class for defining the cem_win_spec:test task
|
|
110
|
+
class Test
|
|
111
|
+
attr_accessor :pattern, :fail_fast, :format
|
|
112
|
+
|
|
113
|
+
def initialize(namespace = :cem_win_spec)
|
|
114
|
+
@namespace = namespace
|
|
115
|
+
@pattern = 'spec/{classes,data_tests,defines,functions,unit}/**/*_spec.rb'
|
|
116
|
+
@fail_fast = false
|
|
117
|
+
@format = 'progress'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def define_task
|
|
121
|
+
namespace @namespace do
|
|
122
|
+
desc 'Run RSpec tests on Windows host'
|
|
123
|
+
task :parallel_test do
|
|
124
|
+
spec_files = Rake::FileList[pattern]
|
|
125
|
+
pargs = ['--type', 'rspec']
|
|
126
|
+
rspec_args = ['--']
|
|
127
|
+
rspec_args << '--fail-fast' if fail_fast
|
|
128
|
+
rspec_argss.concat(['--format', @format])
|
|
129
|
+
pargs.concat(rspec_args)
|
|
130
|
+
pargs << '--'
|
|
131
|
+
puts "Running tests on Windows host with pattern #{pattern} and args #{pargs}..."
|
|
132
|
+
pargs.concat(spec_files)
|
|
133
|
+
ParallelTests::CLI.new.run(pargs)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rake'
|
|
4
|
+
|
|
5
|
+
module CemWinSpec
|
|
6
|
+
class RspecTestCmds
|
|
7
|
+
DEFAULT_PATTERN = 'spec/{classes,defines}/**/*_spec.rb'
|
|
8
|
+
|
|
9
|
+
def initialize(pattern: DEFAULT_PATTERN)
|
|
10
|
+
@pattern = pattern
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def cmd_standalone(*args)
|
|
14
|
+
"bundle exec rake 'cem:spec_standalone#{rake_args(nil, *args)}'"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cmd_parallel(*args)
|
|
18
|
+
"bundle exec rake 'cem:parallel_spec_standalone#{rake_args(*args)}'"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cmd_chunked(*files)
|
|
22
|
+
"bundle exec rake 'cem:parallel_spec_files#{rake_args(files.join(' '))}'"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def prep_cmd
|
|
26
|
+
'bundle exec rake cem:win_spec_prep'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cmds(*args)
|
|
30
|
+
spec_files.to_a.collect { |file| rspec_cmd(file, *args) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cleanup_cmd
|
|
34
|
+
'bundle exec rake cem:win_spec_clean'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def spec_files
|
|
38
|
+
@spec_files ||= Rake::FileList[@pattern]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def rake_args(*args)
|
|
44
|
+
args.empty? ? '' : "[#{args.join(',')}]"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def rspec_cmd(file, *args)
|
|
48
|
+
"bundle exec rake 'cem:spec_standalone#{rake_args(file, *args)}'"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
require_relative 'iap_tunnel'
|
|
7
|
+
require_relative 'logging'
|
|
8
|
+
require_relative 'module_archive_builder'
|
|
9
|
+
require_relative 'rspec_test_cmds'
|
|
10
|
+
require_relative 'win_exec'
|
|
11
|
+
|
|
12
|
+
module CemWinSpec
|
|
13
|
+
# Class for running tests on a remote Windows host
|
|
14
|
+
class TestRunner
|
|
15
|
+
include CemWinSpec::Logging
|
|
16
|
+
|
|
17
|
+
attr_reader :iap_tunnel
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@module_archive_builder = ModuleArchiveBuilder.new
|
|
21
|
+
@rspec_cmds = RspecTestCmds.new
|
|
22
|
+
@iap_tunnel = IapTunnel.new
|
|
23
|
+
@win_exec_factory = CemWinSpec::WinExec::Factory.new(@iap_tunnel, @module_archive_builder, @rspec_cmds)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run_all
|
|
27
|
+
check_connectivity.run
|
|
28
|
+
enable_long_paths.run
|
|
29
|
+
enable_symlinks.run
|
|
30
|
+
@working_dir = create_working_dir.run
|
|
31
|
+
upload_module.run(@working_dir)
|
|
32
|
+
setup_ruby.run(@working_dir)
|
|
33
|
+
rspec_tests_parallel.run(@working_dir)
|
|
34
|
+
ensure
|
|
35
|
+
clean_up(@working_dir)
|
|
36
|
+
@iap_tunnel.stop if @iap_tunnel.running?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def enable_long_paths(**opts)
|
|
40
|
+
new_command('Enable long paths', **opts) do
|
|
41
|
+
remote_exec('Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def enable_symlinks(**opts)
|
|
46
|
+
@enable_symlinks ||= new_command('Enable symlinks', **opts) do
|
|
47
|
+
remote_exec('fsutil behavior set SymlinkEvaluation L2L:1 R2R:1 L2R:1 R2L:1')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_working_dir(**opts)
|
|
52
|
+
@create_working_dir ||= new_command('Create working directory', **opts) do
|
|
53
|
+
dir_name = if remote_conn_opts.user.nil?
|
|
54
|
+
"cem_windows_#{Time.now.to_i}"
|
|
55
|
+
else
|
|
56
|
+
"cem_windows_#{remote_conn_opts.user}_#{Time.now.to_i}"
|
|
57
|
+
end
|
|
58
|
+
work_dir = File.join(remote_temp_dir, dir_name.gsub(/[^0-9A-Za-z]/, '_')).gsub(%r{\/}, "\\")
|
|
59
|
+
remote_create_dir(work_dir)
|
|
60
|
+
work_dir
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def upload_module(**opts)
|
|
65
|
+
@upload_module ||= new_command('Upload module', **opts) do
|
|
66
|
+
raise 'No working directory or working directory is not full path' unless remote_working_dir.match?(%r{^[a-zA-Z]:.*})
|
|
67
|
+
|
|
68
|
+
module_archive_build { |a| remote_upload(a, remote_working_dir) }
|
|
69
|
+
module_dir = "#{remote_working_dir}\\#{File.basename(module_archive_path, '.tar.gz')}"
|
|
70
|
+
logger.debug "Module uploaded to #{module_dir}.tar.gz, extracting..."
|
|
71
|
+
remote_exec("tar -xzf #{module_dir}.tar.gz -C #{remote_working_dir}")
|
|
72
|
+
logger.debug "Module extracted to #{module_dir}"
|
|
73
|
+
module_dir
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def setup_ruby(**opts)
|
|
78
|
+
@setup_ruby ||= new_command('Set up ruby', **opts) do
|
|
79
|
+
remote_exec('bundle config disable_platform_warnings true')
|
|
80
|
+
remote_exec('bundle install')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def rspec_prep(**opts)
|
|
85
|
+
@rspec_prep ||= new_command('Prepare rspec tests', **opts) do
|
|
86
|
+
remote_exec('bundle exec rake cem_win_spec:prep')
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def rspec_tests(**opts)
|
|
91
|
+
@rspec_tests ||= new_command('Run rspec tests', **opts) do
|
|
92
|
+
remote_exec(rspec_cmd_standalone('false', 'progress', 'true'))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def rspec_tests_parallel(**opts)
|
|
97
|
+
@rspec_tests_parallel ||= new_command('Run rspec tests in parallel', **opts) do
|
|
98
|
+
remote_exec('bundle exec rake cem_win_spec:parallel_test')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def clean_up
|
|
103
|
+
@clean_up ||= new_command('Cleanup') do |working_dir|
|
|
104
|
+
if remote_available?
|
|
105
|
+
remote_exec(cleanup_cmd, quiet: true)
|
|
106
|
+
else
|
|
107
|
+
logger.warn 'Cleanup not available'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def title_sym(title)
|
|
115
|
+
title.downcase.gsub(/\s*/, '_').to_sym
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def new_command(title, **opts, &block)
|
|
119
|
+
logger.debug "Creating command #{title}"
|
|
120
|
+
wexec = @win_exec_factory.build(title, **opts, &block)
|
|
121
|
+
logger.info "Created command #{wexec.title}"
|
|
122
|
+
wexec
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def run_remote(reuse_tunnel: true, &block)
|
|
126
|
+
if reuse_tunnel
|
|
127
|
+
@iap_tunnel.start # ensure tunnel is running
|
|
128
|
+
block.call
|
|
129
|
+
else
|
|
130
|
+
@iap_tunnel.stop # ensure tunnel is stopped
|
|
131
|
+
@iap_tunnel.with do # start tunnel for this block
|
|
132
|
+
block.call
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def cleanup_cmd(working_dir)
|
|
138
|
+
<<~EOS
|
|
139
|
+
Get-ChildItem #{working_dir} -Recurse | Remove-Item -Force
|
|
140
|
+
Remove-Item -Force #{working_dir}
|
|
141
|
+
EOS
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'io/console'
|
|
5
|
+
require_relative '../logging'
|
|
6
|
+
|
|
7
|
+
module CemWinSpec
|
|
8
|
+
module WinExec
|
|
9
|
+
# Class for holding and managing WinExec options
|
|
10
|
+
class ConnectionOpts
|
|
11
|
+
include CemWinSpec::Logging
|
|
12
|
+
|
|
13
|
+
CONN_DEFAULTS = {
|
|
14
|
+
transport: :ssl,
|
|
15
|
+
max_envelope_size: 307_200,
|
|
16
|
+
operation_timeout: 60,
|
|
17
|
+
receive_timeout: 70,
|
|
18
|
+
retry_limit: 3,
|
|
19
|
+
retry_delay: 10,
|
|
20
|
+
}.freeze
|
|
21
|
+
TPORT_DEFAULTS = {
|
|
22
|
+
negotiate: {},
|
|
23
|
+
ssl: {
|
|
24
|
+
no_ssl_peer_verification: true,
|
|
25
|
+
},
|
|
26
|
+
kerberos: {},
|
|
27
|
+
plaintext: {
|
|
28
|
+
basic_auth_only: true,
|
|
29
|
+
disable_sspi: true,
|
|
30
|
+
},
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
attr_reader :user
|
|
34
|
+
|
|
35
|
+
def initialize(new_host: nil, new_port: nil, user: nil, pass: nil, **kwargs)
|
|
36
|
+
@host = new_host
|
|
37
|
+
@port = new_port
|
|
38
|
+
@user = user || get_user
|
|
39
|
+
@pass = pass || get_pass
|
|
40
|
+
@kwargs = kwargs
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def host
|
|
44
|
+
@host ||= 'localhost'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def port
|
|
48
|
+
@port ||= port_for_transport(opts[:transport])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pass
|
|
52
|
+
@pass ||= Digest::SHA256.hexdigest(pt_pass) unless pt_pass.nil?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def endpoint
|
|
56
|
+
@endpoint ||= endpoint_for_transport(opts[:transport])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def opts
|
|
60
|
+
@opts ||= new_opts(@kwargs)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def ==(other)
|
|
64
|
+
raise ArgumentError, 'Can only compare ConnectionOpts with ConnectionOpts' unless other.is_a? ConnectionOpts
|
|
65
|
+
|
|
66
|
+
host == other.host &&
|
|
67
|
+
port == other.port &&
|
|
68
|
+
user == other.user &&
|
|
69
|
+
pass == other.pass &&
|
|
70
|
+
opts == other.opts
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_h
|
|
74
|
+
opts.dup.merge({ user: user, password: pt_pass, endpoint: endpoint })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def digest
|
|
78
|
+
Digest::SHA256.hexdigest([host, port, user, @pass, opts.values].join(':'))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Merge new options into existing options and return a new ConnectionOpts object
|
|
82
|
+
# @param [Hash] new_opts
|
|
83
|
+
# @return [ConnectionOpts]
|
|
84
|
+
def merge(**new_opts)
|
|
85
|
+
self.class.new(opts_merge(**new_opts))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def merge!(**new_opts)
|
|
89
|
+
@host = new_opts[:new_host] || host
|
|
90
|
+
@port = new_opts[:new_port] || port
|
|
91
|
+
@user = new_opts[:user] || user
|
|
92
|
+
@pass = new_opts[:pass] || pt_pass
|
|
93
|
+
@opts = new_opts(opts.merge(new_opts.reject { |k, _v| %i[new_host new_port user pass].include? k }.to_h))
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
protected
|
|
98
|
+
|
|
99
|
+
def pt_pass
|
|
100
|
+
@pass
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def opts_merge(**new_opts)
|
|
106
|
+
{
|
|
107
|
+
new_host: new_opts[:new_host] || host,
|
|
108
|
+
new_port: new_opts[:new_port] || port,
|
|
109
|
+
user: new_opts[:user] || user,
|
|
110
|
+
pass: new_opts[:pass] || pt_pass,
|
|
111
|
+
**opts.merge(new_opts.reject { |k, _v| %i[new_host new_port user pass].include? k }.to_h),
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def get_user
|
|
116
|
+
username = ENV['WINRM_USERNAME']
|
|
117
|
+
|
|
118
|
+
if username.nil? && !ENV['CI']
|
|
119
|
+
puts 'WinRM Username: '
|
|
120
|
+
username = $stdin.cooked(&:gets).chomp
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
raise 'No username provided' if username.nil? || username.empty?
|
|
124
|
+
|
|
125
|
+
logger.debug 'WinRM username is set'
|
|
126
|
+
username
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def get_pass
|
|
130
|
+
password = ENV['WINRM_PASSWORD']
|
|
131
|
+
|
|
132
|
+
if password.nil? && !ENV['CI']
|
|
133
|
+
password = $stdin.getpass('WinRM Password: ')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
raise 'No password provided' if password.nil? || password.empty?
|
|
137
|
+
|
|
138
|
+
logger.debug 'WinRM password is set'
|
|
139
|
+
password
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def new_opts(nopts = {})
|
|
143
|
+
logger.debug 'Creating new connection options opts'
|
|
144
|
+
new_opts_h = CONN_DEFAULTS.dup.merge(nopts)
|
|
145
|
+
new_opts_h[:transport] = new_opts_h[:transport].to_sym
|
|
146
|
+
TPORT_DEFAULTS.dup[new_opts_h[:transport]].merge(new_opts_h)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def endpoint_for_transport(transport)
|
|
150
|
+
case transport
|
|
151
|
+
when :ssl
|
|
152
|
+
"https://#{host}:#{port}/wsman"
|
|
153
|
+
else
|
|
154
|
+
"http://#{host}:#{port}/wsman"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def port_for_transport(transport)
|
|
159
|
+
logger.debug "Getting default port for transport #{transport}, current port is #{@port}"
|
|
160
|
+
case transport
|
|
161
|
+
when :ssl
|
|
162
|
+
5986
|
|
163
|
+
else
|
|
164
|
+
5985
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|