train 3.2.14 → 3.2.20

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. metadata +29 -149
  3. data/LICENSE +0 -201
  4. data/lib/train.rb +0 -193
  5. data/lib/train/errors.rb +0 -44
  6. data/lib/train/extras.rb +0 -11
  7. data/lib/train/extras/command_wrapper.rb +0 -201
  8. data/lib/train/extras/stat.rb +0 -136
  9. data/lib/train/file.rb +0 -212
  10. data/lib/train/file/local.rb +0 -82
  11. data/lib/train/file/local/unix.rb +0 -96
  12. data/lib/train/file/local/windows.rb +0 -68
  13. data/lib/train/file/remote.rb +0 -40
  14. data/lib/train/file/remote/aix.rb +0 -29
  15. data/lib/train/file/remote/linux.rb +0 -21
  16. data/lib/train/file/remote/qnx.rb +0 -41
  17. data/lib/train/file/remote/unix.rb +0 -110
  18. data/lib/train/file/remote/windows.rb +0 -110
  19. data/lib/train/globals.rb +0 -5
  20. data/lib/train/options.rb +0 -81
  21. data/lib/train/platforms.rb +0 -102
  22. data/lib/train/platforms/common.rb +0 -34
  23. data/lib/train/platforms/detect.rb +0 -12
  24. data/lib/train/platforms/detect/helpers/os_common.rb +0 -160
  25. data/lib/train/platforms/detect/helpers/os_linux.rb +0 -80
  26. data/lib/train/platforms/detect/helpers/os_windows.rb +0 -142
  27. data/lib/train/platforms/detect/scanner.rb +0 -85
  28. data/lib/train/platforms/detect/specifications/api.rb +0 -20
  29. data/lib/train/platforms/detect/specifications/os.rb +0 -629
  30. data/lib/train/platforms/detect/uuid.rb +0 -32
  31. data/lib/train/platforms/family.rb +0 -31
  32. data/lib/train/platforms/platform.rb +0 -109
  33. data/lib/train/plugin_test_helper.rb +0 -51
  34. data/lib/train/plugins.rb +0 -40
  35. data/lib/train/plugins/base_connection.rb +0 -198
  36. data/lib/train/plugins/transport.rb +0 -49
  37. data/lib/train/transports/cisco_ios_connection.rb +0 -133
  38. data/lib/train/transports/local.rb +0 -240
  39. data/lib/train/transports/mock.rb +0 -183
  40. data/lib/train/transports/ssh.rb +0 -271
  41. data/lib/train/transports/ssh_connection.rb +0 -342
  42. data/lib/train/version.rb +0 -7
@@ -1,49 +0,0 @@
1
- # encoding: utf-8
2
- #
3
- # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
- # Author:: Christoph Hartmann (<chris@lollyrock.com>)
5
-
6
- require "logger"
7
- require_relative "../errors"
8
- require_relative "../extras"
9
- require_relative "../options"
10
-
11
- class Train::Plugins
12
- class Transport
13
- include Train::Extras
14
- Train::Options.attach(self)
15
-
16
- require_relative "base_connection"
17
-
18
- # Initialize a new Transport object
19
- #
20
- # @param [Hash] config = nil the configuration for this transport
21
- # @return [Transport] the transport object
22
- def initialize(options = {})
23
- @options = merge_options({}, options || {})
24
- @logger = @options[:logger] || Logger.new($stdout, level: :fatal)
25
- end
26
-
27
- # Create a connection to the target. Options may be provided
28
- # for additional configuration.
29
- #
30
- # @param [Hash] _options = nil provide optional configuration params
31
- # @return [Connection] the connection for this configuration
32
- def connection(_options = nil)
33
- raise Train::ClientError, "#{self.class} does not implement #connection()"
34
- end
35
-
36
- # Register the inheriting class with as a train plugin using the
37
- # provided name.
38
- #
39
- # @param [String] name of the plugin, by which it will be found
40
- def self.name(name)
41
- Train::Plugins.registry[name] = self
42
- end
43
-
44
- private
45
-
46
- # @return [Logger] logger for reporting information
47
- attr_reader :logger
48
- end
49
- end
@@ -1,133 +0,0 @@
1
- # encoding: utf-8
2
-
3
- class Train::Transports::SSH
4
- class CiscoIOSConnection < BaseConnection
5
- class BadEnablePassword < Train::TransportError; end
6
-
7
- def initialize(options)
8
- super(options)
9
-
10
- # Extract options to avoid passing them in to `Net::SSH.start` later
11
- @host = options.delete(:host)
12
- @user = options.delete(:user)
13
- @port = options.delete(:port)
14
- @enable_password = options.delete(:enable_password)
15
-
16
- # Use all options left that are not `nil` for `Net::SSH.start` later
17
- @ssh_options = options.reject { |_key, value| value.nil? }
18
-
19
- @prompt = /^\S+[>#]\r\n.*$/
20
- end
21
-
22
- def uri
23
- "ssh://#{@user}@#{@host}:#{@port}"
24
- end
25
-
26
- def unique_identifier
27
- result = run_command_via_connection("show version | include Processor")
28
- result.stdout.split(" ")[-1]
29
- end
30
-
31
- private
32
-
33
- def establish_connection
34
- logger.debug("[SSH] opening connection to #{self}")
35
-
36
- Net::SSH.start(@host, @user, @ssh_options)
37
- end
38
-
39
- def session
40
- return @session unless @session.nil?
41
-
42
- @session = open_channel(establish_connection)
43
-
44
- # Escalate privilege to enable mode if password is given
45
- if @enable_password
46
- # This verifies we are not in privileged exec mode before running the
47
- # enable command. Otherwise, the password will be in history.
48
- if run_command_via_connection("show privilege").stdout.split[-1] != "15"
49
- # Extra newlines to get back to prompt if incorrect password is used
50
- run_command_via_connection("enable\n#{@enable_password}\n\n\n")
51
- end
52
- end
53
-
54
- # Prevent `--MORE--` by removing terminal length limit
55
- run_command_via_connection("terminal length 0")
56
-
57
- @session
58
- end
59
-
60
- def run_command_via_connection(cmd, &_data_handler)
61
- # Ensure buffer is empty before sending data
62
- @buf = ""
63
-
64
- logger.debug("[SSH] Running `#{cmd}` on #{self}")
65
- session.send_data(cmd + "\r\n")
66
-
67
- logger.debug("[SSH] waiting for prompt")
68
- until @buf =~ @prompt
69
- if @buf =~ /Bad (secrets|password)|Access denied/
70
- raise BadEnablePassword
71
- end
72
-
73
- session.connection.process(0)
74
- end
75
-
76
- # Save the buffer and clear it for the next command
77
- output = @buf.dup
78
- @buf = ""
79
-
80
- format_result(format_output(output, cmd))
81
- end
82
-
83
- ERROR_MATCHERS = [
84
- "Bad IP address",
85
- "Incomplete command",
86
- "Invalid input detected",
87
- "Unrecognized host",
88
- ].freeze
89
-
90
- # IOS commands do not have an exit code so we must compare the command
91
- # output with partial segments of known errors. Then, we return a
92
- # `CommandResult` with arguments in the correct position based on the
93
- # result.
94
- def format_result(result)
95
- if ERROR_MATCHERS.none? { |e| result.include?(e) }
96
- CommandResult.new(result, "", 0)
97
- else
98
- CommandResult.new("", result, 1)
99
- end
100
- end
101
-
102
- # The buffer (@buf) contains all data sent/received on the SSH channel so
103
- # we need to format the data to match what we would expect from Train
104
- def format_output(output, cmd)
105
- leading_prompt = /(\r\n|^)\S+[>#]/
106
- command_string = /#{Regexp.quote(cmd)}\r\n/
107
- trailing_prompt = /\S+[>#](\r\n|$)/
108
- trailing_line_endings = /(\r\n)+$/
109
-
110
- output
111
- .sub(leading_prompt, "")
112
- .sub(command_string, "")
113
- .gsub(trailing_prompt, "")
114
- .gsub(trailing_line_endings, "")
115
- end
116
-
117
- # Create an SSH channel that writes to @buf when data is received
118
- def open_channel(ssh)
119
- logger.debug("[SSH] opening SSH channel to #{self}")
120
- ssh.open_channel do |ch|
121
- ch.on_data do |_, data|
122
- @buf += data
123
- end
124
-
125
- ch.send_channel_request("shell") do |_, success|
126
- raise "Failed to open SSH shell" unless success
127
-
128
- logger.debug("[SSH] shell opened")
129
- end
130
- end
131
- end
132
- end
133
- end
@@ -1,240 +0,0 @@
1
- # encoding: utf-8
2
- #
3
- # author: Dominik Richter
4
- # author: Christoph Hartmann
5
-
6
- require_relative "../plugins"
7
- require_relative "../errors"
8
- require "mixlib/shellout"
9
-
10
- module Train::Transports
11
- class Local < Train.plugin(1)
12
- name "local"
13
-
14
- class PipeError < Train::TransportError; end
15
-
16
- def connection(_ = nil)
17
- @connection ||= Connection.new(@options)
18
- end
19
-
20
- class Connection < BaseConnection
21
- def initialize(options)
22
- super(options)
23
-
24
- @runner = if options[:command_runner]
25
- force_runner(options[:command_runner])
26
- else
27
- select_runner(options)
28
- end
29
- end
30
-
31
- def login_command
32
- nil # none, open your shell
33
- end
34
-
35
- def uri
36
- "local://"
37
- end
38
-
39
- private
40
-
41
- def select_runner(options)
42
- if os.windows?
43
- # Force a 64 bit poweshell if needed
44
- if RUBY_PLATFORM == "i386-mingw32" && os.arch == "x86_64"
45
- powershell_cmd = "#{ENV["SystemRoot"]}\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe"
46
- else
47
- powershell_cmd = "powershell"
48
- end
49
-
50
- # Attempt to use a named pipe but fallback to ShellOut if that fails
51
- begin
52
- WindowsPipeRunner.new(powershell_cmd)
53
- rescue PipeError
54
- WindowsShellRunner.new(powershell_cmd)
55
- end
56
- else
57
- GenericRunner.new(self, options)
58
- end
59
- end
60
-
61
- def force_runner(command_runner)
62
- case command_runner
63
- when :generic
64
- GenericRunner.new(self, options)
65
- when :windows_pipe
66
- WindowsPipeRunner.new
67
- when :windows_shell
68
- WindowsShellRunner.new
69
- else
70
- raise "Runner type `#{command_runner}` not supported"
71
- end
72
- end
73
-
74
- def run_command_via_connection(cmd, &_data_handler)
75
- # Use the runner if it is available
76
- return @runner.run_command(cmd) if defined?(@runner)
77
-
78
- # If we don't have a runner, such as at the beginning of setting up the
79
- # transport and performing the first few steps of OS detection, fall
80
- # back to shelling out.
81
- res = Mixlib::ShellOut.new(cmd)
82
- res.run_command
83
- Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
84
- rescue Errno::ENOENT => _
85
- CommandResult.new("", "", 1)
86
- end
87
-
88
- def file_via_connection(path)
89
- if os.windows?
90
- Train::File::Local::Windows.new(self, path)
91
- else
92
- Train::File::Local::Unix.new(self, path)
93
- end
94
- end
95
-
96
- class GenericRunner
97
- include_options Train::Extras::CommandWrapper
98
-
99
- def initialize(connection, options)
100
- @cmd_wrapper = Local::CommandWrapper.load(connection, options)
101
- end
102
-
103
- def run_command(cmd)
104
- if defined?(@cmd_wrapper) && !@cmd_wrapper.nil?
105
- cmd = @cmd_wrapper.run(cmd)
106
- end
107
-
108
- res = Mixlib::ShellOut.new(cmd)
109
- res.run_command
110
- Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
111
- end
112
- end
113
-
114
- class WindowsShellRunner
115
- require "json"
116
- require "base64"
117
-
118
- def initialize(powershell_cmd = "powershell")
119
- @powershell_cmd = powershell_cmd
120
- end
121
-
122
- def run_command(script)
123
- # Prevent progress stream from leaking into stderr
124
- script = "$ProgressPreference='SilentlyContinue';" + script
125
-
126
- # Encode script so PowerShell can use it
127
- script = script.encode("UTF-16LE", "UTF-8")
128
- base64_script = Base64.strict_encode64(script)
129
-
130
- cmd = "#{@powershell_cmd} -NoProfile -EncodedCommand #{base64_script}"
131
-
132
- res = Mixlib::ShellOut.new(cmd)
133
- res.run_command
134
- Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
135
- end
136
- end
137
-
138
- class WindowsPipeRunner
139
- require "json"
140
- require "base64"
141
- require "securerandom"
142
-
143
- def initialize(powershell_cmd = "powershell")
144
- @powershell_cmd = powershell_cmd
145
- @pipe = acquire_pipe
146
- raise PipeError if @pipe.nil?
147
- end
148
-
149
- # @param cmd The command to execute
150
- # @return Local::ComandResult with stdout, stderr and exitstatus
151
- # Note that exitstatus ($?) in PowerShell is boolean, but we use a numeric exit code.
152
- # A command that succeeds without setting an exit code will have exitstatus 0
153
- # A command that exits with an exit code will have that value as exitstatus
154
- # A command that fails (e.g. throws exception) before setting an exit code will have exitstatus 1
155
- def run_command(cmd)
156
- script = "$ProgressPreference='SilentlyContinue';" + cmd
157
- encoded_script = Base64.strict_encode64(script)
158
- @pipe.puts(encoded_script)
159
- @pipe.flush
160
- res = OpenStruct.new(JSON.parse(Base64.decode64(@pipe.readline)))
161
- Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
162
- end
163
-
164
- private
165
-
166
- def acquire_pipe
167
- pipe_name = "inspec_#{SecureRandom.hex}"
168
-
169
- start_pipe_server(pipe_name)
170
-
171
- pipe = nil
172
-
173
- # PowerShell needs time to create pipe.
174
- 100.times do
175
- begin
176
- pipe = open("//./pipe/#{pipe_name}", "r+")
177
- break
178
- rescue
179
- sleep 0.1
180
- end
181
- end
182
-
183
- pipe
184
- end
185
-
186
- def start_pipe_server(pipe_name)
187
- require "win32/process"
188
-
189
- script = <<-EOF
190
- $ErrorActionPreference = 'Stop'
191
-
192
- $pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream('#{pipe_name}')
193
- $pipeReader = New-Object System.IO.StreamReader($pipeServer)
194
- $pipeWriter = New-Object System.IO.StreamWriter($pipeServer)
195
-
196
- $pipeServer.WaitForConnection()
197
-
198
- # Create loop to receive and process user commands/scripts
199
- $clientConnected = $true
200
- while($clientConnected) {
201
- $input = $pipeReader.ReadLine()
202
- $command = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($input))
203
-
204
- # Execute user command/script and convert result to JSON
205
- $scriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock($command)
206
- try {
207
- $stdout = & $scriptBlock | Out-String
208
- $exit_code = $LastExitCode
209
- if ($exit_code -eq $null)
210
- {
211
- $exit_code = 0
212
- }
213
- $result = @{ 'stdout' = $stdout ; 'stderr' = ''; 'exitstatus' = $exit_code }
214
- } catch {
215
- $stderr = $_ | Out-String
216
- $exit_code = $LastExitCode
217
- $result = @{ 'stdout' = ''; 'stderr' = $stderr; 'exitstatus' = $exit_code }
218
- }
219
- $resultJSON = $result | ConvertTo-JSON
220
-
221
- # Encode JSON in Base64 and write to pipe
222
- $encodedResult = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($resultJSON))
223
- $pipeWriter.WriteLine($encodedResult)
224
- $pipeWriter.Flush()
225
- }
226
- EOF
227
-
228
- utf8_script = script.encode("UTF-16LE", "UTF-8")
229
- base64_script = Base64.strict_encode64(utf8_script)
230
- cmd = "#{@powershell_cmd} -NoProfile -ExecutionPolicy bypass -NonInteractive -EncodedCommand #{base64_script}"
231
-
232
- server_pid = Process.create(command_line: cmd).process_id
233
-
234
- # Ensure process is killed when the Train process exits
235
- at_exit { Process.kill("KILL", server_pid) }
236
- end
237
- end
238
- end
239
- end
240
- end
@@ -1,183 +0,0 @@
1
- require_relative "../plugins"
2
- require "digest"
3
-
4
- module Train::Transports
5
- class Mock < Train.plugin(1)
6
- name "mock"
7
-
8
- def initialize(conf = nil)
9
- @conf = conf || {}
10
- trace_calls if @conf[:trace]
11
- end
12
-
13
- def connection
14
- @connection ||= Connection.new(@conf)
15
- end
16
-
17
- def to_s
18
- "Mock Transport"
19
- end
20
-
21
- private
22
-
23
- def trace_calls
24
- interface_methods = {
25
- "Train::Transports::Mock" =>
26
- Train::Transports::Mock.instance_methods(false),
27
- "Train::Transports::Mock::Connection" =>
28
- Connection.instance_methods(false),
29
- "Train::Transports::Mock::Connection::File" =>
30
- Connection::FileCommon.instance_methods(false),
31
- "Train::Transports::Mock::Connection::OS" =>
32
- Train::Platform.instance_methods(false),
33
- }
34
-
35
- # rubocop:disable Metrics/ParameterLists
36
- # rubocop:disable Security/Eval
37
- set_trace_func(proc { |event, _file, _line, id, binding, classname|
38
- unless classname.to_s.start_with?("Train::Transports::Mock") &&
39
- (event == "call") &&
40
- interface_methods[classname.to_s].include?(id)
41
- next
42
- end
43
-
44
- # kindly borrowed from the wonderful simple-tracer by matugm
45
- arg_names = eval(
46
- "method(__method__).parameters.map { |arg| arg[1].to_s }",
47
- binding
48
- )
49
- args = eval("#{arg_names}.map { |arg| eval(arg) }", binding).join(", ")
50
- prefix = "-" * (classname.to_s.count(":") - 2) + "> "
51
- puts("#{prefix}#{id} #{args}")
52
- })
53
- # rubocop:enable all
54
- end
55
- end
56
- end
57
-
58
- class Train::Transports::Mock
59
- class Connection < BaseConnection
60
- attr_reader :options
61
-
62
- def initialize(conf = nil)
63
- super(conf)
64
- mock_os
65
- enable_cache(:file)
66
- enable_cache(:command)
67
- end
68
-
69
- def uri
70
- "mock://"
71
- end
72
-
73
- DEFAULTS = {
74
- name: "mock",
75
- family: "mock",
76
- release: "unknown",
77
- arch: "unknown",
78
- }.freeze
79
-
80
- def mock_os(value = {})
81
- value = DEFAULTS
82
- .merge(value || {})
83
- .transform_values { |v| v || "unknown" }
84
-
85
- platform = Train::Platforms.name(value[:name])
86
- platform.find_family_hierarchy
87
- platform.platform = value
88
- platform.add_platform_methods
89
- @platform = platform
90
- end
91
-
92
- def commands=(commands)
93
- @cache[:command] = commands
94
- end
95
-
96
- def commands
97
- @cache[:command]
98
- end
99
-
100
- def files=(files)
101
- @cache[:file] = files
102
- end
103
-
104
- def files
105
- @cache[:file]
106
- end
107
-
108
- def mock_command(cmd, stdout = nil, stderr = nil, exit_status = 0)
109
- @cache[:command][cmd] = Command.new(stdout || "", stderr || "", exit_status)
110
- end
111
-
112
- def command_not_found(cmd)
113
- if @options[:verbose]
114
- $stderr.puts("Command not mocked:")
115
- $stderr.puts(" " + cmd.to_s.split("\n").join("\n "))
116
- $stderr.puts(" SHA: " + Digest::SHA256.hexdigest(cmd.to_s))
117
- end
118
- # return a non-zero exit code
119
- mock_command(cmd, nil, nil, 1)
120
- end
121
-
122
- def file_not_found(path)
123
- $stderr.puts("File not mocked: " + path.to_s) if @options[:verbose]
124
- File.new(self, path)
125
- end
126
-
127
- def to_s
128
- "Mock Connection"
129
- end
130
-
131
- private
132
-
133
- def run_command_via_connection(cmd, &_data_handler)
134
- @cache[:command][Digest::SHA256.hexdigest cmd.to_s] ||
135
- command_not_found(cmd)
136
- end
137
-
138
- def file_via_connection(path)
139
- file_not_found(path)
140
- end
141
- end
142
- end
143
-
144
- class Train::Transports::Mock::Connection
145
- Command = Struct.new(:stdout, :stderr, :exit_status)
146
- end
147
-
148
- class Train::Transports::Mock::Connection
149
- class File < Train::File
150
- def self.from_json(json)
151
- res = new(json["backend"],
152
- json["path"],
153
- json["follow_symlink"])
154
- res.type = json["type"]
155
- Train::File::DATA_FIELDS.each do |f|
156
- m = (f.tr("?", "") + "=").to_sym
157
- res.method(m).call(json[f])
158
- end
159
- res
160
- end
161
-
162
- Train::File::DATA_FIELDS.each do |m|
163
- attr_accessor m.tr("?", "").to_sym
164
- next unless m.include?("?")
165
-
166
- define_method m.to_sym do
167
- method(m.tr("?", "").to_sym).call
168
- end
169
- end
170
- attr_accessor :type
171
-
172
- def initialize(backend, path, follow_symlink = true)
173
- super(backend, path, follow_symlink)
174
- @type = :unknown
175
- @exist = false
176
- end
177
-
178
- def mounted
179
- @mounted ||=
180
- @backend.run_command("mount | grep -- ' on #{@path}'")
181
- end
182
- end
183
- end