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 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
@@ -0,0 +1,3 @@
1
+ *.swp
2
+ test/.vagrant
3
+
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
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,3 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/minionizer'
3
+ Minionizer::Minionization.new(ARGV, Minionizer::Configuration.instance).call
@@ -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,10 @@
1
+ module Minionizer
2
+ class Configuration
3
+ include Singleton
4
+
5
+ def minions
6
+ @minions ||= YAML::load_file('./config/minions.yml')
7
+ end
8
+ end
9
+ end
10
+
@@ -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,13 @@
1
+ module Minionizer
2
+ class RoleTemplate
3
+ attr_reader :session
4
+
5
+ def initialize(session)
6
+ @session = session
7
+ end
8
+
9
+ def call
10
+ raise StandardError.new('call method must be defined by inheriting role.')
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module Minionizer
2
+ VERSION = '0.0.1'
3
+ end
data/lib/minionizer.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'active_support/inflector'
2
+ require 'net/ssh'
3
+ require 'singleton'
4
+ require 'yaml'
5
+
6
+ Dir[File.dirname(__FILE__) + '/**/*.rb'].each { |file| require file }
@@ -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
@@ -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