minionizer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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