train-core 1.4.4

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