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,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