minionizer 0.0.1
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 +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
|
+
[](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
|