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,148 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'base64'
6
+ require 'winrm'
7
+ require 'train/errors'
8
+
9
+ module Train::Extras
10
+ # Define the interface of all command wrappers.
11
+ class CommandWrapperBase
12
+ # Verify that the command wrapper is initialized properly and working.
13
+ #
14
+ # @return [Any] verification result, nil if all went well, otherwise a message
15
+ def verify
16
+ fail Train::ClientError, "#{self.class} does not implement #verify()"
17
+ end
18
+
19
+ # Wrap a command and return the augmented command which can be executed.
20
+ #
21
+ # @param [Strin] command that will be wrapper
22
+ # @return [String] result of wrapping the command
23
+ def run(_command)
24
+ fail Train::ClientError, "#{self.class} does not implement #run(command)"
25
+ end
26
+ end
27
+
28
+ # Wrap linux commands and add functionality like sudo.
29
+ class LinuxCommand < CommandWrapperBase
30
+ Train::Options.attach(self)
31
+
32
+ option :sudo, default: false
33
+ option :sudo_options, default: nil
34
+ option :sudo_password, default: nil
35
+ option :sudo_command, default: nil
36
+ option :user
37
+
38
+ def initialize(backend, options)
39
+ @backend = backend
40
+ validate_options(options)
41
+
42
+ @sudo = options[:sudo]
43
+ @sudo_options = options[:sudo_options]
44
+ @sudo_password = options[:sudo_password]
45
+ @sudo_command = options[:sudo_command]
46
+ @user = options[:user]
47
+ @prefix = build_prefix
48
+ end
49
+
50
+ # (see CommandWrapperBase::verify)
51
+ def verify
52
+ res = @backend.run_command(run('echo'))
53
+ return nil if res.exit_status == 0
54
+ rawerr = res.stdout + ' ' + res.stderr
55
+
56
+ {
57
+ 'Sorry, try again' => 'Wrong sudo password.',
58
+ 'sudo: no tty present and no askpass program specified' =>
59
+ 'Sudo requires a password, please configure it.',
60
+ 'sudo: command not found' =>
61
+ "Can't find sudo command. Please either install and "\
62
+ 'configure it on the target or deactivate sudo.',
63
+ 'sudo: sorry, you must have a tty to run sudo' =>
64
+ 'Sudo requires a TTY. Please see the README on how to configure '\
65
+ 'sudo to allow for non-interactive usage.',
66
+ }.each do |sudo, human|
67
+ rawerr = human if rawerr.include? sudo
68
+ end
69
+
70
+ rawerr
71
+ end
72
+
73
+ # (see CommandWrapperBase::run)
74
+ def run(command)
75
+ @prefix + command
76
+ end
77
+
78
+ def self.active?(options)
79
+ options.is_a?(Hash) && options[:sudo]
80
+ end
81
+
82
+ private
83
+
84
+ def build_prefix
85
+ return '' unless @sudo
86
+ return '' if @user == 'root'
87
+
88
+ res = (@sudo_command || 'sudo') + ' '
89
+
90
+ unless @sudo_password.nil?
91
+ b64pw = Base64.strict_encode64(@sudo_password + "\n")
92
+ res = "echo #{b64pw} | base64 -d | #{res}-S "
93
+ end
94
+
95
+ res << @sudo_options.to_s + ' ' unless @sudo_options.nil?
96
+
97
+ res
98
+ end
99
+ end
100
+
101
+ # this is required if you run locally on windows,
102
+ # winrm connections provide a PowerShell shell automatically
103
+ # TODO: only activate in local mode
104
+ class PowerShellCommand < CommandWrapperBase
105
+ Train::Options.attach(self)
106
+
107
+ def initialize(backend, options)
108
+ @backend = backend
109
+ validate_options(options)
110
+ end
111
+
112
+ def run(script)
113
+ # wrap the script to ensure we always run it via powershell
114
+ # especially in local mode, we cannot be sure that we get a Powershell
115
+ # we may just get a `cmd`.
116
+ # TODO: we may want to opt for powershell.exe -command instead of `encodeCommand`
117
+ "powershell -encodedCommand #{WinRM::PowershellScript.new(safe_script(script)).encoded}"
118
+ end
119
+
120
+ # reused from https://github.com/WinRb/WinRM/blob/master/lib/winrm/command_executor.rb
121
+ # suppress the progress stream from leaking to stderr
122
+ def safe_script(script)
123
+ "$ProgressPreference='SilentlyContinue';" + script
124
+ end
125
+
126
+ def to_s
127
+ 'PowerShell CommandWrapper'
128
+ end
129
+ end
130
+
131
+ class CommandWrapper
132
+ include_options LinuxCommand
133
+
134
+ def self.load(transport, options)
135
+ if transport.os.unix?
136
+ return nil unless LinuxCommand.active?(options)
137
+ res = LinuxCommand.new(transport, options)
138
+ msg = res.verify
139
+ fail Train::UserError, "Sudo failed: #{msg}" unless msg.nil?
140
+ res
141
+ # only use powershell command for local transport. winrm transport
142
+ # uses powershell as default
143
+ elsif transport.os.windows? && transport.class == Train::Transports::Local::Connection
144
+ PowerShellCommand.new(transport, options)
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ require 'shellwords'
4
+ require 'train/extras/stat'
5
+
6
+ module Train::Extras
7
+ class AixFile < UnixFile
8
+ def link_path
9
+ return nil unless symlink?
10
+ @link_path ||=
11
+ @backend.run_command("perl -e 'print readlink shift' #{@spath}")
12
+ .stdout.chomp
13
+ end
14
+
15
+ def mounted
16
+ @mounted ||=
17
+ @backend.run_command("lsfs -c #{@spath}")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,161 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'digest/sha2'
6
+ require 'digest/md5'
7
+
8
+ module Train::Extras
9
+ class FileCommon # rubocop:disable Metrics/ClassLength
10
+ # interface methods: these fields should be implemented by every
11
+ # backend File
12
+ %w{
13
+ exist? mode owner group uid gid content mtime size selinux_label path
14
+ product_version file_version
15
+ }.each do |m|
16
+ define_method m.to_sym do
17
+ fail NotImplementedError, "File must implement the #{m}() method."
18
+ end
19
+ end
20
+
21
+ def initialize(backend, path, follow_symlink = true)
22
+ @backend = backend
23
+ @path = path || ''
24
+ @follow_symlink = follow_symlink
25
+ end
26
+
27
+ def type
28
+ :unknown
29
+ end
30
+
31
+ # The following methods can be overwritten by a derived class
32
+ # if desired, to e.g. achieve optimizations.
33
+
34
+ def md5sum
35
+ res = Digest::MD5.new
36
+ res.update(content)
37
+ res.hexdigest
38
+ rescue TypeError => _
39
+ nil
40
+ end
41
+
42
+ def sha256sum
43
+ res = Digest::SHA256.new
44
+ res.update(content)
45
+ res.hexdigest
46
+ rescue TypeError => _
47
+ nil
48
+ end
49
+
50
+ # Additional methods for convenience
51
+
52
+ def file?
53
+ type == :file
54
+ end
55
+
56
+ def block_device?
57
+ type == :block_device
58
+ end
59
+
60
+ def character_device?
61
+ type == :character_device
62
+ end
63
+
64
+ def socket?
65
+ type == :socket
66
+ end
67
+
68
+ def directory?
69
+ type == :directory
70
+ end
71
+
72
+ def symlink?
73
+ source.type == :symlink
74
+ end
75
+
76
+ def source_path
77
+ @path
78
+ end
79
+
80
+ def source
81
+ if @follow_symlink
82
+ self.class.new(@backend, @path, false)
83
+ else
84
+ self
85
+ end
86
+ end
87
+
88
+ def pipe?
89
+ type == :pipe
90
+ end
91
+
92
+ def mode?(sth)
93
+ mode == sth
94
+ end
95
+
96
+ def owned_by?(sth)
97
+ owner == sth
98
+ end
99
+
100
+ def grouped_into?(sth)
101
+ group == sth
102
+ end
103
+
104
+ def linked_to?(dst)
105
+ link_path == dst
106
+ end
107
+
108
+ def link_path
109
+ symlink? ? path : nil
110
+ end
111
+
112
+ def version?(version)
113
+ product_version == version or
114
+ file_version == version
115
+ end
116
+
117
+ def unix_mode_mask(owner, type)
118
+ o = UNIX_MODE_OWNERS[owner.to_sym]
119
+ return nil if o.nil?
120
+
121
+ t = UNIX_MODE_TYPES[type.to_sym]
122
+ return nil if t.nil?
123
+
124
+ t & o
125
+ end
126
+
127
+ def mounted?
128
+ !mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty?
129
+ end
130
+
131
+ def basename(suffix = nil, sep = '/')
132
+ fail 'Not yet supported: Suffix in file.basename' unless suffix.nil?
133
+ @basename ||= detect_filename(path, sep || '/')
134
+ end
135
+
136
+ # helper methods provided to any implementing class
137
+
138
+ private
139
+
140
+ def detect_filename(path, sep)
141
+ idx = path.rindex(sep)
142
+ return path if idx.nil?
143
+ idx += 1
144
+ return detect_filename(path[0..-2], sep) if idx == path.length
145
+ path[idx..-1]
146
+ end
147
+
148
+ UNIX_MODE_OWNERS = {
149
+ all: 00777,
150
+ owner: 00700,
151
+ group: 00070,
152
+ other: 00007,
153
+ }.freeze
154
+
155
+ UNIX_MODE_TYPES = {
156
+ r: 00444,
157
+ w: 00222,
158
+ x: 00111,
159
+ }.freeze
160
+ end
161
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ module Train::Extras
6
+ class LinuxFile < UnixFile
7
+ def content
8
+ return @content if defined?(@content)
9
+ @content = @backend.run_command(
10
+ "cat #{@spath} || echo -n").stdout
11
+ return @content unless @content.empty?
12
+ @content = nil if directory? or size.nil? or size > 0
13
+ @content
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'shellwords'
6
+ require 'train/extras/stat'
7
+
8
+ module Train::Extras
9
+ class UnixFile < FileCommon
10
+ attr_reader :path
11
+ def initialize(backend, path, follow_symlink = true)
12
+ super(backend, path, follow_symlink)
13
+ @spath = Shellwords.escape(@path)
14
+ end
15
+
16
+ def content
17
+ @content ||= case
18
+ when !exist?, directory?
19
+ nil
20
+ when size.nil?, size == 0
21
+ ''
22
+ else
23
+ @backend.run_command("cat #{@spath}").stdout || ''
24
+ end
25
+ end
26
+
27
+ def exist?
28
+ @exist ||= (
29
+ f = @follow_symlink ? '' : " || test -L #{@spath}"
30
+ @backend.run_command("test -e #{@spath}"+f)
31
+ .exit_status == 0
32
+ )
33
+ end
34
+
35
+ def path
36
+ return @path unless @follow_symlink && symlink?
37
+ @link_path ||= read_target_path
38
+ end
39
+
40
+ def mounted
41
+ @mounted ||=
42
+ @backend.run_command("mount | grep -- ' on #{@spath} '")
43
+ end
44
+
45
+ %w{
46
+ type mode owner group uid gid mtime size selinux_label
47
+ }.each do |field|
48
+ define_method field.to_sym do
49
+ stat[field.to_sym]
50
+ end
51
+ end
52
+
53
+ def product_version
54
+ nil
55
+ end
56
+
57
+ def file_version
58
+ nil
59
+ end
60
+
61
+ def stat
62
+ return @stat if defined?(@stat)
63
+ @stat = Train::Extras::Stat.stat(@spath, @backend, @follow_symlink)
64
+ end
65
+
66
+ private
67
+
68
+ # Returns full path of a symlink target(real dest) or '' on symlink loop
69
+ def read_target_path
70
+ full_path = @backend.run_command("readlink -n #{@spath} -f").stdout
71
+ # Needed for some OSes like OSX that returns relative path
72
+ # when the link and target are in the same directory
73
+ if !full_path.start_with?('/') && full_path != ''
74
+ full_path = File.expand_path("../#{full_path}", @spath)
75
+ end
76
+ full_path
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'shellwords'
6
+ require 'train/extras/stat'
7
+
8
+ # PS C:\Users\Administrator> Get-Item -Path C:\test.txt | Select-Object -Property BaseName, FullName, IsReadOnly, Exists,
9
+ # LinkType, Mode, VersionInfo, Owner, Archive, Hidden, ReadOnly, System | ConvertTo-Json
10
+
11
+ module Train::Extras
12
+ class WindowsFile < FileCommon
13
+ attr_reader :path
14
+ def initialize(backend, path, follow_symlink = false)
15
+ super(backend, path, follow_symlink)
16
+ @spath = sanitize_filename(@path)
17
+ end
18
+
19
+ def basename(suffix = nil, sep = '\\')
20
+ super(suffix, sep)
21
+ end
22
+
23
+ # Ensures we do not use invalid characters for file names
24
+ # @see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions
25
+ def sanitize_filename(filename)
26
+ return if filename.nil?
27
+ # we do not filter :, backslash and forward slash, since they are part of the path
28
+ filename.gsub(/[<>"|?*]/, '')
29
+ end
30
+
31
+ def content
32
+ return @content if defined?(@content)
33
+ @content = @backend.run_command(
34
+ "Get-Content(\"#{@spath}\") | Out-String").stdout
35
+ return @content unless @content.empty?
36
+ @content = nil if directory? # or size.nil? or size > 0
37
+ @content
38
+ end
39
+
40
+ def exist?
41
+ return @exist if defined?(@exist)
42
+ @exist = @backend.run_command(
43
+ "(Test-Path -Path \"#{@spath}\").ToString()").stdout.chomp == 'True'
44
+ end
45
+
46
+ def link_path
47
+ nil
48
+ end
49
+
50
+ def mounted
51
+ nil
52
+ end
53
+
54
+ def type
55
+ if attributes.include?('Archive')
56
+ return :file
57
+ elsif attributes.include?('Directory')
58
+ return :directory
59
+ end
60
+ :unknown
61
+ end
62
+
63
+ %w{
64
+ mode owner group uid gid mtime size selinux_label
65
+ }.each do |field|
66
+ define_method field.to_sym do
67
+ nil
68
+ end
69
+ end
70
+
71
+ def product_version
72
+ nil
73
+ end
74
+
75
+ def file_version
76
+ nil
77
+ end
78
+
79
+ def stat
80
+ nil
81
+ end
82
+
83
+ private
84
+
85
+ def attributes
86
+ return @attributes if defined?(@attributes)
87
+ @attributes = @backend.run_command(
88
+ "(Get-ItemProperty -Path \"#{@spath}\").attributes.ToString()").stdout.chomp.split(/\s*,\s*/)
89
+ end
90
+ end
91
+ end