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
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
module Train::Extras
|
6
|
+
class Stat
|
7
|
+
TYPES = {
|
8
|
+
socket: 00140000,
|
9
|
+
symlink: 00120000,
|
10
|
+
file: 00100000,
|
11
|
+
block_device: 00060000,
|
12
|
+
directory: 00040000,
|
13
|
+
character_device: 00020000,
|
14
|
+
pipe: 00010000,
|
15
|
+
}
|
16
|
+
|
17
|
+
def self.find_type(mode)
|
18
|
+
res = TYPES.find { |_, mask| mask & mode == mask }
|
19
|
+
res.nil? ? :unknown : res[0]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.stat(shell_escaped_path, backend)
|
23
|
+
return bsd_stat(shell_escaped_path, backend) if backend.os.bsd?
|
24
|
+
return linux_stat(shell_escaped_path, backend) if backend.os.unix?
|
25
|
+
# all other cases we don't handle
|
26
|
+
# TODO: print an error if we get here, as it shouldn't be invoked
|
27
|
+
# on non-unix
|
28
|
+
{}
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.linux_stat(shell_escaped_path, backend)
|
32
|
+
res = backend.run_command("stat #{shell_escaped_path} 2>/dev/null --printf '%s\n%f\n%U\n%u\n%G\n%g\n%X\n%Y\n%C'")
|
33
|
+
|
34
|
+
# ignore the exit_code: it is != 0 if selinux labels are not supported
|
35
|
+
# on the system.
|
36
|
+
|
37
|
+
fields = res.stdout.split("\n")
|
38
|
+
return {} if fields.length != 9
|
39
|
+
|
40
|
+
tmask = fields[1].to_i(16)
|
41
|
+
selinux = fields[8]
|
42
|
+
selinux = nil if selinux == '?' or selinux == '(null)'
|
43
|
+
|
44
|
+
{
|
45
|
+
type: find_type(tmask),
|
46
|
+
mode: tmask & 00777,
|
47
|
+
owner: fields[2],
|
48
|
+
group: fields[4],
|
49
|
+
mtime: fields[7].to_i,
|
50
|
+
size: fields[0].to_i,
|
51
|
+
selinux_label: selinux,
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.bsd_stat(shell_escaped_path, backend)
|
56
|
+
# From stat man page on FreeBSD:
|
57
|
+
# z The size of file in bytes (st_size).
|
58
|
+
# p File type and permissions (st_mode).
|
59
|
+
# u, g User ID and group ID of file's owner (st_uid, st_gid).
|
60
|
+
# a, m, c, B
|
61
|
+
# The time file was last accessed or modified, or when the
|
62
|
+
# inode was last changed, or the birth time of the inode
|
63
|
+
# (st_atime, st_mtime, st_ctime, st_birthtime).
|
64
|
+
#
|
65
|
+
# The special output specifier S may be used to indicate that the
|
66
|
+
# output, if applicable, should be in string format. May be used
|
67
|
+
# in combination with:
|
68
|
+
# ...
|
69
|
+
# gu Display group or user name.
|
70
|
+
res = backend.run_command(
|
71
|
+
"stat -f '%z\n%p\n%Su\n%u\n%Sg\n%g\n%a\n%m' "\
|
72
|
+
"#{shell_escaped_path}")
|
73
|
+
|
74
|
+
return {} if res.exit_status != 0
|
75
|
+
|
76
|
+
fields = res.stdout.split("\n")
|
77
|
+
return {} if fields.length != 8
|
78
|
+
|
79
|
+
tmask = fields[1].to_i(8)
|
80
|
+
|
81
|
+
{
|
82
|
+
type: find_type(tmask),
|
83
|
+
mode: tmask & 00777,
|
84
|
+
owner: fields[2],
|
85
|
+
group: fields[4],
|
86
|
+
mtime: fields[7].to_i,
|
87
|
+
size: fields[0].to_i,
|
88
|
+
selinux_label: fields[8],
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'train/extras/stat'
|
6
|
+
|
7
|
+
# PS C:\Users\Administrator> Get-Item -Path C:\test.txt | Select-Object -Property BaseName, FullName, IsReadOnly, Exists,
|
8
|
+
# LinkType, Mode, VersionInfo, Owner, Archive, Hidden, ReadOnly, System | ConvertTo-Json
|
9
|
+
|
10
|
+
module Train::Extras
|
11
|
+
class WindowsFile < FileCommon
|
12
|
+
attr_reader :path
|
13
|
+
def initialize(backend, path)
|
14
|
+
@backend = backend
|
15
|
+
@path = path
|
16
|
+
@spath = Shellwords.escape(@path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def content
|
20
|
+
return @content if defined?(@content)
|
21
|
+
@content = @backend.run_command(
|
22
|
+
"Get-Content(\"#{@spath}\") | Out-String").stdout
|
23
|
+
return @content unless @content.empty?
|
24
|
+
@content = nil if directory? # or size.nil? or size > 0
|
25
|
+
@content
|
26
|
+
end
|
27
|
+
|
28
|
+
def exist?
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def link_target
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def link_path
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def mounted?
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def type
|
45
|
+
:unknown
|
46
|
+
end
|
47
|
+
|
48
|
+
%w{
|
49
|
+
mode owner group mtime size selinux_label
|
50
|
+
}.each do |field|
|
51
|
+
define_method field.to_sym do
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def product_version
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def file_version
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def stat
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def attributes
|
71
|
+
return @attributes if defined?(@attributes)
|
72
|
+
@attributes = @backend.run_command(
|
73
|
+
"(Get-ItemProperty -Path \"#{@spath}\").attributes.ToString()").stdout.chomp.split(',')
|
74
|
+
end
|
75
|
+
|
76
|
+
def target_type
|
77
|
+
if attributes.include?('Archive')
|
78
|
+
return :file
|
79
|
+
elsif attributes.include?('Directory')
|
80
|
+
return :directory
|
81
|
+
end
|
82
|
+
:unknown
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
|
+
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
5
|
+
|
6
|
+
module Train
|
7
|
+
module Options
|
8
|
+
def self.attach(target)
|
9
|
+
target.class.method(:include).call(ClassOptions)
|
10
|
+
target.method(:include).call(InstanceOptions)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassOptions
|
14
|
+
def option(name, conf = nil, &block)
|
15
|
+
d = conf || {}
|
16
|
+
unless d.is_a? Hash
|
17
|
+
fail Train::ClientError,
|
18
|
+
"The transport plugin #{self} declared an option #{name} "\
|
19
|
+
"and didn't provide a valid configuration hash."
|
20
|
+
end
|
21
|
+
|
22
|
+
if !conf.nil? and !conf[:default].nil? and block_given?
|
23
|
+
fail Train::ClientError,
|
24
|
+
"The transport plugin #{self} declared an option #{name} "\
|
25
|
+
'with both a default value and block. Only use one of these.'
|
26
|
+
end
|
27
|
+
|
28
|
+
d[:default] = block if block_given?
|
29
|
+
|
30
|
+
default_options[name] = d
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_options
|
34
|
+
@default_options = {} unless defined? @default_options
|
35
|
+
@default_options
|
36
|
+
end
|
37
|
+
|
38
|
+
def include_options(other)
|
39
|
+
unless other.respond_to?(:default_options)
|
40
|
+
fail "Trying to include options from module #{other.inspect}, "\
|
41
|
+
"which doesn't seem to support options."
|
42
|
+
end
|
43
|
+
default_options.merge!(other.default_options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module InstanceOptions
|
48
|
+
# @return [Hash] options, which created this Transport
|
49
|
+
attr_reader :options
|
50
|
+
|
51
|
+
def default_options
|
52
|
+
self.class.default_options
|
53
|
+
end
|
54
|
+
|
55
|
+
def merge_options(base, opts)
|
56
|
+
res = base.merge(opts || {})
|
57
|
+
default_options.each do |field, hm|
|
58
|
+
next unless res[field].nil? and hm.key?(:default)
|
59
|
+
default = hm[:default]
|
60
|
+
if default.is_a? Proc
|
61
|
+
res[field] = default.call(res)
|
62
|
+
else
|
63
|
+
res[field] = default
|
64
|
+
end
|
65
|
+
end
|
66
|
+
res
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_options(opts)
|
70
|
+
default_options.each do |field, hm|
|
71
|
+
if opts[field].nil? and hm[:required]
|
72
|
+
fail Train::ClientError,
|
73
|
+
"You must provide a value for #{field.to_s.inspect}."
|
74
|
+
end
|
75
|
+
end
|
76
|
+
opts
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
|
+
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
5
|
+
|
6
|
+
require 'train/errors'
|
7
|
+
|
8
|
+
module Train
|
9
|
+
class Plugins
|
10
|
+
autoload :Transport, 'train/plugins/transport'
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Retrieve the current plugin registry, containing all plugin names
|
14
|
+
# and their transport handlers.
|
15
|
+
#
|
16
|
+
# @return [Hash] map with plugin names and plugins
|
17
|
+
def registry
|
18
|
+
@registry ||= {}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create a new plugin by inheriting from the class returned by this method.
|
24
|
+
# Create a versioned plugin by providing the transport layer plugin version
|
25
|
+
# to this method. It will then select the correct class to inherit from.
|
26
|
+
#
|
27
|
+
# The plugin version determins what methods will be available to your plugin.
|
28
|
+
#
|
29
|
+
# @param [Int] version = 1 the plugin version to use
|
30
|
+
# @return [Transport] the versioned transport base class
|
31
|
+
def self.plugin(version = 1)
|
32
|
+
if version != 1
|
33
|
+
fail ClientError,
|
34
|
+
'Only understand train plugin version 1. You are trying to '\
|
35
|
+
"initialize a train plugin #{version}, which is not supported "\
|
36
|
+
'in the current release of train.'
|
37
|
+
end
|
38
|
+
::Train::Plugins::Transport
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Salim Afiune (<salim@afiunemaya.com.mx>)
|
4
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
5
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
6
|
+
|
7
|
+
require 'train/errors'
|
8
|
+
require 'train/extras'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
class Train::Plugins::Transport
|
12
|
+
# A Connection instance can be generated and re-generated, given new
|
13
|
+
# connection details such as connection port, hostname, credentials, etc.
|
14
|
+
# This object is responsible for carrying out the actions on the remote
|
15
|
+
# host such as executing commands, transferring files, etc.
|
16
|
+
#
|
17
|
+
# @author Fletcher Nichol <fnichol@nichol.ca>
|
18
|
+
class BaseConnection
|
19
|
+
include Train::Extras
|
20
|
+
|
21
|
+
# Create a new Connection instance.
|
22
|
+
#
|
23
|
+
# @param options [Hash] connection options
|
24
|
+
# @yield [self] yields itself for block-style invocation
|
25
|
+
def initialize(options = nil)
|
26
|
+
@options = options || {}
|
27
|
+
@logger = @options.delete(:logger) || Logger.new(STDOUT)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Closes the session connection, if it is still active.
|
31
|
+
def close
|
32
|
+
# this method may be left unimplemented if that is applicable
|
33
|
+
end
|
34
|
+
|
35
|
+
# Execute a command using this connection.
|
36
|
+
#
|
37
|
+
# @param command [String] command string to execute
|
38
|
+
# @return [CommandResult] contains the result of running the command
|
39
|
+
def run_command(_command)
|
40
|
+
fail Train::ClientError, "#{self.class} does not implement #run_command()"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get information on the operating system which this transport connects to.
|
44
|
+
#
|
45
|
+
# @return [OSCommon] operating system information
|
46
|
+
def os
|
47
|
+
fail Train::ClientError, "#{self.class} does not implement #os()"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Interact with files on the target. Read, write, and get metadata
|
51
|
+
# from files via the transport.
|
52
|
+
#
|
53
|
+
# @param [String] path which is being inspected
|
54
|
+
# @return [FileCommon] file object that allows for interaction
|
55
|
+
def file(_path, *_args)
|
56
|
+
fail Train::ClientError, "#{self.class} does not implement #file(...)"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Builds a LoginCommand which can be used to open an interactive
|
60
|
+
# session on the remote host.
|
61
|
+
#
|
62
|
+
# @return [LoginCommand] array of command line tokens
|
63
|
+
def login_command
|
64
|
+
fail Train::ClientError, "#{self.class} does not implement #run_command()"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Block and return only when the remote host is prepared and ready to
|
68
|
+
# execute command and upload files. The semantics and details will
|
69
|
+
# vary by implementation, but a round trip through the hosted
|
70
|
+
# service is preferred to simply waiting on a socket to become
|
71
|
+
# available.
|
72
|
+
def wait_until_ready
|
73
|
+
# this method may be left unimplemented if that is applicablelog
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# @return [Logger] logger for reporting information
|
79
|
+
# @api private
|
80
|
+
attr_reader :logger
|
81
|
+
|
82
|
+
# @return [Hash] connection options
|
83
|
+
# @api private
|
84
|
+
attr_reader :options
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
|
+
# Author:: Christoph Hartmann (<chris@lollyrock.com>)
|
5
|
+
|
6
|
+
require 'logger'
|
7
|
+
require 'train/errors'
|
8
|
+
require 'train/extras'
|
9
|
+
require 'train/options'
|
10
|
+
|
11
|
+
class Train::Plugins
|
12
|
+
class Transport
|
13
|
+
include Train::Extras
|
14
|
+
Train::Options.attach(self)
|
15
|
+
|
16
|
+
autoload :BaseConnection, 'train/plugins/base_connection'
|
17
|
+
|
18
|
+
# Initialize a new Transport object
|
19
|
+
#
|
20
|
+
# @param [Hash] config = nil the configuration for this transport
|
21
|
+
# @return [Transport] the transport object
|
22
|
+
def initialize(options = {})
|
23
|
+
@options = merge_options({}, options || {})
|
24
|
+
@logger = @options[:logger] || Logger.new(STDOUT)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create a connection to the target. Options may be provided
|
28
|
+
# for additional configuration.
|
29
|
+
#
|
30
|
+
# @param [Hash] _options = nil provide optional configuration params
|
31
|
+
# @return [Connection] the connection for this configuration
|
32
|
+
def connection(_options = nil)
|
33
|
+
fail Train::ClientError, "#{self.class} does not implement #connect()"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Register the inheriting class with as a train plugin using the
|
37
|
+
# provided name.
|
38
|
+
#
|
39
|
+
# @param [String] name of the plugin, by which it will be found
|
40
|
+
def self.name(name)
|
41
|
+
Train::Plugins.registry[name] = self
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# @return [Logger] logger for reporting information
|
47
|
+
attr_reader :logger
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter
|
4
|
+
# Author:: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'docker'
|
7
|
+
|
8
|
+
module Train::Transports
|
9
|
+
class Docker < Train.plugin(1)
|
10
|
+
name 'docker'
|
11
|
+
|
12
|
+
include_options Train::Extras::CommandWrapper
|
13
|
+
option :host, required: true
|
14
|
+
|
15
|
+
def connection(state = {}, &block)
|
16
|
+
opts = merge_options(options, state || {})
|
17
|
+
validate_options(opts)
|
18
|
+
|
19
|
+
if @connection && @connection_options == opts
|
20
|
+
reuse_connection(&block)
|
21
|
+
else
|
22
|
+
create_new_connection(opts, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Creates a new Docker connection instance and save it for potential future
|
29
|
+
# reuse.
|
30
|
+
#
|
31
|
+
# @param options [Hash] connection options
|
32
|
+
# @return [Docker::Connection] a Docker connection instance
|
33
|
+
# @api private
|
34
|
+
def create_new_connection(options, &block)
|
35
|
+
if @connection
|
36
|
+
logger.debug("[Docker] shutting previous connection #{@connection}")
|
37
|
+
@connection.close
|
38
|
+
end
|
39
|
+
|
40
|
+
@connection_options = options
|
41
|
+
@connection = Connection.new(options, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return the last saved Docker connection instance.
|
45
|
+
#
|
46
|
+
# @return [Docker::Connection] a Docker connection instance
|
47
|
+
# @api private
|
48
|
+
def reuse_connection
|
49
|
+
logger.debug("[Docker] reusing existing connection #{@connection}")
|
50
|
+
yield @connection if block_given?
|
51
|
+
@connection
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Train::Transports::Docker
|
57
|
+
class Connection < BaseConnection
|
58
|
+
def initialize(conf)
|
59
|
+
super(conf)
|
60
|
+
@id = options[:host]
|
61
|
+
@container = ::Docker::Container.get(@id) ||
|
62
|
+
fail("Can't find Docker container #{@id}")
|
63
|
+
@files = {}
|
64
|
+
@cmd_wrapper = nil
|
65
|
+
@cmd_wrapper = CommandWrapper.load(self, @options)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def close
|
70
|
+
# nothing to do at the moment
|
71
|
+
end
|
72
|
+
|
73
|
+
def os
|
74
|
+
@os ||= OS.new(self)
|
75
|
+
end
|
76
|
+
|
77
|
+
def file(path)
|
78
|
+
@files[path] ||= LinuxFile.new(self, path)
|
79
|
+
end
|
80
|
+
|
81
|
+
def run_command(cmd)
|
82
|
+
cmd = @cmd_wrapper.run(cmd) unless @cmd_wrapper.nil?
|
83
|
+
stdout, stderr, exit_status = @container.exec([
|
84
|
+
'/bin/sh', '-c', cmd
|
85
|
+
])
|
86
|
+
CommandResult.new(stdout.join, stderr.join, exit_status)
|
87
|
+
rescue ::Docker::Error::DockerError => _
|
88
|
+
raise
|
89
|
+
rescue => _
|
90
|
+
# @TODO: differentiate any other error
|
91
|
+
raise
|
92
|
+
end
|
93
|
+
|
94
|
+
class OS < OSCommon
|
95
|
+
def initialize(backend)
|
96
|
+
# hardcoded to unix/linux for now, until other operating systems
|
97
|
+
# are supported
|
98
|
+
super(backend, { family: 'unix' })
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|