minionizer 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +37 -15
- data/README.md +4 -0
- data/Rakefile +17 -0
- data/lib/core/file_injection.rb +11 -8
- data/lib/core/folder_creation.rb +12 -0
- data/lib/core/public_ssh_key_injection.rb +40 -0
- data/lib/core/task_template.rb +25 -0
- data/lib/core/user_creation.rb +18 -0
- data/lib/minionizer.rb +3 -0
- data/lib/minionizer/command_execution.rb +60 -0
- data/lib/minionizer/session.rb +29 -14
- data/lib/minionizer/version.rb +1 -1
- data/minionizer.gemspec +5 -4
- data/test/integration/core_library_test.rb +193 -0
- data/test/role_template.rb.erb +16 -0
- data/test/test_helper.rb +125 -32
- data/test/unit/lib/core/file_injection_test.rb +45 -12
- data/test/unit/lib/core/folder_creation_test.rb +58 -0
- data/test/unit/lib/core/public_ssh_key_injection_test.rb +46 -0
- data/test/unit/lib/core/task_template_test.rb +35 -0
- data/test/unit/lib/core/user_creation_test.rb +44 -0
- data/test/unit/lib/minionizer/command_execution_test.rb +85 -0
- data/test/unit/lib/minionizer/configuration_test.rb +4 -2
- data/test/unit/lib/minionizer/minion_test.rb +3 -3
- data/test/unit/lib/minionizer/minionization_test.rb +8 -8
- data/test/unit/lib/minionizer/role_template_test.rb +2 -2
- data/test/unit/lib/minionizer/session_test.rb +65 -20
- metadata +53 -20
- data/test/integration/acceptance_test.rb +0 -82
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Minionizer
|
5
|
+
class MinionTestFailure < StandardError; end
|
6
|
+
class CoreLibraryTest < MiniTest::Test
|
7
|
+
roll_back_to_blank_snapshot if minion_available?
|
8
|
+
|
9
|
+
describe 'core library' do
|
10
|
+
let(:fqdn) { '192.168.49.181' }
|
11
|
+
let(:username) { 'vagrant' }
|
12
|
+
let(:password) { 'vagrant' }
|
13
|
+
let(:credentials) {{ 'username' => username, 'password' => password }}
|
14
|
+
let(:session) { Session.new(fqdn, credentials) }
|
15
|
+
let(:minionization) { Minionization.new([fqdn], Configuration.instance) }
|
16
|
+
let(:minions) {{ fqdn => { 'ssh' => credentials, 'roles' => ['test_role'] } }}
|
17
|
+
|
18
|
+
before do
|
19
|
+
skip unless minion_available?
|
20
|
+
Configuration.instance.instance_variable_set(:@minions, nil)
|
21
|
+
write_file('config/minions.yml', minions.to_yaml)
|
22
|
+
create_role(code)
|
23
|
+
end
|
24
|
+
|
25
|
+
describe UserCreation do
|
26
|
+
let(:new_name) { 'Test User' }
|
27
|
+
let(:new_username) { 'testuser' }
|
28
|
+
let(:options) {{ name: new_name, username: new_username }}
|
29
|
+
let(:code) { %Q{
|
30
|
+
session.sudo do
|
31
|
+
Minionizer::UserCreation.new( session, #{options}).call
|
32
|
+
end
|
33
|
+
} }
|
34
|
+
|
35
|
+
before do
|
36
|
+
refute_user_exists(new_username)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'creates a user' do
|
40
|
+
2.times { assert_throws(:high_five) { minionization.call } }
|
41
|
+
assert_user_exists(new_username)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe FolderCreation do
|
46
|
+
let(:filename) { "foo/dir" }
|
47
|
+
let(:ownername) { 'otheruser' }
|
48
|
+
let(:path) { "/home/vagrant/#{filename}" }
|
49
|
+
let(:options) {{ path: path, mode: '0700', owner: ownername, group: ownername }}
|
50
|
+
let(:code) { %Q{
|
51
|
+
session.sudo do
|
52
|
+
Minionizer::FolderCreation.new( session, #{options}).call
|
53
|
+
end
|
54
|
+
} }
|
55
|
+
|
56
|
+
before do
|
57
|
+
refute_directory_exists(path)
|
58
|
+
session.sudo("adduser --disabled-password --gecos '#{ownername}' #{ownername}")
|
59
|
+
end
|
60
|
+
|
61
|
+
after do
|
62
|
+
skip unless minion_available?
|
63
|
+
session.sudo("userdel #{ownername}")
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'creates a folder' do
|
67
|
+
2.times { assert_throws(:high_five) { minionization.call } }
|
68
|
+
assert_directory_exists(path)
|
69
|
+
mode = session.exec("stat --format=%a #{path}")[:stdout]
|
70
|
+
assert_equal('700',mode)
|
71
|
+
owner = session.exec("stat --format=%U #{path}")[:stdout]
|
72
|
+
assert_equal(ownername, owner)
|
73
|
+
group = session.exec("stat --format=%G #{path}")[:stdout]
|
74
|
+
assert_equal(ownername, group)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe FileInjection do
|
79
|
+
let(:filename) { 'foobar.txt' }
|
80
|
+
let(:source_path) { "/some/source/#{filename}" }
|
81
|
+
let(:target_path) { "/home/vagrant/#{filename}" }
|
82
|
+
let(:ownername) { 'otheruser' }
|
83
|
+
let(:options) {{
|
84
|
+
source_path: source_path,
|
85
|
+
target_path: target_path,
|
86
|
+
mode: '0700',
|
87
|
+
owner: ownername,
|
88
|
+
group: ownername
|
89
|
+
}}
|
90
|
+
let(:code) { %Q{
|
91
|
+
session.sudo do
|
92
|
+
Minionizer::FileInjection.new( session, #{options}).call
|
93
|
+
end
|
94
|
+
} }
|
95
|
+
|
96
|
+
before do
|
97
|
+
refute_file_exists(target_path)
|
98
|
+
write_file(source_path, 'FooBar')
|
99
|
+
session.sudo("adduser --disabled-password --gecos '#{ownername}' #{ownername}")
|
100
|
+
end
|
101
|
+
|
102
|
+
after do
|
103
|
+
skip unless minion_available?
|
104
|
+
session.sudo("userdel #{ownername}")
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'injects a file' do
|
108
|
+
2.times { assert_throws(:high_five) { minionization.call } }
|
109
|
+
assert_file_exists(target_path)
|
110
|
+
mode = session.exec("stat --format=%a #{target_path}")[:stdout]
|
111
|
+
assert_equal('700',mode)
|
112
|
+
owner = session.exec("stat --format=%U #{target_path}")[:stdout]
|
113
|
+
assert_equal(ownername, owner)
|
114
|
+
group = session.exec("stat --format=%G #{target_path}")[:stdout]
|
115
|
+
assert_equal(ownername, group)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe PublicSshKeyInjection do
|
120
|
+
let(:source_path) { "data/public_keys" }
|
121
|
+
let(:target_username) { 'otheruser' }
|
122
|
+
let(:options) {{ target_username: target_username }}
|
123
|
+
let(:code) { %Q{
|
124
|
+
session.sudo do |sudo_session|
|
125
|
+
Minionizer::PublicSshKeyInjection.new(sudo_session, #{options}).call
|
126
|
+
end
|
127
|
+
} }
|
128
|
+
|
129
|
+
before do
|
130
|
+
Dir.mkdir('/tmp')
|
131
|
+
refute_file_exists("~#{target_username}/.ssh/authorized_keys")
|
132
|
+
write_file("#{source_path}/foobar.pubkey", 'FooBar')
|
133
|
+
write_file("#{source_path}/foobaz.pubkey", 'FooBaz')
|
134
|
+
session.exec("sudo adduser --disabled-password --gecos '#{target_username}' #{target_username}")
|
135
|
+
end
|
136
|
+
|
137
|
+
after do
|
138
|
+
File.delete("#{source_path}/foobar.pubkey")
|
139
|
+
File.delete("#{source_path}/foobaz.pubkey")
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'injects public keys' do
|
143
|
+
2.times { assert_throws(:high_five) { minionization.call } }
|
144
|
+
assert_file_exists("~#{target_username}/.ssh/authorized_keys")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
#######
|
149
|
+
private
|
150
|
+
#######
|
151
|
+
|
152
|
+
def create_role(injected_code)
|
153
|
+
role_code = without_fakefs do
|
154
|
+
ERB.new(File.open('test/role_template.rb.erb').read.strip).result(binding)
|
155
|
+
end
|
156
|
+
write_file('roles/test_role.rb', role_code)
|
157
|
+
end
|
158
|
+
|
159
|
+
def assert_file_exists(path)
|
160
|
+
assert(link_exists?(path, :f), "#{path} file expected to exist")
|
161
|
+
end
|
162
|
+
|
163
|
+
def refute_file_exists(path)
|
164
|
+
refute(link_exists?(path, :f), "#{path} file NOT expected to exist")
|
165
|
+
end
|
166
|
+
|
167
|
+
def assert_directory_exists(path)
|
168
|
+
assert(link_exists?(path, :d), "#{path} directory expected to exist")
|
169
|
+
end
|
170
|
+
|
171
|
+
def refute_directory_exists(path)
|
172
|
+
refute(link_exists?(path, :d), "#{path} directory NOT expected to exist")
|
173
|
+
end
|
174
|
+
|
175
|
+
def link_exists?(path, parameter = :e)
|
176
|
+
session.exec("[ -#{parameter} #{path} ] && echo 'yes' || echo 'no'")[:stdout] == 'yes'
|
177
|
+
end
|
178
|
+
|
179
|
+
def assert_user_exists(username)
|
180
|
+
assert(user_exists?(username), "User '#{username}' expected to exist")
|
181
|
+
end
|
182
|
+
|
183
|
+
def refute_user_exists(username)
|
184
|
+
refute(user_exists?(username), "User '#{username}' expected to NOT exist")
|
185
|
+
end
|
186
|
+
|
187
|
+
def user_exists?(username)
|
188
|
+
session.exec("id #{username} || echo 'no'")[:stdout] != 'no'
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class TestRole < Minionizer::RoleTemplate
|
2
|
+
|
3
|
+
def call
|
4
|
+
if hostname == 'precise32'
|
5
|
+
<%= injected_code %>
|
6
|
+
throw :high_five
|
7
|
+
else
|
8
|
+
raise Minionizer::MinionTestFailure.new("Whawhawhaaaa... #{hostname}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def hostname
|
13
|
+
@hostname ||= session.exec(:hostname)[:stdout]
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
require 'rubygems'
|
2
|
+
require 'simplecov'
|
3
|
+
require 'coveralls'
|
2
4
|
require 'minitest/autorun'
|
3
5
|
require 'fakefs/safe'
|
4
6
|
require 'socket'
|
7
|
+
require 'tempfile'
|
5
8
|
require 'timeout'
|
6
9
|
|
10
|
+
if Coveralls.will_run?
|
11
|
+
Coveralls.wear!
|
12
|
+
else
|
13
|
+
SimpleCov.start
|
14
|
+
end
|
15
|
+
|
16
|
+
PRE_REQUIRED_LIBS = %w{tempfile}
|
17
|
+
|
7
18
|
require_relative '../lib/minionizer'
|
8
19
|
|
9
20
|
module Minionizer
|
10
|
-
class MiniTest::
|
21
|
+
class MiniTest::Test
|
11
22
|
|
12
23
|
def before_setup
|
13
24
|
super
|
@@ -17,6 +28,9 @@ module Minionizer
|
|
17
28
|
def after_teardown
|
18
29
|
super
|
19
30
|
FakeFS.deactivate!
|
31
|
+
Kernel.class_eval do
|
32
|
+
alias_method :require, :real_require
|
33
|
+
end
|
20
34
|
end
|
21
35
|
|
22
36
|
#######
|
@@ -25,17 +39,44 @@ module Minionizer
|
|
25
39
|
|
26
40
|
def initialize_fakefs
|
27
41
|
FakeFS.activate!
|
28
|
-
FakeFS::FileSystem.clear
|
29
42
|
Kernel.class_eval do
|
43
|
+
def fake_require(path)
|
44
|
+
if PRE_REQUIRED_LIBS.include?(path)
|
45
|
+
return false #real require returns false if library is already loaded
|
46
|
+
else
|
47
|
+
File.open(path, "r") {|f| Object.class_eval f.read, path, 1 }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias_method :real_require, :require
|
30
51
|
alias_method :require, :fake_require
|
31
52
|
end
|
32
53
|
end
|
33
54
|
|
55
|
+
def without_fakefs
|
56
|
+
FakeFS.deactivate!
|
57
|
+
yield
|
58
|
+
ensure
|
59
|
+
FakeFS.activate!
|
60
|
+
end
|
61
|
+
|
34
62
|
def minion_available?
|
35
|
-
|
36
|
-
|
63
|
+
self.class.minion_available?
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.minion_available?
|
67
|
+
if MinionMonitor.minion_available?
|
68
|
+
return true
|
69
|
+
else
|
70
|
+
Timeout.timeout(1) do
|
71
|
+
if Net::SSH.start('192.168.49.181', 'vagrant', password: 'vagrant')
|
72
|
+
MinionMonitor.minion_available!
|
73
|
+
return true
|
74
|
+
else
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
end
|
37
78
|
end
|
38
|
-
rescue Errno::ECONNREFUSED, Timeout::Error
|
79
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Timeout::Error
|
39
80
|
return false
|
40
81
|
end
|
41
82
|
|
@@ -43,10 +84,8 @@ module Minionizer
|
|
43
84
|
@@previously_initialized ||= `cd #{File.dirname(__FILE__)}; vagrant up`
|
44
85
|
end
|
45
86
|
|
46
|
-
def roll_back_to_blank_snapshot
|
47
|
-
FakeFS.deactivate!
|
87
|
+
def self.roll_back_to_blank_snapshot
|
48
88
|
`cd #{File.dirname(__FILE__)}; vagrant snapshot go blank-test-slate`
|
49
|
-
FakeFS.activate!
|
50
89
|
end
|
51
90
|
|
52
91
|
def write_role_file(name)
|
@@ -64,38 +103,92 @@ module Minionizer
|
|
64
103
|
Object.const_set(name.classify, Class.new)
|
65
104
|
end
|
66
105
|
|
67
|
-
|
68
|
-
|
106
|
+
def quacks_like(klass)
|
107
|
+
mock("Mock(#{klass.to_s})").tap do |object|
|
108
|
+
object.responds_like(klass)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def quacks_like_instance_of(klass)
|
113
|
+
mock("InstanceMock(#{klass.to_s})").tap do |object|
|
114
|
+
object.responds_like_instance_of(klass)
|
115
|
+
end
|
116
|
+
end
|
69
117
|
|
70
|
-
|
118
|
+
def assert_equal(first, second)
|
119
|
+
assert(first === second, "'#{second}' expected to be equal to '#{first}'")
|
120
|
+
end
|
71
121
|
|
72
|
-
|
73
|
-
|
122
|
+
def sudoized(command)
|
123
|
+
%Q{sudo bash -c "#{command}"}
|
124
|
+
end
|
74
125
|
end
|
75
|
-
|
76
126
|
end
|
77
127
|
|
78
|
-
|
79
|
-
class NamedMock < Mock
|
80
|
-
attr_reader :name
|
81
|
-
|
82
|
-
def initialize(name)
|
83
|
-
@name = name
|
84
|
-
super()
|
85
|
-
end
|
128
|
+
require 'mocha/setup'
|
86
129
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
130
|
+
module MinionMonitor
|
131
|
+
def self.minion_available?; !!@minion_available; end
|
132
|
+
def self.minion_available!; @minion_available = true; end
|
133
|
+
end
|
92
134
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
135
|
+
## Only need this until we have FakeFS > 0.5.2 that includes this commit.
|
136
|
+
## https://github.com/defunkt/fakefs/commit/06eb002da7fb8119a60fef7d50307bd3358c85f3
|
137
|
+
module FakeFS
|
138
|
+
class Dir
|
139
|
+
if RUBY_VERSION >= '2.1'
|
140
|
+
module Tmpname # :nodoc:
|
141
|
+
module_function
|
142
|
+
|
143
|
+
def tmpdir
|
144
|
+
Dir.tmpdir
|
145
|
+
end
|
146
|
+
|
147
|
+
def make_tmpname(prefix_suffix, n)
|
148
|
+
case prefix_suffix
|
149
|
+
when String
|
150
|
+
prefix = prefix_suffix
|
151
|
+
suffix = ""
|
152
|
+
when Array
|
153
|
+
prefix = prefix_suffix[0]
|
154
|
+
suffix = prefix_suffix[1]
|
155
|
+
else
|
156
|
+
raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
|
157
|
+
end
|
158
|
+
t = Time.now.strftime("%Y%m%d")
|
159
|
+
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
|
160
|
+
path << "-#{n}" if n
|
161
|
+
path << suffix
|
162
|
+
end
|
163
|
+
|
164
|
+
def create(basename, *rest)
|
165
|
+
if opts = Hash.try_convert(rest[-1])
|
166
|
+
opts = opts.dup if rest.pop.equal?(opts)
|
167
|
+
max_try = opts.delete(:max_try)
|
168
|
+
opts = [opts]
|
169
|
+
else
|
170
|
+
opts = []
|
171
|
+
end
|
172
|
+
tmpdir, = *rest
|
173
|
+
if $SAFE > 0 and tmpdir.tainted?
|
174
|
+
tmpdir = '/tmp'
|
175
|
+
else
|
176
|
+
tmpdir ||= tmpdir()
|
177
|
+
end
|
178
|
+
n = nil
|
179
|
+
begin
|
180
|
+
path = File.join(tmpdir, make_tmpname(basename, n))
|
181
|
+
yield(path, n, *opts)
|
182
|
+
rescue Errno::EEXIST
|
183
|
+
n ||= 0
|
184
|
+
n += 1
|
185
|
+
retry if !max_try or n < max_try
|
186
|
+
raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'"
|
187
|
+
end
|
188
|
+
path
|
189
|
+
end
|
190
|
+
end
|
97
191
|
end
|
98
192
|
end
|
99
193
|
end
|
100
194
|
|
101
|
-
require 'mocha/setup'
|
@@ -1,29 +1,62 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
3
|
module Minionizer
|
4
|
-
class FileInjectionTest < MiniTest::
|
4
|
+
class FileInjectionTest < MiniTest::Test
|
5
5
|
|
6
6
|
describe FileInjection do
|
7
7
|
let(:session) { 'MockSession' }
|
8
|
-
let(:
|
9
|
-
|
10
|
-
|
11
|
-
assert_kind_of(FileInjection, injection)
|
12
|
-
end
|
8
|
+
let(:source_path) { 'data/source_file.txt'}
|
9
|
+
let(:target_path) { '/var/target_file.txt'}
|
10
|
+
let(:injection) { FileInjection.new(session, options) }
|
13
11
|
|
14
12
|
describe '#call' do
|
15
13
|
let(:source_contents) { 'Source Contents' }
|
16
|
-
let(:source) { 'data/source_file.txt'}
|
17
|
-
let(:target) { '/var/target_file.txt'}
|
18
14
|
|
19
15
|
before do
|
20
|
-
write_file(
|
21
|
-
session.expects(:exec).with(%Q{
|
16
|
+
write_file(source_path, source_contents)
|
17
|
+
session.expects(:exec).with(%Q{mkdir --parents #{File.dirname(target_path)}})
|
18
|
+
session.expects(:exec).with(%Q{echo '#{source_contents}' > #{target_path}})
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'only source and target are provided' do
|
22
|
+
let(:options) {{ source_path: source_path, target_path: target_path }}
|
23
|
+
|
24
|
+
it 'sends a command to session' do
|
25
|
+
injection.call
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'mode is provided' do
|
31
|
+
let(:mode) { '0700' }
|
32
|
+
let(:options) {{ source_path: source_path, target_path: target_path, mode: mode }}
|
33
|
+
|
34
|
+
it 'sets the file permissions' do
|
35
|
+
session.expects(:exec).with(%Q{chmod #{mode} #{target_path}})
|
36
|
+
injection.call
|
37
|
+
end
|
22
38
|
end
|
23
39
|
|
24
|
-
|
25
|
-
|
40
|
+
describe 'owner is provided' do
|
41
|
+
let(:ownername) { 'otheruser' }
|
42
|
+
let(:options) {{ source_path: source_path, target_path: target_path, owner: ownername }}
|
43
|
+
|
44
|
+
it 'sets the file owner' do
|
45
|
+
session.expects(:exec).with(%Q{chown #{ownername} #{target_path}})
|
46
|
+
injection.call
|
47
|
+
end
|
26
48
|
end
|
49
|
+
|
50
|
+
describe 'group is provided' do
|
51
|
+
let(:groupname) { 'othergroup' }
|
52
|
+
let(:options) {{ source_path: source_path, target_path: target_path, group: groupname }}
|
53
|
+
|
54
|
+
it 'sets the file group' do
|
55
|
+
session.expects(:exec).with(%Q{chgrp #{groupname} #{target_path}})
|
56
|
+
injection.call
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
27
60
|
end
|
28
61
|
|
29
62
|
end
|