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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +45 -0
  4. data/.travis.yml +12 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +201 -0
  7. data/README.md +137 -0
  8. data/Rakefile +39 -0
  9. data/lib/train.rb +100 -0
  10. data/lib/train/errors.rb +23 -0
  11. data/lib/train/extras.rb +15 -0
  12. data/lib/train/extras/command_wrapper.rb +105 -0
  13. data/lib/train/extras/file_common.rb +131 -0
  14. data/lib/train/extras/linux_file.rb +74 -0
  15. data/lib/train/extras/linux_lsb.rb +60 -0
  16. data/lib/train/extras/os_common.rb +131 -0
  17. data/lib/train/extras/os_detect_darwin.rb +32 -0
  18. data/lib/train/extras/os_detect_linux.rb +126 -0
  19. data/lib/train/extras/os_detect_unix.rb +77 -0
  20. data/lib/train/extras/os_detect_windows.rb +73 -0
  21. data/lib/train/extras/stat.rb +92 -0
  22. data/lib/train/extras/windows_file.rb +85 -0
  23. data/lib/train/options.rb +80 -0
  24. data/lib/train/plugins.rb +40 -0
  25. data/lib/train/plugins/base_connection.rb +86 -0
  26. data/lib/train/plugins/transport.rb +49 -0
  27. data/lib/train/transports/docker.rb +102 -0
  28. data/lib/train/transports/local.rb +52 -0
  29. data/lib/train/transports/local_file.rb +77 -0
  30. data/lib/train/transports/local_os.rb +51 -0
  31. data/lib/train/transports/mock.rb +125 -0
  32. data/lib/train/transports/ssh.rb +163 -0
  33. data/lib/train/transports/ssh_connection.rb +216 -0
  34. data/lib/train/transports/winrm.rb +187 -0
  35. data/lib/train/transports/winrm_connection.rb +258 -0
  36. data/lib/train/version.rb +7 -0
  37. data/test/integration/.kitchen.yml +43 -0
  38. data/test/integration/Berksfile +3 -0
  39. data/test/integration/bootstrap.sh +17 -0
  40. data/test/integration/chefignore +1 -0
  41. data/test/integration/cookbooks/test/metadata.rb +1 -0
  42. data/test/integration/cookbooks/test/recipes/default.rb +101 -0
  43. data/test/integration/docker_run.rb +153 -0
  44. data/test/integration/docker_test.rb +24 -0
  45. data/test/integration/docker_test_container.rb +24 -0
  46. data/test/integration/helper.rb +58 -0
  47. data/test/integration/sudo/nopasswd.rb +16 -0
  48. data/test/integration/sudo/passwd.rb +21 -0
  49. data/test/integration/sudo/run_as.rb +12 -0
  50. data/test/integration/test-runner.yaml +24 -0
  51. data/test/integration/test_local.rb +19 -0
  52. data/test/integration/test_ssh.rb +24 -0
  53. data/test/integration/tests/path_block_device_test.rb +74 -0
  54. data/test/integration/tests/path_character_device_test.rb +74 -0
  55. data/test/integration/tests/path_file_test.rb +79 -0
  56. data/test/integration/tests/path_folder_test.rb +88 -0
  57. data/test/integration/tests/path_missing_test.rb +77 -0
  58. data/test/integration/tests/path_pipe_test.rb +78 -0
  59. data/test/integration/tests/path_symlink_test.rb +83 -0
  60. data/test/integration/tests/run_command_test.rb +28 -0
  61. data/test/unit/extras/command_wrapper_test.rb +41 -0
  62. data/test/unit/extras/file_common_test.rb +133 -0
  63. data/test/unit/extras/linux_file_test.rb +98 -0
  64. data/test/unit/extras/os_common_test.rb +258 -0
  65. data/test/unit/extras/stat_test.rb +105 -0
  66. data/test/unit/helper.rb +6 -0
  67. data/test/unit/plugins/connection_test.rb +44 -0
  68. data/test/unit/plugins/transport_test.rb +111 -0
  69. data/test/unit/plugins_test.rb +22 -0
  70. data/test/unit/train_test.rb +132 -0
  71. data/test/unit/transports/local_file_test.rb +112 -0
  72. data/test/unit/transports/local_test.rb +73 -0
  73. data/test/unit/transports/mock_test.rb +76 -0
  74. data/test/unit/transports/ssh_test.rb +95 -0
  75. data/test/unit/version_test.rb +8 -0
  76. data/train.gemspec +32 -0
  77. metadata +299 -0
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ require 'minitest/autorun'
4
+ require 'minitest/spec'
5
+
6
+ require 'train'
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+ require 'helper'
3
+
4
+ describe 'v1 Connection Plugin' do
5
+ describe 'empty v1 connection plugin' do
6
+ let(:cls) { Train::Plugins::Transport::BaseConnection }
7
+ let(:connection) { cls.new({}) }
8
+
9
+ it 'provides a close method' do
10
+ connection.close # wont raise
11
+ end
12
+
13
+ it 'provides a run_command method' do
14
+ proc { connection.run_command('') }.must_raise Train::ClientError
15
+ end
16
+
17
+ it 'provides an os method' do
18
+ proc { connection.os }.must_raise Train::ClientError
19
+ end
20
+
21
+ it 'provides a file method' do
22
+ proc { connection.file('') }.must_raise Train::ClientError
23
+ end
24
+
25
+ it 'provides a login command method' do
26
+ proc { connection.login_command }.must_raise Train::ClientError
27
+ end
28
+
29
+ it 'can wait until ready' do
30
+ connection.wait_until_ready # wont raise
31
+ end
32
+
33
+ it 'provides a default logger' do
34
+ connection.method(:logger).call
35
+ .must_be_instance_of(Logger)
36
+ end
37
+
38
+ it 'must use the user-provided logger' do
39
+ l = rand
40
+ cls.new({logger: l})
41
+ .method(:logger).call.must_equal(l)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,111 @@
1
+ # encoding: utf-8
2
+ require 'helper'
3
+
4
+ describe 'v1 Transport Plugin' do
5
+ describe 'empty v1 transport plugin' do
6
+ let(:plugin) { Class.new(Train.plugin(1)) }
7
+
8
+ it 'initializes an empty configuration' do
9
+ plugin.new.options.must_equal({})
10
+ end
11
+
12
+ it 'saves the provided configuration' do
13
+ conf = { a: rand }
14
+ plugin.new(conf).options.must_equal(conf)
15
+ end
16
+
17
+ it 'saves the provided configuration' do
18
+ conf = { a: rand }
19
+ plugin.new(conf).options.must_equal(conf)
20
+ end
21
+
22
+ it 'provides a default logger' do
23
+ conf = { a: rand }
24
+ plugin.new(conf)
25
+ .method(:logger).call
26
+ .must_be_instance_of(Logger)
27
+ end
28
+
29
+ it 'can configure custom loggers' do
30
+ l = rand
31
+ plugin.new({ logger: l })
32
+ .method(:logger).call
33
+ .must_equal(l)
34
+ end
35
+
36
+ it 'provides a connection method' do
37
+ proc { plugin.new.connection }.must_raise Train::ClientError
38
+ end
39
+ end
40
+
41
+ describe 'registered with a name' do
42
+ before do
43
+ Train::Plugins.registry.clear
44
+ end
45
+
46
+ it 'doesnt have any plugins in the registry if none were configured' do
47
+ Train::Plugins.registry.empty?.must_equal true
48
+ end
49
+
50
+ it 'is is added to the plugins registry' do
51
+ plugin_name = rand
52
+ Train::Plugins.registry.wont_include(plugin_name)
53
+
54
+ plugin = Class.new(Train.plugin(1)) do
55
+ name plugin_name
56
+ end
57
+
58
+ Train::Plugins.registry[plugin_name].must_equal(plugin)
59
+ end
60
+ end
61
+
62
+ describe 'with options' do
63
+ def train_class(opts = {})
64
+ name = rand.to_s
65
+ plugin = Class.new(Train.plugin(1)) do
66
+ option name, opts
67
+ end
68
+ [name, plugin]
69
+ end
70
+
71
+ it 'exposes the parameters via api' do
72
+ name, plugin = train_class
73
+ plugin.default_options.keys.must_equal [name]
74
+ end
75
+
76
+ it 'exposes the parameters via api' do
77
+ default = rand.to_s
78
+ name, plugin = train_class({ default: default })
79
+ plugin.default_options[name][:default].must_equal default
80
+ end
81
+
82
+ it 'option must be required' do
83
+ name, plugin = train_class(required: true)
84
+ plugin.default_options[name][:required].must_equal true
85
+ end
86
+
87
+ it 'default option must not be required' do
88
+ name, plugin = train_class
89
+ plugin.default_options[name][:required].must_equal nil
90
+ end
91
+
92
+ it 'can include options from another module' do
93
+ nameA, pluginA = train_class
94
+ b = Class.new(Train.plugin(1)) do
95
+ include_options(pluginA)
96
+ end
97
+ b.default_options[nameA].wont_be_nil
98
+ end
99
+
100
+ it 'overwrites existing options when including' do
101
+ old = rand.to_s
102
+ nu = rand.to_s
103
+ nameA, pluginA = train_class({ default: nu })
104
+ b = Class.new(Train.plugin(1)) do
105
+ option nameA, default: old
106
+ include_options(pluginA)
107
+ end
108
+ b.default_options[nameA][:default].must_equal nu
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+ require 'helper'
3
+
4
+ describe Train::Plugins do
5
+ it 'provides a method to create new v1 transport plugins' do
6
+ Train.plugin(1).must_equal Train::Plugins::Transport
7
+ end
8
+
9
+ it 'fails when called with an unsupported plugin version' do
10
+ proc {
11
+ Train.plugin(2)
12
+ }.must_raise Train::ClientError
13
+ end
14
+
15
+ it 'defaults to v1 plugins' do
16
+ Train.plugin.must_equal Train::Plugins::Transport
17
+ end
18
+
19
+ it 'provides a registry of plugins' do
20
+ Train::Plugins.registry.must_be_instance_of(Hash)
21
+ end
22
+ end
@@ -0,0 +1,132 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+ require 'helper'
5
+
6
+ describe Train do
7
+ before do
8
+ Train::Plugins.registry.clear
9
+ end
10
+
11
+ describe '#create' do
12
+ it 'raises an error if the plugin isnt found' do
13
+ proc { Train.create('missing') }.must_raise Train::UserError
14
+ end
15
+
16
+ it 'load a plugin if it isnt in the registry yet via symbol' do
17
+ Kernel.stub :require, true do
18
+ ex = Class.new(Train.plugin 1) { name 'existing' }
19
+ train = Train.create(:existing)
20
+ train.class.must_equal ex
21
+ end
22
+ end
23
+
24
+ it 'load a plugin if it isnt in the registry yet via string' do
25
+ Kernel.stub :require, true do
26
+ ex = Class.new(Train.plugin 1) { name 'existing' }
27
+ train = Train.create('existing')
28
+ train.class.must_equal ex
29
+ end
30
+ end
31
+ end
32
+
33
+ describe '#options' do
34
+ it 'raises exception if a given transport plugin isnt found' do
35
+ proc { Train.options('missing') }.must_raise Train::UserError
36
+ end
37
+
38
+ it 'provides empty options of a transport plugin' do
39
+ Class.new(Train.plugin 1) { name 'none' }
40
+ Train.options('none').must_equal({})
41
+ end
42
+
43
+ it 'provides all options of a transport plugin' do
44
+ Class.new(Train.plugin 1) {
45
+ name 'one'
46
+ option :one, required: true, default: 123
47
+ }
48
+ Train.options('one').must_equal({
49
+ one: {
50
+ required: true,
51
+ default: 123,
52
+ }
53
+ })
54
+ end
55
+ end
56
+
57
+ describe '#target_config' do
58
+ it 'configures resolves target' do
59
+ org = {
60
+ target: 'ssh://user:pass@host.com:123/path',
61
+ }
62
+ res = Train.target_config(org)
63
+ res[:backend].must_equal 'ssh'
64
+ res[:host].must_equal 'host.com'
65
+ res[:user].must_equal 'user'
66
+ res[:password].must_equal 'pass'
67
+ res[:port].must_equal 123
68
+ res[:target].must_equal org[:target]
69
+ res[:path].must_equal '/path'
70
+ org.keys.must_equal [:target]
71
+ end
72
+
73
+ it 'resolves a target while keeping existing fields' do
74
+ org = {
75
+ target: 'ssh://user:pass@host.com:123/path',
76
+ backend: rand,
77
+ host: rand,
78
+ user: rand,
79
+ password: rand,
80
+ port: rand,
81
+ path: rand
82
+ }
83
+ res = Train.target_config(org)
84
+ res.must_equal org
85
+ end
86
+
87
+ it 'resolves a winrm target' do
88
+ org = {
89
+ target: 'winrm://Administrator@192.168.10.140',
90
+ backend: 'winrm',
91
+ host: '192.168.10.140',
92
+ user: 'Administrator',
93
+ password: nil,
94
+ port: nil,
95
+ path: nil
96
+ }
97
+ res = Train.target_config(org)
98
+ res.must_equal org
99
+ end
100
+
101
+ it 'keeps the configuration when incorrect target is supplied' do
102
+ org = {
103
+ target: 'wrong',
104
+ }
105
+ res = Train.target_config(org)
106
+ res[:backend].must_be_nil
107
+ res[:host].must_be_nil
108
+ res[:user].must_be_nil
109
+ res[:password].must_be_nil
110
+ res[:port].must_be_nil
111
+ res[:path].must_be_nil
112
+ res[:target].must_equal org[:target]
113
+ end
114
+
115
+ it 'always takes ruby sumbols as configuration fields' do
116
+ org = {
117
+ 'target' => 'ssh://user:pass@host.com:123/path',
118
+ 'backend' => rand,
119
+ 'host' => rand,
120
+ 'user' => rand,
121
+ 'password' => rand,
122
+ 'port' => rand,
123
+ 'path' => rand
124
+ }
125
+ nu = org.each_with_object({}) { |(x, y), acc|
126
+ acc[x.to_sym] = y; acc
127
+ }
128
+ res = Train.target_config(org)
129
+ res.must_equal nu
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,112 @@
1
+ # encoding: utf-8
2
+ #
3
+ # author: Dominik Richter
4
+ # author: Christoph Hartmann
5
+
6
+ require 'helper'
7
+ require 'train/transports/local'
8
+
9
+ describe 'local file transport' do
10
+ let(:transport) { Train::Transports::Local.new }
11
+ let(:connection) { transport.connection }
12
+
13
+ it 'gets file contents' do
14
+ res = rand.to_s
15
+ File.stub :read, res do
16
+ connection.file(rand.to_s).content.must_equal(res)
17
+ end
18
+ end
19
+
20
+ it 'checks for file existance' do
21
+ File.stub :exist?, true do
22
+ connection.file(rand.to_s).exist?.must_equal(true)
23
+ end
24
+ end
25
+
26
+ {
27
+ exist?: :exist?,
28
+ file?: :file?,
29
+ socket?: :socket?,
30
+ directory?: :directory?,
31
+ symlink?: :symlink?,
32
+ pipe?: :pipe?,
33
+ character_device?: :chardev?,
34
+ block_device?: :blockdev?,
35
+ }.each do |method, file_method|
36
+ it "checks if file is a #{method}" do
37
+ File.stub file_method.to_sym, true do
38
+ connection.file(rand.to_s).method(method.to_sym).call.must_equal(true)
39
+ end
40
+ end
41
+ end
42
+
43
+ it 'checks a file\'s link path' do
44
+ out = rand.to_s
45
+ File.stub :readlink, out do
46
+ File.stub :symlink?, true do
47
+ connection.file(rand.to_s).link_path.must_equal out
48
+ end
49
+ end
50
+ end
51
+
52
+ describe 'file metadata' do
53
+ let(:stat) { Struct.new(:mode, :size, :mtime, :uid, :gid) }
54
+ let(:statres) { stat.new(00140755, rand, (rand*100).to_i, rand, rand) }
55
+
56
+ def meta_stub(method, param, &block)
57
+ pwres = Struct.new(:name)
58
+ Etc.stub :getpwuid, pwres.new('owner') do
59
+ Etc.stub :getgrgid, pwres.new('group') do
60
+ File.stub method, param do; yield; end
61
+ end
62
+ end
63
+ end
64
+
65
+ it 'recognizes type' do
66
+ meta_stub :lstat, statres do
67
+ connection.file(rand.to_s).type.must_equal :socket
68
+ end
69
+ end
70
+
71
+ it 'recognizes mode' do
72
+ meta_stub :lstat, statres do
73
+ connection.file(rand.to_s).mode.must_equal 00755
74
+ end
75
+ end
76
+
77
+ it 'recognizes mtime' do
78
+ meta_stub :lstat, statres do
79
+ connection.file(rand.to_s).mtime.must_equal statres.mtime
80
+ end
81
+ end
82
+
83
+ it 'recognizes size' do
84
+ meta_stub :lstat, statres do
85
+ connection.file(rand.to_s).size.must_equal statres.size
86
+ end
87
+ end
88
+
89
+ it 'recognizes owner' do
90
+ meta_stub :lstat, statres do
91
+ connection.file(rand.to_s).owner.must_equal 'owner'
92
+ end
93
+ end
94
+
95
+ it 'recognizes group' do
96
+ meta_stub :lstat, statres do
97
+ connection.file(rand.to_s).group.must_equal 'group'
98
+ end
99
+ end
100
+
101
+ it 'recognizes selinux label' do
102
+ meta_stub :lstat, statres do
103
+ label = rand.to_s
104
+ res = Train::Extras::CommandResult.new(label, nil, 0)
105
+ connection.stub :run_command, res do
106
+ connection.file(rand.to_s).selinux_label.must_equal label
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ end
@@ -0,0 +1,73 @@
1
+ # encoding: utf-8
2
+ #
3
+ # author: Dominik Richter
4
+ # author: Christoph Hartmann
5
+
6
+ require 'helper'
7
+ require 'train/transports/local'
8
+
9
+ describe 'local transport' do
10
+ let(:transport) { Train::Transports::Local.new }
11
+ let(:connection) { transport.connection }
12
+
13
+ it 'can be instantiated' do
14
+ transport.wont_be_nil
15
+ end
16
+
17
+ it 'gets the connection' do
18
+ connection.must_be_kind_of Train::Transports::Local::Connection
19
+ end
20
+
21
+ it 'doesnt wait to be read' do
22
+ connection.wait_until_ready.must_be_nil
23
+ end
24
+
25
+ it 'can be closed' do
26
+ connection.close.must_be_nil
27
+ end
28
+
29
+ it 'has no login command' do
30
+ connection.login_command.must_be_nil
31
+ end
32
+
33
+ describe 'when running a local command' do
34
+ let(:mock) { Minitest::Mock.new }
35
+
36
+ def mock_run_cmd(cmd, &block)
37
+ mock.expect :run_command, nil
38
+ Mixlib::ShellOut.stub :new, mock do |*args|
39
+ block.call()
40
+ end
41
+ end
42
+
43
+ it 'gets stdout' do
44
+ mock_run_cmd(rand) do
45
+ x = rand
46
+ mock.expect :stdout, x
47
+ mock.expect :stderr, nil
48
+ mock.expect :exitstatus, nil
49
+ connection.run_command(rand).stdout.must_equal x
50
+ end
51
+ end
52
+
53
+ it 'gets stderr' do
54
+ mock_run_cmd(rand) do
55
+ x = rand
56
+ mock.expect :stdout, nil
57
+ mock.expect :stderr, x
58
+ mock.expect :exitstatus, nil
59
+ connection.run_command(rand).stderr.must_equal x
60
+ end
61
+ end
62
+
63
+ it 'gets exit_status' do
64
+ mock_run_cmd(rand) do
65
+ x = rand
66
+ mock.expect :stdout, nil
67
+ mock.expect :stderr, nil
68
+ mock.expect :exitstatus, x
69
+ connection.run_command(rand).exit_status.must_equal x
70
+ end
71
+ end
72
+ end
73
+ end