train 0.12.1

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