train-core 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +780 -0
  3. data/Gemfile +36 -0
  4. data/LICENSE +201 -0
  5. data/README.md +197 -0
  6. data/lib/train.rb +158 -0
  7. data/lib/train/errors.rb +32 -0
  8. data/lib/train/extras.rb +11 -0
  9. data/lib/train/extras/command_wrapper.rb +137 -0
  10. data/lib/train/extras/stat.rb +132 -0
  11. data/lib/train/file.rb +151 -0
  12. data/lib/train/file/local.rb +75 -0
  13. data/lib/train/file/local/unix.rb +96 -0
  14. data/lib/train/file/local/windows.rb +63 -0
  15. data/lib/train/file/remote.rb +36 -0
  16. data/lib/train/file/remote/aix.rb +21 -0
  17. data/lib/train/file/remote/linux.rb +19 -0
  18. data/lib/train/file/remote/qnx.rb +41 -0
  19. data/lib/train/file/remote/unix.rb +106 -0
  20. data/lib/train/file/remote/windows.rb +94 -0
  21. data/lib/train/options.rb +80 -0
  22. data/lib/train/platforms.rb +84 -0
  23. data/lib/train/platforms/common.rb +34 -0
  24. data/lib/train/platforms/detect.rb +12 -0
  25. data/lib/train/platforms/detect/helpers/os_common.rb +145 -0
  26. data/lib/train/platforms/detect/helpers/os_linux.rb +75 -0
  27. data/lib/train/platforms/detect/helpers/os_windows.rb +120 -0
  28. data/lib/train/platforms/detect/scanner.rb +84 -0
  29. data/lib/train/platforms/detect/specifications/api.rb +15 -0
  30. data/lib/train/platforms/detect/specifications/os.rb +578 -0
  31. data/lib/train/platforms/detect/uuid.rb +34 -0
  32. data/lib/train/platforms/family.rb +26 -0
  33. data/lib/train/platforms/platform.rb +101 -0
  34. data/lib/train/plugins.rb +40 -0
  35. data/lib/train/plugins/base_connection.rb +169 -0
  36. data/lib/train/plugins/transport.rb +49 -0
  37. data/lib/train/transports/local.rb +232 -0
  38. data/lib/train/version.rb +7 -0
  39. data/train-core.gemspec +27 -0
  40. metadata +116 -0
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ require 'digest/sha1'
4
+ require 'securerandom'
5
+ require 'json'
6
+
7
+ module Train::Platforms::Detect
8
+ class UUID
9
+ include Train::Platforms::Detect::Helpers::OSCommon
10
+
11
+ def initialize(platform)
12
+ @platform = platform
13
+ @backend = @platform.backend
14
+ end
15
+
16
+ def find_or_create_uuid
17
+ # for api transports uuid is defined on the connection
18
+ if defined?(@backend.unique_identifier)
19
+ uuid_from_string(@backend.unique_identifier)
20
+ elsif @platform.unix?
21
+ unix_uuid
22
+ elsif @platform.windows?
23
+ windows_uuid
24
+ else
25
+ if @platform[:uuid_command]
26
+ result = @backend.run_command(@platform[:uuid_command])
27
+ return uuid_from_string(result.stdout.chomp) if result.exit_status.zero? && !result.stdout.empty?
28
+ end
29
+
30
+ raise 'Could not find platform uuid! Please set a uuid_command for your platform.'
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+
3
+ module Train::Platforms
4
+ class Family
5
+ include Train::Platforms::Common
6
+ attr_accessor :children, :condition, :families, :name
7
+
8
+ def initialize(name, condition)
9
+ @name = name
10
+ @condition = condition
11
+ @families = {}
12
+ @children = {}
13
+ @detect = nil
14
+ @title = "#{name.to_s.capitalize} Family"
15
+
16
+ # add itself to the families list
17
+ Train::Platforms.families[@name.to_s] = self
18
+ end
19
+
20
+ def title(title = nil)
21
+ return @title if title.nil?
22
+ @title = title
23
+ self
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,101 @@
1
+ # encoding: utf-8
2
+
3
+ module Train::Platforms
4
+ class Platform
5
+ include Train::Platforms::Common
6
+ attr_accessor :backend, :condition, :families, :family_hierarchy, :platform
7
+
8
+ def initialize(name, condition = {})
9
+ @name = name
10
+ @condition = condition
11
+ @families = {}
12
+ @family_hierarchy = []
13
+ @platform = {}
14
+ @detect = nil
15
+ @title = name.to_s.capitalize
16
+
17
+ # add itself to the platform list
18
+ Train::Platforms.list[name] = self
19
+ end
20
+
21
+ def direct_families
22
+ @families.collect { |k, _v| k.name }
23
+ end
24
+
25
+ def family
26
+ @platform[:family] || @family_hierarchy[0]
27
+ end
28
+
29
+ def name
30
+ # Override here incase a updated name was set
31
+ # during the detect logic
32
+ clean_name
33
+ end
34
+
35
+ def clean_name(force: false)
36
+ @cleaned_name = nil if force
37
+ @cleaned_name ||= begin
38
+ name = (@platform[:name] || @name)
39
+ name.downcase!.tr!(' ', '_') if name =~ /[A-Z ]/
40
+ name
41
+ end
42
+ end
43
+
44
+ def uuid
45
+ @uuid ||= Train::Platforms::Detect::UUID.new(self).find_or_create_uuid.downcase
46
+ end
47
+
48
+ # This is for backwords compatability with
49
+ # the current inspec os resource.
50
+ def[](name)
51
+ if respond_to?(name)
52
+ send(name)
53
+ else
54
+ 'unknown'
55
+ end
56
+ end
57
+
58
+ def title(title = nil)
59
+ return @title if title.nil?
60
+ @title = title
61
+ self
62
+ end
63
+
64
+ def to_hash
65
+ @platform
66
+ end
67
+
68
+ # Add generic family? and platform methods to an existing platform
69
+ #
70
+ # This is done later to add any custom
71
+ # families/properties that were created
72
+ def add_platform_methods
73
+ # Redo clean name if there is a detect override
74
+ clean_name(force: true) unless @platform[:name].nil?
75
+
76
+ # Add in family methods
77
+ family_list = Train::Platforms.families
78
+ family_list.each_value do |k|
79
+ next if respond_to?(k.name + '?')
80
+ define_singleton_method(k.name + '?') do
81
+ family_hierarchy.include?(k.name)
82
+ end
83
+ end
84
+
85
+ # Helper methods for direct platform info
86
+ @platform.each_key do |m|
87
+ next if respond_to?(m)
88
+ define_singleton_method(m) do
89
+ @platform[m]
90
+ end
91
+ end
92
+
93
+ # Create method for name if its not already true
94
+ m = name + '?'
95
+ return if respond_to?(m)
96
+ define_singleton_method(m) do
97
+ true
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+ # Author:: Christoph Hartmann (<chris@lollyrock.com>)
5
+
6
+ require 'train/errors'
7
+
8
+ module Train
9
+ class Plugins
10
+ require 'train/plugins/transport'
11
+
12
+ class << self
13
+ # Retrieve the current plugin registry, containing all plugin names
14
+ # and their transport handlers.
15
+ #
16
+ # @return [Hash] map with plugin names and plugins
17
+ def registry
18
+ @registry ||= {}
19
+ end
20
+ end
21
+ end
22
+
23
+ # Create a new plugin by inheriting from the class returned by this method.
24
+ # Create a versioned plugin by providing the transport layer plugin version
25
+ # to this method. It will then select the correct class to inherit from.
26
+ #
27
+ # The plugin version determins what methods will be available to your plugin.
28
+ #
29
+ # @param [Int] version = 1 the plugin version to use
30
+ # @return [Transport] the versioned transport base class
31
+ def self.plugin(version = 1)
32
+ if version != 1
33
+ fail ClientError,
34
+ 'Only understand train plugin version 1. You are trying to '\
35
+ "initialize a train plugin #{version}, which is not supported "\
36
+ 'in the current release of train.'
37
+ end
38
+ ::Train::Plugins::Transport
39
+ end
40
+ end
@@ -0,0 +1,169 @@
1
+ # encoding: utf-8
2
+
3
+ require 'train/errors'
4
+ require 'train/extras'
5
+ require 'train/file'
6
+ require 'logger'
7
+
8
+ class Train::Plugins::Transport
9
+ # A Connection instance can be generated and re-generated, given new
10
+ # connection details such as connection port, hostname, credentials, etc.
11
+ # This object is responsible for carrying out the actions on the remote
12
+ # host such as executing commands, transferring files, etc.
13
+ #
14
+ # @author Fletcher Nichol <fnichol@nichol.ca>
15
+ class BaseConnection
16
+ include Train::Extras
17
+
18
+ # Create a new Connection instance.
19
+ #
20
+ # @param options [Hash] connection options
21
+ # @yield [self] yields itself for block-style invocation
22
+ def initialize(options = nil)
23
+ @options = options || {}
24
+ @logger = @options.delete(:logger) || Logger.new(STDOUT)
25
+ Train::Platforms::Detect::Specifications::OS.load
26
+ Train::Platforms::Detect::Specifications::Api.load
27
+
28
+ # default caching options
29
+ @cache_enabled = {
30
+ file: true,
31
+ command: false,
32
+ }
33
+
34
+ @cache = {}
35
+ @cache_enabled.each_key do |type|
36
+ clear_cache(type)
37
+ end
38
+ end
39
+
40
+ def cache_enabled?(type)
41
+ @cache_enabled[type.to_sym]
42
+ end
43
+
44
+ # Enable caching types for Train. Currently we support
45
+ # :file and :command types
46
+ def enable_cache(type)
47
+ fail Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)
48
+ @cache_enabled[type.to_sym] = true
49
+ end
50
+
51
+ def disable_cache(type)
52
+ fail Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)
53
+ @cache_enabled[type.to_sym] = false
54
+ clear_cache(type.to_sym)
55
+ end
56
+
57
+ # Closes the session connection, if it is still active.
58
+ def close
59
+ # this method may be left unimplemented if that is applicable
60
+ end
61
+
62
+ def to_json
63
+ {
64
+ 'files' => Hash[@cache[:file].map { |x, y| [x, y.to_json] }],
65
+ }
66
+ end
67
+
68
+ def load_json(j)
69
+ require 'train/transports/mock'
70
+ j['files'].each do |path, jf|
71
+ @cache[:file][path] = Train::Transports::Mock::Connection::File.from_json(jf)
72
+ end
73
+ end
74
+
75
+ # Is this a local transport?
76
+ def local?
77
+ false
78
+ end
79
+
80
+ def direct_platform(name, platform_details = nil)
81
+ plat = Train::Platforms.name(name)
82
+ plat.backend = self
83
+ plat.platform = platform_details unless platform_details.nil?
84
+ plat.family_hierarchy = family_hierarchy(plat).flatten
85
+ plat.add_platform_methods
86
+ plat
87
+ end
88
+
89
+ def family_hierarchy(plat)
90
+ plat.families.each_with_object([]) do |(k, _v), memo|
91
+ memo << k.name
92
+ memo << family_hierarchy(k) unless k.families.empty?
93
+ end
94
+ end
95
+
96
+ # Get information on the operating system which this transport connects to.
97
+ #
98
+ # @return [Platform] system information
99
+ def platform
100
+ @platform ||= Train::Platforms::Detect.scan(self)
101
+ end
102
+ # we need to keep os as a method for backwards compatibility with inspec
103
+ alias os platform
104
+
105
+ # This is the main command call for all connections. This will call the private
106
+ # run_command_via_connection on the connection with optional caching
107
+ def run_command(cmd)
108
+ return run_command_via_connection(cmd) unless cache_enabled?(:command)
109
+
110
+ @cache[:command][cmd] ||= run_command_via_connection(cmd)
111
+ end
112
+
113
+ # This is the main file call for all connections. This will call the private
114
+ # file_via_connection on the connection with optional caching
115
+ def file(path, *args)
116
+ return file_via_connection(path, *args) unless cache_enabled?(:file)
117
+
118
+ @cache[:file][path] ||= file_via_connection(path, *args)
119
+ end
120
+
121
+ # Builds a LoginCommand which can be used to open an interactive
122
+ # session on the remote host.
123
+ #
124
+ # @return [LoginCommand] array of command line tokens
125
+ def login_command
126
+ fail NotImplementedError, "#{self.class} does not implement #login_command()"
127
+ end
128
+
129
+ # Block and return only when the remote host is prepared and ready to
130
+ # execute command and upload files. The semantics and details will
131
+ # vary by implementation, but a round trip through the hosted
132
+ # service is preferred to simply waiting on a socket to become
133
+ # available.
134
+ def wait_until_ready
135
+ # this method may be left unimplemented if that is applicablelog
136
+ end
137
+
138
+ private
139
+
140
+ # Execute a command using this connection.
141
+ #
142
+ # @param command [String] command string to execute
143
+ # @return [CommandResult] contains the result of running the command
144
+ def run_command_via_connection(_command)
145
+ fail NotImplementedError, "#{self.class} does not implement #run_command_via_connection()"
146
+ end
147
+
148
+ # Interact with files on the target. Read, write, and get metadata
149
+ # from files via the transport.
150
+ #
151
+ # @param [String] path which is being inspected
152
+ # @return [FileCommon] file object that allows for interaction
153
+ def file_via_connection(_path, *_args)
154
+ fail NotImplementedError, "#{self.class} does not implement #file_via_connection(...)"
155
+ end
156
+
157
+ def clear_cache(type)
158
+ @cache[type.to_sym] = {}
159
+ end
160
+
161
+ # @return [Logger] logger for reporting information
162
+ # @api private
163
+ attr_reader :logger
164
+
165
+ # @return [Hash] connection options
166
+ # @api private
167
+ attr_reader :options
168
+ end
169
+ end
@@ -0,0 +1,49 @@
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 'train/errors'
8
+ require 'train/extras'
9
+ require 'train/options'
10
+
11
+ class Train::Plugins
12
+ class Transport
13
+ include Train::Extras
14
+ Train::Options.attach(self)
15
+
16
+ require 'train/plugins/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)
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
+ fail 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
@@ -0,0 +1,232 @@
1
+ # encoding: utf-8
2
+ #
3
+ # author: Dominik Richter
4
+ # author: Christoph Hartmann
5
+
6
+ require 'train/plugins'
7
+ require 'train/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 local?
32
+ true
33
+ end
34
+
35
+ def login_command
36
+ nil # none, open your shell
37
+ end
38
+
39
+ def uri
40
+ 'local://'
41
+ end
42
+
43
+ private
44
+
45
+ def select_runner(options)
46
+ if os.windows?
47
+ # Force a 64 bit poweshell if needed
48
+ if RUBY_PLATFORM == 'i386-mingw32' && os.arch == 'x86_64'
49
+ powershell_cmd = "#{ENV['SystemRoot']}\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe"
50
+ else
51
+ powershell_cmd = 'powershell'
52
+ end
53
+
54
+ # Attempt to use a named pipe but fallback to ShellOut if that fails
55
+ begin
56
+ WindowsPipeRunner.new(powershell_cmd)
57
+ rescue PipeError
58
+ WindowsShellRunner.new(powershell_cmd)
59
+ end
60
+ else
61
+ GenericRunner.new(self, options)
62
+ end
63
+ end
64
+
65
+ def force_runner(command_runner)
66
+ case command_runner
67
+ when :generic
68
+ GenericRunner.new(self, options)
69
+ when :windows_pipe
70
+ WindowsPipeRunner.new
71
+ when :windows_shell
72
+ WindowsShellRunner.new
73
+ else
74
+ fail "Runner type `#{command_runner}` not supported"
75
+ end
76
+ end
77
+
78
+ def run_command_via_connection(cmd)
79
+ # Use the runner if it is available
80
+ return @runner.run_command(cmd) if defined?(@runner)
81
+
82
+ # If we don't have a runner, such as at the beginning of setting up the
83
+ # transport and performing the first few steps of OS detection, fall
84
+ # back to shelling out.
85
+ res = Mixlib::ShellOut.new(cmd)
86
+ res.run_command
87
+ Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
88
+ rescue Errno::ENOENT => _
89
+ CommandResult.new('', '', 1)
90
+ end
91
+
92
+ def file_via_connection(path)
93
+ if os.windows?
94
+ Train::File::Local::Windows.new(self, path)
95
+ else
96
+ Train::File::Local::Unix.new(self, path)
97
+ end
98
+ end
99
+
100
+ class GenericRunner
101
+ include_options Train::Extras::CommandWrapper
102
+
103
+ def initialize(connection, options)
104
+ @cmd_wrapper = Local::CommandWrapper.load(connection, options)
105
+ end
106
+
107
+ def run_command(cmd)
108
+ if defined?(@cmd_wrapper) && !@cmd_wrapper.nil?
109
+ cmd = @cmd_wrapper.run(cmd)
110
+ end
111
+
112
+ res = Mixlib::ShellOut.new(cmd)
113
+ res.run_command
114
+ Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
115
+ end
116
+ end
117
+
118
+ class WindowsShellRunner
119
+ require 'json'
120
+ require 'base64'
121
+
122
+ def initialize(powershell_cmd = 'powershell')
123
+ @powershell_cmd = powershell_cmd
124
+ end
125
+
126
+ def run_command(script)
127
+ # Prevent progress stream from leaking into stderr
128
+ script = "$ProgressPreference='SilentlyContinue';" + script
129
+
130
+ # Encode script so PowerShell can use it
131
+ script = script.encode('UTF-16LE', 'UTF-8')
132
+ base64_script = Base64.strict_encode64(script)
133
+
134
+ cmd = "#{@powershell_cmd} -NoProfile -EncodedCommand #{base64_script}"
135
+
136
+ res = Mixlib::ShellOut.new(cmd)
137
+ res.run_command
138
+ Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
139
+ end
140
+ end
141
+
142
+ class WindowsPipeRunner
143
+ require 'json'
144
+ require 'base64'
145
+ require 'securerandom'
146
+
147
+ def initialize(powershell_cmd = 'powershell')
148
+ @powershell_cmd = powershell_cmd
149
+ @pipe = acquire_pipe
150
+ fail PipeError if @pipe.nil?
151
+ end
152
+
153
+ def run_command(cmd)
154
+ script = "$ProgressPreference='SilentlyContinue';" + cmd
155
+ encoded_script = Base64.strict_encode64(script)
156
+ @pipe.puts(encoded_script)
157
+ @pipe.flush
158
+ res = OpenStruct.new(JSON.parse(Base64.decode64(@pipe.readline)))
159
+ Local::CommandResult.new(res.stdout, res.stderr, res.exitstatus)
160
+ end
161
+
162
+ private
163
+
164
+ def acquire_pipe
165
+ pipe_name = "inspec_#{SecureRandom.hex}"
166
+
167
+ start_pipe_server(pipe_name)
168
+
169
+ pipe = nil
170
+
171
+ # PowerShell needs time to create pipe.
172
+ 100.times do
173
+ begin
174
+ pipe = open("//./pipe/#{pipe_name}", 'r+')
175
+ break
176
+ rescue
177
+ sleep 0.1
178
+ end
179
+ end
180
+
181
+ pipe
182
+ end
183
+
184
+ def start_pipe_server(pipe_name)
185
+ require 'win32/process'
186
+
187
+ script = <<-EOF
188
+ $ErrorActionPreference = 'Stop'
189
+
190
+ $pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream('#{pipe_name}')
191
+ $pipeReader = New-Object System.IO.StreamReader($pipeServer)
192
+ $pipeWriter = New-Object System.IO.StreamWriter($pipeServer)
193
+
194
+ $pipeServer.WaitForConnection()
195
+
196
+ # Create loop to receive and process user commands/scripts
197
+ $clientConnected = $true
198
+ while($clientConnected) {
199
+ $input = $pipeReader.ReadLine()
200
+ $command = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($input))
201
+
202
+ # Execute user command/script and convert result to JSON
203
+ $scriptBlock = $ExecutionContext.InvokeCommand.NewScriptBlock($command)
204
+ try {
205
+ $stdout = & $scriptBlock | Out-String
206
+ $result = @{ 'stdout' = $stdout ; 'stderr' = ''; 'exitstatus' = 0 }
207
+ } catch {
208
+ $stderr = $_ | Out-String
209
+ $result = @{ 'stdout' = ''; 'stderr' = $_; 'exitstatus' = 1 }
210
+ }
211
+ $resultJSON = $result | ConvertTo-JSON
212
+
213
+ # Encode JSON in Base64 and write to pipe
214
+ $encodedResult = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($resultJSON))
215
+ $pipeWriter.WriteLine($encodedResult)
216
+ $pipeWriter.Flush()
217
+ }
218
+ EOF
219
+
220
+ utf8_script = script.encode('UTF-16LE', 'UTF-8')
221
+ base64_script = Base64.strict_encode64(utf8_script)
222
+ cmd = "#{@powershell_cmd} -NoProfile -ExecutionPolicy bypass -NonInteractive -EncodedCommand #{base64_script}"
223
+
224
+ server_pid = Process.create(command_line: cmd).process_id
225
+
226
+ # Ensure process is killed when the Train process exits
227
+ at_exit { Process.kill('KILL', server_pid) }
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end