r-train 0.9.1
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/.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
|