jgrevich-knife-solo 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module KnifeSolo
2
+ def self.version
3
+ '0.1.1'
4
+ end
5
+ 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
@@ -0,0 +1,7 @@
1
+ module KnifeSolo
2
+ module Tools
3
+ def system!(command)
4
+ raise "Failed to launch command #{command}" unless system(command)
5
+ end
6
+ end
7
+ 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: []