minionizer 0.0.1 → 0.1.0
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 +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
|