butcher 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rbenv-version +1 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Guardfile +18 -0
- data/README.md +57 -0
- data/Rakefile +1 -0
- data/bin/stab +66 -0
- data/butcher.gemspec +29 -0
- data/features/stab.feature +121 -0
- data/features/step_definitions/chef_steps.rb +32 -0
- data/features/support/aruba.rb +3 -0
- data/features/support/env.rb +9 -0
- data/features/support/mocha.rb +15 -0
- data/features/support/test_cache.rb +4 -0
- data/lib/butcher.rb +12 -0
- data/lib/butcher/cache.rb +65 -0
- data/lib/butcher/stab.rb +2 -0
- data/lib/butcher/stab/cli.rb +36 -0
- data/lib/butcher/version.rb +3 -0
- data/spec/lib/cache_spec.rb +94 -0
- data/spec/lib/stab/cli_spec.rb +60 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/test_cache.rb +112 -0
- metadata +148 -0
data/.gitignore
ADDED
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p125
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2 do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch(%r{spec/support/.+\.rb}) { "spec" }
|
8
|
+
watch('spec/spec_helper.rb') { "spec" }
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
guard 'cucumber' do
|
13
|
+
watch(%r{^bin/(.+)$}) { |m| "features/#{m[1]}.feature" }
|
14
|
+
watch(%r{^lib/butcher/(.+)$}) { |m| "features/#{m[1]}.feature" }
|
15
|
+
watch(%r{^features/.+\.feature$})
|
16
|
+
watch(%r{^features/support/.+$}) { 'features' }
|
17
|
+
watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
|
18
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Butcher
|
2
|
+
|
3
|
+
Butcher is a set of command line tools intended to ease the use of Chef with a managed
|
4
|
+
Opscode account.
|
5
|
+
|
6
|
+
## Commands
|
7
|
+
|
8
|
+
The following commands are currently available. All commands should be run from the top
|
9
|
+
level of a chef directory.
|
10
|
+
|
11
|
+
### Stab
|
12
|
+
|
13
|
+
SSH into a node based on a grep of the node name.
|
14
|
+
|
15
|
+
Usage: stab [options] node_name
|
16
|
+
-c, --cache-dir DIR # saves node list here (default: ~/.butcher/cache)
|
17
|
+
-f, --force # download new node list even if a cache file exists
|
18
|
+
-h, --help # prints usage
|
19
|
+
-l, --login LOGIN # ssh with specified username
|
20
|
+
-v, --verbose # be expressive
|
21
|
+
|
22
|
+
Node name is loosely matched against name attributes given to nodes in chef. If multiple
|
23
|
+
nodes match a given string, a Butcher::AmbiguousNode error is thrown and the program exits.
|
24
|
+
|
25
|
+
> knife status
|
26
|
+
1 hours ago, app.node, app.domain.com, 1.1.1.1, solaris2 5.11.
|
27
|
+
5 minutes ago, other.node, other.domain.com, 1.1.1.2, solaris2 5.11.
|
28
|
+
|
29
|
+
> stab something
|
30
|
+
Unable to find node "something"
|
31
|
+
|
32
|
+
> stab node
|
33
|
+
Multiple nodes match "node"
|
34
|
+
["app.node", "app.domain.com"] => 1.1.1.1
|
35
|
+
["other.node", "other.domain.com"] => 1.1.1.2
|
36
|
+
|
37
|
+
Nodes are cached in a file named after your organization in Opscode. Stab discovers this
|
38
|
+
by looking at the chef_server_url in your knife.rb. For this reason, stab can only be run
|
39
|
+
from the top level of a chef repo.
|
40
|
+
|
41
|
+
|
42
|
+
## License
|
43
|
+
|
44
|
+
Copyright 2012 ModCloth
|
45
|
+
|
46
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
47
|
+
you may not use this file except in compliance with the License.
|
48
|
+
You may obtain a copy of the License at
|
49
|
+
|
50
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
51
|
+
|
52
|
+
Unless required by applicable law or agreed to in writing, software
|
53
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
54
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
55
|
+
See the License for the specific language governing permissions and
|
56
|
+
limitations under the License.
|
57
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/stab
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'butcher'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
EX_USAGE = 64
|
7
|
+
EX_UNMATCHED = 65
|
8
|
+
EX_AMBIGUOUS = 66
|
9
|
+
EX_KNIFE = 67
|
10
|
+
|
11
|
+
options = {}
|
12
|
+
parser = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: stab [options] node_name"
|
14
|
+
|
15
|
+
opts.on('-c', '--cache-dir DIR', 'Location to save cache files (default: ~/.butcher/cache)') do |dir|
|
16
|
+
ENV["CACHE_DIR"] = dir
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on('-f', '--force', 'Check for new nodes even if a cache file exists') do
|
20
|
+
options[:force] = true
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on('-h', '--help', 'Display this screen') do
|
24
|
+
puts opts
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on('-l', '--login LOGIN', 'Use LOGIN for ssh session') do |login|
|
29
|
+
options[:login] = login
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
33
|
+
options[:verbose] = v
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
begin parser.parse!
|
38
|
+
rescue OptionParser::InvalidOption => e
|
39
|
+
puts e
|
40
|
+
puts parser
|
41
|
+
exit EX_USAGE
|
42
|
+
end
|
43
|
+
|
44
|
+
if ARGV == []
|
45
|
+
puts parser
|
46
|
+
exit EX_USAGE
|
47
|
+
end
|
48
|
+
|
49
|
+
begin
|
50
|
+
Butcher::Stab::CLI.new.run(ARGV, options)
|
51
|
+
rescue Butcher::UnmatchedNode
|
52
|
+
STDERR.puts %Q{Unable to find node "#{ARGV.first}"}
|
53
|
+
exit EX_UNMATCHED
|
54
|
+
rescue Butcher::AmbiguousNode => e
|
55
|
+
STDERR.puts %Q{Multiple nodes match "#{ARGV.first}"}
|
56
|
+
STDERR.puts e.message
|
57
|
+
exit EX_AMBIGUOUS
|
58
|
+
rescue Butcher::NoKnifeRB
|
59
|
+
STDERR.puts "Unable to find knife.rb in ./.chef"
|
60
|
+
STDERR.puts "Are you stabbing from a valid working chef directory?"
|
61
|
+
exit EX_KNIFE
|
62
|
+
rescue Butcher::NoKnifeOrganization
|
63
|
+
STDERR.puts "Unable to read organization from knife.rb"
|
64
|
+
STDERR.puts "Expected .chef/knife.rb to contain a chef_server_url"
|
65
|
+
exit EX_KNIFE
|
66
|
+
end
|
data/butcher.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "butcher/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "butcher"
|
7
|
+
s.version = Butcher::VERSION
|
8
|
+
s.authors = ["ModCloth", "Eric Saxby"]
|
9
|
+
s.email = %W(git@modcloth.com)
|
10
|
+
s.homepage = "https://github.com/modcloth/butcher"
|
11
|
+
s.summary = %q{All the things to make a chef}
|
12
|
+
s.description = %q{Chef is a tool for managing server automation. A good butcher makes for a good chef.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "butcher"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = %W(lib)
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec", "> 2"
|
23
|
+
s.add_development_dependency "aruba"
|
24
|
+
s.add_development_dependency "aruba-doubles"
|
25
|
+
s.add_development_dependency "guard-rspec"
|
26
|
+
s.add_development_dependency "guard-cucumber"
|
27
|
+
s.add_development_dependency "mocha"
|
28
|
+
# s.add_runtime_dependency "x"
|
29
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
Feature: Stab
|
2
|
+
|
3
|
+
Background:
|
4
|
+
# stubbing knife currently doesn't work, but tests pass anyways
|
5
|
+
#Given I could run `knife status` with stdout:
|
6
|
+
#"""
|
7
|
+
#1 minute ago, app.node, app.domain, 1.1.1.1, os
|
8
|
+
#"""
|
9
|
+
Given I have a knife configuration file
|
10
|
+
And I could run `ssh 1.1.1.1` with stdout:
|
11
|
+
"""
|
12
|
+
ssh yay!
|
13
|
+
"""
|
14
|
+
|
15
|
+
Scenario: Usage information with --help option
|
16
|
+
When I run `stab --help`
|
17
|
+
Then the exit status should be 0
|
18
|
+
And the output should contain:
|
19
|
+
"""
|
20
|
+
Usage: stab [options] node_name
|
21
|
+
"""
|
22
|
+
|
23
|
+
Scenario: Usage information when no node name is given
|
24
|
+
When I run `stab`
|
25
|
+
Then the exit status should be 64
|
26
|
+
And the output should contain:
|
27
|
+
"""
|
28
|
+
Usage: stab [options] node_name
|
29
|
+
"""
|
30
|
+
|
31
|
+
Scenario: Invalid option
|
32
|
+
When I run `stab --invalid-option`
|
33
|
+
Then the exit status should be 64
|
34
|
+
And the output should contain:
|
35
|
+
"""
|
36
|
+
invalid option: --invalid-option
|
37
|
+
Usage: stab [options] node_name
|
38
|
+
"""
|
39
|
+
|
40
|
+
Scenario: Set cache directory
|
41
|
+
Given a directory named "tmp/test_dir" should not exist
|
42
|
+
When I run `stab app.node -c tmp/test_dir`
|
43
|
+
Then a directory named "tmp/test_dir" should exist
|
44
|
+
|
45
|
+
Scenario: Tell user what IP address ssh uses
|
46
|
+
Given I have the following chef nodes:
|
47
|
+
| 1 minute ago | app.node | app.domain | 1.1.1.1 | os |
|
48
|
+
When I run `stab app.node -c tmp/test -v`
|
49
|
+
Then the output should contain "Connecting to app.node at 1.1.1.1"
|
50
|
+
Then the output should contain "ssh yay!"
|
51
|
+
|
52
|
+
Scenario: Don't download node list if already cached
|
53
|
+
Given I have the following chef nodes:
|
54
|
+
| 1 minute ago | app.node | app.domain | 1.1.1.1 | os |
|
55
|
+
Then a file named "tmp/test/my_organization.cache" should exist
|
56
|
+
When I run `stab app.node -c tmp/test -v`
|
57
|
+
Then the output should not contain "Creating cache file of nodes"
|
58
|
+
|
59
|
+
Scenario: Force download of cache file
|
60
|
+
Given I have the following chef nodes:
|
61
|
+
| 1 minute ago | app.node | app.domain | 1.1.1.1 | os |
|
62
|
+
When I run `stab app.node -c tmp/test -f -v`
|
63
|
+
Then the output should contain "Creating cache file of nodes"
|
64
|
+
#And the exit status should be 0 ## Butcher::Cache does not use stubbed knife
|
65
|
+
|
66
|
+
Scenario: User sees error message if no node matches given name
|
67
|
+
Given I have the following chef nodes:
|
68
|
+
| 1 minute ago | app.node | app.domain | 1.1.1.1 | os |
|
69
|
+
When I run `stab some.node -c tmp/test`
|
70
|
+
Then the stderr should contain:
|
71
|
+
"""
|
72
|
+
Unable to find node "some.node"
|
73
|
+
"""
|
74
|
+
And the exit status should be 65
|
75
|
+
|
76
|
+
Scenario: User sees error message if multiple nodes match given name
|
77
|
+
Given I have the following chef nodes:
|
78
|
+
| 1 minute ago | other.node | other.domain | 1.1.1.2 | os |
|
79
|
+
| 1 minute ago | app.node | app.domain | 1.1.1.1 | os |
|
80
|
+
When I run `stab node -c tmp/test`
|
81
|
+
Then the stderr should contain:
|
82
|
+
"""
|
83
|
+
Multiple nodes match "node"
|
84
|
+
["app.node", "app.domain"] => 1.1.1.1
|
85
|
+
["other.node", "other.domain"] => 1.1.1.2
|
86
|
+
"""
|
87
|
+
And the exit status should be 66
|
88
|
+
|
89
|
+
Scenario: User can connect to server with given user name
|
90
|
+
Given I have the following chef nodes:
|
91
|
+
| 1 minute ago | app.node | app.domain | 1.1.1.1 | os |
|
92
|
+
Given I could run `ssh 1.1.1.1 -l user` with stdout:
|
93
|
+
"""
|
94
|
+
user: I'm a computer!
|
95
|
+
"""
|
96
|
+
When I run `stab app.node -c tmp/test -l user`
|
97
|
+
Then the stdout should contain:
|
98
|
+
"""
|
99
|
+
user: I'm a computer!
|
100
|
+
"""
|
101
|
+
|
102
|
+
Scenario: User sees error message if knife.rb cannot be found
|
103
|
+
Given I don't have a knife configuration file
|
104
|
+
When I run `stab app.node -c tmp/test`
|
105
|
+
Then the stderr should contain:
|
106
|
+
"""
|
107
|
+
Unable to find knife.rb in ./.chef
|
108
|
+
Are you stabbing from a valid working chef directory?
|
109
|
+
"""
|
110
|
+
And the exit status should be 67
|
111
|
+
|
112
|
+
Scenario: User sees error message if knife.rb is invalid
|
113
|
+
Given I have an invalid knife configuration file
|
114
|
+
When I run `stab app.node -c tmp/test`
|
115
|
+
Then the stderr should contain:
|
116
|
+
"""
|
117
|
+
Unable to read organization from knife.rb
|
118
|
+
Expected .chef/knife.rb to contain a chef_server_url
|
119
|
+
"""
|
120
|
+
And the exit status should be 67
|
121
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Given /I (don't|) ?have a knife configuration file/ do |expectation|
|
2
|
+
if expectation == "don't"
|
3
|
+
steps %q{
|
4
|
+
When I remove the file ".chef/knife.rb"
|
5
|
+
}
|
6
|
+
else
|
7
|
+
steps %q{
|
8
|
+
Given a file named ".chef/knife.rb" with:
|
9
|
+
"""
|
10
|
+
chef_server_url "https://opscode.url/organizations/my_organization"
|
11
|
+
"""
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Given /I have an invalid knife configuration file/ do
|
17
|
+
steps %q{
|
18
|
+
Given a file named ".chef/knife.rb" with:
|
19
|
+
"""
|
20
|
+
some invalid content
|
21
|
+
"""
|
22
|
+
}
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
Given /^I have the following chef nodes:$/ do |table|
|
27
|
+
create_cache_file("my_organization.cache") do |f|
|
28
|
+
table.raw.each do |row|
|
29
|
+
f.puts row.join(", ")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
|
2
|
+
|
3
|
+
require 'aruba-doubles/cucumber'
|
4
|
+
require 'aruba/cucumber'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'rspec/expectations'
|
7
|
+
require 'butcher'
|
8
|
+
|
9
|
+
Dir["#{File.dirname(__FILE__)}/../../spec/support/**/*.rb"].each { |f| require f unless /_spec\.rb$/.match(f) }
|
data/lib/butcher.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "butcher/version"
|
2
|
+
|
3
|
+
module Butcher
|
4
|
+
class UnmatchedNode < Exception;end
|
5
|
+
class AmbiguousNode < Exception;end
|
6
|
+
class NoKnifeRB < Exception;end
|
7
|
+
class NoKnifeOrganization < Exception;end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'butcher/stab'
|
11
|
+
require 'butcher/stab/cli'
|
12
|
+
require 'butcher/cache'
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
class Butcher::Cache
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
CACHE_DIR = "#{ENV["HOME"]}/.butcher/cache"
|
7
|
+
KNIFE_FILE = ".chef/knife.rb"
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
FileUtils.mkdir_p(cache_dir)
|
11
|
+
end
|
12
|
+
|
13
|
+
def nodes(options = {})
|
14
|
+
hash = {}
|
15
|
+
cache_file(options) do |file|
|
16
|
+
while file.gets
|
17
|
+
node = $_.split(", ")
|
18
|
+
hash[node[3]] = [node[1],node[2]]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def cache_dir # :nodoc:
|
25
|
+
ENV["CACHE_DIR"] || CACHE_DIR
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.format_nodes_for_stderr(nodes)
|
29
|
+
nodes.map do |key, value|
|
30
|
+
%Q{#{value} => #{key}}
|
31
|
+
end.sort.join("\n")
|
32
|
+
end
|
33
|
+
|
34
|
+
def nodes_file
|
35
|
+
"#{cache_dir}/#{organization}.cache"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def organization
|
41
|
+
raise Butcher::NoKnifeRB unless(File.exists?(KNIFE_FILE))
|
42
|
+
if m = File.read(KNIFE_FILE).match(/chef_server_url\s+".+organizations\/([^\/"]+)"/)
|
43
|
+
m[1]
|
44
|
+
else
|
45
|
+
raise Butcher::NoKnifeOrganization
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_node_cachefile
|
50
|
+
File.open(nodes_file, "w") do |file|
|
51
|
+
file.puts %x[knife status]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def cache_file(options, &block)
|
56
|
+
if options[:force] || !File.exists?(nodes_file)
|
57
|
+
puts "Creating cache file of nodes" if options[:verbose]
|
58
|
+
create_node_cachefile
|
59
|
+
end
|
60
|
+
|
61
|
+
File.open(nodes_file) do |f|
|
62
|
+
block.call f
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/butcher/stab.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Butcher::Stab::CLI
|
2
|
+
attr_accessor :node_matcher
|
3
|
+
attr_accessor :options
|
4
|
+
|
5
|
+
def run(arguments, options = {})
|
6
|
+
self.options = options
|
7
|
+
self.node_matcher = Array(arguments).first
|
8
|
+
return "" if node_matcher.nil?
|
9
|
+
|
10
|
+
ssh_to(matching_node)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def ssh_to(ip)
|
16
|
+
STDOUT.sync = true # exec takes over stdout in tests, so sync output
|
17
|
+
puts "Connecting to #{node_matcher} at #{ip}" if options[:verbose]
|
18
|
+
exec("ssh #{ip}#{ssh_options}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def matching_node
|
22
|
+
nodes = Butcher::Cache.instance.nodes(options).select do |k, v|
|
23
|
+
String(v).include? self.node_matcher
|
24
|
+
end
|
25
|
+
|
26
|
+
raise(Butcher::UnmatchedNode) if nodes.size == 0
|
27
|
+
raise(Butcher::AmbiguousNode, Butcher::Cache.format_nodes_for_stderr(nodes)) if nodes.size > 1
|
28
|
+
nodes.keys.first
|
29
|
+
end
|
30
|
+
|
31
|
+
def ssh_options
|
32
|
+
if options[:login]
|
33
|
+
" -l #{options[:login]}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Butcher::Cache, "initialization" do
|
4
|
+
## singletons do not reset after initialization, so we run tests against clones
|
5
|
+
let(:cache_class) { Butcher::Cache.clone }
|
6
|
+
|
7
|
+
it "should accept cache_dir from env" do
|
8
|
+
ENV["CACHE_DIR"] = "tmp/cache_from_options"
|
9
|
+
test(?d, "tmp/cache_from_options").should be_false
|
10
|
+
cache_class.instance
|
11
|
+
ENV["CACHE_DIR"] = nil
|
12
|
+
test(?d, "tmp/cache_from_options").should be_true
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should create cache directory" do
|
16
|
+
cache_class.any_instance.stubs(:cache_dir).returns("tmp/cache_stub")
|
17
|
+
test(?d, "tmp/cache_stub").should be_false
|
18
|
+
cache_class.instance
|
19
|
+
test(?d, "tmp/cache_stub").should be_true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe Butcher::Cache, "#nodes_file" do
|
24
|
+
context "cannot find knife.rb" do
|
25
|
+
it "should raise an error" do
|
26
|
+
File.expects(:exists?).with(".chef/knife.rb").returns(false)
|
27
|
+
lambda {
|
28
|
+
Butcher::Cache.instance.nodes_file
|
29
|
+
}.should raise_error(Butcher::NoKnifeRB)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "sees a knife.rb" do
|
34
|
+
before { File.expects(:exists?).with(".chef/knife.rb").returns(true) }
|
35
|
+
|
36
|
+
context "without chef_server_url" do
|
37
|
+
it "should raise an error" do
|
38
|
+
File.expects(:read).with(".chef/knife.rb").returns(
|
39
|
+
'some random content'
|
40
|
+
)
|
41
|
+
lambda {
|
42
|
+
Butcher::Cache.instance.nodes_file
|
43
|
+
}.should raise_error(Butcher::NoKnifeOrganization)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with chef_server_url" do
|
48
|
+
let(:expected_file) { "#{Butcher::TestCache.cache_dir}/my_organization.cache" }
|
49
|
+
|
50
|
+
it "should set filename based on chef_server_url" do
|
51
|
+
File.expects(:read).with(".chef/knife.rb").returns(
|
52
|
+
'chef_server_url "https://api.opscode.com/organizations/my_organization"'
|
53
|
+
)
|
54
|
+
Butcher::Cache.instance.nodes_file.should == expected_file
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe Butcher::Cache, "#nodes" do
|
61
|
+
before { Butcher::Cache.any_instance.stubs(:organization).returns("ops_org") }
|
62
|
+
|
63
|
+
context "cache file does not exist" do
|
64
|
+
let(:cache_file) { "#{Butcher::TestCache.cache_dir}/ops_org.cache" }
|
65
|
+
|
66
|
+
before do
|
67
|
+
File.exists?(cache_file).should be_false
|
68
|
+
Butcher::Cache.any_instance.stubs(:`).with("knife status").returns("knife return codes")
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should not raise an error" do
|
72
|
+
lambda { Butcher::Cache.instance.nodes }.should_not raise_error
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should create a node list from knife" do
|
76
|
+
Butcher::Cache.instance.nodes
|
77
|
+
File.exists?(cache_file).should be_true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context "cache file exists" do
|
82
|
+
create_cache_file("ops_org.cache") do |f|
|
83
|
+
f.puts "5 minutes ago, app.node, app.domain.com, 192.168.1.1, some os"
|
84
|
+
f.puts "1 minute ago, other.node, other.domain.com, 192.168.1.2, some os"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "maps file to hash" do
|
88
|
+
Butcher::Cache.instance.nodes.should == {
|
89
|
+
"192.168.1.1" => %W(app.node app.domain.com),
|
90
|
+
"192.168.1.2" => %W(other.node other.domain.com)
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Butcher::Stab::CLI do
|
4
|
+
|
5
|
+
context "arguments are nil" do
|
6
|
+
it "should return empty string" do
|
7
|
+
Butcher::Stab::CLI.new.run(nil).should == ""
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context "node cache file exists" do
|
12
|
+
mock_cache(:nodes) do
|
13
|
+
{"10.1.1.1" => %W(app.node app.node.com), "10.1.1.2" => %W(other.node other.node.com)}
|
14
|
+
end
|
15
|
+
|
16
|
+
context "stabbing an existing node" do
|
17
|
+
it "should open an SSH session to named node" do
|
18
|
+
Butcher::Stab::CLI.any_instance.expects(:exec).with("ssh 10.1.1.1").returns(true).once
|
19
|
+
Butcher::Stab::CLI.new.run("app")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should ssh to IP based on matched node" do
|
23
|
+
Butcher::Stab::CLI.any_instance.expects(:exec).with("ssh 10.1.1.1").never
|
24
|
+
Butcher::Stab::CLI.any_instance.expects(:exec).with("ssh 10.1.1.2").returns(true).once
|
25
|
+
Butcher::Stab::CLI.new.run("other")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "stabbing a non-existing node" do
|
30
|
+
it "should raise an UnmatchedNode error" do
|
31
|
+
Butcher::Stab::CLI.any_instance.expects(:exec).never
|
32
|
+
lambda {
|
33
|
+
Butcher::Stab::CLI.new.run("nil.node")
|
34
|
+
}.should raise_error(Butcher::UnmatchedNode)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "ambiguous stabbing" do
|
39
|
+
it "should raise an AmbiguousNode error" do
|
40
|
+
Butcher::Stab::CLI.any_instance.expects(:exec).never
|
41
|
+
lambda {
|
42
|
+
Butcher::Stab::CLI.new.run("node")
|
43
|
+
}.should raise_error(Butcher::AmbiguousNode)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "options" do
|
49
|
+
mock_cache(:nodes) do
|
50
|
+
{"10.1.1.1" => %W(app.node app.node.com)}
|
51
|
+
end
|
52
|
+
|
53
|
+
describe ":login" do
|
54
|
+
it "should include login in ssh params" do
|
55
|
+
Butcher::Stab::CLI.any_instance.expects(:exec).with("ssh 10.1.1.1 -l username").returns(true).once
|
56
|
+
Butcher::Stab::CLI.new.run("app", {:login => "username"})
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
require "butcher"
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f unless /_spec\.rb$/.match(f) }
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
12
|
+
config.run_all_when_everything_filtered = true
|
13
|
+
config.filter_run :focus
|
14
|
+
|
15
|
+
config.mock_with :mocha
|
16
|
+
|
17
|
+
Butcher::TestCache.setup_rspec(config)
|
18
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# = Butcher::TestCache
|
2
|
+
#
|
3
|
+
# Providing helpers for testing Butcher using RSpec and Cucumber.
|
4
|
+
#
|
5
|
+
# Author:: Eric Saxby (mailto:e.saxby@modcloth.com)
|
6
|
+
# Copyright:: Copyright (c) 2012 ModCloth
|
7
|
+
# License:: Distributes under the same terms as Ruby
|
8
|
+
#
|
9
|
+
# == RSpec Usage:
|
10
|
+
#
|
11
|
+
# RSpec.configure do |config|
|
12
|
+
# Butcher::TestCache.setup_rspec(config)
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# == Cucumber Usage:
|
16
|
+
#
|
17
|
+
# require 'butcher'
|
18
|
+
# Before { Butcher::TestCache.reset }
|
19
|
+
# After { Butcher::TestCache.cleanup }
|
20
|
+
# World(Butcher::TestCache::TestHelpers)
|
21
|
+
#
|
22
|
+
module Butcher::TestCache
|
23
|
+
def self.setup_rspec(config)
|
24
|
+
config.before(:each) do
|
25
|
+
Butcher::TestCache.reset
|
26
|
+
end
|
27
|
+
|
28
|
+
config.after(:suite) do
|
29
|
+
Butcher::TestCache.cleanup
|
30
|
+
end
|
31
|
+
|
32
|
+
config.extend(RSpecExampleHelpers)
|
33
|
+
config.include(TestHelpers)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.reset # :nodoc:
|
37
|
+
cleanup
|
38
|
+
setup
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.cleanup # :nodoc:
|
42
|
+
FileUtils.rm_rf("tmp")
|
43
|
+
FileUtils.rm_rf(".chef")
|
44
|
+
ENV.delete("CACHE_DIR")
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.cache_dir # :nodoc:
|
48
|
+
File.expand_path("tmp/test")
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def self.setup
|
54
|
+
stub_cache
|
55
|
+
FileUtils.mkdir_p("tmp/test")
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.stub_cache
|
59
|
+
ENV["CACHE_DIR"] = cache_dir
|
60
|
+
end
|
61
|
+
|
62
|
+
public
|
63
|
+
|
64
|
+
# == RSpecExampleHelpers
|
65
|
+
#
|
66
|
+
# RSpec helpers that can be used in the scope of an example group or a describe block.
|
67
|
+
# These cannot be called from within tests themselves.
|
68
|
+
module RSpecExampleHelpers
|
69
|
+
|
70
|
+
# Creates a file that Butcher::Cache can parse before every test in the current context.
|
71
|
+
# Used for testing the Cache class itself.
|
72
|
+
#
|
73
|
+
# example:
|
74
|
+
# create_cache_file("nodes.cache") do |f|
|
75
|
+
# f.puts "1 hour ago, node.name, node.domain, 192.168.1.1, os name"
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
def create_cache_file(filename, &block)
|
79
|
+
before do
|
80
|
+
create_cache_file(filename, &block)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Mock out responses on Butcher::Cache for every test in the current context.
|
85
|
+
# Used in classes where you want to test against expected output from Cache.
|
86
|
+
#
|
87
|
+
# example:
|
88
|
+
# mock_cache(:nodes) do
|
89
|
+
# {"192.168.1.1" => ["node.name","node.domain"]}
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
def mock_cache(type, &block)
|
93
|
+
before do
|
94
|
+
Butcher::Cache.any_instance.stubs(type).returns(block.call)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# == TestHelpers
|
100
|
+
#
|
101
|
+
# RSpec helpers that can be used within tests
|
102
|
+
module TestHelpers
|
103
|
+
|
104
|
+
# Creates a file that Butcher::Cache can parse
|
105
|
+
def create_cache_file(filename)
|
106
|
+
File.open("#{Butcher::TestCache.cache_dir}/#{filename}", "w") do |file|
|
107
|
+
yield file
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
metadata
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: butcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- ModCloth
|
9
|
+
- Eric Saxby
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-03-08 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
requirement: &70346417175740 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>'
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '2'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70346417175740
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: aruba
|
28
|
+
requirement: &70346417175140 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70346417175140
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: aruba-doubles
|
39
|
+
requirement: &70346417174680 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *70346417174680
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: guard-rspec
|
50
|
+
requirement: &70346417174260 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *70346417174260
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: guard-cucumber
|
61
|
+
requirement: &70346417173820 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ! '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *70346417173820
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: mocha
|
72
|
+
requirement: &70346417173380 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *70346417173380
|
81
|
+
description: Chef is a tool for managing server automation. A good butcher makes for
|
82
|
+
a good chef.
|
83
|
+
email:
|
84
|
+
- git@modcloth.com
|
85
|
+
executables:
|
86
|
+
- stab
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- .gitignore
|
91
|
+
- .rbenv-version
|
92
|
+
- .rspec
|
93
|
+
- Gemfile
|
94
|
+
- Guardfile
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/stab
|
98
|
+
- butcher.gemspec
|
99
|
+
- features/stab.feature
|
100
|
+
- features/step_definitions/chef_steps.rb
|
101
|
+
- features/support/aruba.rb
|
102
|
+
- features/support/env.rb
|
103
|
+
- features/support/mocha.rb
|
104
|
+
- features/support/test_cache.rb
|
105
|
+
- lib/butcher.rb
|
106
|
+
- lib/butcher/cache.rb
|
107
|
+
- lib/butcher/stab.rb
|
108
|
+
- lib/butcher/stab/cli.rb
|
109
|
+
- lib/butcher/version.rb
|
110
|
+
- spec/lib/cache_spec.rb
|
111
|
+
- spec/lib/stab/cli_spec.rb
|
112
|
+
- spec/spec_helper.rb
|
113
|
+
- spec/support/test_cache.rb
|
114
|
+
homepage: https://github.com/modcloth/butcher
|
115
|
+
licenses: []
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project: butcher
|
134
|
+
rubygems_version: 1.8.17
|
135
|
+
signing_key:
|
136
|
+
specification_version: 3
|
137
|
+
summary: All the things to make a chef
|
138
|
+
test_files:
|
139
|
+
- features/stab.feature
|
140
|
+
- features/step_definitions/chef_steps.rb
|
141
|
+
- features/support/aruba.rb
|
142
|
+
- features/support/env.rb
|
143
|
+
- features/support/mocha.rb
|
144
|
+
- features/support/test_cache.rb
|
145
|
+
- spec/lib/cache_spec.rb
|
146
|
+
- spec/lib/stab/cli_spec.rb
|
147
|
+
- spec/spec_helper.rb
|
148
|
+
- spec/support/test_cache.rb
|