train-core 1.4.4

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +780 -0
  3. data/Gemfile +36 -0
  4. data/LICENSE +201 -0
  5. data/README.md +197 -0
  6. data/lib/train.rb +158 -0
  7. data/lib/train/errors.rb +32 -0
  8. data/lib/train/extras.rb +11 -0
  9. data/lib/train/extras/command_wrapper.rb +137 -0
  10. data/lib/train/extras/stat.rb +132 -0
  11. data/lib/train/file.rb +151 -0
  12. data/lib/train/file/local.rb +75 -0
  13. data/lib/train/file/local/unix.rb +96 -0
  14. data/lib/train/file/local/windows.rb +63 -0
  15. data/lib/train/file/remote.rb +36 -0
  16. data/lib/train/file/remote/aix.rb +21 -0
  17. data/lib/train/file/remote/linux.rb +19 -0
  18. data/lib/train/file/remote/qnx.rb +41 -0
  19. data/lib/train/file/remote/unix.rb +106 -0
  20. data/lib/train/file/remote/windows.rb +94 -0
  21. data/lib/train/options.rb +80 -0
  22. data/lib/train/platforms.rb +84 -0
  23. data/lib/train/platforms/common.rb +34 -0
  24. data/lib/train/platforms/detect.rb +12 -0
  25. data/lib/train/platforms/detect/helpers/os_common.rb +145 -0
  26. data/lib/train/platforms/detect/helpers/os_linux.rb +75 -0
  27. data/lib/train/platforms/detect/helpers/os_windows.rb +120 -0
  28. data/lib/train/platforms/detect/scanner.rb +84 -0
  29. data/lib/train/platforms/detect/specifications/api.rb +15 -0
  30. data/lib/train/platforms/detect/specifications/os.rb +578 -0
  31. data/lib/train/platforms/detect/uuid.rb +34 -0
  32. data/lib/train/platforms/family.rb +26 -0
  33. data/lib/train/platforms/platform.rb +101 -0
  34. data/lib/train/plugins.rb +40 -0
  35. data/lib/train/plugins/base_connection.rb +169 -0
  36. data/lib/train/plugins/transport.rb +49 -0
  37. data/lib/train/transports/local.rb +232 -0
  38. data/lib/train/version.rb +7 -0
  39. data/train-core.gemspec +27 -0
  40. metadata +116 -0
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
4
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
5
+ # Author:: Christoph Hartmann (<chris@lollyrock.com>)
6
+ #
7
+ # Copyright (C) 2013, Fletcher Nichol
8
+ #
9
+ # Licensed under the Apache License, Version 2.0 (the "License");
10
+
11
+ module Train
12
+ # Base exception for any exception explicitly raised by the Train library.
13
+ class Error < ::StandardError; end
14
+
15
+ # Base exception class for all exceptions that are caused by user input
16
+ # errors.
17
+ class UserError < Error; end
18
+
19
+ # Base exception class for all exceptions that are caused by incorrect use
20
+ # of an API.
21
+ class ClientError < Error; end
22
+
23
+ # Base exception class for all exceptions that are caused by other failures
24
+ # in the transport layer.
25
+ class TransportError < Error; end
26
+
27
+ # Exception for when no platform can be detected.
28
+ class PlatformDetectionFailed < Error; end
29
+
30
+ # Exception for when a invalid cache type is passed.
31
+ class UnknownCacheType < Error; end
32
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+
5
+ module Train::Extras
6
+ require 'train/extras/command_wrapper'
7
+ require 'train/extras/stat'
8
+
9
+ CommandResult = Struct.new(:stdout, :stderr, :exit_status)
10
+ LoginCommand = Struct.new(:command, :arguments)
11
+ end
@@ -0,0 +1,137 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'base64'
6
+ require 'train/errors'
7
+
8
+ module Train::Extras
9
+ # Define the interface of all command wrappers.
10
+ class CommandWrapperBase
11
+ # Verify that the command wrapper is initialized properly and working.
12
+ #
13
+ # @return [Any] verification result, nil if all went well, otherwise a message
14
+ def verify
15
+ fail Train::ClientError, "#{self.class} does not implement #verify()"
16
+ end
17
+
18
+ # Wrap a command and return the augmented command which can be executed.
19
+ #
20
+ # @param [Strin] command that will be wrapper
21
+ # @return [String] result of wrapping the command
22
+ def run(_command)
23
+ fail Train::ClientError, "#{self.class} does not implement #run(command)"
24
+ end
25
+ end
26
+
27
+ # Wrap linux commands and add functionality like sudo.
28
+ class LinuxCommand < CommandWrapperBase
29
+ Train::Options.attach(self)
30
+
31
+ option :shell, default: false
32
+ option :shell_options, default: nil
33
+ option :shell_command, default: nil
34
+ option :sudo, default: false
35
+ option :sudo_options, default: nil
36
+ option :sudo_password, default: nil
37
+ option :sudo_command, default: nil
38
+ option :user
39
+
40
+ def initialize(backend, options)
41
+ @backend = backend
42
+ validate_options(options)
43
+
44
+ @shell = options[:shell]
45
+ @shell_options = options[:shell_options] # e.g. '--login'
46
+ @shell_command = options[:shell_command] # e.g. '/bin/sh'
47
+ @sudo = options[:sudo]
48
+ @sudo_options = options[:sudo_options]
49
+ @sudo_password = options[:sudo_password]
50
+ @sudo_command = options[:sudo_command]
51
+ @user = options[:user]
52
+ end
53
+
54
+ # (see CommandWrapperBase::verify)
55
+ def verify
56
+ res = @backend.run_command(run('echo'))
57
+ return nil if res.exit_status == 0
58
+ rawerr = res.stdout + ' ' + res.stderr
59
+
60
+ {
61
+ 'Sorry, try again' => 'Wrong sudo password.',
62
+ 'sudo: no tty present and no askpass program specified' =>
63
+ 'Sudo requires a password, please configure it.',
64
+ 'sudo: command not found' =>
65
+ "Can't find sudo command. Please either install and "\
66
+ 'configure it on the target or deactivate sudo.',
67
+ 'sudo: sorry, you must have a tty to run sudo' =>
68
+ 'Sudo requires a TTY. Please see the README on how to configure '\
69
+ 'sudo to allow for non-interactive usage.',
70
+ }.each do |sudo, human|
71
+ rawerr = human if rawerr.include? sudo
72
+ end
73
+
74
+ rawerr
75
+ end
76
+
77
+ # (see CommandWrapperBase::run)
78
+ def run(command)
79
+ shell_wrap(sudo_wrap(command))
80
+ end
81
+
82
+ def self.active?(options)
83
+ options.is_a?(Hash) && (
84
+ options[:sudo] ||
85
+ options[:shell]
86
+ )
87
+ end
88
+
89
+ private
90
+
91
+ # wrap the cmd in a sudo command
92
+ def sudo_wrap(cmd)
93
+ return cmd unless @sudo
94
+ return cmd if @user == 'root'
95
+
96
+ res = (@sudo_command || 'sudo') + ' '
97
+
98
+ res = "#{safe_string(@sudo_password + "\n")} | #{res}-S " unless @sudo_password.nil?
99
+
100
+ res << @sudo_options.to_s + ' ' unless @sudo_options.nil?
101
+
102
+ res + cmd
103
+ end
104
+
105
+ # wrap the cmd in a subshell allowing for options to
106
+ # passed to the subshell
107
+ def shell_wrap(cmd)
108
+ return cmd unless @shell
109
+
110
+ shell = @shell_command || '$SHELL'
111
+ options = ' ' + @shell_options.to_s unless @shell_options.nil?
112
+
113
+ "#{safe_string(cmd)} | #{shell}#{options}"
114
+ end
115
+
116
+ # encapsulates encoding the string into a safe form, and decoding for use.
117
+ # @return [String] A command line snippet that can be used as part of a pipeline.
118
+ def safe_string(str)
119
+ b64str = Base64.strict_encode64(str)
120
+ "echo #{b64str} | base64 --decode"
121
+ end
122
+ end
123
+
124
+ class CommandWrapper
125
+ include_options LinuxCommand
126
+
127
+ def self.load(transport, options)
128
+ if transport.os.unix?
129
+ return nil unless LinuxCommand.active?(options)
130
+ res = LinuxCommand.new(transport, options)
131
+ msg = res.verify
132
+ fail Train::UserError, "Sudo failed: #{msg}" unless msg.nil?
133
+ res
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,132 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+ module Train::Extras
5
+ class Stat
6
+ TYPES = {
7
+ socket: 00140000,
8
+ symlink: 00120000,
9
+ file: 00100000,
10
+ block_device: 00060000,
11
+ directory: 00040000,
12
+ character_device: 00020000,
13
+ pipe: 00010000,
14
+ }.freeze
15
+
16
+ def self.find_type(mode)
17
+ res = TYPES.find { |_, mask| mask & mode == mask }
18
+ res.nil? ? :unknown : res[0]
19
+ end
20
+
21
+ def self.stat(shell_escaped_path, backend, follow_symlink)
22
+ # use perl scripts for aix, solaris 10 and hpux
23
+ if backend.os.aix? || (backend.os.solaris? && backend.os[:release].to_i < 11) || backend.os.hpux?
24
+ return aix_stat(shell_escaped_path, backend, follow_symlink)
25
+ end
26
+ return bsd_stat(shell_escaped_path, backend, follow_symlink) if backend.os.bsd?
27
+ # linux,solaris 11 and esx will use standard linux stats
28
+ return linux_stat(shell_escaped_path, backend, follow_symlink) if backend.os.unix? || backend.os.esx?
29
+ # all other cases we don't handle
30
+ # TODO: print an error if we get here, as it shouldn't be invoked
31
+ # on non-unix
32
+ {}
33
+ end
34
+
35
+ def self.linux_stat(shell_escaped_path, backend, follow_symlink)
36
+ lstat = follow_symlink ? ' -L' : ''
37
+ format = (backend.os.esx? || backend.os[:name] == 'alpine') ? '-c' : '--printf'
38
+ res = backend.run_command("stat#{lstat} #{shell_escaped_path} 2>/dev/null #{format} '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'")
39
+ # ignore the exit_code: it is != 0 if selinux labels are not supported
40
+ # on the system.
41
+
42
+ fields = res.stdout.split("\n")
43
+ return {} if fields.length != 9
44
+
45
+ tmask = fields[1].to_i(16)
46
+ selinux = fields[8]
47
+ ## selinux security context string not available on esxi
48
+ selinux = nil if selinux == '?' or selinux == '(null)' or selinux == 'C'
49
+ {
50
+ type: find_type(tmask),
51
+ mode: tmask & 07777,
52
+ owner: fields[2],
53
+ uid: fields[3].to_i,
54
+ group: fields[4],
55
+ gid: fields[5].to_i,
56
+ mtime: fields[7].to_i,
57
+ size: fields[0].to_i,
58
+ selinux_label: selinux,
59
+ }
60
+ end
61
+
62
+ def self.bsd_stat(shell_escaped_path, backend, follow_symlink)
63
+ # From stat man page on FreeBSD:
64
+ # z The size of file in bytes (st_size).
65
+ # p File type and permissions (st_mode).
66
+ # u, g User ID and group ID of file's owner (st_uid, st_gid).
67
+ # a, m, c, B
68
+ # The time file was last accessed or modified, or when the
69
+ # inode was last changed, or the birth time of the inode
70
+ # (st_atime, st_mtime, st_ctime, st_birthtime).
71
+ #
72
+ # The special output specifier S may be used to indicate that the
73
+ # output, if applicable, should be in string format. May be used
74
+ # in combination with:
75
+ # ...
76
+ # gu Display group or user name.
77
+ lstat = follow_symlink ? ' -L' : ''
78
+ res = backend.run_command(
79
+ "stat#{lstat} -f '%z\n%p\n%Su\n%u\n%Sg\n%g\n%a\n%m' "\
80
+ "#{shell_escaped_path}")
81
+
82
+ return {} if res.exit_status != 0
83
+
84
+ fields = res.stdout.split("\n")
85
+ return {} if fields.length != 8
86
+
87
+ tmask = fields[1].to_i(8)
88
+
89
+ {
90
+ type: find_type(tmask),
91
+ mode: tmask & 07777,
92
+ owner: fields[2],
93
+ uid: fields[3].to_i,
94
+ group: fields[4],
95
+ gid: fields[5].to_i,
96
+ mtime: fields[7].to_i,
97
+ size: fields[0].to_i,
98
+ selinux_label: fields[8],
99
+ }
100
+ end
101
+
102
+ def self.aix_stat(shell_escaped_path, backend, follow_symlink)
103
+ # Perl here b/c it is default on AIX
104
+ lstat = follow_symlink ? 'lstat' : 'stat'
105
+ stat_cmd = <<-EOP
106
+ perl -e '
107
+ @a = #{lstat}(shift) or exit 2;
108
+ $u = getpwuid($a[4]);
109
+ $g = getgrgid($a[5]);
110
+ printf("0%o\\n%s\\n%d\\n%s\\n%d\\n%d\\n%d\\n", $a[2], $u, $a[4], $g, $a[5], $a[9], $a[7])
111
+ ' #{shell_escaped_path}
112
+ EOP
113
+
114
+ res = backend.run_command(stat_cmd)
115
+ return {} if res.exit_status != 0
116
+ fields = res.stdout.split("\n")
117
+ return {} if fields.length != 7
118
+ tmask = fields[0].to_i(8)
119
+ {
120
+ type: find_type(tmask),
121
+ mode: tmask & 07777,
122
+ owner: fields[1],
123
+ uid: fields[2].to_i,
124
+ group: fields[3],
125
+ gid: fields[4].to_i,
126
+ mtime: fields[5].to_i,
127
+ size: fields[6].to_i,
128
+ selinux_label: nil,
129
+ }
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,151 @@
1
+ # encoding: utf-8
2
+ #
3
+ # author: Christoph Hartmann
4
+ # author: Dominik Richter
5
+
6
+ require 'train/file/local'
7
+ require 'train/file/remote'
8
+ require 'digest/sha2'
9
+ require 'digest/md5'
10
+ require 'train/extras/stat'
11
+
12
+ module Train
13
+ class File
14
+ def initialize(backend, path, follow_symlink = true)
15
+ @backend = backend
16
+ @path = path || ''
17
+ @follow_symlink = follow_symlink
18
+
19
+ sanitize_filename(path)
20
+ end
21
+
22
+ # This method gets override by particular os class.
23
+ def sanitize_filename(_path)
24
+ nil
25
+ end
26
+
27
+ # interface methods: these fields should be implemented by every
28
+ # backend File
29
+ DATA_FIELDS = %w{
30
+ exist? mode owner group uid gid content mtime size selinux_label path
31
+ }.freeze
32
+
33
+ DATA_FIELDS.each do |m|
34
+ define_method m.to_sym do
35
+ fail NotImplementedError, "File must implement the #{m}() method."
36
+ end
37
+ end
38
+
39
+ def to_json
40
+ res = Hash[DATA_FIELDS.map { |x| [x, method(x).call] }]
41
+ # additional fields provided as input
42
+ res['type'] = type
43
+ res['follow_symlink'] = @follow_symlink
44
+ res
45
+ end
46
+
47
+ def type
48
+ :unknown
49
+ end
50
+
51
+ def md5sum
52
+ res = Digest::MD5.new
53
+ res.update(content)
54
+ res.hexdigest
55
+ rescue TypeError => _
56
+ nil
57
+ end
58
+
59
+ def sha256sum
60
+ res = Digest::SHA256.new
61
+ res.update(content)
62
+ res.hexdigest
63
+ rescue TypeError => _
64
+ nil
65
+ end
66
+
67
+ def source
68
+ if @follow_symlink
69
+ self.class.new(@backend, @path, false)
70
+ else
71
+ self
72
+ end
73
+ end
74
+
75
+ def source_path
76
+ @path
77
+ end
78
+
79
+ # product_version is primarily used by Windows operating systems only and will be overwritten
80
+ # in Windows-related classes. Since this field is returned for all file objects, the acceptable
81
+ # default value is nil
82
+ def product_version
83
+ nil
84
+ end
85
+
86
+ # file_version is primarily used by Windows operating systems only and will be overwritten
87
+ # in Windows-related classes. Since this field is returned for all file objects, the acceptable
88
+ # default value is nil
89
+ def file_version
90
+ nil
91
+ end
92
+
93
+ def version?(version)
94
+ product_version == version or
95
+ file_version == version
96
+ end
97
+
98
+ def block_device?
99
+ type.to_s == 'block_device'
100
+ end
101
+
102
+ def character_device?
103
+ type.to_s == 'character_device'
104
+ end
105
+
106
+ def pipe?
107
+ type.to_s == 'pipe'
108
+ end
109
+
110
+ def file?
111
+ type.to_s == 'file'
112
+ end
113
+
114
+ def socket?
115
+ type.to_s == 'socket'
116
+ end
117
+
118
+ def directory?
119
+ type.to_s == 'directory'
120
+ end
121
+
122
+ def symlink?
123
+ source.type.to_s == 'symlink'
124
+ end
125
+
126
+ def owned_by?(sth)
127
+ owner == sth
128
+ end
129
+
130
+ def path
131
+ if symlink? && @follow_symlink
132
+ link_path
133
+ else
134
+ @path
135
+ end
136
+ end
137
+
138
+ # if the OS-specific file class supports inquirying as to whether the
139
+ # file/device is mounted, the #mounted method should return a command
140
+ # object whose stdout will not be nil if indeed the device is mounted.
141
+ #
142
+ # if the OS-specific file class does not support checking for mount
143
+ # status, the method should not be implemented and this method will
144
+ # return false.
145
+ def mounted?
146
+ return false unless respond_to?(:mounted)
147
+
148
+ !mounted.nil? && !mounted.stdout.nil? && !mounted.stdout.empty?
149
+ end
150
+ end
151
+ end