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.
@@ -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
@@ -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::Unit::TestCase
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
- Timeout.timeout(1) do
36
- @@minion_available ||= TCPSocket.new('192.168.49.181', 22)
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
- end
68
- end
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
- module Kernel
118
+ def assert_equal(first, second)
119
+ assert(first === second, "'#{second}' expected to be equal to '#{first}'")
120
+ end
71
121
 
72
- def fake_require(path)
73
- File.open(path, "r") {|f| Object.class_eval f.read, path, 1 }
122
+ def sudoized(command)
123
+ %Q{sudo bash -c "#{command}"}
124
+ end
74
125
  end
75
-
76
126
  end
77
127
 
78
- module MiniTest
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
- # Because you ought to be able to
88
- # test two effing mocks for equality.
89
- def ==(x)
90
- object_id == x.object_id
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
- def method_missing(sym, *args, &block)
94
- super(sym, *args, &block)
95
- rescue NoMethodError, MockExpectationError, ArgumentError => error
96
- raise(error.class, "#{error.message} (mock:#{name}) ")
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::Unit::TestCase
4
+ class FileInjectionTest < MiniTest::Test
5
5
 
6
6
  describe FileInjection do
7
7
  let(:session) { 'MockSession' }
8
- let(:injection) { FileInjection.new(session) }
9
-
10
- it 'instantiates' do
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(source, source_contents)
21
- session.expects(:exec).with(%Q{echo '#{source_contents}' > #{target}})
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
- it 'sends a command to session' do
25
- injection.inject(source, target)
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