train 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +71 -0
  3. data/CHANGELOG.md +308 -0
  4. data/Gemfile +30 -0
  5. data/LICENSE +201 -0
  6. data/README.md +156 -0
  7. data/Rakefile +148 -0
  8. data/lib/train.rb +117 -0
  9. data/lib/train/errors.rb +23 -0
  10. data/lib/train/extras.rb +17 -0
  11. data/lib/train/extras/command_wrapper.rb +148 -0
  12. data/lib/train/extras/file_aix.rb +20 -0
  13. data/lib/train/extras/file_common.rb +161 -0
  14. data/lib/train/extras/file_linux.rb +16 -0
  15. data/lib/train/extras/file_unix.rb +79 -0
  16. data/lib/train/extras/file_windows.rb +91 -0
  17. data/lib/train/extras/linux_lsb.rb +60 -0
  18. data/lib/train/extras/os_common.rb +136 -0
  19. data/lib/train/extras/os_detect_darwin.rb +32 -0
  20. data/lib/train/extras/os_detect_linux.rb +148 -0
  21. data/lib/train/extras/os_detect_unix.rb +99 -0
  22. data/lib/train/extras/os_detect_windows.rb +57 -0
  23. data/lib/train/extras/stat.rb +133 -0
  24. data/lib/train/options.rb +80 -0
  25. data/lib/train/plugins.rb +40 -0
  26. data/lib/train/plugins/base_connection.rb +86 -0
  27. data/lib/train/plugins/transport.rb +49 -0
  28. data/lib/train/transports/docker.rb +103 -0
  29. data/lib/train/transports/local.rb +52 -0
  30. data/lib/train/transports/local_file.rb +90 -0
  31. data/lib/train/transports/local_os.rb +51 -0
  32. data/lib/train/transports/mock.rb +147 -0
  33. data/lib/train/transports/ssh.rb +163 -0
  34. data/lib/train/transports/ssh_connection.rb +225 -0
  35. data/lib/train/transports/winrm.rb +184 -0
  36. data/lib/train/transports/winrm_connection.rb +194 -0
  37. data/lib/train/version.rb +7 -0
  38. data/test/integration/.kitchen.yml +43 -0
  39. data/test/integration/Berksfile +3 -0
  40. data/test/integration/bootstrap.sh +17 -0
  41. data/test/integration/chefignore +1 -0
  42. data/test/integration/cookbooks/test/metadata.rb +1 -0
  43. data/test/integration/cookbooks/test/recipes/default.rb +100 -0
  44. data/test/integration/cookbooks/test/recipes/prep_files.rb +47 -0
  45. data/test/integration/docker_run.rb +153 -0
  46. data/test/integration/docker_test.rb +24 -0
  47. data/test/integration/docker_test_container.rb +24 -0
  48. data/test/integration/helper.rb +61 -0
  49. data/test/integration/sudo/customcommand.rb +15 -0
  50. data/test/integration/sudo/nopasswd.rb +16 -0
  51. data/test/integration/sudo/passwd.rb +21 -0
  52. data/test/integration/sudo/reqtty.rb +17 -0
  53. data/test/integration/sudo/run_as.rb +12 -0
  54. data/test/integration/test-travis-1.yaml +13 -0
  55. data/test/integration/test-travis-2.yaml +13 -0
  56. data/test/integration/test_local.rb +19 -0
  57. data/test/integration/test_ssh.rb +39 -0
  58. data/test/integration/tests/path_block_device_test.rb +74 -0
  59. data/test/integration/tests/path_character_device_test.rb +74 -0
  60. data/test/integration/tests/path_file_test.rb +79 -0
  61. data/test/integration/tests/path_folder_test.rb +90 -0
  62. data/test/integration/tests/path_missing_test.rb +77 -0
  63. data/test/integration/tests/path_pipe_test.rb +78 -0
  64. data/test/integration/tests/path_symlink_test.rb +95 -0
  65. data/test/integration/tests/run_command_test.rb +28 -0
  66. data/test/unit/extras/command_wrapper_test.rb +78 -0
  67. data/test/unit/extras/file_common_test.rb +180 -0
  68. data/test/unit/extras/linux_file_test.rb +167 -0
  69. data/test/unit/extras/os_common_test.rb +269 -0
  70. data/test/unit/extras/os_detect_linux_test.rb +189 -0
  71. data/test/unit/extras/os_detect_windows_test.rb +99 -0
  72. data/test/unit/extras/stat_test.rb +148 -0
  73. data/test/unit/extras/windows_file_test.rb +44 -0
  74. data/test/unit/helper.rb +7 -0
  75. data/test/unit/plugins/connection_test.rb +44 -0
  76. data/test/unit/plugins/transport_test.rb +111 -0
  77. data/test/unit/plugins_test.rb +22 -0
  78. data/test/unit/train_test.rb +156 -0
  79. data/test/unit/transports/local_file_test.rb +184 -0
  80. data/test/unit/transports/local_test.rb +87 -0
  81. data/test/unit/transports/mock_test.rb +87 -0
  82. data/test/unit/transports/ssh_test.rb +109 -0
  83. data/test/unit/version_test.rb +8 -0
  84. data/test/windows/local_test.rb +46 -0
  85. data/test/windows/winrm_test.rb +52 -0
  86. data/train.gemspec +38 -0
  87. metadata +295 -0
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Dominik Richter (<dominik.richter@gmail.com>)
4
+
5
+ module Train
6
+ VERSION = '0.12.1'.freeze
7
+ end
@@ -0,0 +1,43 @@
1
+ ---
2
+ driver:
3
+ name: vagrant
4
+
5
+ provisioner:
6
+ name: chef_solo
7
+ data_path: ../../.
8
+
9
+ platforms:
10
+ - name: centos-7.1
11
+ - name: centos-6.7
12
+ - name: centos-6.7-i386
13
+ - name: centos-5.11
14
+ - name: centos-5.11-i386
15
+ - name: debian-6.0.10
16
+ - name: debian-6.0.10-i386
17
+ - name: debian-7.8
18
+ - name: debian-7.8-i386
19
+ - name: debian-8.1
20
+ - name: debian-8.1-i386
21
+ - name: fedora-21
22
+ - name: fedora-21-i386
23
+ - name: fedora-22
24
+ - name: freebsd-9.3
25
+ - name: freebsd-10.2
26
+ - name: opensuse-13.2-x86_64
27
+ - name: opensuse-13.2-i386
28
+ - name: ubuntu-14.04
29
+ - name: ubuntu-14.04-i386
30
+ - name: ubuntu-12.04
31
+ - name: ubuntu-12.04-i386
32
+ - name: ubuntu-10.04
33
+ - name: ubuntu-10.04-i386
34
+
35
+ suites:
36
+ - name: default
37
+ run_list:
38
+ - recipe[sudo]
39
+ - recipe[test]
40
+ attributes:
41
+ authorization:
42
+ sudo:
43
+ include_sudoers_d: true
@@ -0,0 +1,3 @@
1
+ source 'https://supermarket.chef.io'
2
+ cookbook 'sudo', '~> 2.7.2'
3
+ cookbook 'test', path: 'cookbooks/test'
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ test ! -e /tmp/folder && \
3
+ mkdir /tmp/folder
4
+ chmod 0567 /tmp/folder
5
+
6
+ echo -n 'hello world' > /tmp/file
7
+ test ! -e /tmp/symlink && \
8
+ ln -s /tmp/file /tmp/symlink
9
+ chmod 0777 /tmp/symlink
10
+ chmod 0765 /tmp/file
11
+
12
+ test ! -e /tmp/pipe && \
13
+ mkfifo /tmp/pipe
14
+
15
+ test ! -e /tmp/block_device && \
16
+ mknod /tmp/block_device b 7 7
17
+ chmod 0666 /tmp/block_device
@@ -0,0 +1 @@
1
+ .kitchen
@@ -0,0 +1 @@
1
+ name 'test'
@@ -0,0 +1,100 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ #
4
+ # Helper recipe to create create a few files in the operating
5
+ # systems, which the runner will test against.
6
+ # It also initializes the runner inside the machines
7
+ # and makes sure all dependencies are ready to go.
8
+ #
9
+ # Finally (for now), it actually executes the all tests with
10
+ # the local execution backend
11
+
12
+ include_recipe('test::prep_files')
13
+
14
+ # prepare ssh for backend
15
+ execute 'create ssh key' do
16
+ command 'ssh-keygen -t rsa -b 2048 -f /root/.ssh/id_rsa -N ""'
17
+ not_if 'test -e /root/.ssh/id_rsa'
18
+ end
19
+
20
+ execute 'add ssh key to vagrant user' do
21
+ command 'cat /root/.ssh/id_rsa.pub >> /home/vagrant/.ssh/authorized_keys'
22
+ end
23
+
24
+ execute 'test ssh connection' do
25
+ command 'ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa vagrant@localhost "echo 1"'
26
+ end
27
+
28
+ # prepare a few users
29
+ %w{ nopasswd passwd nosudo reqtty customcommand }.each do |name|
30
+ user name do
31
+ password '$1$7MCNTXPI$r./jqCEoVlLlByYKSL3sZ.'
32
+ manage_home true
33
+ end
34
+ end
35
+
36
+ %w{nopasswd vagrant}.each do |name|
37
+ sudo name do
38
+ user '%'+name
39
+ nopasswd true
40
+ defaults ['!requiretty']
41
+ end
42
+ end
43
+
44
+ sudo 'passwd' do
45
+ user 'passwd'
46
+ nopasswd false
47
+ defaults ['!requiretty']
48
+ end
49
+
50
+ sudo 'reqtty' do
51
+ user 'reqtty'
52
+ nopasswd true
53
+ defaults ['requiretty']
54
+ end
55
+
56
+ sudo 'customcommand' do
57
+ user 'customcommand'
58
+ nopasswd true
59
+ defaults ['!requiretty']
60
+ end
61
+
62
+ # execute tests
63
+ execute 'bundle install' do
64
+ command '/opt/chef/embedded/bin/bundle install --without integration tools'
65
+ cwd '/tmp/kitchen/data'
66
+ end
67
+
68
+ execute 'run local tests' do
69
+ command '/opt/chef/embedded/bin/ruby -I lib test/integration/test_local.rb test/integration/tests/*_test.rb'
70
+ cwd '/tmp/kitchen/data'
71
+ end
72
+
73
+ execute 'run ssh tests' do
74
+ command '/opt/chef/embedded/bin/ruby -I lib test/integration/test_ssh.rb test/integration/tests/*_test.rb'
75
+ cwd '/tmp/kitchen/data'
76
+ end
77
+
78
+ %w{passwd nopasswd reqtty customcommand}.each do |name|
79
+ execute "run local sudo tests as #{name}" do
80
+ command "/opt/chef/embedded/bin/ruby -I lib test/integration/sudo/#{name}.rb"
81
+ cwd '/tmp/kitchen/data'
82
+ user name
83
+ end
84
+ end
85
+
86
+ execute 'fix sudoers for reqtty' do
87
+ command 'chef-apply contrib/fixup_requiretty.rb'
88
+ cwd '/tmp/kitchen/data'
89
+ environment(
90
+ 'TRAIN_SUDO_USER' => 'reqtty',
91
+ 'TRAIN_SUDO_VERY_MUCH' => 'yes',
92
+ )
93
+ end
94
+
95
+ # if it's fixed, it should behave like user 'nopasswd'
96
+ execute 'run local sudo tests as reqtty, no longer requiring a tty' do
97
+ command "/opt/chef/embedded/bin/ruby -I lib test/integration/sudo/nopasswd.rb"
98
+ cwd '/tmp/kitchen/data'
99
+ user 'reqtty'
100
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+ #
5
+ # Helper recipe to create create a few files in the operating
6
+ # systems, which the runner will test against.
7
+
8
+ gid = 'root'
9
+ gid = 'wheel' if node['platform_family'] == 'freebsd'
10
+ gid = 'system' if node['platform_family'] == 'aix'
11
+
12
+ file '/tmp/file' do
13
+ mode '0765'
14
+ owner 'root'
15
+ group gid
16
+ content 'hello world'
17
+ end
18
+
19
+ directory '/tmp/folder' do
20
+ mode '0567'
21
+ owner 'root'
22
+ group gid
23
+ end
24
+
25
+ link '/tmp/symlink' do
26
+ to '/tmp/file'
27
+ owner 'root'
28
+ group gid
29
+ mode '0777'
30
+ end
31
+
32
+ link '/usr/bin/allyourbase' do
33
+ to '/usr/bin/sudo'
34
+ owner 'root'
35
+ group gid
36
+ mode '0777'
37
+ end
38
+
39
+ execute 'create pipe/fifo' do
40
+ command 'mkfifo /tmp/pipe'
41
+ not_if 'test -e /tmp/pipe'
42
+ end
43
+
44
+ execute 'create block_device' do
45
+ command "mknod /tmp/block_device b 7 7 && chmod 0666 /tmp/block_device && chown root:#{gid} /tmp/block_device"
46
+ not_if 'test -e /tmp/block_device'
47
+ end
@@ -0,0 +1,153 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+
4
+ require 'docker'
5
+ require 'yaml'
6
+ require 'concurrent'
7
+
8
+ class DockerRunner
9
+ def initialize(conf_path = nil)
10
+ @conf_path = conf_path || ENV['config']
11
+ unless File.file?(@conf_path)
12
+ fail "Can't find configuration in #{@conf_path}"
13
+ end
14
+
15
+ @conf = YAML.load_file(@conf_path)
16
+ if @conf.nil? or @conf.empty?
17
+ fail "Can't read coniguration in #{@conf_path}"
18
+ end
19
+ if @conf['images'].nil?
20
+ fail "You must configure test images in your #{@conf_path}"
21
+ end
22
+
23
+ @images = docker_images_by_tag
24
+ @image_pull_tickets = Concurrent::Semaphore.new(2)
25
+ @docker_run_tickets = Concurrent::Semaphore.new(5)
26
+ end
27
+
28
+ def run_all(&block)
29
+ fail 'You must provide a block for run_all' unless block_given?
30
+
31
+ promises = @conf['images'].map do |id|
32
+ run_on_target(id, &block)
33
+ end
34
+
35
+ # wait for all tests to be finished
36
+ sleep(0.1) until promises.all?(&:fulfilled?)
37
+
38
+ # return resulting values
39
+ promises.map(&:value)
40
+ end
41
+
42
+ def run_on_target(name, &block)
43
+ pr = Concurrent::Promise.new {
44
+ begin
45
+ container = start_container(name)
46
+ res = block.call(name, container)
47
+ # special rescue block to handle not implemented error
48
+ rescue NotImplementedError => err
49
+ raise err.message
50
+ end
51
+ # always stop the container
52
+ stop_container(container)
53
+ res
54
+ }.execute
55
+
56
+ # failure handling
57
+ pr.rescue do |err|
58
+ msg = "\033[31;1m#{err.message}\033[0m"
59
+ puts msg
60
+ msg + "\n" + err.backtrace.join("\n")
61
+ end
62
+ end
63
+
64
+ def provision_image(image, prov, files)
65
+ tries ||= 3
66
+ return image if prov['script'].nil?
67
+ path = File.join(File.dirname(@conf_path), prov['script'])
68
+ unless File.file?(path)
69
+ puts "Can't find script file #{path}"
70
+ return image
71
+ end
72
+ puts " script #{path}"
73
+ dst = "/bootstrap#{files.length}.sh"
74
+ files.push(dst)
75
+ image.insert_local('localPath' => path, 'outputPath' => dst)
76
+ rescue StandardError => _
77
+ retry unless (tries -= 1).zero?
78
+ end
79
+
80
+ def bootstrap_image(name, image)
81
+ files = []
82
+ provisions = Array(@conf['provision'])
83
+ puts "--> provision docker #{name}" unless provisions.empty?
84
+ provisions.each do |prov|
85
+ image = provision_image(image, prov, files)
86
+ end
87
+ [image, files]
88
+ end
89
+
90
+ def start_container(name, version = nil)
91
+ unless name.include?(':')
92
+ version ||= 'latest'
93
+ name = "#{name}:#{version}"
94
+ end
95
+ puts "--> schedule docker #{name}"
96
+
97
+ image = @images[name]
98
+ if image.nil?
99
+ puts "\033[35;1m--> pull docker images #{name} "\
100
+ "(this may take a while)\033[0m"
101
+
102
+ @image_pull_tickets.acquire(1)
103
+ puts "... start pull image #{name}"
104
+ image = Docker::Image.create('fromImage' => name)
105
+ @image_pull_tickets.release(1)
106
+
107
+ unless image.nil?
108
+ puts "\033[35;1m--> pull docker images finished for #{name}\033[0m"
109
+ end
110
+ end
111
+
112
+ fail "Can't find nor pull docker image #{name}" if image.nil?
113
+
114
+ @docker_run_tickets.acquire(1)
115
+
116
+ image, scripts = bootstrap_image(name, image)
117
+
118
+ puts "--> start docker #{name}"
119
+ container = Docker::Container.create(
120
+ 'Cmd' => %w{sleep 3600},
121
+ 'Image' => image.id,
122
+ 'OpenStdin' => true,
123
+ )
124
+ container.start
125
+
126
+ scripts.each do |script|
127
+ container.exec(%w{chmod +x}.push(script))
128
+ container.exec(%w{sh -c}.push(script))
129
+ end
130
+
131
+ container
132
+ end
133
+
134
+ def stop_container(container)
135
+ @docker_run_tickets.release(1)
136
+ puts "--> killrm docker #{container.id}"
137
+ container.kill
138
+ container.delete(force: true)
139
+ end
140
+
141
+ private
142
+
143
+ # get all docker image tags
144
+ def docker_images_by_tag
145
+ images = {}
146
+ Docker::Image.all.map do |img|
147
+ Array(img.info['RepoTags']).each do |tag|
148
+ images[tag] = img
149
+ end
150
+ end
151
+ images
152
+ end
153
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ require_relative 'docker_run'
3
+ tests = ARGV
4
+
5
+ def test_container(container, tests)
6
+ puts "--> run test on docker #{container.id}"
7
+ pid = Process.fork do
8
+ ENV['CONTAINER'] = container.id
9
+ require_relative 'docker_test_container.rb'
10
+ Process.exit
11
+ end
12
+
13
+ _, status = Process.waitpid2(pid)
14
+ status.exitstatus == 0
15
+ end
16
+
17
+ results = DockerRunner.new.run_all do |name, container|
18
+ status = test_container(container, tests)
19
+ status ? nil : "Failed to run tests on #{name}"
20
+ end
21
+
22
+ failures = results.compact
23
+ failures.each { |f| puts "\033[31;1m#{f}\033[0m\n\n" }
24
+ failures.empty? or fail 'Test failures'
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+
4
+ require 'train'
5
+ require_relative 'helper'
6
+
7
+ container_id = ENV['CONTAINER'] or
8
+ fail 'You must provide a container ID via CONTAINER env'
9
+
10
+ tests = ARGV
11
+ puts ['Running tests:', tests].flatten.join("\n- ")
12
+ puts ''
13
+
14
+ backends = {}
15
+ backends[:docker] = proc { |*args|
16
+ opt = Train.target_config({ host: container_id })
17
+ Train.create('docker', opt).connection(args[0])
18
+ }
19
+
20
+ backends.each do |type, get_backend|
21
+ tests.each do |test|
22
+ instance_eval(File.read(test), test, 1)
23
+ end
24
+ end
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+
4
+ require 'minitest/autorun'
5
+ require 'minitest/spec'
6
+
7
+ # Tests configuration:
8
+ module Test
9
+ class << self
10
+ # MTime tracks the maximum range of modification time in seconds.
11
+ # i.e. MTime == 60*60*1 is 1 hour of modification time range,
12
+ # which translates to a modification time range of:
13
+ # [ now-1hour, now ]
14
+ def mtime
15
+ 60 * 60 * 24 * 1
16
+ end
17
+
18
+ def dup(o)
19
+ Marshal.load(Marshal.dump(o))
20
+ end
21
+
22
+ def root_group(os)
23
+ case os[:family]
24
+ when 'freebsd'
25
+ 'wheel'
26
+ when 'aix'
27
+ 'system'
28
+ else
29
+ 'root'
30
+ end
31
+ end
32
+
33
+ def selinux_label(backend, path = nil)
34
+ return nil if backend.class.to_s =~ /docker/i
35
+
36
+ os = backend.os
37
+ labels = {}
38
+
39
+ h = {}
40
+ h.default = Hash.new(nil)
41
+ h['redhat'] = {}
42
+ h['redhat'].default = 'unconfined_u:object_r:user_tmp_t:s0'
43
+ h['redhat']['5.11'] = 'user_u:object_r:tmp_t'
44
+ h['centos'] = h['fedora'] = h['redhat']
45
+ labels.default = dup(h)
46
+
47
+ h['redhat'].default = 'unconfined_u:object_r:tmp_t:s0'
48
+ labels['/tmp/block_device'] = dup(h)
49
+
50
+ h = {}
51
+ h.default = Hash.new(nil)
52
+ h['redhat'] = {}
53
+ h['redhat'].default = 'system_u:object_r:null_device_t:s0'
54
+ h['redhat']['5.11'] = 'system_u:object_r:null_device_t'
55
+ h['centos'] = h['fedora'] = h['redhat']
56
+ labels['/dev/null'] = dup(h)
57
+
58
+ labels[path][os[:family]][os[:release]]
59
+ end
60
+ end
61
+ end