jgrevich-knife-solo 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/chef/knife/cook.rb +130 -0
- data/lib/chef/knife/kitchen.rb +29 -0
- data/lib/chef/knife/patches/parser.rb +211 -0
- data/lib/chef/knife/patches/search_patch.rb +81 -0
- data/lib/chef/knife/prepare.rb +39 -0
- data/lib/knife-solo.rb +1 -0
- data/lib/knife-solo/bootstraps.rb +95 -0
- data/lib/knife-solo/bootstraps/darwin.rb +38 -0
- data/lib/knife-solo/bootstraps/linux.rb +112 -0
- data/lib/knife-solo/info.rb +5 -0
- data/lib/knife-solo/kitchen_command.rb +29 -0
- data/lib/knife-solo/ssh_command.rb +233 -0
- data/lib/knife-solo/tools.rb +7 -0
- metadata +121 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
require 'chef/knife'
|
4
|
+
require 'chef/config'
|
5
|
+
require 'chef/cookbook/chefignore'
|
6
|
+
|
7
|
+
require 'knife-solo/ssh_command'
|
8
|
+
require 'knife-solo/kitchen_command'
|
9
|
+
require 'knife-solo/tools'
|
10
|
+
|
11
|
+
class Chef
|
12
|
+
class Knife
|
13
|
+
# Approach ported from spatula (https://github.com/trotter/spatula)
|
14
|
+
# Copyright 2009, Trotter Cashion
|
15
|
+
class Cook < Knife
|
16
|
+
include KnifeSolo::SshCommand
|
17
|
+
include KnifeSolo::KitchenCommand
|
18
|
+
include KnifeSolo::Tools
|
19
|
+
|
20
|
+
banner "knife cook [user@]hostname [json] (options)"
|
21
|
+
|
22
|
+
option :skip_chef_check,
|
23
|
+
:long => '--skip-chef-check',
|
24
|
+
:boolean => true,
|
25
|
+
:description => "Skip the version check on the Chef gem"
|
26
|
+
|
27
|
+
option :sync_only,
|
28
|
+
:long => '--sync-only',
|
29
|
+
:boolean => false,
|
30
|
+
:description => "Only sync the cookbook - do not run Chef"
|
31
|
+
|
32
|
+
option :skip_syntax_check,
|
33
|
+
:long => '--skip-syntax-check',
|
34
|
+
:boolean => true,
|
35
|
+
:description => "Skip Ruby syntax checks"
|
36
|
+
|
37
|
+
def run
|
38
|
+
super
|
39
|
+
check_syntax unless config[:skip_syntax_check]
|
40
|
+
Chef::Config.from_file('solo.rb')
|
41
|
+
check_chef_version unless config[:skip_chef_check]
|
42
|
+
rsync_kitchen
|
43
|
+
add_patches
|
44
|
+
cook unless config[:sync_only]
|
45
|
+
end
|
46
|
+
|
47
|
+
def check_syntax
|
48
|
+
ui.msg('Checking cookbook syntax...')
|
49
|
+
chefignore.remove_ignores_from(Dir["**/*.rb"]).each do |recipe|
|
50
|
+
ok = system "ruby -c #{recipe} >/dev/null 2>&1"
|
51
|
+
raise "Syntax error in #{recipe}" if not ok
|
52
|
+
end
|
53
|
+
|
54
|
+
chefignore.remove_ignores_from(Dir["**/*.json"]).each do |json|
|
55
|
+
begin
|
56
|
+
require 'json'
|
57
|
+
# parse without instantiating Chef classes
|
58
|
+
JSON.parse File.read(json), :create_additions => false
|
59
|
+
rescue => error
|
60
|
+
raise "Syntax error in #{json}: #{error.message}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def node_config
|
66
|
+
@name_args[1] || super
|
67
|
+
end
|
68
|
+
|
69
|
+
def chef_path
|
70
|
+
Chef::Config.file_cache_path
|
71
|
+
end
|
72
|
+
|
73
|
+
def chefignore
|
74
|
+
@chefignore ||= ::Chef::Cookbook::Chefignore.new("./")
|
75
|
+
end
|
76
|
+
|
77
|
+
# cygwin rsync path must be adjusted to work
|
78
|
+
def adjust_rsync_path(path)
|
79
|
+
return path unless windows_node?
|
80
|
+
path.gsub(/^(\w):/) { "/cygdrive/#{$1}" }
|
81
|
+
end
|
82
|
+
|
83
|
+
def patch_path
|
84
|
+
Array(Chef::Config.cookbook_path).first + "/chef_solo_patches/libraries"
|
85
|
+
end
|
86
|
+
|
87
|
+
def rsync_exclude
|
88
|
+
(%w{revision-deploys tmp '.*'} + chefignore.ignores).uniq
|
89
|
+
end
|
90
|
+
|
91
|
+
def rsync_kitchen
|
92
|
+
system! %Q{rsync -rl --rsh="ssh #{ssh_args}" --delete #{rsync_exclude.collect{ |ignore| "--exclude #{ignore} " }.join} ./ :#{adjust_rsync_path(chef_path)}}
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_patches
|
96
|
+
run_portable_mkdir_p(patch_path)
|
97
|
+
Dir[Pathname.new(__FILE__).dirname.join("patches", "*.rb")].each do |patch|
|
98
|
+
system! %Q{rsync -rl --rsh="ssh #{ssh_args}" #{patch} :#{adjust_rsync_path(patch_path)}}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def check_chef_version
|
103
|
+
constraint = "~>0.10.4"
|
104
|
+
result = run_command <<-BASH
|
105
|
+
opscode_ruby="/opt/opscode/embedded/bin/ruby"
|
106
|
+
|
107
|
+
if command -v $opscode_ruby &>/dev/null
|
108
|
+
then
|
109
|
+
ruby_bin=$opscode_ruby
|
110
|
+
else
|
111
|
+
ruby_bin="ruby"
|
112
|
+
fi
|
113
|
+
|
114
|
+
$ruby_bin -rubygems -e "gem 'chef', '#{constraint}'"
|
115
|
+
BASH
|
116
|
+
raise "The chef gem on #{host} is out of date. Please run `#{$0} prepare #{ssh_args}` to upgrade Chef to #{constraint}." unless result.success?
|
117
|
+
end
|
118
|
+
|
119
|
+
def cook
|
120
|
+
logging_arg = "-l debug" if config[:verbosity] > 0
|
121
|
+
|
122
|
+
stream_command <<-BASH
|
123
|
+
chef-solo -c #{chef_path}/solo.rb \
|
124
|
+
-j #{chef_path}/#{node_config} \
|
125
|
+
#{logging_arg}
|
126
|
+
BASH
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
|
3
|
+
class Chef
|
4
|
+
class Knife
|
5
|
+
class Kitchen < Knife
|
6
|
+
include FileUtils
|
7
|
+
|
8
|
+
banner "knife kitchen NAME"
|
9
|
+
|
10
|
+
def run
|
11
|
+
name = @name_args.first
|
12
|
+
mkdir name
|
13
|
+
%w(nodes roles data_bags site-cookbooks cookbooks).each do |dir|
|
14
|
+
mkdir name + "/#{dir}"
|
15
|
+
touch name + "/#{dir}/.gitkeep"
|
16
|
+
end
|
17
|
+
File.open(name + "/solo.rb", 'w') do |f|
|
18
|
+
f << <<-RUBY.gsub(/^ {12}/, '')
|
19
|
+
file_cache_path "/tmp/chef-solo"
|
20
|
+
data_bag_path "/tmp/chef-solo/data_bags"
|
21
|
+
cookbook_path [ "/tmp/chef-solo/site-cookbooks",
|
22
|
+
"/tmp/chef-solo/cookbooks" ]
|
23
|
+
role_path "/tmp/chef-solo/roles"
|
24
|
+
RUBY
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2011, edelight GmbH
|
3
|
+
#
|
4
|
+
# Authors:
|
5
|
+
# Markus Korn <markus.korn@edelight.de>
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'treetop'
|
21
|
+
require 'chef/solr_query/query_transform'
|
22
|
+
|
23
|
+
# mock QueryTransform such that we can access the location of the lucene grammar
|
24
|
+
class Chef
|
25
|
+
class SolrQuery
|
26
|
+
class QueryTransform
|
27
|
+
def self.base_path
|
28
|
+
class_variable_get(:@@base_path)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_flat_hash(hsh, prefix="")
|
35
|
+
result = {}
|
36
|
+
hsh.each_pair do |key, value|
|
37
|
+
if value.kind_of?(Hash)
|
38
|
+
result.merge!(build_flat_hash(value, "#{prefix}#{key}_"))
|
39
|
+
else
|
40
|
+
result[prefix+key] = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
module Lucene
|
47
|
+
|
48
|
+
class Term < Treetop::Runtime::SyntaxNode
|
49
|
+
# compares a query value and a value, tailing '*'-wildcards are handled correctly.
|
50
|
+
# Value can either be a string or an array, all other objects are converted
|
51
|
+
# to a string and than checked.
|
52
|
+
def match( value )
|
53
|
+
if value.is_a?(Array)
|
54
|
+
value.any?{ |x| self.match(x) }
|
55
|
+
else
|
56
|
+
File.fnmatch(self.text_value, value.to_s)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Field < Treetop::Runtime::SyntaxNode
|
62
|
+
# simple field -> value matches, supporting tailing '*'-wildcards in keys
|
63
|
+
# as well as in values
|
64
|
+
def match( item )
|
65
|
+
keys = self.elements[0].match(item)
|
66
|
+
if keys.nil?
|
67
|
+
false
|
68
|
+
else
|
69
|
+
keys.any?{ |key| self.elements[1].match(item[key]) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# we don't support range matches
|
75
|
+
# range of integers would be easy to implement
|
76
|
+
# but string ranges are hard
|
77
|
+
class FiledRange < Treetop::Runtime::SyntaxNode
|
78
|
+
end
|
79
|
+
|
80
|
+
class InclFieldRange < FieldRange
|
81
|
+
end
|
82
|
+
|
83
|
+
class ExclFieldRange < FieldRange
|
84
|
+
end
|
85
|
+
|
86
|
+
class RangeValue < Treetop::Runtime::SyntaxNode
|
87
|
+
end
|
88
|
+
|
89
|
+
class FieldName < Treetop::Runtime::SyntaxNode
|
90
|
+
def match( item )
|
91
|
+
if self.text_value.count("_") > 0
|
92
|
+
item.merge!(build_flat_hash(item))
|
93
|
+
end
|
94
|
+
if self.text_value.end_with?("*")
|
95
|
+
part = self.text_value.chomp("*")
|
96
|
+
item.keys.collect{ |key| key.start_with?(part)? key: nil}.compact
|
97
|
+
else
|
98
|
+
if item.has_key?(self.text_value)
|
99
|
+
[self.text_value,]
|
100
|
+
else
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Body < Treetop::Runtime::SyntaxNode
|
108
|
+
def match( item )
|
109
|
+
self.elements[0].match( item )
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Group < Treetop::Runtime::SyntaxNode
|
114
|
+
def match( item )
|
115
|
+
self.elements[0].match(item)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class BinaryOp < Treetop::Runtime::SyntaxNode
|
120
|
+
def match( item )
|
121
|
+
self.elements[1].match(
|
122
|
+
self.elements[0].match(item),
|
123
|
+
self.elements[2].match(item)
|
124
|
+
)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class OrOperator < Treetop::Runtime::SyntaxNode
|
129
|
+
def match( cond1, cond2 )
|
130
|
+
cond1 or cond2
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class AndOperator < Treetop::Runtime::SyntaxNode
|
135
|
+
def match( cond1, cond2 )
|
136
|
+
cond1 and cond2
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# we don't support fuzzy string matching
|
141
|
+
class FuzzyOp < Treetop::Runtime::SyntaxNode
|
142
|
+
end
|
143
|
+
|
144
|
+
class BoostOp < Treetop::Runtime::SyntaxNode
|
145
|
+
end
|
146
|
+
|
147
|
+
class FuzzyParam < Treetop::Runtime::SyntaxNode
|
148
|
+
end
|
149
|
+
|
150
|
+
class UnaryOp < Treetop::Runtime::SyntaxNode
|
151
|
+
def match( item )
|
152
|
+
self.elements[0].match(
|
153
|
+
self.elements[1].match(item)
|
154
|
+
)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class NotOperator < Treetop::Runtime::SyntaxNode
|
159
|
+
def match( cond )
|
160
|
+
not cond
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class RequiredOperator < Treetop::Runtime::SyntaxNode
|
165
|
+
end
|
166
|
+
|
167
|
+
class ProhibitedOperator < Treetop::Runtime::SyntaxNode
|
168
|
+
end
|
169
|
+
|
170
|
+
class Phrase < Treetop::Runtime::SyntaxNode
|
171
|
+
# a quoted ::Term
|
172
|
+
def match( value )
|
173
|
+
self.elements[0].match(value)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
class Query
|
179
|
+
# initialize the parser by using the grammar shipped with chef
|
180
|
+
@@grammar = File.join(Chef::SolrQuery::QueryTransform.base_path, "lucene.treetop")
|
181
|
+
Treetop.load(@@grammar)
|
182
|
+
@@parser = LuceneParser.new
|
183
|
+
|
184
|
+
def self.parse(data)
|
185
|
+
# parse the query into a query tree
|
186
|
+
if data.nil?
|
187
|
+
data = "*:*"
|
188
|
+
end
|
189
|
+
tree = @@parser.parse(data)
|
190
|
+
if tree.nil?
|
191
|
+
msg = "Parse error at offset: #{@@parser.index}\n"
|
192
|
+
msg += "Reason: #{@@parser.failure_reason}"
|
193
|
+
raise "Query #{data} is not supported: #{msg}"
|
194
|
+
end
|
195
|
+
self.clean_tree(tree)
|
196
|
+
tree
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def self.clean_tree(root_node)
|
202
|
+
# remove all SyntaxNode elements from the tree, we don't need them as
|
203
|
+
# the related ruby class already knowns what to do.
|
204
|
+
return if root_node.elements.nil?
|
205
|
+
root_node.elements.delete_if do |node|
|
206
|
+
node.class.name == "Treetop::Runtime::SyntaxNode"
|
207
|
+
end
|
208
|
+
root_node.elements.each { |node| self.clean_tree(node) }
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#
|
2
|
+
# Copyright 2011, edelight GmbH
|
3
|
+
#
|
4
|
+
# Authors:
|
5
|
+
# Markus Korn <markus.korn@edelight.de>
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
if Chef::Config[:solo]
|
21
|
+
|
22
|
+
if (defined? require_relative).nil?
|
23
|
+
# defenition of 'require_relative' for ruby < 1.9, found on stackoverflow.com
|
24
|
+
def require_relative(relative_feature)
|
25
|
+
c = caller.first
|
26
|
+
fail "Can't parse #{c}" unless c.rindex(/:\d+(:in `.*')?$/)
|
27
|
+
file = $`
|
28
|
+
if /\A\((.*)\)/ =~ file # eval, etc.
|
29
|
+
raise LoadError, "require_relative is called in #{$1}"
|
30
|
+
end
|
31
|
+
absolute = File.expand_path(relative_feature, File.dirname(file))
|
32
|
+
require absolute
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
require_relative 'parser.rb'
|
37
|
+
|
38
|
+
class Chef
|
39
|
+
class Recipe
|
40
|
+
|
41
|
+
# Overwrite the search method of recipes to operate locally by using
|
42
|
+
# data found in data_bags.
|
43
|
+
# Only very basic lucene syntax is supported and also sorting the result
|
44
|
+
# is not implemented, if this search method does not support a given query
|
45
|
+
# an exception is raised.
|
46
|
+
# This search() method returns a block iterator or an Array, depending
|
47
|
+
# on how this method is called.
|
48
|
+
def search(bag_name, query=nil, sort=nil, start=0, rows=1000, &block)
|
49
|
+
if !sort.nil?
|
50
|
+
raise "Sorting search results is not supported"
|
51
|
+
end
|
52
|
+
@_query = Query.parse(query)
|
53
|
+
if @_query.nil?
|
54
|
+
raise "Query #{query} is not supported"
|
55
|
+
end
|
56
|
+
if block_given?
|
57
|
+
pos = 0
|
58
|
+
else
|
59
|
+
result = []
|
60
|
+
end
|
61
|
+
data_bag(bag_name.to_s).each do |bag_item_id|
|
62
|
+
bag_item = data_bag_item(bag_name.to_s, bag_item_id)
|
63
|
+
if @_query.match(bag_item)
|
64
|
+
if block_given?
|
65
|
+
if (pos >= start and pos < (start + rows))
|
66
|
+
yield bag_item
|
67
|
+
end
|
68
|
+
pos += 1
|
69
|
+
else
|
70
|
+
result << bag_item
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
if !block_given?
|
75
|
+
return result.slice(start, rows)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
require 'knife-solo/ssh_command'
|
3
|
+
require 'knife-solo/kitchen_command'
|
4
|
+
require 'knife-solo/bootstraps'
|
5
|
+
|
6
|
+
class Chef
|
7
|
+
class Knife
|
8
|
+
# Approach ported from littlechef (https://github.com/tobami/littlechef)
|
9
|
+
# Copyright 2010, 2011, Miquel Torres <tobami@googlemail.com>
|
10
|
+
class Prepare < Knife
|
11
|
+
include KnifeSolo::SshCommand
|
12
|
+
include KnifeSolo::KitchenCommand
|
13
|
+
|
14
|
+
banner "knife prepare [user@]hostname (options)"
|
15
|
+
|
16
|
+
def run
|
17
|
+
super
|
18
|
+
bootstrap.bootstrap!
|
19
|
+
generate_node_config
|
20
|
+
end
|
21
|
+
|
22
|
+
def bootstrap
|
23
|
+
KnifeSolo::Bootstraps.class_for_operating_system(operating_system()).new(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_node_config
|
27
|
+
File.open(node_config, 'w') do |f|
|
28
|
+
f.print <<-JSON.gsub(/^\s+/, '')
|
29
|
+
{ "run_list": [] }
|
30
|
+
JSON
|
31
|
+
end unless node_config.exist?
|
32
|
+
end
|
33
|
+
|
34
|
+
def operating_system
|
35
|
+
@operating_system ||= run_command('uname -s').stdout.strip
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/knife-solo.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'knife-solo/info'
|
@@ -0,0 +1,95 @@
|
|
1
|
+
class OperatingSystemNotSupportedError < StandardError ; end
|
2
|
+
|
3
|
+
module KnifeSolo
|
4
|
+
module Bootstraps
|
5
|
+
class OperatingSystemNotImplementedError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.class_exists_for?(os_name)
|
9
|
+
begin
|
10
|
+
true if self.class_for_operating_system(os_name).class == Class
|
11
|
+
rescue => exception
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.class_for_operating_system(os_name)
|
17
|
+
begin
|
18
|
+
os_class_name = os_name.gsub(/\s/,'')
|
19
|
+
eval("KnifeSolo::Bootstraps::#{os_class_name}")
|
20
|
+
rescue
|
21
|
+
raise OperatingSystemNotImplementedError.new("#{os_name} not implemented. Feel free to add a bootstrap implementation in KnifeSolo::Bootstraps::#{os_class_name}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module Delegates
|
26
|
+
def run_command(cmd)
|
27
|
+
prepare.run_command(cmd)
|
28
|
+
end
|
29
|
+
|
30
|
+
def ui
|
31
|
+
prepare.ui
|
32
|
+
end
|
33
|
+
|
34
|
+
def prepare
|
35
|
+
@prepare
|
36
|
+
end
|
37
|
+
end #Delegates
|
38
|
+
|
39
|
+
module InstallCommands
|
40
|
+
|
41
|
+
def bootstrap!
|
42
|
+
run_pre_bootstrap_checks()
|
43
|
+
send("#{distro[:type]}_install")
|
44
|
+
end
|
45
|
+
|
46
|
+
def distro
|
47
|
+
raise "implement distro detection for #{self.class.name}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def gem_packages
|
51
|
+
raise "implement gem packages for #{self.class.name}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def http_client_get_url(url)
|
55
|
+
"wget #{url}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def omnibus_install
|
59
|
+
run_command(http_client_get_url("http://opscode.com/chef/install.sh"))
|
60
|
+
run_command("sudo bash install.sh")
|
61
|
+
end
|
62
|
+
|
63
|
+
def gem_install
|
64
|
+
ui.msg "Installing rubygems from source..."
|
65
|
+
release = "rubygems-1.8.10"
|
66
|
+
file = "#{release}.tgz"
|
67
|
+
url = "http://production.cf.rubygems.org/rubygems/#{file}"
|
68
|
+
run_command(http_client_get_url(url))
|
69
|
+
run_command("tar zxf #{file}")
|
70
|
+
run_command("sudo ruby #{release}/setup.rb --no-format-executable")
|
71
|
+
run_command("sudo rm -rf #{release} #{file}")
|
72
|
+
run_command("sudo gem install --no-rdoc --no-ri #{gem_packages().join(' ')}")
|
73
|
+
end
|
74
|
+
end #InstallCommands
|
75
|
+
|
76
|
+
class Base
|
77
|
+
include KnifeSolo::Bootstraps::Delegates
|
78
|
+
include KnifeSolo::Bootstraps::InstallCommands
|
79
|
+
|
80
|
+
def initialize(prepare)
|
81
|
+
# instance of Chef::Knife::Prepare
|
82
|
+
@prepare = prepare
|
83
|
+
end
|
84
|
+
|
85
|
+
def run_pre_bootstrap_checks ; end
|
86
|
+
# run right before we run #{distro[:type]}_install method
|
87
|
+
# barf out here if need be
|
88
|
+
end
|
89
|
+
|
90
|
+
end # Bootstraps
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# bootstrap classes for different OSes
|
95
|
+
Dir[File.dirname(__FILE__) + '/bootstraps/*.rb'].each {|p| require p}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module KnifeSolo::Bootstraps
|
2
|
+
class Darwin < Base
|
3
|
+
|
4
|
+
def issue
|
5
|
+
run_command("sw_vers -productVersion").stdout.strip
|
6
|
+
end
|
7
|
+
|
8
|
+
def gem_packages
|
9
|
+
['chef']
|
10
|
+
end
|
11
|
+
|
12
|
+
def distro
|
13
|
+
case issue
|
14
|
+
when %r{10.5}
|
15
|
+
{:type => 'gem', :version => 'leopard'}
|
16
|
+
when %r{10.6}
|
17
|
+
{:type => 'gem', :version => 'snow_leopard'}
|
18
|
+
else
|
19
|
+
raise "OSX version #{issue} not supported"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_xcode_installed?
|
24
|
+
result = run_command("xcodebuild -version")
|
25
|
+
result.success?
|
26
|
+
end
|
27
|
+
|
28
|
+
def http_client_get_url(url)
|
29
|
+
filename = url.split("/").last
|
30
|
+
"curl '#{url}' >> #{filename}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def run_pre_bootstrap_checks
|
34
|
+
raise 'xcode not installed, which is required to do anything. please install and run again.' unless has_xcode_installed?
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module KnifeSolo::Bootstraps
|
2
|
+
class Linux < Base
|
3
|
+
|
4
|
+
def issue
|
5
|
+
prepare.run_command("cat /etc/issue").stdout.strip || perepare.run_command("lsb_release -d -s").stdout.strip
|
6
|
+
end
|
7
|
+
|
8
|
+
def package_list
|
9
|
+
@packages.join(' ')
|
10
|
+
end
|
11
|
+
|
12
|
+
def gem_packages
|
13
|
+
['ruby-shadow','chef']
|
14
|
+
end
|
15
|
+
|
16
|
+
def http_client_get_url(url)
|
17
|
+
"wget #{url}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def zypper_gem_install
|
21
|
+
ui.msg("Installing required packages...")
|
22
|
+
run_command("sudo zypper --non-interactive install ruby-devel make gcc rsync")
|
23
|
+
gem_install
|
24
|
+
end
|
25
|
+
|
26
|
+
def emerge_gem_install
|
27
|
+
ui.msg("Installing required packages...")
|
28
|
+
run_command("sudo USE='-test' ACCEPT_KEYWORDS='~amd64' emerge -u chef")
|
29
|
+
gem_install
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_yum_repos(repo_path)
|
33
|
+
repo_url = "http://rbel.co/"
|
34
|
+
|
35
|
+
tmp_file = "/tmp/rbel"
|
36
|
+
installed = "is already installed"
|
37
|
+
run_command("sudo yum -y install curl")
|
38
|
+
run_command("curl #{repo_url}#{repo_path} -o #{tmp_file}")
|
39
|
+
result = run_command("sudo rpm -Uvh #{tmp_file} && rm #{tmp_file}")
|
40
|
+
raise result.stderr_or_stdout unless result.success? || result.stdout.match(installed)
|
41
|
+
end
|
42
|
+
|
43
|
+
def yum_install
|
44
|
+
ui.msg("Installing required packages...")
|
45
|
+
|
46
|
+
if distro[:version] == "RHEL5"
|
47
|
+
repo_path = "rbel5"
|
48
|
+
else
|
49
|
+
repo_path = "rbel6"
|
50
|
+
end
|
51
|
+
|
52
|
+
add_yum_repos(repo_path)
|
53
|
+
@packages = %w(rubygem-chef rsync)
|
54
|
+
run_command("sudo yum -y --disablerepo=* --enablerepo=#{repo_path} install #{package_list}")
|
55
|
+
end
|
56
|
+
|
57
|
+
def debian_gem_install
|
58
|
+
ui.msg "Updating apt caches..."
|
59
|
+
run_command("sudo apt-get update")
|
60
|
+
|
61
|
+
ui.msg "Installing required packages..."
|
62
|
+
@packages = %w(ruby ruby-dev libopenssl-ruby irb
|
63
|
+
build-essential wget ssl-cert rsync)
|
64
|
+
run_command <<-BASH
|
65
|
+
sudo DEBIAN_FRONTEND=noninteractive apt-get --yes install #{package_list}
|
66
|
+
BASH
|
67
|
+
|
68
|
+
gem_install
|
69
|
+
end
|
70
|
+
|
71
|
+
def distro
|
72
|
+
return @distro if @distro
|
73
|
+
@distro = case issue
|
74
|
+
when %r{Debian GNU/Linux 5}
|
75
|
+
{:type => "omnibus", :version => "lenny"}
|
76
|
+
when %r{Debian GNU/Linux 6}
|
77
|
+
{:type => "omnibus", :version => "squeeze"}
|
78
|
+
when %r{Debian GNU/Linux wheezy}
|
79
|
+
{:type => "debian_gem", :version => "wheezy"}
|
80
|
+
when %r{Ubuntu}
|
81
|
+
version = run_command("lsb_release -cs").stdout.strip
|
82
|
+
{:type => "debian_gem", :version => version}
|
83
|
+
when %r{Linaro}
|
84
|
+
version = run_command("lsb_release -cs").stdout.strip
|
85
|
+
{:type => "debian_gem", :version => version}
|
86
|
+
when %r{CentOS.*? 5}
|
87
|
+
{:type => "omnibus", :version => "RHEL5"}
|
88
|
+
when %r{CentOS.*? 6}
|
89
|
+
{:type => "omnibus", :version => "RHEL6"}
|
90
|
+
when %r{Red Hat Enterprise.*? 5}
|
91
|
+
{:type => "omnibus", :version => "RHEL5"}
|
92
|
+
when %r{Red Hat Enterprise.*? 6}
|
93
|
+
{:type => "omnibus", :version => "RHEL6"}
|
94
|
+
when %r{Scientific Linux.*? 5}
|
95
|
+
{:type => "omnibus", :version => "RHEL5"}
|
96
|
+
when %r{Scientific Linux.*? 6}
|
97
|
+
{:type => "omnibus", :version => "RHEL6"}
|
98
|
+
when %r{SUSE Linux Enterprise Server 11 SP1}
|
99
|
+
{:type => "zypper_gem", :version => "SLES11"}
|
100
|
+
when %r{openSUSE 11.4}
|
101
|
+
{:type => "zypper_gem", :version => "openSUSE"}
|
102
|
+
when %r{This is \\n\.\\O \(\\s \\m \\r\) \\t}
|
103
|
+
{:type => "emerge_gem", :version => "Gentoo"}
|
104
|
+
else
|
105
|
+
raise "Distro not recognized from looking at /etc/issue. Please fork https://github.com/matschaffer/knife-solo and add support for your distro."
|
106
|
+
end
|
107
|
+
Chef::Log.debug("Distro detection yielded: #{@distro}")
|
108
|
+
@distro
|
109
|
+
end #issue
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module KnifeSolo
|
2
|
+
module KitchenCommand
|
3
|
+
class OutOfKitchenError < StandardError
|
4
|
+
def message
|
5
|
+
"This command must be run inside a Chef solo kitchen."
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.required_directories
|
10
|
+
%w(nodes roles cookbooks data_bags site-cookbooks)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.required_files
|
14
|
+
%w(solo.rb)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.all_requirements
|
18
|
+
required_files + required_directories
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
raise OutOfKitchenError.new unless required_files_present?
|
23
|
+
end
|
24
|
+
|
25
|
+
def required_files_present?
|
26
|
+
KitchenCommand.all_requirements.inject(true) { |m, f| m && File.exists?(f) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module KnifeSolo
|
4
|
+
module SshCommand
|
5
|
+
def self.included(other)
|
6
|
+
other.instance_eval do
|
7
|
+
deps do
|
8
|
+
require 'net/ssh'
|
9
|
+
end
|
10
|
+
|
11
|
+
option :ssh_config,
|
12
|
+
:short => "-F CONFIG_FILE",
|
13
|
+
:long => "--ssh-config-file CONFIG_FILE",
|
14
|
+
:description => "Alternate location for ssh config file"
|
15
|
+
|
16
|
+
option :ssh_password,
|
17
|
+
:short => "-P PASSWORD",
|
18
|
+
:long => "--ssh-password PASSWORD",
|
19
|
+
:description => "The ssh password"
|
20
|
+
|
21
|
+
option :ssh_identity,
|
22
|
+
:short => "-i FILE",
|
23
|
+
:long => "--ssh-identity FILE",
|
24
|
+
:description => "The ssh identity file"
|
25
|
+
|
26
|
+
option :ssh_port,
|
27
|
+
:short => "-p PORT",
|
28
|
+
:long => "--ssh-port PORT",
|
29
|
+
:description => "The ssh port"
|
30
|
+
|
31
|
+
option :startup_script,
|
32
|
+
:short => "-s FILE",
|
33
|
+
:long => "--startup-script FILE",
|
34
|
+
:description => "The startup script on the remote server containing variable definitions"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def node_config
|
39
|
+
Pathname.new("nodes/#{host}.json")
|
40
|
+
end
|
41
|
+
|
42
|
+
def host_descriptor
|
43
|
+
return @host_descriptor if @host_descriptor
|
44
|
+
parts = @name_args.first.split('@')
|
45
|
+
@host_descriptor = {
|
46
|
+
:host => parts.pop,
|
47
|
+
:user => parts.pop
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def user
|
52
|
+
host_descriptor[:user] || config_file_options[:user] || ENV['USER']
|
53
|
+
end
|
54
|
+
|
55
|
+
def host
|
56
|
+
host_descriptor[:host]
|
57
|
+
end
|
58
|
+
|
59
|
+
def ask_password
|
60
|
+
ui.ask("Enter the password for #{user}@#{host}: ") do |q|
|
61
|
+
q.echo = false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def password
|
66
|
+
config[:ssh_password] ||= ask_password
|
67
|
+
end
|
68
|
+
|
69
|
+
def try_connection
|
70
|
+
Net::SSH.start(host, user, connection_options) do |ssh|
|
71
|
+
ssh.exec!("true")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def config_file_options
|
76
|
+
Net::SSH::Config.for(host, config_files)
|
77
|
+
end
|
78
|
+
|
79
|
+
def connection_options
|
80
|
+
options = config_file_options
|
81
|
+
options[:port] = config[:ssh_port] if config[:ssh_port]
|
82
|
+
options[:password] = config[:ssh_password] if config[:ssh_password]
|
83
|
+
options[:keys] = [config[:ssh_identity]] if config[:ssh_identity]
|
84
|
+
options
|
85
|
+
end
|
86
|
+
|
87
|
+
def config_files
|
88
|
+
Array(config[:ssh_config] || Net::SSH::Config.default_files)
|
89
|
+
end
|
90
|
+
|
91
|
+
def detect_authentication_method
|
92
|
+
return @detected if @detected
|
93
|
+
begin
|
94
|
+
try_connection
|
95
|
+
rescue Errno::ETIMEDOUT
|
96
|
+
raise "Unable to connect to #{host}"
|
97
|
+
rescue Net::SSH::AuthenticationFailed
|
98
|
+
# Ensure the password is set or ask for it immediately
|
99
|
+
password
|
100
|
+
end
|
101
|
+
@detected = true
|
102
|
+
end
|
103
|
+
|
104
|
+
def ssh_args
|
105
|
+
host_arg = [user, host].compact.join('@')
|
106
|
+
config_arg = "-F #{config[:ssh_config]}" if config[:ssh_config]
|
107
|
+
ident_arg = "-i #{config[:ssh_identity]}" if config[:ssh_identity]
|
108
|
+
port_arg = "-p #{config[:ssh_port]}" if config[:ssh_port]
|
109
|
+
|
110
|
+
[host_arg, config_arg, ident_arg, port_arg].compact.join(' ')
|
111
|
+
end
|
112
|
+
|
113
|
+
def startup_script
|
114
|
+
config[:startup_script]
|
115
|
+
end
|
116
|
+
|
117
|
+
class ExecResult
|
118
|
+
attr_accessor :stdout, :stderr, :exit_code
|
119
|
+
|
120
|
+
def initialize
|
121
|
+
@stdout = ""
|
122
|
+
@stderr = ""
|
123
|
+
end
|
124
|
+
|
125
|
+
def success?
|
126
|
+
exit_code == 0
|
127
|
+
end
|
128
|
+
|
129
|
+
# Helper to use when raising exceptions since some operations
|
130
|
+
# (e.g., command not found) error on stdout
|
131
|
+
def stderr_or_stdout
|
132
|
+
return stderr unless stderr.empty?
|
133
|
+
stdout
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def windows_node?
|
138
|
+
return @windows_node unless @windows_node.nil?
|
139
|
+
@windows_node = run_command('ver', :process_sudo => false).stdout =~ /Windows/i
|
140
|
+
Chef::Log.debug("Windows node detected") if @windows_node
|
141
|
+
@windows_node
|
142
|
+
end
|
143
|
+
|
144
|
+
def sudo_available?
|
145
|
+
return @sudo_available unless @sudo_available.nil?
|
146
|
+
@sudo_available = run_command('sudo -V', :process_sudo => false).success?
|
147
|
+
Chef::Log.debug("`sudo` not available on #{host}") unless @sudo_available
|
148
|
+
@sudo_available
|
149
|
+
end
|
150
|
+
|
151
|
+
def process_sudo(command)
|
152
|
+
if sudo_available?
|
153
|
+
replacement = 'sudo -p \'knife sudo password: \''
|
154
|
+
else
|
155
|
+
replacement = ''
|
156
|
+
end
|
157
|
+
command.sub(/^\s*sudo/, replacement)
|
158
|
+
end
|
159
|
+
|
160
|
+
def process_startup_file(command)
|
161
|
+
command.insert(0, "source #{startup_script} && ")
|
162
|
+
end
|
163
|
+
|
164
|
+
def stream_command(command)
|
165
|
+
run_command(command, :streaming => true)
|
166
|
+
end
|
167
|
+
|
168
|
+
def processed_command(command, options = {})
|
169
|
+
command = process_sudo(command) if options[:process_sudo]
|
170
|
+
command = process_startup_file(command) if startup_script
|
171
|
+
command
|
172
|
+
end
|
173
|
+
|
174
|
+
def run_command(command, options={})
|
175
|
+
defaults = {:process_sudo => true}
|
176
|
+
options = defaults.merge(options)
|
177
|
+
|
178
|
+
detect_authentication_method
|
179
|
+
|
180
|
+
Chef::Log.debug("Initial command #{command}")
|
181
|
+
result = ExecResult.new
|
182
|
+
|
183
|
+
command = processed_command(command, options)
|
184
|
+
Chef::Log.debug("Running processed command #{command}")
|
185
|
+
|
186
|
+
Net::SSH.start(host, user, connection_options) do |ssh|
|
187
|
+
ssh.open_channel do |channel|
|
188
|
+
channel.request_pty
|
189
|
+
channel.exec(command) do |ch, success|
|
190
|
+
raise "ssh.channel.exec failure" unless success
|
191
|
+
|
192
|
+
channel.on_data do |ch, data| # stdout
|
193
|
+
if data =~ /^knife sudo password: /
|
194
|
+
ch.send_data("#{password}\n")
|
195
|
+
else
|
196
|
+
Chef::Log.debug("#{command} stdout: #{data}")
|
197
|
+
ui.stdout << data if options[:streaming]
|
198
|
+
result.stdout << data
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
channel.on_extended_data do |ch, type, data|
|
203
|
+
next unless type == 1
|
204
|
+
Chef::Log.debug("#{command} stderr: #{data}")
|
205
|
+
ui.stderr << data if options[:streaming]
|
206
|
+
result.stderr << data
|
207
|
+
end
|
208
|
+
|
209
|
+
channel.on_request("exit-status") do |ch, data|
|
210
|
+
result.exit_code = data.read_long
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
ssh.loop
|
215
|
+
end
|
216
|
+
end
|
217
|
+
result
|
218
|
+
end
|
219
|
+
|
220
|
+
# TODO:
|
221
|
+
# - move this to a dedicated "portability" module?
|
222
|
+
# - use ruby in all cases instead?
|
223
|
+
def run_portable_mkdir_p(folder)
|
224
|
+
if windows_node?
|
225
|
+
# no mkdir -p on windows - fake it
|
226
|
+
run_command %Q{ruby -e "require 'fileutils'; FileUtils.mkdir_p('#{folder}')"}
|
227
|
+
else
|
228
|
+
run_command "mkdir -p #{folder}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
end
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jgrevich-knife-solo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mat Schaffer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-29 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: mocha
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: chef
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.10.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.10.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: net-ssh
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 2.1.3
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 2.1.3
|
78
|
+
description: Handles bootstrapping, running chef solo, rsyncing cookbooks etc
|
79
|
+
email: mat@schaffer.me
|
80
|
+
executables: []
|
81
|
+
extensions: []
|
82
|
+
extra_rdoc_files: []
|
83
|
+
files:
|
84
|
+
- lib/chef/knife/cook.rb
|
85
|
+
- lib/chef/knife/kitchen.rb
|
86
|
+
- lib/chef/knife/patches/parser.rb
|
87
|
+
- lib/chef/knife/patches/search_patch.rb
|
88
|
+
- lib/chef/knife/prepare.rb
|
89
|
+
- lib/knife-solo/bootstraps/darwin.rb
|
90
|
+
- lib/knife-solo/bootstraps/linux.rb
|
91
|
+
- lib/knife-solo/bootstraps.rb
|
92
|
+
- lib/knife-solo/info.rb
|
93
|
+
- lib/knife-solo/kitchen_command.rb
|
94
|
+
- lib/knife-solo/ssh_command.rb
|
95
|
+
- lib/knife-solo/tools.rb
|
96
|
+
- lib/knife-solo.rb
|
97
|
+
homepage: https://github.com/matschaffer/knife-solo
|
98
|
+
licenses: []
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ! '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ! '>='
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubyforge_project: nowarning
|
117
|
+
rubygems_version: 1.8.24
|
118
|
+
signing_key:
|
119
|
+
specification_version: 3
|
120
|
+
summary: A collection of knife plugins for dealing with chef solo
|
121
|
+
test_files: []
|