train 0.12.1

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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +71 -0
  3. data/CHANGELOG.md +308 -0
  4. data/Gemfile +30 -0
  5. data/LICENSE +201 -0
  6. data/README.md +156 -0
  7. data/Rakefile +148 -0
  8. data/lib/train.rb +117 -0
  9. data/lib/train/errors.rb +23 -0
  10. data/lib/train/extras.rb +17 -0
  11. data/lib/train/extras/command_wrapper.rb +148 -0
  12. data/lib/train/extras/file_aix.rb +20 -0
  13. data/lib/train/extras/file_common.rb +161 -0
  14. data/lib/train/extras/file_linux.rb +16 -0
  15. data/lib/train/extras/file_unix.rb +79 -0
  16. data/lib/train/extras/file_windows.rb +91 -0
  17. data/lib/train/extras/linux_lsb.rb +60 -0
  18. data/lib/train/extras/os_common.rb +136 -0
  19. data/lib/train/extras/os_detect_darwin.rb +32 -0
  20. data/lib/train/extras/os_detect_linux.rb +148 -0
  21. data/lib/train/extras/os_detect_unix.rb +99 -0
  22. data/lib/train/extras/os_detect_windows.rb +57 -0
  23. data/lib/train/extras/stat.rb +133 -0
  24. data/lib/train/options.rb +80 -0
  25. data/lib/train/plugins.rb +40 -0
  26. data/lib/train/plugins/base_connection.rb +86 -0
  27. data/lib/train/plugins/transport.rb +49 -0
  28. data/lib/train/transports/docker.rb +103 -0
  29. data/lib/train/transports/local.rb +52 -0
  30. data/lib/train/transports/local_file.rb +90 -0
  31. data/lib/train/transports/local_os.rb +51 -0
  32. data/lib/train/transports/mock.rb +147 -0
  33. data/lib/train/transports/ssh.rb +163 -0
  34. data/lib/train/transports/ssh_connection.rb +225 -0
  35. data/lib/train/transports/winrm.rb +184 -0
  36. data/lib/train/transports/winrm_connection.rb +194 -0
  37. data/lib/train/version.rb +7 -0
  38. data/test/integration/.kitchen.yml +43 -0
  39. data/test/integration/Berksfile +3 -0
  40. data/test/integration/bootstrap.sh +17 -0
  41. data/test/integration/chefignore +1 -0
  42. data/test/integration/cookbooks/test/metadata.rb +1 -0
  43. data/test/integration/cookbooks/test/recipes/default.rb +100 -0
  44. data/test/integration/cookbooks/test/recipes/prep_files.rb +47 -0
  45. data/test/integration/docker_run.rb +153 -0
  46. data/test/integration/docker_test.rb +24 -0
  47. data/test/integration/docker_test_container.rb +24 -0
  48. data/test/integration/helper.rb +61 -0
  49. data/test/integration/sudo/customcommand.rb +15 -0
  50. data/test/integration/sudo/nopasswd.rb +16 -0
  51. data/test/integration/sudo/passwd.rb +21 -0
  52. data/test/integration/sudo/reqtty.rb +17 -0
  53. data/test/integration/sudo/run_as.rb +12 -0
  54. data/test/integration/test-travis-1.yaml +13 -0
  55. data/test/integration/test-travis-2.yaml +13 -0
  56. data/test/integration/test_local.rb +19 -0
  57. data/test/integration/test_ssh.rb +39 -0
  58. data/test/integration/tests/path_block_device_test.rb +74 -0
  59. data/test/integration/tests/path_character_device_test.rb +74 -0
  60. data/test/integration/tests/path_file_test.rb +79 -0
  61. data/test/integration/tests/path_folder_test.rb +90 -0
  62. data/test/integration/tests/path_missing_test.rb +77 -0
  63. data/test/integration/tests/path_pipe_test.rb +78 -0
  64. data/test/integration/tests/path_symlink_test.rb +95 -0
  65. data/test/integration/tests/run_command_test.rb +28 -0
  66. data/test/unit/extras/command_wrapper_test.rb +78 -0
  67. data/test/unit/extras/file_common_test.rb +180 -0
  68. data/test/unit/extras/linux_file_test.rb +167 -0
  69. data/test/unit/extras/os_common_test.rb +269 -0
  70. data/test/unit/extras/os_detect_linux_test.rb +189 -0
  71. data/test/unit/extras/os_detect_windows_test.rb +99 -0
  72. data/test/unit/extras/stat_test.rb +148 -0
  73. data/test/unit/extras/windows_file_test.rb +44 -0
  74. data/test/unit/helper.rb +7 -0
  75. data/test/unit/plugins/connection_test.rb +44 -0
  76. data/test/unit/plugins/transport_test.rb +111 -0
  77. data/test/unit/plugins_test.rb +22 -0
  78. data/test/unit/train_test.rb +156 -0
  79. data/test/unit/transports/local_file_test.rb +184 -0
  80. data/test/unit/transports/local_test.rb +87 -0
  81. data/test/unit/transports/mock_test.rb +87 -0
  82. data/test/unit/transports/ssh_test.rb +109 -0
  83. data/test/unit/version_test.rb +8 -0
  84. data/test/windows/local_test.rb +46 -0
  85. data/test/windows/winrm_test.rb +52 -0
  86. data/train.gemspec +38 -0
  87. metadata +295 -0
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+ #
5
+ # This is heavily based on:
6
+ #
7
+ # OHAI https://github.com/chef/ohai
8
+ # by Adam Jacob, Chef Software Inc
9
+ #
10
+ module Train::Extras
11
+ module DetectWindows
12
+ def detect_windows
13
+ res = @backend.run_command('cmd /c ver')
14
+ return false if res.exit_status != 0 or res.stdout.empty?
15
+
16
+ # if the ver contains `Windows`, we know its a Windows system
17
+ version = res.stdout.strip
18
+ return false unless version.downcase =~ /windows/
19
+ @platform[:family] = 'windows'
20
+
21
+ # try to extract release from eg. `Microsoft Windows [Version 6.3.9600]`
22
+ release = /\[(?<name>.*)\]/.match(version)
23
+ unless release[:name].nil?
24
+ # release is 6.3.9600 now
25
+ @platform[:release] = release[:name].downcase.gsub('version', '').strip
26
+ # fallback, if we are not able to extract the name from wmic later
27
+ @platform[:name] = "Windows #{@platform[:release]}"
28
+ end
29
+
30
+ # try to use wmic, but lets keep it optional
31
+ read_wmic
32
+
33
+ true
34
+ end
35
+
36
+ # reads os name and version from wmic
37
+ # @see https://msdn.microsoft.com/en-us/library/bb742610.aspx#EEAA
38
+ # Thanks to Matt Wrock (https://github.com/mwrock) for this hint
39
+ def read_wmic
40
+ res = @backend.run_command('wmic os get * /format:list')
41
+ if res.exit_status == 0
42
+ sys_info = {}
43
+ res.stdout.lines.each { |line|
44
+ m = /^\s*([^=]*?)\s*=\s*(.*?)\s*$/.match(line)
45
+ sys_info[m[1].to_sym] = m[2] unless m.nil? || m[1].nil?
46
+ }
47
+
48
+ @platform[:release] = sys_info[:Version]
49
+ # additional info on windows
50
+ @platform[:build] = sys_info[:BuildNumber]
51
+ @platform[:name] = sys_info[:Caption]
52
+ @platform[:name] = @platform[:name].gsub('Microsoft', '').strip unless @platform[:name].empty?
53
+ @platform[:arch] = sys_info[:OSArchitecture]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,133 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ module Train::Extras
6
+ class Stat
7
+ TYPES = {
8
+ socket: 00140000,
9
+ symlink: 00120000,
10
+ file: 00100000,
11
+ block_device: 00060000,
12
+ directory: 00040000,
13
+ character_device: 00020000,
14
+ pipe: 00010000,
15
+ }.freeze
16
+
17
+ def self.find_type(mode)
18
+ res = TYPES.find { |_, mask| mask & mode == mask }
19
+ res.nil? ? :unknown : res[0]
20
+ end
21
+
22
+ def self.stat(shell_escaped_path, backend, follow_symlink)
23
+ # use perl scripts for aix and solaris 10
24
+ if backend.os.aix? || (backend.os.solaris? && backend.os[:release].to_i < 11) || backend.os.hpux?
25
+ return aix_stat(shell_escaped_path, backend, follow_symlink)
26
+ end
27
+ return bsd_stat(shell_escaped_path, backend, follow_symlink) if backend.os.bsd?
28
+ # linux and solaris 11 will use standard linux stats
29
+ return linux_stat(shell_escaped_path, backend, follow_symlink) if backend.os.unix?
30
+ # all other cases we don't handle
31
+ # TODO: print an error if we get here, as it shouldn't be invoked
32
+ # on non-unix
33
+ {}
34
+ end
35
+
36
+ def self.linux_stat(shell_escaped_path, backend, follow_symlink)
37
+ lstat = follow_symlink ? ' -L' : ''
38
+ res = backend.run_command("stat#{lstat} #{shell_escaped_path} 2>/dev/null --printf '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'")
39
+
40
+ # ignore the exit_code: it is != 0 if selinux labels are not supported
41
+ # on the system.
42
+
43
+ fields = res.stdout.split("\n")
44
+ return {} if fields.length != 9
45
+
46
+ tmask = fields[1].to_i(16)
47
+ selinux = fields[8]
48
+ selinux = nil if selinux == '?' or selinux == '(null)'
49
+
50
+ {
51
+ type: find_type(tmask),
52
+ mode: tmask & 07777,
53
+ owner: fields[2],
54
+ uid: fields[3].to_i,
55
+ group: fields[4],
56
+ gid: fields[5].to_i,
57
+ mtime: fields[7].to_i,
58
+ size: fields[0].to_i,
59
+ selinux_label: selinux,
60
+ }
61
+ end
62
+
63
+ def self.bsd_stat(shell_escaped_path, backend, follow_symlink)
64
+ # From stat man page on FreeBSD:
65
+ # z The size of file in bytes (st_size).
66
+ # p File type and permissions (st_mode).
67
+ # u, g User ID and group ID of file's owner (st_uid, st_gid).
68
+ # a, m, c, B
69
+ # The time file was last accessed or modified, or when the
70
+ # inode was last changed, or the birth time of the inode
71
+ # (st_atime, st_mtime, st_ctime, st_birthtime).
72
+ #
73
+ # The special output specifier S may be used to indicate that the
74
+ # output, if applicable, should be in string format. May be used
75
+ # in combination with:
76
+ # ...
77
+ # gu Display group or user name.
78
+ lstat = follow_symlink ? ' -L' : ''
79
+ res = backend.run_command(
80
+ "stat#{lstat} -f '%z\n%p\n%Su\n%u\n%Sg\n%g\n%a\n%m' "\
81
+ "#{shell_escaped_path}")
82
+
83
+ return {} if res.exit_status != 0
84
+
85
+ fields = res.stdout.split("\n")
86
+ return {} if fields.length != 8
87
+
88
+ tmask = fields[1].to_i(8)
89
+
90
+ {
91
+ type: find_type(tmask),
92
+ mode: tmask & 07777,
93
+ owner: fields[2],
94
+ uid: fields[3].to_i,
95
+ group: fields[4],
96
+ gid: fields[5].to_i,
97
+ mtime: fields[7].to_i,
98
+ size: fields[0].to_i,
99
+ selinux_label: fields[8],
100
+ }
101
+ end
102
+
103
+ def self.aix_stat(shell_escaped_path, backend, follow_symlink)
104
+ # Perl here b/c it is default on AIX
105
+ lstat = follow_symlink ? 'lstat' : 'stat'
106
+ stat_cmd = <<-EOP
107
+ perl -e '
108
+ @a = #{lstat}(shift) or exit 2;
109
+ $u = getpwuid($a[4]);
110
+ $g = getgrgid($a[5]);
111
+ printf("0%o\\n%s\\n%d\\n%s\\n%d\\n%d\\n%d\\n", $a[2], $u, $a[4], $u, $a[5], $a[9], $a[7])
112
+ ' #{shell_escaped_path}
113
+ EOP
114
+
115
+ res = backend.run_command(stat_cmd)
116
+ return {} if res.exit_status != 0
117
+ fields = res.stdout.split("\n")
118
+ return {} if fields.length != 7
119
+ tmask = fields[0].to_i(8)
120
+ {
121
+ type: find_type(tmask),
122
+ mode: tmask & 07777,
123
+ owner: fields[1],
124
+ uid: fields[2].to_i,
125
+ group: fields[3],
126
+ gid: fields[4].to_i,
127
+ mtime: fields[5].to_i,
128
+ size: fields[6].to_i,
129
+ selinux_label: nil,
130
+ }
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+ # Author:: Christoph Hartmann (<chris@lollyrock.com>)
5
+
6
+ module Train
7
+ module Options
8
+ def self.attach(target)
9
+ target.class.method(:include).call(ClassOptions)
10
+ target.method(:include).call(InstanceOptions)
11
+ end
12
+
13
+ module ClassOptions
14
+ def option(name, conf = nil, &block)
15
+ d = conf || {}
16
+ unless d.is_a? Hash
17
+ fail Train::ClientError,
18
+ "The transport plugin #{self} declared an option #{name} "\
19
+ "and didn't provide a valid configuration hash."
20
+ end
21
+
22
+ if !conf.nil? and !conf[:default].nil? and block_given?
23
+ fail Train::ClientError,
24
+ "The transport plugin #{self} declared an option #{name} "\
25
+ 'with both a default value and block. Only use one of these.'
26
+ end
27
+
28
+ d[:default] = block if block_given?
29
+
30
+ default_options[name] = d
31
+ end
32
+
33
+ def default_options
34
+ @default_options = {} unless defined? @default_options
35
+ @default_options
36
+ end
37
+
38
+ def include_options(other)
39
+ unless other.respond_to?(:default_options)
40
+ fail "Trying to include options from module #{other.inspect}, "\
41
+ "which doesn't seem to support options."
42
+ end
43
+ default_options.merge!(other.default_options)
44
+ end
45
+ end
46
+
47
+ module InstanceOptions
48
+ # @return [Hash] options, which created this Transport
49
+ attr_reader :options
50
+
51
+ def default_options
52
+ self.class.default_options
53
+ end
54
+
55
+ def merge_options(base, opts)
56
+ res = base.merge(opts || {})
57
+ default_options.each do |field, hm|
58
+ next unless res[field].nil? and hm.key?(:default)
59
+ default = hm[:default]
60
+ if default.is_a? Proc
61
+ res[field] = default.call(res)
62
+ else
63
+ res[field] = default
64
+ end
65
+ end
66
+ res
67
+ end
68
+
69
+ def validate_options(opts)
70
+ default_options.each do |field, hm|
71
+ if opts[field].nil? and hm[:required]
72
+ fail Train::ClientError,
73
+ "You must provide a value for #{field.to_s.inspect}."
74
+ end
75
+ end
76
+ opts
77
+ end
78
+ end
79
+ end
80
+ 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
+ autoload :Transport, '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,86 @@
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
+
7
+ require 'train/errors'
8
+ require 'train/extras'
9
+ require 'logger'
10
+
11
+ class Train::Plugins::Transport
12
+ # A Connection instance can be generated and re-generated, given new
13
+ # connection details such as connection port, hostname, credentials, etc.
14
+ # This object is responsible for carrying out the actions on the remote
15
+ # host such as executing commands, transferring files, etc.
16
+ #
17
+ # @author Fletcher Nichol <fnichol@nichol.ca>
18
+ class BaseConnection
19
+ include Train::Extras
20
+
21
+ # Create a new Connection instance.
22
+ #
23
+ # @param options [Hash] connection options
24
+ # @yield [self] yields itself for block-style invocation
25
+ def initialize(options = nil)
26
+ @options = options || {}
27
+ @logger = @options.delete(:logger) || Logger.new(STDOUT)
28
+ end
29
+
30
+ # Closes the session connection, if it is still active.
31
+ def close
32
+ # this method may be left unimplemented if that is applicable
33
+ end
34
+
35
+ # Execute a command using this connection.
36
+ #
37
+ # @param command [String] command string to execute
38
+ # @return [CommandResult] contains the result of running the command
39
+ def run_command(_command)
40
+ fail Train::ClientError, "#{self.class} does not implement #run_command()"
41
+ end
42
+
43
+ # Get information on the operating system which this transport connects to.
44
+ #
45
+ # @return [OSCommon] operating system information
46
+ def os
47
+ fail Train::ClientError, "#{self.class} does not implement #os()"
48
+ end
49
+
50
+ # Interact with files on the target. Read, write, and get metadata
51
+ # from files via the transport.
52
+ #
53
+ # @param [String] path which is being inspected
54
+ # @return [FileCommon] file object that allows for interaction
55
+ def file(_path, *_args)
56
+ fail Train::ClientError, "#{self.class} does not implement #file(...)"
57
+ end
58
+
59
+ # Builds a LoginCommand which can be used to open an interactive
60
+ # session on the remote host.
61
+ #
62
+ # @return [LoginCommand] array of command line tokens
63
+ def login_command
64
+ fail Train::ClientError, "#{self.class} does not implement #run_command()"
65
+ end
66
+
67
+ # Block and return only when the remote host is prepared and ready to
68
+ # execute command and upload files. The semantics and details will
69
+ # vary by implementation, but a round trip through the hosted
70
+ # service is preferred to simply waiting on a socket to become
71
+ # available.
72
+ def wait_until_ready
73
+ # this method may be left unimplemented if that is applicablelog
74
+ end
75
+
76
+ private
77
+
78
+ # @return [Logger] logger for reporting information
79
+ # @api private
80
+ attr_reader :logger
81
+
82
+ # @return [Hash] connection options
83
+ # @api private
84
+ attr_reader :options
85
+ end
86
+ 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
+ autoload :BaseConnection, '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 #connect()"
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