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