train-core 1.4.4

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