butcher 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.
- 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
|