train 0.29.2 → 0.30.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/lib/train.rb +1 -0
  4. data/lib/train/errors.rb +6 -0
  5. data/lib/train/extras.rb +0 -1
  6. data/lib/train/platforms.rb +82 -0
  7. data/lib/train/platforms/common.rb +34 -0
  8. data/lib/train/platforms/detect.rb +12 -0
  9. data/lib/train/platforms/detect/helpers/os_common.rb +56 -0
  10. data/lib/train/platforms/detect/helpers/os_linux.rb +75 -0
  11. data/lib/train/{extras/os_detect_windows.rb → platforms/detect/helpers/os_windows.rb} +3 -10
  12. data/lib/train/platforms/detect/scanner.rb +84 -0
  13. data/lib/train/platforms/detect/specifications/os.rb +480 -0
  14. data/lib/train/platforms/family.rb +26 -0
  15. data/lib/train/platforms/platform.rb +80 -0
  16. data/lib/train/plugins/base_connection.rb +75 -27
  17. data/lib/train/transports/docker.rb +17 -28
  18. data/lib/train/transports/local.rb +21 -22
  19. data/lib/train/transports/mock.rb +44 -30
  20. data/lib/train/transports/ssh_connection.rb +55 -67
  21. data/lib/train/transports/winrm_connection.rb +16 -26
  22. data/lib/train/version.rb +1 -1
  23. data/test/unit/file/remote/linux_test.rb +2 -2
  24. data/test/unit/platforms/detect/os_common_test.rb +85 -0
  25. data/test/unit/platforms/detect/os_linux_test.rb +124 -0
  26. data/test/unit/{extras/os_detect_windows_test.rb → platforms/detect/os_windows_test.rb} +5 -2
  27. data/test/unit/platforms/detect/scanner_test.rb +61 -0
  28. data/test/unit/platforms/family_test.rb +32 -0
  29. data/test/unit/platforms/os_detect_test.rb +175 -0
  30. data/test/unit/{extras/os_common_test.rb → platforms/platform_test.rb} +103 -18
  31. data/test/unit/platforms/platforms_test.rb +42 -0
  32. data/test/unit/plugins/connection_test.rb +106 -8
  33. data/test/unit/transports/local_test.rb +20 -15
  34. data/test/unit/transports/mock_test.rb +16 -6
  35. data/test/unit/transports/ssh_test.rb +17 -15
  36. metadata +28 -19
  37. data/lib/train/extras/linux_lsb.rb +0 -60
  38. data/lib/train/extras/os_common.rb +0 -151
  39. data/lib/train/extras/os_detect_arista_eos.rb +0 -34
  40. data/lib/train/extras/os_detect_darwin.rb +0 -40
  41. data/lib/train/extras/os_detect_esx.rb +0 -22
  42. data/lib/train/extras/os_detect_linux.rb +0 -164
  43. data/lib/train/extras/os_detect_openvms.rb +0 -29
  44. data/lib/train/extras/os_detect_unix.rb +0 -106
  45. data/lib/train/extras/uname.rb +0 -28
  46. data/lib/train/transports/local_os.rb +0 -51
  47. data/test/unit/extras/os_detect_linux_test.rb +0 -230
@@ -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,80 @@
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 name
26
+ # Override here incase a updated name was set
27
+ # during the detect logic
28
+ @platform[:name] || @name
29
+ end
30
+
31
+ # This is for backwords compatability with
32
+ # the current inspec os resource.
33
+ def[](name)
34
+ if respond_to?(name)
35
+ send(name)
36
+ else
37
+ 'unknown'
38
+ end
39
+ end
40
+
41
+ def title(title = nil)
42
+ return @title if title.nil?
43
+ @title = title
44
+ self
45
+ end
46
+
47
+ def to_hash
48
+ @platform
49
+ end
50
+
51
+ # Add generic family? and platform methods to an existing platform
52
+ #
53
+ # This is done later to add any custom
54
+ # families/properties that were created
55
+ def add_platform_methods
56
+ family_list = Train::Platforms.families
57
+ family_list.each_value do |k|
58
+ next if respond_to?(k.name + '?')
59
+ define_singleton_method(k.name + '?') do
60
+ family_hierarchy.include?(k.name)
61
+ end
62
+ end
63
+
64
+ # Helper methods for direct platform info
65
+ @platform.each_key do |m|
66
+ next if respond_to?(m)
67
+ define_singleton_method(m) do
68
+ @platform[m]
69
+ end
70
+ end
71
+
72
+ # Create method for name if its not already true
73
+ plat_name = name.downcase.tr(' ', '_') + '?'
74
+ return if respond_to?(plat_name)
75
+ define_singleton_method(plat_name) do
76
+ true
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,8 +1,4 @@
1
1
  # encoding: utf-8
2
- #
3
- # Author:: Salim Afiune (<salim@afiunemaya.com.mx>)
4
- # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
5
- # Author:: Dominik Richter (<dominik.richter@gmail.com>)
6
2
 
7
3
  require 'train/errors'
8
4
  require 'train/extras'
@@ -19,9 +15,6 @@ class Train::Plugins::Transport
19
15
  class BaseConnection
20
16
  include Train::Extras
21
17
 
22
- # Provide access to the files cache.
23
- attr_reader :files
24
-
25
18
  # Create a new Connection instance.
26
19
  #
27
20
  # @param options [Hash] connection options
@@ -29,7 +22,35 @@ class Train::Plugins::Transport
29
22
  def initialize(options = nil)
30
23
  @options = options || {}
31
24
  @logger = @options.delete(:logger) || Logger.new(STDOUT)
32
- @files = {}
25
+ Train::Platforms::Detect::Specifications::OS.load
26
+
27
+ # default caching options
28
+ @cache_enabled = {
29
+ file: true,
30
+ command: false,
31
+ }
32
+
33
+ @cache = {}
34
+ @cache_enabled.each_key do |type|
35
+ clear_cache(type)
36
+ end
37
+ end
38
+
39
+ def cache_enabled?(type)
40
+ @cache_enabled[type.to_sym]
41
+ end
42
+
43
+ # Enable caching types for Train. Currently we support
44
+ # :file and :command types
45
+ def enable_cache(type)
46
+ fail Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)
47
+ @cache_enabled[type.to_sym] = true
48
+ end
49
+
50
+ def disable_cache(type)
51
+ fail Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)
52
+ @cache_enabled[type.to_sym] = false
53
+ clear_cache(type.to_sym)
33
54
  end
34
55
 
35
56
  # Closes the session connection, if it is still active.
@@ -39,39 +60,45 @@ class Train::Plugins::Transport
39
60
 
40
61
  def to_json
41
62
  {
42
- 'files' => Hash[@files.map { |x, y| [x, y.to_json] }],
63
+ 'files' => Hash[@cache[:file].map { |x, y| [x, y.to_json] }],
43
64
  }
44
65
  end
45
66
 
46
67
  def load_json(j)
47
68
  require 'train/transports/mock'
48
69
  j['files'].each do |path, jf|
49
- @files[path] = Train::Transports::Mock::Connection::File.from_json(jf)
70
+ @cache[:file][path] = Train::Transports::Mock::Connection::File.from_json(jf)
50
71
  end
51
72
  end
52
73
 
53
- # Execute a command using this connection.
54
- #
55
- # @param command [String] command string to execute
56
- # @return [CommandResult] contains the result of running the command
57
- def run_command(_command)
58
- fail Train::ClientError, "#{self.class} does not implement #run_command()"
74
+ # Is this a local transport?
75
+ def local?
76
+ false
59
77
  end
60
78
 
61
79
  # Get information on the operating system which this transport connects to.
62
80
  #
63
- # @return [OSCommon] operating system information
64
- def os
65
- fail Train::ClientError, "#{self.class} does not implement #os()"
81
+ # @return [Platform] system information
82
+ def platform
83
+ @platform ||= Train::Platforms::Detect.scan(self)
66
84
  end
85
+ # we need to keep os as a method for backwards compatibility with inspec
86
+ alias os platform
67
87
 
68
- # Interact with files on the target. Read, write, and get metadata
69
- # from files via the transport.
70
- #
71
- # @param [String] path which is being inspected
72
- # @return [FileCommon] file object that allows for interaction
73
- def file(_path, *_args)
74
- fail Train::ClientError, "#{self.class} does not implement #file(...)"
88
+ # This is the main command call for all connections. This will call the private
89
+ # run_command_via_connection on the connection with optional caching
90
+ def run_command(cmd)
91
+ return run_command_via_connection(cmd) unless cache_enabled?(:command)
92
+
93
+ @cache[:command][cmd] ||= run_command_via_connection(cmd)
94
+ end
95
+
96
+ # This is the main file call for all connections. This will call the private
97
+ # file_via_connection on the connection with optional caching
98
+ def file(path, *args)
99
+ return file_via_connection(path, *args) unless cache_enabled?(:file)
100
+
101
+ @cache[:file][path] ||= file_via_connection(path, *args)
75
102
  end
76
103
 
77
104
  # Builds a LoginCommand which can be used to open an interactive
@@ -79,7 +106,7 @@ class Train::Plugins::Transport
79
106
  #
80
107
  # @return [LoginCommand] array of command line tokens
81
108
  def login_command
82
- fail Train::ClientError, "#{self.class} does not implement #login_command()"
109
+ fail NotImplementedError, "#{self.class} does not implement #login_command()"
83
110
  end
84
111
 
85
112
  # Block and return only when the remote host is prepared and ready to
@@ -93,6 +120,27 @@ class Train::Plugins::Transport
93
120
 
94
121
  private
95
122
 
123
+ # Execute a command using this connection.
124
+ #
125
+ # @param command [String] command string to execute
126
+ # @return [CommandResult] contains the result of running the command
127
+ def run_command_via_connection(_command)
128
+ fail NotImplementedError, "#{self.class} does not implement #run_command_via_connection()"
129
+ end
130
+
131
+ # Interact with files on the target. Read, write, and get metadata
132
+ # from files via the transport.
133
+ #
134
+ # @param [String] path which is being inspected
135
+ # @return [FileCommon] file object that allows for interaction
136
+ def file_via_connection(_path, *_args)
137
+ fail NotImplementedError, "#{self.class} does not implement #file_via_connection(...)"
138
+ end
139
+
140
+ def clear_cache(type)
141
+ @cache[type.to_sym] = {}
142
+ end
143
+
96
144
  # @return [Logger] logger for reporting information
97
145
  # @api private
98
146
  attr_reader :logger
@@ -69,22 +69,27 @@ class Train::Transports::Docker
69
69
  # nothing to do at the moment
70
70
  end
71
71
 
72
- def os
73
- @os ||= OS.new(self)
72
+ def uri
73
+ if @container.nil?
74
+ "docker://#{@id}"
75
+ else
76
+ "docker://#{@container.id}"
77
+ end
74
78
  end
75
79
 
76
- def file(path)
77
- @files[path] ||=\
78
- if os.aix?
79
- Train::File::Remote::Aix.new(self, path)
80
- elsif os.solaris?
81
- Train::File::Remote::Unix.new(self, path)
82
- else
83
- Train::File::Remote::Linux.new(self, path)
84
- end
80
+ private
81
+
82
+ def file_via_connection(path)
83
+ if os.aix?
84
+ Train::File::Remote::Aix.new(self, path)
85
+ elsif os.solaris?
86
+ Train::File::Remote::Unix.new(self, path)
87
+ else
88
+ Train::File::Remote::Linux.new(self, path)
89
+ end
85
90
  end
86
91
 
87
- def run_command(cmd)
92
+ def run_command_via_connection(cmd)
88
93
  cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
89
94
  stdout, stderr, exit_status = @container.exec(
90
95
  [
@@ -97,21 +102,5 @@ class Train::Transports::Docker
97
102
  # @TODO: differentiate any other error
98
103
  raise
99
104
  end
100
-
101
- def uri
102
- if @container.nil?
103
- "docker://#{@id}"
104
- else
105
- "docker://#{@container.id}"
106
- end
107
- end
108
-
109
- class OS < OSCommon
110
- def initialize(backend)
111
- # hardcoded to unix/linux for now, until other operating systems
112
- # are supported
113
- super(backend, { family: 'unix' })
114
- end
115
- end
116
105
  end
117
106
  end
@@ -17,42 +17,41 @@ module Train::Transports
17
17
  end
18
18
 
19
19
  class Connection < BaseConnection
20
- require 'train/transports/local_os'
21
-
22
20
  def initialize(options)
23
21
  super(options)
24
22
  @cmd_wrapper = nil
25
23
  @cmd_wrapper = CommandWrapper.load(self, options)
26
24
  end
27
25
 
28
- def run_command(cmd)
29
- cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
30
- res = Mixlib::ShellOut.new(cmd)
31
- res.run_command
32
- CommandResult.new(res.stdout, res.stderr, res.exitstatus)
33
- rescue Errno::ENOENT => _
34
- CommandResult.new('', '', 1)
26
+ def login_command
27
+ nil # none, open your shell
35
28
  end
36
29
 
37
- def os
38
- @os ||= OS.new(self)
30
+ def uri
31
+ 'local://'
39
32
  end
40
33
 
41
- def file(path)
42
- @files[path] ||= \
43
- if os.windows?
44
- Train::File::Local::Windows.new(self, path)
45
- else
46
- Train::File::Local::Unix.new(self, path)
47
- end
34
+ def local?
35
+ true
48
36
  end
49
37
 
50
- def login_command
51
- nil # none, open your shell
38
+ private
39
+
40
+ def run_command_via_connection(cmd)
41
+ cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
42
+ res = Mixlib::ShellOut.new(cmd)
43
+ res.run_command
44
+ CommandResult.new(res.stdout, res.stderr, res.exitstatus)
45
+ rescue Errno::ENOENT => _
46
+ CommandResult.new('', '', 1)
52
47
  end
53
48
 
54
- def uri
55
- 'local://'
49
+ def file_via_connection(path)
50
+ if os.windows?
51
+ Train::File::Local::Windows.new(self, path)
52
+ else
53
+ Train::File::Local::Unix.new(self, path)
54
+ end
56
55
  end
57
56
  end
58
57
  end
@@ -1,7 +1,4 @@
1
1
  # encoding: utf-8
2
- #
3
- # author: Dominik Richter
4
- # author: Christoph Hartmann
5
2
 
6
3
  require 'train/plugins'
7
4
  require 'digest'
@@ -34,7 +31,7 @@ module Train::Transports
34
31
  'Train::Transports::Mock::Connection::File' =>
35
32
  Connection::FileCommon.instance_methods(false),
36
33
  'Train::Transports::Mock::Connection::OS' =>
37
- Connection::OSCommon.instance_methods(false),
34
+ Train::Platform.instance_methods(false),
38
35
  }
39
36
 
40
37
  # rubocop:disable Metrics/ParameterLists
@@ -60,13 +57,13 @@ end
60
57
 
61
58
  class Train::Transports::Mock
62
59
  class Connection < BaseConnection
63
- attr_accessor :files, :commands
64
60
  attr_reader :os
65
61
 
66
62
  def initialize(conf = nil)
67
63
  super(conf)
68
64
  mock_os
69
- @commands = {}
65
+ enable_cache(:file)
66
+ enable_cache(:command)
70
67
  end
71
68
 
72
69
  def uri
@@ -76,13 +73,41 @@ class Train::Transports::Mock
76
73
  def mock_os(value = {})
77
74
  # if a user passes a nil value, set to an empty hash so the merge still succeeds
78
75
  value ||= {}
76
+ value.each { |k, v| value[k] = 'unknown' if v.nil? }
77
+ value = { name: 'mock', family: 'mock', release: 'unknown', arch: 'unknown' }.merge(value)
79
78
 
80
- os_params = { name: 'unknown', family: 'unknown', release: 'unknown', arch: 'unknown' }.merge(value)
81
- @os = OS.new(self, os_params)
79
+ platform = Train::Platforms.name(value[:name])
80
+ platform.family_hierarchy = mock_os_hierarchy(platform).flatten
81
+ platform.platform = value
82
+ platform.add_platform_methods
83
+ @os = platform
84
+ end
85
+
86
+ def mock_os_hierarchy(plat)
87
+ plat.families.each_with_object([]) do |(k, _v), memo|
88
+ memo << k.name
89
+ memo << mock_os_hierarchy(k) unless k.families.empty?
90
+ end
91
+ end
92
+
93
+ def commands=(commands)
94
+ @cache[:command] = commands
95
+ end
96
+
97
+ def commands
98
+ @cache[:command]
99
+ end
100
+
101
+ def files=(files)
102
+ @cache[:file] = files
103
+ end
104
+
105
+ def files
106
+ @cache[:file]
82
107
  end
83
108
 
84
109
  def mock_command(cmd, stdout = nil, stderr = nil, exit_status = 0)
85
- @commands[cmd] = Command.new(stdout || '', stderr || '', exit_status)
110
+ @cache[:command][cmd] = Command.new(stdout || '', stderr || '', exit_status)
86
111
  end
87
112
 
88
113
  def command_not_found(cmd)
@@ -95,43 +120,32 @@ class Train::Transports::Mock
95
120
  mock_command(cmd, nil, nil, 1)
96
121
  end
97
122
 
98
- def run_command(cmd)
99
- @commands[cmd] ||
100
- @commands[Digest::SHA256.hexdigest cmd.to_s] ||
101
- command_not_found(cmd)
102
- end
103
-
104
123
  def file_not_found(path)
105
124
  STDERR.puts('File not mocked: '+path.to_s) if @options[:verbose]
106
125
  File.new(self, path)
107
126
  end
108
127
 
109
- def file(path)
110
- @files[path] ||= file_not_found(path)
111
- end
112
-
113
128
  def to_s
114
129
  'Mock Connection'
115
130
  end
116
- end
117
- end
118
131
 
119
- class Train::Transports::Mock::Connection
120
- Command = Struct.new(:stdout, :stderr, :exit_status)
121
- end
132
+ private
122
133
 
123
- class Train::Transports::Mock::Connection
124
- class OS < OSCommon
125
- def initialize(backend, desc)
126
- super(backend, desc)
134
+ def run_command_via_connection(cmd)
135
+ @cache[:command][Digest::SHA256.hexdigest cmd.to_s] ||
136
+ command_not_found(cmd)
127
137
  end
128
138
 
129
- def detect_family
130
- # no op, we do not need to detect the os
139
+ def file_via_connection(path)
140
+ file_not_found(path)
131
141
  end
132
142
  end
133
143
  end
134
144
 
145
+ class Train::Transports::Mock::Connection
146
+ Command = Struct.new(:stdout, :stderr, :exit_status)
147
+ end
148
+
135
149
  class Train::Transports::Mock::Connection
136
150
  class File < Train::File
137
151
  def self.from_json(json)