r-train 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +45 -0
- data/.travis.yml +12 -0
- data/Gemfile +22 -0
- data/LICENSE +201 -0
- data/README.md +137 -0
- data/Rakefile +39 -0
- data/lib/train.rb +100 -0
- data/lib/train/errors.rb +23 -0
- data/lib/train/extras.rb +15 -0
- data/lib/train/extras/command_wrapper.rb +105 -0
- data/lib/train/extras/file_common.rb +131 -0
- data/lib/train/extras/linux_file.rb +74 -0
- data/lib/train/extras/linux_lsb.rb +60 -0
- data/lib/train/extras/os_common.rb +131 -0
- data/lib/train/extras/os_detect_darwin.rb +32 -0
- data/lib/train/extras/os_detect_linux.rb +126 -0
- data/lib/train/extras/os_detect_unix.rb +77 -0
- data/lib/train/extras/os_detect_windows.rb +73 -0
- data/lib/train/extras/stat.rb +92 -0
- data/lib/train/extras/windows_file.rb +85 -0
- data/lib/train/options.rb +80 -0
- data/lib/train/plugins.rb +40 -0
- data/lib/train/plugins/base_connection.rb +86 -0
- data/lib/train/plugins/transport.rb +49 -0
- data/lib/train/transports/docker.rb +102 -0
- data/lib/train/transports/local.rb +52 -0
- data/lib/train/transports/local_file.rb +77 -0
- data/lib/train/transports/local_os.rb +51 -0
- data/lib/train/transports/mock.rb +125 -0
- data/lib/train/transports/ssh.rb +163 -0
- data/lib/train/transports/ssh_connection.rb +216 -0
- data/lib/train/transports/winrm.rb +187 -0
- data/lib/train/transports/winrm_connection.rb +258 -0
- data/lib/train/version.rb +7 -0
- data/test/integration/.kitchen.yml +43 -0
- data/test/integration/Berksfile +3 -0
- data/test/integration/bootstrap.sh +17 -0
- data/test/integration/chefignore +1 -0
- data/test/integration/cookbooks/test/metadata.rb +1 -0
- data/test/integration/cookbooks/test/recipes/default.rb +101 -0
- data/test/integration/docker_run.rb +153 -0
- data/test/integration/docker_test.rb +24 -0
- data/test/integration/docker_test_container.rb +24 -0
- data/test/integration/helper.rb +58 -0
- data/test/integration/sudo/nopasswd.rb +16 -0
- data/test/integration/sudo/passwd.rb +21 -0
- data/test/integration/sudo/run_as.rb +12 -0
- data/test/integration/test-runner.yaml +24 -0
- data/test/integration/test_local.rb +19 -0
- data/test/integration/test_ssh.rb +24 -0
- data/test/integration/tests/path_block_device_test.rb +74 -0
- data/test/integration/tests/path_character_device_test.rb +74 -0
- data/test/integration/tests/path_file_test.rb +79 -0
- data/test/integration/tests/path_folder_test.rb +88 -0
- data/test/integration/tests/path_missing_test.rb +77 -0
- data/test/integration/tests/path_pipe_test.rb +78 -0
- data/test/integration/tests/path_symlink_test.rb +83 -0
- data/test/integration/tests/run_command_test.rb +28 -0
- data/test/unit/extras/command_wrapper_test.rb +41 -0
- data/test/unit/extras/file_common_test.rb +133 -0
- data/test/unit/extras/linux_file_test.rb +98 -0
- data/test/unit/extras/os_common_test.rb +258 -0
- data/test/unit/extras/stat_test.rb +105 -0
- data/test/unit/helper.rb +6 -0
- data/test/unit/plugins/connection_test.rb +44 -0
- data/test/unit/plugins/transport_test.rb +111 -0
- data/test/unit/plugins_test.rb +22 -0
- data/test/unit/train_test.rb +132 -0
- data/test/unit/transports/local_file_test.rb +112 -0
- data/test/unit/transports/local_test.rb +73 -0
- data/test/unit/transports/mock_test.rb +76 -0
- data/test/unit/transports/ssh_test.rb +95 -0
- data/test/unit/version_test.rb +8 -0
- data/train.gemspec +32 -0
- metadata +299 -0
data/lib/train.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
|
+
|
5
|
+
require 'train/version'
|
6
|
+
require 'train/plugins'
|
7
|
+
require 'train/errors'
|
8
|
+
require 'uri'
|
9
|
+
|
10
|
+
module Train
|
11
|
+
# Create a new transport instance, with the plugin indicated by the
|
12
|
+
# given name.
|
13
|
+
#
|
14
|
+
# @param [String] name of the plugin
|
15
|
+
# @param [Array] *args list of arguments for the plugin
|
16
|
+
# @return [Transport] instance of the new transport or nil
|
17
|
+
def self.create(name, *args)
|
18
|
+
cls = load_transport(name)
|
19
|
+
cls.new(*args) unless cls.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
# Retrieve the configuration options of a transport plugin.
|
23
|
+
#
|
24
|
+
# @param [String] name of the plugin
|
25
|
+
# @return [Hash] map of default options
|
26
|
+
def self.options(name)
|
27
|
+
cls = load_transport(name)
|
28
|
+
cls.default_options unless cls.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Load the transport plugin indicated by name. If the plugin is not
|
32
|
+
# yet found in the plugin registry, it will be attempted to load from
|
33
|
+
# `train/transports/plugin_name`.
|
34
|
+
#
|
35
|
+
# @param [String] name of the plugin
|
36
|
+
# @return [Train::Transport] the transport plugin
|
37
|
+
def self.load_transport(name)
|
38
|
+
res = Train::Plugins.registry[name.to_s]
|
39
|
+
return res unless res.nil?
|
40
|
+
|
41
|
+
# if the plugin wasnt loaded yet:
|
42
|
+
require 'train/transports/' + name.to_s
|
43
|
+
Train::Plugins.registry[name.to_s]
|
44
|
+
rescue LoadError => _
|
45
|
+
raise Train::UserError,
|
46
|
+
"Can't find train plugin #{name.inspect}. Please install it first."
|
47
|
+
end
|
48
|
+
|
49
|
+
# Resolve target configuration in URI-scheme into
|
50
|
+
# all respective fields and merge with existing configuration.
|
51
|
+
# e.g. ssh://bob@remote => backend: ssh, user: bob, host: remote
|
52
|
+
def self.target_config(config = nil) # rubocop:disable Metrics/AbcSize
|
53
|
+
conf = config.nil? ? {} : config.dup
|
54
|
+
|
55
|
+
# symbolize keys
|
56
|
+
conf = conf.each_with_object({}) do |(k, v), acc|
|
57
|
+
acc[k.to_sym] = v
|
58
|
+
acc
|
59
|
+
end
|
60
|
+
|
61
|
+
group_keys_and_keyfiles(conf)
|
62
|
+
|
63
|
+
return conf if conf[:target].to_s.empty?
|
64
|
+
|
65
|
+
uri = URI.parse(conf[:target].to_s)
|
66
|
+
unless uri.host.nil? and uri.scheme.nil?
|
67
|
+
conf[:backend] ||= uri.scheme
|
68
|
+
conf[:host] ||= uri.host
|
69
|
+
conf[:port] ||= uri.port
|
70
|
+
conf[:user] ||= uri.user
|
71
|
+
conf[:password] ||= uri.password
|
72
|
+
conf[:path] ||= uri.path
|
73
|
+
end
|
74
|
+
|
75
|
+
# ensure path is nil, if its empty; e.g. required to reset defaults for winrm
|
76
|
+
conf[:path] = nil if !conf[:path].nil? && conf[:path].to_s.empty?
|
77
|
+
|
78
|
+
# return the updated config
|
79
|
+
conf
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def self.group_keys_and_keyfiles(conf)
|
85
|
+
# in case the user specified a key-file, register it that way
|
86
|
+
# we will clear the list of keys and put keys and key_files separately
|
87
|
+
keys_mixed = conf[:keys]
|
88
|
+
return if keys_mixed.nil?
|
89
|
+
|
90
|
+
conf[:key_files] = []
|
91
|
+
conf[:keys] = []
|
92
|
+
keys_mixed.each do |key|
|
93
|
+
if !key.nil? and File.file?(key)
|
94
|
+
conf[:key_files].push(key)
|
95
|
+
else
|
96
|
+
conf[:keys].push(key)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/lib/train/errors.rb
ADDED
@@ -0,0 +1,23 @@
|
|
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 class for all exceptions that are caused by user input
|
13
|
+
# errors.
|
14
|
+
class UserError < ::StandardError; end
|
15
|
+
|
16
|
+
# Base exception class for all exceptions that are caused by incorrect use
|
17
|
+
# of an API.
|
18
|
+
class ClientError < ::StandardError; end
|
19
|
+
|
20
|
+
# Base exception class for all exceptions that are caused by other failures
|
21
|
+
# in the transport layer.
|
22
|
+
class TransportError < ::StandardError; end
|
23
|
+
end
|
data/lib/train/extras.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
|
+
|
5
|
+
module Train::Extras
|
6
|
+
autoload :CommandWrapper, 'train/extras/command_wrapper'
|
7
|
+
autoload :FileCommon, 'train/extras/file_common'
|
8
|
+
autoload :LinuxFile, 'train/extras/linux_file'
|
9
|
+
autoload :WindowsFile, 'train/extras/windows_file'
|
10
|
+
autoload :OSCommon, 'train/extras/os_common'
|
11
|
+
autoload :Stat, 'train/extras/stat'
|
12
|
+
|
13
|
+
CommandResult = Struct.new(:stdout, :stderr, :exit_status)
|
14
|
+
LoginCommand = Struct.new(:command, :arguments)
|
15
|
+
end
|
@@ -0,0 +1,105 @@
|
|
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 :sudo, default: false
|
32
|
+
option :sudo_options, default: nil
|
33
|
+
option :sudo_password, default: nil
|
34
|
+
option :user
|
35
|
+
|
36
|
+
def initialize(backend, options)
|
37
|
+
@backend = backend
|
38
|
+
validate_options(options)
|
39
|
+
|
40
|
+
@sudo = options[:sudo]
|
41
|
+
@sudo_options = options[:sudo_options]
|
42
|
+
@sudo_password = options[:sudo_password]
|
43
|
+
@user = options[:user]
|
44
|
+
@prefix = build_prefix
|
45
|
+
end
|
46
|
+
|
47
|
+
# (see CommandWrapperBase::verify)
|
48
|
+
def verify
|
49
|
+
res = @backend.run_command(run('echo'))
|
50
|
+
return nil if res.exit_status == 0
|
51
|
+
rawerr = res.stdout + ' ' + res.stderr
|
52
|
+
|
53
|
+
rawerr = 'Wrong sudo password.' if rawerr.include? 'Sorry, try again'
|
54
|
+
if rawerr.include? 'sudo: no tty present and no askpass program specified'
|
55
|
+
rawerr = 'Sudo requires a password, please configure it.'
|
56
|
+
end
|
57
|
+
if rawerr.include? 'sudo: command not found'
|
58
|
+
rawerr = "Can't find sudo command. Please either install and "\
|
59
|
+
'configure it on the target or deactivate sudo.'
|
60
|
+
end
|
61
|
+
|
62
|
+
rawerr
|
63
|
+
end
|
64
|
+
|
65
|
+
# (see CommandWrapperBase::run)
|
66
|
+
def run(command)
|
67
|
+
@prefix + command
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.active?(options)
|
71
|
+
options.is_a?(Hash) && options[:sudo]
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def build_prefix
|
77
|
+
return '' unless @sudo
|
78
|
+
return '' if @user == 'root'
|
79
|
+
|
80
|
+
res = 'sudo '
|
81
|
+
|
82
|
+
unless @sudo_password.nil?
|
83
|
+
b64pw = Base64.strict_encode64(@sudo_password + "\n")
|
84
|
+
res = "echo #{b64pw} | base64 -d | sudo -S "
|
85
|
+
end
|
86
|
+
|
87
|
+
res << @sudo_options.to_s + ' ' unless @sudo_options.nil?
|
88
|
+
|
89
|
+
res
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class CommandWrapper
|
94
|
+
include_options LinuxCommand
|
95
|
+
|
96
|
+
def self.load(transport, options)
|
97
|
+
return nil unless LinuxCommand.active?(options)
|
98
|
+
return nil unless transport.os.unix?
|
99
|
+
res = LinuxCommand.new(transport, options)
|
100
|
+
msg = res.verify
|
101
|
+
fail Train::UserError, "Sudo failed: #{msg}" unless msg.nil?
|
102
|
+
res
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,131 @@
|
|
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
|
10
|
+
# interface methods: these fields should be implemented by every
|
11
|
+
# backend File
|
12
|
+
%w{
|
13
|
+
exist? mode owner group link_target link_path content mtime size
|
14
|
+
selinux_label product_version file_version path
|
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 type
|
22
|
+
:unknown
|
23
|
+
end
|
24
|
+
|
25
|
+
# The following methods can be overwritten by a derived class
|
26
|
+
# if desired, to e.g. achieve optimizations.
|
27
|
+
|
28
|
+
def md5sum
|
29
|
+
res = Digest::MD5.new
|
30
|
+
res.update(content)
|
31
|
+
res.hexdigest
|
32
|
+
rescue TypeError => _
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def sha256sum
|
37
|
+
res = Digest::SHA256.new
|
38
|
+
res.update(content)
|
39
|
+
res.hexdigest
|
40
|
+
rescue TypeError => _
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Additional methods for convenience
|
45
|
+
|
46
|
+
def file?
|
47
|
+
target_type == :file
|
48
|
+
end
|
49
|
+
|
50
|
+
def block_device?
|
51
|
+
target_type == :block_device
|
52
|
+
end
|
53
|
+
|
54
|
+
def character_device?
|
55
|
+
target_type == :character_device
|
56
|
+
end
|
57
|
+
|
58
|
+
def socket?
|
59
|
+
target_type == :socket
|
60
|
+
end
|
61
|
+
|
62
|
+
def directory?
|
63
|
+
target_type == :directory
|
64
|
+
end
|
65
|
+
|
66
|
+
def symlink?
|
67
|
+
type == :symlink
|
68
|
+
end
|
69
|
+
|
70
|
+
def pipe?
|
71
|
+
target_type == :pipe
|
72
|
+
end
|
73
|
+
|
74
|
+
def mode?(sth)
|
75
|
+
mode == sth
|
76
|
+
end
|
77
|
+
|
78
|
+
def owned_by?(sth)
|
79
|
+
owner == sth
|
80
|
+
end
|
81
|
+
|
82
|
+
def grouped_into?(sth)
|
83
|
+
group == sth
|
84
|
+
end
|
85
|
+
|
86
|
+
def linked_to?(dst)
|
87
|
+
link_path == dst
|
88
|
+
end
|
89
|
+
|
90
|
+
def version?(version)
|
91
|
+
product_version == version or
|
92
|
+
file_version == version
|
93
|
+
end
|
94
|
+
|
95
|
+
def unix_mode_mask(owner, type)
|
96
|
+
o = UNIX_MODE_OWNERS[owner.to_sym]
|
97
|
+
return nil if o.nil?
|
98
|
+
|
99
|
+
t = UNIX_MODE_TYPES[type.to_sym]
|
100
|
+
return nil if t.nil?
|
101
|
+
|
102
|
+
t & o
|
103
|
+
end
|
104
|
+
|
105
|
+
# helper methods provided to any implementing class
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def target_type
|
110
|
+
# Just return the type unless this is a symlink
|
111
|
+
return type unless type == :symlink
|
112
|
+
# Get the link's target type, i.e. the real destination's type
|
113
|
+
return link_target.type unless link_target.nil?
|
114
|
+
# Return unknown if we don't know where this is pointing to
|
115
|
+
:unknown
|
116
|
+
end
|
117
|
+
|
118
|
+
UNIX_MODE_OWNERS = {
|
119
|
+
all: 00777,
|
120
|
+
owner: 00700,
|
121
|
+
group: 00070,
|
122
|
+
other: 00007,
|
123
|
+
}
|
124
|
+
|
125
|
+
UNIX_MODE_TYPES = {
|
126
|
+
r: 00444,
|
127
|
+
w: 00222,
|
128
|
+
x: 00111,
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,74 @@
|
|
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 LinuxFile < FileCommon
|
10
|
+
attr_reader :path
|
11
|
+
def initialize(backend, path)
|
12
|
+
@backend = backend
|
13
|
+
@path = path
|
14
|
+
@spath = Shellwords.escape(@path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def content
|
18
|
+
return @content if defined?(@content)
|
19
|
+
@content = @backend.run_command(
|
20
|
+
"cat #{@spath} || echo -n").stdout
|
21
|
+
return @content unless @content.empty?
|
22
|
+
@content = nil if directory? or size.nil? or size > 0
|
23
|
+
@content
|
24
|
+
end
|
25
|
+
|
26
|
+
def exist?
|
27
|
+
@exist ||= (
|
28
|
+
@backend.run_command("test -e #{@spath}")
|
29
|
+
.exit_status == 0
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def link_target
|
34
|
+
return @link_target if defined? @link_target
|
35
|
+
return @link_target = nil if link_path.nil?
|
36
|
+
@link_target = @backend.file(link_path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def link_path
|
40
|
+
return nil unless symlink?
|
41
|
+
@link_path ||= (
|
42
|
+
@backend.run_command("readlink #{@spath}").stdout.chomp
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def mounted?
|
47
|
+
@mounted ||= (
|
48
|
+
!@backend.run_command("mount | grep -- ' on #{@spath}'")
|
49
|
+
.stdout.empty?
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
%w{
|
54
|
+
type mode owner group mtime size selinux_label
|
55
|
+
}.each do |field|
|
56
|
+
define_method field.to_sym do
|
57
|
+
stat[field.to_sym]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def product_version
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def file_version
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def stat
|
70
|
+
return @stat if defined?(@stat)
|
71
|
+
@stat = Train::Extras::Stat.stat(@spath, @backend)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|