train-core 1.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +780 -0
- data/Gemfile +36 -0
- data/LICENSE +201 -0
- data/README.md +197 -0
- data/lib/train.rb +158 -0
- data/lib/train/errors.rb +32 -0
- data/lib/train/extras.rb +11 -0
- data/lib/train/extras/command_wrapper.rb +137 -0
- data/lib/train/extras/stat.rb +132 -0
- data/lib/train/file.rb +151 -0
- data/lib/train/file/local.rb +75 -0
- data/lib/train/file/local/unix.rb +96 -0
- data/lib/train/file/local/windows.rb +63 -0
- data/lib/train/file/remote.rb +36 -0
- data/lib/train/file/remote/aix.rb +21 -0
- data/lib/train/file/remote/linux.rb +19 -0
- data/lib/train/file/remote/qnx.rb +41 -0
- data/lib/train/file/remote/unix.rb +106 -0
- data/lib/train/file/remote/windows.rb +94 -0
- data/lib/train/options.rb +80 -0
- data/lib/train/platforms.rb +84 -0
- data/lib/train/platforms/common.rb +34 -0
- data/lib/train/platforms/detect.rb +12 -0
- data/lib/train/platforms/detect/helpers/os_common.rb +145 -0
- data/lib/train/platforms/detect/helpers/os_linux.rb +75 -0
- data/lib/train/platforms/detect/helpers/os_windows.rb +120 -0
- data/lib/train/platforms/detect/scanner.rb +84 -0
- data/lib/train/platforms/detect/specifications/api.rb +15 -0
- data/lib/train/platforms/detect/specifications/os.rb +578 -0
- data/lib/train/platforms/detect/uuid.rb +34 -0
- data/lib/train/platforms/family.rb +26 -0
- data/lib/train/platforms/platform.rb +101 -0
- data/lib/train/plugins.rb +40 -0
- data/lib/train/plugins/base_connection.rb +169 -0
- data/lib/train/plugins/transport.rb +49 -0
- data/lib/train/transports/local.rb +232 -0
- data/lib/train/version.rb +7 -0
- data/train-core.gemspec +27 -0
- metadata +116 -0
data/lib/train/errors.rb
ADDED
@@ -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
|
data/lib/train/extras.rb
ADDED
@@ -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
|
data/lib/train/file.rb
ADDED
@@ -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
|