minionizer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +36 -0
- data/README.md +87 -0
- data/Rakefile +63 -0
- data/bin/minionize +3 -0
- data/lib/core/file_injection.rb +21 -0
- data/lib/minionizer/configuration.rb +10 -0
- data/lib/minionizer/minion.rb +34 -0
- data/lib/minionizer/minionization.rb +60 -0
- data/lib/minionizer/role_template.rb +13 -0
- data/lib/minionizer/session.rb +39 -0
- data/lib/minionizer/version.rb +3 -0
- data/lib/minionizer.rb +6 -0
- data/minionizer.gemspec +23 -0
- data/test/Vagrantfile +125 -0
- data/test/integration/acceptance_test.rb +82 -0
- data/test/test_helper.rb +101 -0
- data/test/unit/lib/core/file_injection_test.rb +33 -0
- data/test/unit/lib/minionizer/configuration_test.rb +28 -0
- data/test/unit/lib/minionizer/minion_test.rb +44 -0
- data/test/unit/lib/minionizer/minionization_test.rb +46 -0
- data/test/unit/lib/minionizer/role_template_test.rb +23 -0
- data/test/unit/lib/minionizer/session_test.rb +59 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cd7dc40fdbe374f013b0e9e807c3bc974215a951
|
4
|
+
data.tar.gz: ddcb859dd3f2d985341553cf923cc4de4422b19d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2556cc919d6e3dc13fafd41406726bdd8bc640c5b27d75ca67614a4b14a06c9d465f08558e3943caa6daf0752e1a49d4044672996cfc6206db660336a922ed6f
|
7
|
+
data.tar.gz: b73ba6895d71e3b8d3aa9bd37dc3c01368bd160f71383cb09feb49cb7112738346620d283b5448dbcb4eb024184eee9bce9322c9c14ec0afa3f022f9edfb200d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
minionizer (0.0.1)
|
5
|
+
activesupport
|
6
|
+
net-ssh
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activesupport (4.0.4)
|
12
|
+
i18n (~> 0.6, >= 0.6.9)
|
13
|
+
minitest (~> 4.2)
|
14
|
+
multi_json (~> 1.3)
|
15
|
+
thread_safe (~> 0.1)
|
16
|
+
tzinfo (~> 0.3.37)
|
17
|
+
atomic (1.1.16)
|
18
|
+
fakefs (0.5.2)
|
19
|
+
i18n (0.6.9)
|
20
|
+
metaclass (0.0.4)
|
21
|
+
minitest (4.7.5)
|
22
|
+
mocha (1.0.0)
|
23
|
+
metaclass (~> 0.0.1)
|
24
|
+
multi_json (1.9.2)
|
25
|
+
net-ssh (2.8.0)
|
26
|
+
thread_safe (0.3.1)
|
27
|
+
atomic (>= 1.1.7, < 2)
|
28
|
+
tzinfo (0.3.39)
|
29
|
+
|
30
|
+
PLATFORMS
|
31
|
+
ruby
|
32
|
+
|
33
|
+
DEPENDENCIES
|
34
|
+
fakefs
|
35
|
+
minionizer!
|
36
|
+
mocha
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
[![Code Climate](https://codeclimate.com/github/jsgarvin/minionizer.png)](https://codeclimate.com/github/jsgarvin/minionizer)
|
2
|
+
|
3
|
+
# Minionizer
|
4
|
+
|
5
|
+
Minionizer aims to be a light weight, yet powerful, server provisioning tool with minimum learning
|
6
|
+
curve.
|
7
|
+
|
8
|
+
Minionizer is still in alpha development and is not yet ready for anything resembling production use.
|
9
|
+
|
10
|
+
# Overview
|
11
|
+
|
12
|
+
Minionizer allows you keep all of your provisioning "recipies" for a set of servers, along with any
|
13
|
+
data those recipies may need (such as config files), in a single git repository.
|
14
|
+
|
15
|
+
Minionizer uses ssh to connect to machines and run commands. There are no "agents" or other software
|
16
|
+
to install on servers before minionizer can take over.
|
17
|
+
|
18
|
+
Managed machines (minions) are assigned roles (web, db, production, staging, etc) and can be
|
19
|
+
(re)provisioned all at once by any role, or individually by server address.
|
20
|
+
|
21
|
+
Sensitive data, such as passwords, WILL BE gpg encrypted and only the encrypted copies will be checked
|
22
|
+
into the repository. If you change any of these files, Minionizer will detect the change and prompt
|
23
|
+
you to re-encrypt them and commit the newly encrypted versions.
|
24
|
+
|
25
|
+
A core set of commands WILL BE provided, such as uploading files to the server, installing apt
|
26
|
+
packages, etc. You can use these core commands to build more complex recipies, or use any of many
|
27
|
+
minionizer plugins that WILL BE available, such as posgresql installationi/upgrade, ruby
|
28
|
+
installation/upgrade, etc.
|
29
|
+
|
30
|
+
# Installation
|
31
|
+
|
32
|
+
gem install minionizer
|
33
|
+
|
34
|
+
# Usage
|
35
|
+
|
36
|
+
## Setup a new provisioning project in the current folder.
|
37
|
+
Note: This step doesn't actually work yet.
|
38
|
+
|
39
|
+
minionize --init subfolder_name
|
40
|
+
|
41
|
+
Creates `subfolder_name` and initializes it with some initial folders and files to get you started.
|
42
|
+
|
43
|
+
## Modify config/minions.yml
|
44
|
+
|
45
|
+
The minions.yml file is where you define what servers this project will manage and what roles
|
46
|
+
each server will play.
|
47
|
+
|
48
|
+
You will probably want assign each server multiple roles, such as `['production', 'db']`.
|
49
|
+
|
50
|
+
## Create role instructions
|
51
|
+
|
52
|
+
A sample role file WILL BE provided in the ./roles folder to get you started. Each role file defines
|
53
|
+
what servers assigned that role should do on each (re)provisioning.
|
54
|
+
|
55
|
+
It is not necessary to create a role file for every role that you added to your config/minions.yml
|
56
|
+
file. You will likely have some roles, such as "production", that are mearly a means of grouping
|
57
|
+
several servers together and won't have a corresponding role file. You will need at least one role,
|
58
|
+
though, such as "db" or "webserver", that will have a corresponding role file.
|
59
|
+
|
60
|
+
## Provision Servers
|
61
|
+
|
62
|
+
To provision all of the servers that are assigned a particular role, run...
|
63
|
+
|
64
|
+
minionize role_name
|
65
|
+
|
66
|
+
This will loop through each server that is assigned `role_name` and run each role file for each role
|
67
|
+
that that server is assigned. For instance, if a server is assigned the roles 'production' and 'db',
|
68
|
+
and you run `minionize production`, then when minionizer reaches this machine, it will run the 'db'
|
69
|
+
role file (assuming it exists in the ./roles folder).
|
70
|
+
|
71
|
+
or
|
72
|
+
|
73
|
+
minionize my.server.address.com
|
74
|
+
|
75
|
+
This will loop through each role that is assigned to just that server, and any corresponding role
|
76
|
+
files will be run.
|
77
|
+
|
78
|
+
# Contribute
|
79
|
+
|
80
|
+
To contribute to Minionizer development you will need to install [vagrant](http://www.vagrantup.com/)
|
81
|
+
and [VirtualBox](https://www.virtualbox.org/) in order to be able to run acceptance tests.
|
82
|
+
|
83
|
+
Once installed from within your own clone of the Minionizer repo, run `rake test:vm:start` to
|
84
|
+
initialize the acceptance test virtual machine. The first time you do this it may take a long time to
|
85
|
+
download install the initial box.
|
86
|
+
|
87
|
+
To shut down the vm, run `rake test:vm:stop`.
|
data/Rakefile
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'pty'
|
3
|
+
|
4
|
+
require_relative 'lib/minionizer'
|
5
|
+
|
6
|
+
task default: :test
|
7
|
+
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.libs << 'test'
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
namespace :test do
|
16
|
+
namespace :vm do
|
17
|
+
task :start do
|
18
|
+
relay_output(vagrant_command(:up))
|
19
|
+
unless snapshot_plugin_installed?
|
20
|
+
relay_output(vagrant_command('plugin install vagrant-vbox-snapshot'))
|
21
|
+
relay_output(vagrant_command('snapshot take blank-test-slate'))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
task :stop do
|
25
|
+
relay_output(vagrant_command(:halt))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def vagrant_command(command)
|
31
|
+
"cd #{vagrant_path}; vagrant #{command}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def snapshot_plugin_installed?
|
35
|
+
vagrant_plugins['vagrant-vbox-snapshot'] &&
|
36
|
+
Gem::Version.new(vagrant_plugins['vagrant-vbox-snapshot']) >= Gem::Version.new('0.0.4')
|
37
|
+
end
|
38
|
+
|
39
|
+
def vagrant_plugins
|
40
|
+
Hash.new.tap do |hash|
|
41
|
+
`cd #{vagrant_path}; vagrant plugin list`.split("\n").each do |plugin_string|
|
42
|
+
if plugin_string.match(/([^\s]+)\s\(([0-9\.]+)/)
|
43
|
+
hash[$1] = $2
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def vagrant_path
|
50
|
+
File.expand_path('../test', __FILE__)
|
51
|
+
end
|
52
|
+
|
53
|
+
def relay_output(command)
|
54
|
+
begin
|
55
|
+
PTY.spawn(command) do |stdin, stdout, pid|
|
56
|
+
begin
|
57
|
+
stdin.each {|line| print line }
|
58
|
+
rescue Errno::EIO
|
59
|
+
end
|
60
|
+
end
|
61
|
+
rescue PTY::ChildExited
|
62
|
+
end
|
63
|
+
end
|
data/bin/minionize
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Minionizer
|
2
|
+
class FileInjection
|
3
|
+
attr_reader :session
|
4
|
+
|
5
|
+
def initialize(session)
|
6
|
+
@session = session
|
7
|
+
end
|
8
|
+
|
9
|
+
def inject(source, target)
|
10
|
+
session.exec("echo '#{contents_from(source)}' > #{target}")
|
11
|
+
end
|
12
|
+
|
13
|
+
#######
|
14
|
+
private
|
15
|
+
#######
|
16
|
+
|
17
|
+
def contents_from(source)
|
18
|
+
File.open(source).read.strip
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Minionizer
|
2
|
+
class Minion
|
3
|
+
attr_reader :config, :fqdn, :session_constructor
|
4
|
+
|
5
|
+
def initialize(fqdn, config, session_constructor = Session)
|
6
|
+
@fqdn = fqdn
|
7
|
+
@config = config
|
8
|
+
@session_constructor = session_constructor
|
9
|
+
end
|
10
|
+
|
11
|
+
def session
|
12
|
+
@session ||= session_constructor.new(fqdn, ssh_credentials)
|
13
|
+
end
|
14
|
+
|
15
|
+
def roles
|
16
|
+
my_config['roles']
|
17
|
+
end
|
18
|
+
|
19
|
+
#######
|
20
|
+
private
|
21
|
+
#######
|
22
|
+
|
23
|
+
def ssh_credentials
|
24
|
+
{
|
25
|
+
'username' => my_config['ssh']['username'],
|
26
|
+
'password' => my_config['ssh']['password']
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def my_config
|
31
|
+
config.minions[fqdn]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Minionizer
|
2
|
+
class Minionization
|
3
|
+
attr_reader :arguments, :config, :minion_constructor
|
4
|
+
|
5
|
+
def initialize(arguments, config, minion_constructor = Minion)
|
6
|
+
@arguments = arguments
|
7
|
+
@config = config
|
8
|
+
@minion_constructor = minion_constructor
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
minions.each do |minion|
|
13
|
+
minion.roles.each { |name| execute_role(minion.session, name) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
#######
|
18
|
+
private
|
19
|
+
#######
|
20
|
+
|
21
|
+
def minions
|
22
|
+
if first_argument_is_a_minion?
|
23
|
+
[construct_minion(first_argument)]
|
24
|
+
else
|
25
|
+
minions_for_role(first_argument)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def execute_role(session, name)
|
30
|
+
require role_path(name)
|
31
|
+
name.classify.constantize.new(session).call
|
32
|
+
end
|
33
|
+
|
34
|
+
def first_argument_is_a_minion?
|
35
|
+
config.minions.include?(first_argument)
|
36
|
+
end
|
37
|
+
|
38
|
+
def minions_for_role(role_name)
|
39
|
+
minion_names_for_role(role_name).map {|name| construct_minion(name) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def construct_minion(name)
|
43
|
+
minion_constructor.new(name, config)
|
44
|
+
end
|
45
|
+
|
46
|
+
def role_path(name)
|
47
|
+
File.expand_path("./roles/#{name}.rb")
|
48
|
+
end
|
49
|
+
|
50
|
+
def minion_names_for_role(role_name)
|
51
|
+
config.minions.keys.select do |minion|
|
52
|
+
config.minions[minion]['roles'].include?(role_name)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def first_argument
|
57
|
+
@first_argument ||= arguments.pop
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Minionizer
|
2
|
+
class Session
|
3
|
+
attr_reader :fqdn, :username, :password, :connector
|
4
|
+
|
5
|
+
def initialize(fqdn, credentials, connector = Net::SSH)
|
6
|
+
@fqdn = fqdn
|
7
|
+
@username = credentials['username']
|
8
|
+
@password = credentials['password']
|
9
|
+
@connector = connector
|
10
|
+
end
|
11
|
+
|
12
|
+
def exec(arg)
|
13
|
+
if arg.is_a?(Array)
|
14
|
+
arg.map { |command| exec_single_command(command) }
|
15
|
+
else
|
16
|
+
exec_single_command(arg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
#######
|
21
|
+
private
|
22
|
+
#######
|
23
|
+
|
24
|
+
def exec_single_command(command)
|
25
|
+
connection.exec(command) do |channel, stream, output|
|
26
|
+
if stream == :stdout
|
27
|
+
return output.strip
|
28
|
+
else
|
29
|
+
raise StandardError.new(output)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
connection.loop
|
33
|
+
end
|
34
|
+
|
35
|
+
def connection
|
36
|
+
@connection ||= connector.start(fqdn, username, password: password)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/minionizer.rb
ADDED
data/minionizer.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative 'lib/minionizer/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "minionizer"
|
5
|
+
s.version = Minionizer::VERSION
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.authors = ["Jonathan S. Garvin"]
|
8
|
+
s.email = ["jon@5valleys.com"]
|
9
|
+
s.homepage = "https://github.com/jsgarvin/minionizer"
|
10
|
+
s.summary = %q{Simple server provisioning and management.}
|
11
|
+
s.description = %q{Minionizer aims to be a light weight server provisioning tool without bloat or steep learning curves.}
|
12
|
+
|
13
|
+
s.add_dependency('activesupport')
|
14
|
+
s.add_dependency('net-ssh')
|
15
|
+
|
16
|
+
s.add_development_dependency('fakefs')
|
17
|
+
s.add_development_dependency('mocha')
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
end
|
data/test/Vagrantfile
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# -*- mode: ruby -*-
|
2
|
+
# vi: set ft=ruby :
|
3
|
+
|
4
|
+
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
5
|
+
VAGRANTFILE_API_VERSION = "2"
|
6
|
+
|
7
|
+
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
8
|
+
# All Vagrant configuration is done here. The most common configuration
|
9
|
+
# options are documented and commented below. For a complete reference,
|
10
|
+
# please see the online documentation at vagrantup.com.
|
11
|
+
|
12
|
+
config.vm.define 'minion' do |minion|
|
13
|
+
minion.vm.box = 'precise32'
|
14
|
+
minion.vm.box_url = "http://files.vagrantup.com/precise32.box"
|
15
|
+
minion.vm.network "private_network", ip: "192.168.49.181"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Every Vagrant virtual environment requires a box to build off of.
|
19
|
+
# config.vm.box = "precise32"
|
20
|
+
|
21
|
+
# The url from where the 'config.vm.box' box will be fetched if it
|
22
|
+
# doesn't already exist on the user's system.
|
23
|
+
# config.vm.box_url = "http://domain.com/path/to/above.box"
|
24
|
+
# config.vm.box_url = "http://files.vagrantup.com/precise32.box"
|
25
|
+
|
26
|
+
# Create a forwarded port mapping which allows access to a specific port
|
27
|
+
# within the machine from a port on the host machine. In the example below,
|
28
|
+
# accessing "localhost:8080" will access port 80 on the guest machine.
|
29
|
+
# config.vm.network "forwarded_port", guest: 80, host: 8080
|
30
|
+
|
31
|
+
# Create a private network, which allows host-only access to the machine
|
32
|
+
# using a specific IP.
|
33
|
+
# config.vm.network "private_network", ip: "192.168.33.10"
|
34
|
+
|
35
|
+
# Create a public network, which generally matched to bridged network.
|
36
|
+
# Bridged networks make the machine appear as another physical device on
|
37
|
+
# your network.
|
38
|
+
# config.vm.network "public_network"
|
39
|
+
|
40
|
+
# If true, then any SSH connections made will enable agent forwarding.
|
41
|
+
# Default value: false
|
42
|
+
# config.ssh.forward_agent = true
|
43
|
+
|
44
|
+
# Share an additional folder to the guest VM. The first argument is
|
45
|
+
# the path on the host to the actual folder. The second argument is
|
46
|
+
# the path on the guest to mount the folder. And the optional third
|
47
|
+
# argument is a set of non-required options.
|
48
|
+
# config.vm.synced_folder "../data", "/vagrant_data"
|
49
|
+
|
50
|
+
# Provider-specific configuration so you can fine-tune various
|
51
|
+
# backing providers for Vagrant. These expose provider-specific options.
|
52
|
+
# Example for VirtualBox:
|
53
|
+
#
|
54
|
+
# config.vm.provider "virtualbox" do |vb|
|
55
|
+
# # Don't boot with headless mode
|
56
|
+
# vb.gui = true
|
57
|
+
#
|
58
|
+
# # Use VBoxManage to customize the VM. For example to change memory:
|
59
|
+
# vb.customize ["modifyvm", :id, "--memory", "1024"]
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# View the documentation for the provider you're using for more
|
63
|
+
# information on available options.
|
64
|
+
|
65
|
+
# Enable provisioning with Puppet stand alone. Puppet manifests
|
66
|
+
# are contained in a directory path relative to this Vagrantfile.
|
67
|
+
# You will need to create the manifests directory and a manifest in
|
68
|
+
# the file base.pp in the manifests_path directory.
|
69
|
+
#
|
70
|
+
# An example Puppet manifest to provision the message of the day:
|
71
|
+
#
|
72
|
+
# # group { "puppet":
|
73
|
+
# # ensure => "present",
|
74
|
+
# # }
|
75
|
+
# #
|
76
|
+
# # File { owner => 0, group => 0, mode => 0644 }
|
77
|
+
# #
|
78
|
+
# # file { '/etc/motd':
|
79
|
+
# # content => "Welcome to your Vagrant-built virtual machine!
|
80
|
+
# # Managed by Puppet.\n"
|
81
|
+
# # }
|
82
|
+
#
|
83
|
+
# config.vm.provision "puppet" do |puppet|
|
84
|
+
# puppet.manifests_path = "manifests"
|
85
|
+
# puppet.manifest_file = "site.pp"
|
86
|
+
# end
|
87
|
+
|
88
|
+
# Enable provisioning with chef solo, specifying a cookbooks path, roles
|
89
|
+
# path, and data_bags path (all relative to this Vagrantfile), and adding
|
90
|
+
# some recipes and/or roles.
|
91
|
+
#
|
92
|
+
# config.vm.provision "chef_solo" do |chef|
|
93
|
+
# chef.cookbooks_path = "../my-recipes/cookbooks"
|
94
|
+
# chef.roles_path = "../my-recipes/roles"
|
95
|
+
# chef.data_bags_path = "../my-recipes/data_bags"
|
96
|
+
# chef.add_recipe "mysql"
|
97
|
+
# chef.add_role "web"
|
98
|
+
#
|
99
|
+
# # You may also specify custom JSON attributes:
|
100
|
+
# chef.json = { :mysql_password => "foo" }
|
101
|
+
# end
|
102
|
+
|
103
|
+
# Enable provisioning with chef server, specifying the chef server URL,
|
104
|
+
# and the path to the validation key (relative to this Vagrantfile).
|
105
|
+
#
|
106
|
+
# The Opscode Platform uses HTTPS. Substitute your organization for
|
107
|
+
# ORGNAME in the URL and validation key.
|
108
|
+
#
|
109
|
+
# If you have your own Chef Server, use the appropriate URL, which may be
|
110
|
+
# HTTP instead of HTTPS depending on your configuration. Also change the
|
111
|
+
# validation key to validation.pem.
|
112
|
+
#
|
113
|
+
# config.vm.provision "chef_client" do |chef|
|
114
|
+
# chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME"
|
115
|
+
# chef.validation_key_path = "ORGNAME-validator.pem"
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# If you're using the Opscode platform, your validator client is
|
119
|
+
# ORGNAME-validator, replacing ORGNAME with your organization name.
|
120
|
+
#
|
121
|
+
# If you have your own Chef Server, the default validation client name is
|
122
|
+
# chef-validator, unless you changed the configuration.
|
123
|
+
#
|
124
|
+
# chef.validation_client_name = "ORGNAME-validator"
|
125
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Minionizer
|
4
|
+
class VictoryLap < StandardError; end
|
5
|
+
class MinionTestFailure < StandardError; end
|
6
|
+
class AcceptanceTest < MiniTest::Unit::TestCase
|
7
|
+
|
8
|
+
describe 'acceptance testing' do
|
9
|
+
let(:fqdn) { '192.168.49.181' }
|
10
|
+
let (:username) { 'vagrant' }
|
11
|
+
let (:password) { 'vagrant' }
|
12
|
+
let(:credentials) {{ 'username' => username, 'password' => password }}
|
13
|
+
let(:session) { Session.new(fqdn, credentials) }
|
14
|
+
let(:minionization) { Minionization.new([fqdn], Configuration.instance) }
|
15
|
+
let(:minions) {{ fqdn => { 'ssh' => credentials, 'roles' => ['minion_test'] } }}
|
16
|
+
|
17
|
+
before do
|
18
|
+
skip unless minion_available?
|
19
|
+
roll_back_to_blank_snapshot
|
20
|
+
Configuration.instance.instance_variable_set(:@minions, nil)
|
21
|
+
write_file('config/minions.yml', minions.to_yaml)
|
22
|
+
write_file('roles/minion_test.rb', TEST_ROLE)
|
23
|
+
write_file(INJECTION_SOURCE, 'FooBar')
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'setting up a server' do
|
27
|
+
it 'exercises from start to finish' do
|
28
|
+
begin
|
29
|
+
without_fakefs do
|
30
|
+
refute(File.exists?(synced_path_to_injected_file))
|
31
|
+
end
|
32
|
+
assert_raises(VictoryLap) do
|
33
|
+
minionization.call
|
34
|
+
end
|
35
|
+
without_fakefs do
|
36
|
+
assert(File.exists?(synced_path_to_injected_file))
|
37
|
+
end
|
38
|
+
ensure
|
39
|
+
without_fakefs do
|
40
|
+
File.delete(synced_path_to_injected_file)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
#######
|
47
|
+
private
|
48
|
+
#######
|
49
|
+
|
50
|
+
def without_fakefs
|
51
|
+
FakeFS.deactivate!
|
52
|
+
yield
|
53
|
+
ensure
|
54
|
+
FakeFS.activate!
|
55
|
+
end
|
56
|
+
|
57
|
+
def synced_path_to_injected_file
|
58
|
+
@synced_path_to_injected_file ||= File.expand_path("../../#{INJECTION_SOURCE}", __FILE__)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
INJECTION_SOURCE = 'foobar.txt'
|
65
|
+
INJECTION_TARGET = "/vagrant/#{INJECTION_SOURCE}"
|
66
|
+
TEST_ROLE = <<-endofstring
|
67
|
+
class MinionTest < Minionizer::RoleTemplate
|
68
|
+
|
69
|
+
def call
|
70
|
+
if hostname == 'precise32'
|
71
|
+
Minionizer::FileInjection.new(session).inject('#{INJECTION_SOURCE}','#{INJECTION_TARGET}')
|
72
|
+
raise Minionizer::VictoryLap.new('Shazam!')
|
73
|
+
else
|
74
|
+
raise Minionizer::MinionTestFailure.new("Whawhawhaaaa... \#{hostname}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def hostname
|
79
|
+
@hostname ||= session.exec(:hostname)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
endofstring
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'fakefs/safe'
|
4
|
+
require 'socket'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
require_relative '../lib/minionizer'
|
8
|
+
|
9
|
+
module Minionizer
|
10
|
+
class MiniTest::Unit::TestCase
|
11
|
+
|
12
|
+
def before_setup
|
13
|
+
super
|
14
|
+
initialize_fakefs
|
15
|
+
end
|
16
|
+
|
17
|
+
def after_teardown
|
18
|
+
super
|
19
|
+
FakeFS.deactivate!
|
20
|
+
end
|
21
|
+
|
22
|
+
#######
|
23
|
+
private
|
24
|
+
#######
|
25
|
+
|
26
|
+
def initialize_fakefs
|
27
|
+
FakeFS.activate!
|
28
|
+
FakeFS::FileSystem.clear
|
29
|
+
Kernel.class_eval do
|
30
|
+
alias_method :require, :fake_require
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def minion_available?
|
35
|
+
Timeout.timeout(1) do
|
36
|
+
@@minion_available ||= TCPSocket.new('192.168.49.181', 22)
|
37
|
+
end
|
38
|
+
rescue Errno::ECONNREFUSED, Timeout::Error
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize_minion
|
43
|
+
@@previously_initialized ||= `cd #{File.dirname(__FILE__)}; vagrant up`
|
44
|
+
end
|
45
|
+
|
46
|
+
def roll_back_to_blank_snapshot
|
47
|
+
FakeFS.deactivate!
|
48
|
+
`cd #{File.dirname(__FILE__)}; vagrant snapshot go blank-test-slate`
|
49
|
+
FakeFS.activate!
|
50
|
+
end
|
51
|
+
|
52
|
+
def write_role_file(name)
|
53
|
+
write_file("roles/#{name}.rb")
|
54
|
+
end
|
55
|
+
|
56
|
+
def write_file(path, contents = '')
|
57
|
+
FileUtils.mkdir_p File.dirname(path)
|
58
|
+
File.open("./#{path}", 'w') { |file| file.write(contents) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_dynamic_class(name)
|
62
|
+
Object.const_get(name.classify)
|
63
|
+
rescue NameError
|
64
|
+
Object.const_set(name.classify, Class.new)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module Kernel
|
71
|
+
|
72
|
+
def fake_require(path)
|
73
|
+
File.open(path, "r") {|f| Object.class_eval f.read, path, 1 }
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
module MiniTest
|
79
|
+
class NamedMock < Mock
|
80
|
+
attr_reader :name
|
81
|
+
|
82
|
+
def initialize(name)
|
83
|
+
@name = name
|
84
|
+
super()
|
85
|
+
end
|
86
|
+
|
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
|
92
|
+
|
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}) ")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
require 'mocha/setup'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Minionizer
|
4
|
+
class FileInjectionTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
describe FileInjection do
|
7
|
+
let(:session) { 'MockSession' }
|
8
|
+
let(:injection) { FileInjection.new(session) }
|
9
|
+
|
10
|
+
it 'instantiates' do
|
11
|
+
assert_kind_of(FileInjection, injection)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#call' do
|
15
|
+
let(:source_contents) { 'Source Contents' }
|
16
|
+
let(:source) { 'data/source_file.txt'}
|
17
|
+
let(:target) { '/var/target_file.txt'}
|
18
|
+
|
19
|
+
before do
|
20
|
+
write_file(source, source_contents)
|
21
|
+
session.expects(:exec).with(%Q{echo '#{source_contents}' > #{target}})
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'sends a command to session' do
|
25
|
+
injection.inject(source, target)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Minionizer
|
4
|
+
class ConfigurationTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
describe Configuration do
|
7
|
+
let(:config) { Configuration.instance }
|
8
|
+
let(:minions) {{ 'foo.bar.com' => { :ssh => { :username => 'foo', :password => 'bar' } } }}
|
9
|
+
|
10
|
+
before do
|
11
|
+
write_file('config/minions.yml', minions.to_yaml)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'instantiates a configuration' do
|
15
|
+
assert_kind_of(Configuration, config)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'minions' do
|
19
|
+
|
20
|
+
it 'loads the minions' do
|
21
|
+
assert_kind_of(Hash, config.minions)
|
22
|
+
assert_includes(config.minions.keys, 'foo.bar.com')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Minionizer
|
4
|
+
class MinionTest < MiniTest::Unit::TestCase
|
5
|
+
describe Minion do
|
6
|
+
let(:username) { 'foo' }
|
7
|
+
let(:password) { 'bar' }
|
8
|
+
let(:credentials) {{ 'username' => username, 'password' => password }}
|
9
|
+
let(:config) { Configuration.instance }
|
10
|
+
let(:fqdn) { 'foo.bar.com' }
|
11
|
+
let(:session_constructor) { Struct.new(:fqdn, :credentials) }
|
12
|
+
let(:minion) { Minion.new(fqdn, config, session_constructor) }
|
13
|
+
let(:roles) { %w(foo bar) }
|
14
|
+
let (:minion_config) {{ fqdn => { 'roles' => roles , 'ssh' => credentials } }}
|
15
|
+
|
16
|
+
before do
|
17
|
+
config.stubs(:minions).returns(minion_config)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'instantiates' do
|
21
|
+
assert_kind_of(Minion, minion)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#session' do
|
25
|
+
let(:session) { MiniTest::NamedMock.new('session') }
|
26
|
+
|
27
|
+
it 'creates a session' do
|
28
|
+
session_constructor.expects(:new).with(fqdn, credentials).returns(session)
|
29
|
+
minion.session
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#roles' do
|
34
|
+
|
35
|
+
it 'returns a list of roles' do
|
36
|
+
assert_equal(2,minion.roles.count)
|
37
|
+
roles.each { |role| assert_includes(minion.roles, role) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Minionizer
|
4
|
+
class MinionizationTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
describe Minionization do
|
7
|
+
let(:fqdn) { 'foo.bar.com' }
|
8
|
+
let(:config) { Configuration.instance }
|
9
|
+
let(:role_name) { 'web_server' }
|
10
|
+
let(:role_class) { get_dynamic_class(role_name) }
|
11
|
+
let(:minion) { MiniTest::NamedMock.new('minion') }
|
12
|
+
let(:minionization) { Minionization.new(arguments, config, minion_constructor) }
|
13
|
+
let(:minion_roles) {{ fqdn => { 'roles' => [role_name] }}}
|
14
|
+
let(:minion_constructor) { Struct.new(:fqdn, :config) }
|
15
|
+
let(:session) { MiniTest::NamedMock.new('session') }
|
16
|
+
let(:role) { MiniTest::NamedMock.new('role') }
|
17
|
+
|
18
|
+
before do
|
19
|
+
config.stubs(:minions).returns(minion_roles)
|
20
|
+
minion.expect(:roles, [role_name])
|
21
|
+
minion_constructor.expects(:new).with(fqdn, config).returns(minion)
|
22
|
+
role_class.expects(:new).with(session).returns(role)
|
23
|
+
role.expect(:call, true)
|
24
|
+
minionization.expects(:require).with("/roles/#{role_name}.rb")
|
25
|
+
minion.expect(:session, session)
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'calling with a valid minion name' do
|
29
|
+
let(:arguments) { [fqdn] }
|
30
|
+
|
31
|
+
it 'executes a role' do
|
32
|
+
minionization.call
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'calling with a valid role' do
|
37
|
+
let(:arguments) { [role_name] }
|
38
|
+
|
39
|
+
it 'executes the role once for each minion' do
|
40
|
+
minionization.call
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Minionizer
|
4
|
+
class RoleTemplateTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
describe RoleTemplate do
|
7
|
+
let(:session) { 'MockSession' }
|
8
|
+
let(:template) { RoleTemplate.new(session) }
|
9
|
+
|
10
|
+
it 'initilizes' do
|
11
|
+
assert_kind_of(RoleTemplate, template)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#call' do
|
15
|
+
it 'raises' do
|
16
|
+
assert_raises(StandardError) { template.call }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Minionizer
|
4
|
+
class SessionTest < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
describe Session do
|
7
|
+
let(:fqdn) { 'foo.bar.com' }
|
8
|
+
let(:username) { 'foo' }
|
9
|
+
let(:password) { 'bar' }
|
10
|
+
let(:credentials) {{ 'username' => username, 'password' => password }}
|
11
|
+
let(:connector) { MiniTest::NamedMock.new(:connector) }
|
12
|
+
let(:channel) { MiniTest::NamedMock.new(:channel) }
|
13
|
+
let(:connection) { 'MockConnection' }
|
14
|
+
let(:session) { Session.new(fqdn, credentials, connector) }
|
15
|
+
let(:start_args) { [fqdn, username, { password: password }]}
|
16
|
+
|
17
|
+
it 'instantiates' do
|
18
|
+
assert_kind_of(Session, session)
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'running commands' do
|
22
|
+
let(:command) { 'foobar' }
|
23
|
+
|
24
|
+
before do
|
25
|
+
connector.expect(:start, connection, start_args)
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'when a single command is passed' do
|
29
|
+
|
30
|
+
before do
|
31
|
+
connection.expects(:exec).with(command).returns("#{command} pong")
|
32
|
+
connection.expects(:loop).returns('fixme')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns a single result' do
|
36
|
+
assert_kind_of(String, session.exec(command))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'when multiple commands are passed' do
|
41
|
+
let(:commands) { %w(foo bar) }
|
42
|
+
|
43
|
+
before do
|
44
|
+
commands.each do |command|
|
45
|
+
connection.expects(:exec).with(command).returns("#{command} pong")
|
46
|
+
end
|
47
|
+
connection.expects(:loop).twice.returns('fixme')
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'returns multiple results' do
|
51
|
+
assert_kind_of(Array, session.exec(commands))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minionizer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jonathan S. Garvin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-ssh
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: fakefs
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: mocha
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Minionizer aims to be a light weight server provisioning tool without
|
70
|
+
bloat or steep learning curves.
|
71
|
+
email:
|
72
|
+
- jon@5valleys.com
|
73
|
+
executables:
|
74
|
+
- minionize
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".gitignore"
|
79
|
+
- Gemfile
|
80
|
+
- Gemfile.lock
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/minionize
|
84
|
+
- lib/core/file_injection.rb
|
85
|
+
- lib/minionizer.rb
|
86
|
+
- lib/minionizer/configuration.rb
|
87
|
+
- lib/minionizer/minion.rb
|
88
|
+
- lib/minionizer/minionization.rb
|
89
|
+
- lib/minionizer/role_template.rb
|
90
|
+
- lib/minionizer/session.rb
|
91
|
+
- lib/minionizer/version.rb
|
92
|
+
- minionizer.gemspec
|
93
|
+
- test/Vagrantfile
|
94
|
+
- test/integration/acceptance_test.rb
|
95
|
+
- test/test_helper.rb
|
96
|
+
- test/unit/lib/core/file_injection_test.rb
|
97
|
+
- test/unit/lib/minionizer/configuration_test.rb
|
98
|
+
- test/unit/lib/minionizer/minion_test.rb
|
99
|
+
- test/unit/lib/minionizer/minionization_test.rb
|
100
|
+
- test/unit/lib/minionizer/role_template_test.rb
|
101
|
+
- test/unit/lib/minionizer/session_test.rb
|
102
|
+
homepage: https://github.com/jsgarvin/minionizer
|
103
|
+
licenses: []
|
104
|
+
metadata: {}
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 2.2.2
|
122
|
+
signing_key:
|
123
|
+
specification_version: 4
|
124
|
+
summary: Simple server provisioning and management.
|
125
|
+
test_files:
|
126
|
+
- test/Vagrantfile
|
127
|
+
- test/integration/acceptance_test.rb
|
128
|
+
- test/test_helper.rb
|
129
|
+
- test/unit/lib/core/file_injection_test.rb
|
130
|
+
- test/unit/lib/minionizer/configuration_test.rb
|
131
|
+
- test/unit/lib/minionizer/minion_test.rb
|
132
|
+
- test/unit/lib/minionizer/minionization_test.rb
|
133
|
+
- test/unit/lib/minionizer/role_template_test.rb
|
134
|
+
- test/unit/lib/minionizer/session_test.rb
|