iron_chef 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MzczZmZjMTgxNjNlMWQwOGViOGMzNjkxOWFmNmMyNTMyM2FiNTQ3Mw==
5
+ data.tar.gz: !binary |-
6
+ MmQ2MzQ0ZWIwMjE5YmE3MTYxY2YyMDVjYTQ5OTQ4NDk4Y2IwN2QzMw==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YWUxNjA3OTUyYWI5NGZjYTc2ODhkMzJiZDQ3NDgwM2MyNDY5MDNkYWNmNTVl
10
+ YzAyZTZmNmVkM2RkYmVhODc4NGEyZWVhOGM1NDRhODJlYTgxOWJlNjllYWYw
11
+ OGUxZjQ5MTczOTU2ZTQ5NTBjMWRmM2ZhZTgwYTY0YWNiNWMyZjU=
12
+ data.tar.gz: !binary |-
13
+ ZmUxNzA0ZDdhNGMxODdmYWYzMjVlYTJmNzZjMDU5MWVhN2ZhZDI1MzA0ODAz
14
+ YWZlOWI3YmE3OGI1ZTkwOGZiNjYyOGUzOTQ4ZjkxMmNkZTk0NmIzMzdiMDFl
15
+ NmJiZWJiMjJhZDI4YTc0M2Y1Y2MxYTYxODI0MDAyMzU3ZThmYzU=
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :test do
4
+ gem 'rake'
5
+ end
6
+
7
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,33 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ iron_chef (0.0.10)
5
+ capistrano (>= 2.15.4)
6
+ json
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ capistrano (2.15.4)
12
+ highline
13
+ net-scp (>= 1.0.0)
14
+ net-sftp (>= 2.0.0)
15
+ net-ssh (>= 2.0.14)
16
+ net-ssh-gateway (>= 1.1.0)
17
+ highline (1.6.19)
18
+ json (1.8.0)
19
+ net-scp (1.1.1)
20
+ net-ssh (>= 2.6.5)
21
+ net-sftp (2.1.2)
22
+ net-ssh (>= 2.6.5)
23
+ net-ssh (2.6.7)
24
+ net-ssh-gateway (1.2.0)
25
+ net-ssh (>= 2.6.5)
26
+ rake (10.1.0)
27
+
28
+ PLATFORMS
29
+ ruby
30
+
31
+ DEPENDENCIES
32
+ iron_chef!
33
+ rake
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.test_files = FileList['test/*_test.rb']
5
+ end
6
+
7
+ task :default => :test
8
+
9
+ desc "clean"
10
+ task :clean do
11
+ rm_f Dir.glob("*.gem")
12
+ end
13
+
14
+ namespace :gem do
15
+ desc "build the gem"
16
+ task :build => :clean do
17
+ sh "gem build iron_chef.gemspec"
18
+ end
19
+
20
+ desc "push the gem"
21
+ task :push => :build do
22
+ sh "gem push #{Dir.glob("*.gem").first}"
23
+ end
24
+ end
data/bin/ironchef ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'fileutils'
5
+
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: #{File.basename($0)} [project-name-devops]"
8
+
9
+ opts.on("-h", "--help", "Displays this help info") do
10
+ puts opts
11
+ exit 0
12
+ end
13
+
14
+ begin
15
+ opts.parse!(ARGV)
16
+ rescue OptionParser::ParseError => e
17
+ warn e.message
18
+ puts opts
19
+ exit 1
20
+ end
21
+ end
22
+
23
+
24
+ if ARGV.empty?
25
+ abort "Please specify the DevOps project directory , e.g. '#{File.basename($0)}-devops'"
26
+ elsif ARGV.length > 1
27
+ abort "Too many arguments; please specify only the DevOps project directory."
28
+ end
29
+
30
+ def unindent(string)
31
+ indentation = string[/\A\s*/]
32
+ string.strip.gsub(/^#{indentation}/, "")
33
+ end
34
+
35
+ base = ARGV.shift
36
+
37
+ FileUtils.mkdir_p(base)
38
+
39
+ FileUtils.mkdir_p("#{base}/environments")
40
+ FileUtils.mkdir_p("#{base}/nodes")
41
+ FileUtils.mkdir_p("#{base}/data_bags/environments")
42
+
43
+ files = {
44
+ ".gitignore" => unindent(<<-FILE),
45
+ /cookbooks
46
+ /tmp
47
+
48
+ FILE
49
+
50
+ "Gemfile" => unindent(<<-FILE),
51
+ source 'https://rubygems.org'
52
+
53
+ gem 'iron_chef'
54
+
55
+ FILE
56
+
57
+ "Capfile" => unindent(<<-FILE),
58
+ require 'iron_chef'
59
+ load 'config/deploy'
60
+
61
+ FILE
62
+
63
+ "config/deploy.rb" => unindent(<<-FILE),
64
+ set :chef_environments, %w(staging production)
65
+
66
+ # don't forget to run locally
67
+ # chmod 600 /your-full/local-path-to-ssh-key/xxxxx_keypair_ec2.pem
68
+ # ssh-add /your-full/local-path-to-ssh-key/xxxxx_keypair_ec2.pem
69
+
70
+ ssh_options[:keys] = File.expand_path('~/your-local-path-to-ssh-key/xxxxx_keypair_ec2.pem')
71
+ default_run_options[:pty] = true
72
+
73
+ set :user, 'ec2-user'
74
+ set :use_sudo, false
75
+
76
+ FILE
77
+
78
+ "roles/base_server.rb" => unindent(<<-FILE),
79
+ name "base_server"
80
+ description "Base Server Role"
81
+ # run_list
82
+ # default_attributes
83
+ FILE
84
+
85
+ "roles/app_server.rb" => unindent(<<-FILE),
86
+ name "app_server"
87
+ description "Application Server Role"
88
+ # run_list
89
+ # default_attributes
90
+ FILE
91
+
92
+ "roles/db_server.rb" => unindent(<<-FILE),
93
+ name "db_server"
94
+ description "Database Server Role"
95
+ # run_list
96
+ # default_attributes
97
+ FILE
98
+
99
+ "roles/web_server.rb" => unindent(<<-FILE),
100
+ name "web_server"
101
+ description "Web Server Role"
102
+ # run_list
103
+ # default_attributes
104
+ FILE
105
+
106
+ "cookbooks/.gitkeep" => '',
107
+
108
+ "site-cookbooks/commons/recipes/default.rb" => unindent(<<-FILE),
109
+ # retrieve environment specific data bag item
110
+ env = Chef::DataBagItem.load('environments', node['chef_environment'])
111
+ Chef::Log.info("Loaded environments information from DataBagItem env[\#{env['id']}]")
112
+ Chef::Log.info("Loaded environments information from DataBagItem env[\#{env['description']}]")
113
+
114
+ # retrieve global settings specific data bag item
115
+ gs_default = Chef::DataBagItem.load('global_settings', 'default')
116
+ Chef::Log.info("Loaded global settings information from DataBagItem gs_default[\#{gs_default['id']}]")
117
+ Chef::Log.info("Loaded global settings information from DataBagItem gs_default[\#{gs_default['description']}]")
118
+ FILE
119
+
120
+ "site-cookbooks/commons/attributes/default.rb" => '',
121
+
122
+ "data_bags/global_settings/default.json" => "{\n\"id\": \"default\",\n\"description\": \"global settings default data bag item\"\n}",
123
+
124
+ "README.md" => "\# #{base}"
125
+ }
126
+
127
+ files.each do |file, content|
128
+ file = File.join(base, file)
129
+ if File.exists?(file)
130
+ warn "[skip] '#{file}' already exists"
131
+ elsif File.exists?(file.downcase)
132
+ warn "[skip] '#{file.downcase}' exists, which could conflict with `#{file}'"
133
+ else
134
+ unless File.exists?(File.dirname(file))
135
+ puts "[add] making directory '#{File.dirname(file)}'"
136
+ FileUtils.mkdir_p(File.dirname(file))
137
+ end
138
+ puts "[add] writing '#{file}'"
139
+ File.open(file, "w") { |f| f.write(content) }
140
+ end
141
+ end
142
+
143
+ Dir.chdir(base) do
144
+ puts 'Installing gem dependencies'
145
+ system 'bundle'
146
+
147
+ system 'cap env:prepare'
148
+
149
+ puts "Created Iron Chef DevOps project '#{base}'"
150
+
151
+ end
data/iron_chef.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'iron_chef/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "iron_chef"
8
+ gem.platform = Gem::Platform::RUBY
9
+ gem.version = IronChef::VERSION
10
+ gem.license = "MIT"
11
+
12
+ gem.required_ruby_version = '>= 1.9.3'
13
+ gem.required_rubygems_version = '>= 1.8.11'
14
+
15
+ gem.authors = ["Scott Rosenthal"]
16
+ gem.email = ["sr7575@gmail.com"]
17
+ gem.description = %q{Iron Chef is a lightweight Chef Solo wrapper built as a capistrano plugin}
18
+ gem.summary = %q{Iron Chef makes cloud server provisioning with Chef Solo powerful and easy.}
19
+ gem.homepage = "https://github.com/scottvrosenthal/iron_chef"
20
+ gem.license = "MIT"
21
+ gem.files = `git ls-files | grep -vE '(jenkins|.travis.yml|.gitmodules|.gitignore|.ruby-version|.ruby-gemset|README.md)'`.split("\n")
22
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
23
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
24
+ gem.require_paths = ["lib"]
25
+
26
+ gem.add_dependency 'capistrano', '>= 2.15.4'
27
+ gem.add_dependency 'json'
28
+
29
+ end
data/lib/iron_chef.rb ADDED
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'iron_chef/rsync'
4
+ require 'iron_chef/async_enumerable'
5
+ require 'iron_chef/erb'
6
+ require 'iron_chef/plugin'
7
+ require 'iron_chef/thread_pool'
8
+ require 'iron_chef/util'
9
+ require 'iron_chef/writer/batched'
10
+ require 'iron_chef/writer/file'
11
+ require 'iron_chef/writer/streaming'
12
+ require 'iron_chef/tasks'
13
+
14
+ Capistrano.plugin :iron_chef, IronChef::Plugin
@@ -0,0 +1,19 @@
1
+ module IronChef
2
+ module AsyncEnumerable
3
+ def each(&block)
4
+ pool = IronChef::ThreadPool.new(IronChef::Util.thread_pool_size)
5
+ super do |item|
6
+ pool.schedule(item, &block)
7
+ end
8
+ pool.shutdown
9
+ end
10
+
11
+ def map(&block)
12
+ pool = IronChef::ThreadPool.new(IronChef::Util.thread_pool_size)
13
+ super do |item|
14
+ pool.schedule(item, &block)
15
+ end
16
+ pool.shutdown
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+ require 'json'
4
+
5
+ module IronChef
6
+ module ERB
7
+ def self.read_erb(path)
8
+ ::ERB.new(File.read(path)).result binding
9
+ end
10
+
11
+ def self.read_erb_yaml(path)
12
+ YAML::load(read_erb path)
13
+ end
14
+
15
+ def self.read_erb_json(path)
16
+ JSON::parse(read_erb path)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,271 @@
1
+ module IronChef
2
+ module Plugin
3
+
4
+ def fetch_chef_environment_dir
5
+ fetch(:chef_environment_dir, 'environments')
6
+ end
7
+
8
+ def fetch_chef_nodes_dir
9
+ fetch(:chef_nodes_dir, 'nodes')
10
+ end
11
+
12
+ def find_node(node_path)
13
+ raise "Node YAML file #{node_path} not found" unless node_path && File.exists?(node_path)
14
+
15
+ node_name = File.basename(node_path).gsub('.yml','')
16
+ node_config = IronChef::ERB.read_erb_yaml(node_path)
17
+
18
+ node_config['node_name'] = node_name
19
+
20
+ node_config
21
+ end
22
+
23
+ def node(node_name)
24
+ nodes_location = fetch_chef_nodes_dir
25
+ unless node_path = Dir.glob("./#{nodes_location}/**/#{node_name}.yml")[0]
26
+ abort "Node '#{node_name}' is unknown. Known nodes are #{nodes_list.join(', ')}."
27
+ end
28
+ find_node(node_path)
29
+ end
30
+
31
+ def nodes_list
32
+ nodes_location = fetch_chef_nodes_dir
33
+ nodes_available = Dir.glob("./#{nodes_location}/**/*.yml").map { |f| File.basename(f, '.*') }
34
+ nodes_available.sort
35
+ end
36
+
37
+ def env_nodes_list
38
+ nodes_location = fetch_chef_nodes_dir
39
+ nodes_available = Dir.glob("./#{nodes_location}/**/*.yml").map { |f| File.basename(f, '.*') }
40
+ env_nodes_available = []
41
+ nodes_available.each do |node_name|
42
+ node_config = node(node_name)
43
+ if node_config['json']
44
+ if node_config['json']['chef_environment']
45
+ node_env = node_config['json']['chef_environment']
46
+ if "#{chef_environment}" == "#{node_env}"
47
+ env_nodes_available << node_config['node_name']
48
+ end
49
+ else
50
+ puts "Node '#{node_config['node_name']}' ['json']['chef_environment'] attribute is empty or missing."
51
+ end
52
+ else
53
+ puts "Node '#{node_config['node_name']}' ['json'] attribute is empty or missing."
54
+ end
55
+ end
56
+
57
+ if env_nodes_available.size == 0
58
+ abort "No nodes found for chef environment '#{chef_environment}'. Known nodes are #{nodes_available.join(', ')}."
59
+ end
60
+ env_nodes_available.sort
61
+ end
62
+
63
+ def tasks_for_env(nodes_names)
64
+
65
+ servers = []
66
+
67
+ nodes_names.each do |node_name|
68
+ node_config = node(node_name)
69
+ if node_config['server']
70
+ if node_config['server']['host']
71
+
72
+ servers << [node_config['server']['host'], node_config['node_name']]
73
+
74
+ task(node_config['node_name']) do
75
+ role :server, node_config['server']['host'], { node_name: node_config['node_name'] }
76
+ end
77
+ else
78
+ puts "Node '#{node_config['node_name']}' ['server']['host'] attribute is empty or missing."
79
+ end
80
+ else
81
+ puts "Node '#{node_config['node_name']}' ['server'] attribute is empty or missing."
82
+ end
83
+ end
84
+
85
+ if nodes_names
86
+ task(:all_nodes) do
87
+ servers.each do |server|
88
+ host_name, node_name = server
89
+ role :server, host_name, { node_name: node_name }
90
+ end
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ def upload_node_json(node_name)
97
+
98
+ node_config = node(node_name)
99
+
100
+ node_dna = {
101
+ :run_list => node_run_list(node_config)
102
+ }.merge(node_config['json'])
103
+
104
+ put node_dna.to_json, "#{chef_destination}/node.json", :via => :scp
105
+
106
+ end
107
+
108
+ def dump_nodes_json
109
+
110
+ nodes_available = nodes_list
111
+
112
+ nodes_location = fetch_chef_nodes_dir
113
+
114
+ FileUtils.mkdir_p("./tmp/#{nodes_location}")
115
+
116
+ nodes_available.each do |node_name|
117
+ node_config = node(node_name)
118
+
119
+ node_dna = {
120
+ :run_list => node_run_list(node_config)
121
+ }.merge(node_config['json'])
122
+
123
+ node_dna_json_file = "./tmp/#{nodes_location}/#{node_config['node_name']}.json"
124
+ File.open(node_dna_json_file, "w") do |f|
125
+ f.puts node_dna.to_json
126
+ end
127
+
128
+ puts "Created #{node_dna_json_file}"
129
+ end
130
+ end
131
+
132
+ def node_run_list(node_config)
133
+ run_list = []
134
+ run_list += node_config['roles'].map { |r| "role[#{r}]" } if node_config['roles']
135
+ run_list += node_config['recipes'].map { |r| "recipe[#{r}]" } if node_config['recipes']
136
+
137
+ run_list
138
+ end
139
+
140
+ def cookbooks
141
+ Array(fetch(:chef_cookbooks) { (:chef_cookbooks).select { |path| File.exist?(path) } })
142
+ end
143
+
144
+ def upload_solo_rb
145
+ cookbook_paths = cookbooks.map { |c| "File.join(chef_root, #{c.to_s.inspect})" }.join(', ')
146
+ solo_rb = <<-RUBY
147
+ solo true
148
+ chef_root = File.expand_path(File.dirname(__FILE__))
149
+ file_cache_path chef_root
150
+ cookbook_path [ #{cookbook_paths} ]
151
+ role_path File.join(chef_root, "roles")
152
+ data_bag_path File.join(chef_root, "data_bags")
153
+ json_attribs File.join(chef_root, "node.json")
154
+ log_level "#{chef_log_level}".to_sym
155
+ RUBY
156
+ put solo_rb, "#{chef_destination}/solo.rb", :via => :scp
157
+ end
158
+
159
+ def rsync
160
+ IronChef::Util.thread_pool_size = chef_parallel_rsync_pool_size
161
+ servers = IronChef::Util.optionally_async(find_servers_for_task(current_task), chef_parallel_rsync)
162
+ overrides = {}
163
+ overrides[:user] = fetch(:user, ENV['USER'])
164
+ overrides[:port] = fetch(:port) if exists?(:port)
165
+
166
+ failed_servers = servers.map do |server|
167
+ rsync_cmd = IronChef::Rsync.command(
168
+ chef_source,
169
+ IronChef::Rsync.remote_address(server.user || fetch(:user, ENV['USER']), server.host, chef_destination),
170
+ :delete => true,
171
+ :excludes => chef_excludes,
172
+ :ssh => ssh_options.merge(server.options[:ssh_options]||{}).merge(overrides)
173
+ )
174
+ logger.debug rsync_cmd
175
+
176
+ server.host unless system rsync_cmd
177
+ end.compact
178
+
179
+ raise "rsync failed on #{failed_servers.join(',')}" if failed_servers.any?
180
+
181
+ upload_chef_solo_config(servers)
182
+ end
183
+
184
+ def upload_chef_solo_config(servers)
185
+
186
+ servers.map do |server|
187
+ # allows use to use node aliases for Iron Chef
188
+ node_name = server.options[:node_name]
189
+
190
+ raise "upload_chef_solo_config failed on #{server.host} with missing node name on role" unless node_name
191
+ upload_node_json(node_name)
192
+
193
+ upload_solo_rb
194
+ end
195
+
196
+ end
197
+
198
+ def prepare
199
+ run "mkdir -p #{chef_destination}"
200
+ release_chef_client_lock
201
+ run "chown -R $USER: #{chef_destination}"
202
+ end
203
+
204
+ def why_run
205
+ chef(:why_run)
206
+ end
207
+
208
+ def apply
209
+ chef(:apply)
210
+ end
211
+
212
+ def lock
213
+ if should_lock?
214
+ run <<-CHECK_LOCK
215
+ if [ -f #{chef_lock_file} ]; then
216
+ stat -c "#{red_text("Chef in progress, #{chef_lock_file} owned by %U since %x")}" #{chef_lock_file} >&2;
217
+ exit 1;
218
+ fi
219
+ CHECK_LOCK
220
+
221
+ run "touch #{chef_lock_file}"
222
+ end
223
+ end
224
+
225
+ def unlock
226
+ run prepare_sudo_cmd("rm -f #{chef_lock_file}; true") if should_lock?
227
+ end
228
+
229
+ def release_chef_client_lock
230
+ run prepare_sudo_cmd("rm -f #{chef_destination}/chef-client-running.pid; true")
231
+ end
232
+
233
+ def prepare_sudo_cmd(cmd)
234
+ user == 'root' ? cmd : "sudo -- sh -c '#{cmd}'"
235
+ end
236
+
237
+ private
238
+
239
+ def should_lock?
240
+ chef_lock_file && !ENV['NO_CHEF_LOCK']
241
+ end
242
+
243
+ def chef(command = :why_run)
244
+ chef_cmd = "cd #{chef_destination} && #{chef_command} #{chef_parameters}"
245
+ flag = command == :why_run ? '--why-run' : ''
246
+
247
+ writer = if chef_stream_output
248
+ IronChef::Writer::Streaming.new(logger)
249
+ else
250
+ IronChef::Writer::Batched.new(logger)
251
+ end
252
+
253
+ writer = IronChef::Writer::File.new(writer, chef_write_to_file) unless chef_write_to_file.nil?
254
+
255
+ prepared_chef_cmd = prepare_sudo_cmd("#{chef_cmd} #{flag}")
256
+ begin
257
+ run prepared_chef_cmd do |channel, stream, data|
258
+ writer.collect_output(channel[:host], data)
259
+ end
260
+ logger.debug "Chef #{command} complete."
261
+ ensure
262
+ writer.all_output_collected
263
+ end
264
+ end
265
+
266
+ def red_text(text)
267
+ "\033[0;31m#{text}\033[0m"
268
+ end
269
+
270
+ end
271
+ end
@@ -0,0 +1,166 @@
1
+ module IronChef
2
+ class Rsync
3
+ class << self
4
+ def command(from, to, options={})
5
+ flags = ['-az']
6
+ flags << '--delete' if options[:delete]
7
+ flags << excludes(options[:excludes]) if options.has_key?(:excludes)
8
+ flags << ssh_options(options[:ssh]) if options.has_key?(:ssh)
9
+
10
+ "rsync #{flags.compact.join(' ')} #{from} #{to}"
11
+ end
12
+
13
+ def remote_address(user, host, path)
14
+ user_with_host = [user, host].compact.join('@')
15
+ [user_with_host, path].join(':')
16
+ end
17
+
18
+ def excludes(patterns)
19
+ [patterns].flatten.map { |p| "--exclude=#{p}" }
20
+ end
21
+
22
+
23
+ #
24
+ # Convert Net::SSH options into OpenSSH options.
25
+ #
26
+ # For a list of the options normally support by Net::SSH (and thus Capistrano), see
27
+ # http://net-ssh.github.com/net-ssh/classes/Net/SSH.html#method-c-start
28
+ #
29
+ # Also, to see how Net::SSH does the opposite of the conversion we are doing here, check out:
30
+ # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/config.rb
31
+ #
32
+ # API mismatch:
33
+ #
34
+ # * many OpenSSH options not supported
35
+ # * some options only make sense for Net::SSH
36
+ # * compression: for Net::SSH, this option is supposed to accept true, false, or algorithm. OpenSSH accepts 'yes' or 'no'
37
+ #
38
+ def ssh_options(options)
39
+ mapped_options = options.map do |key, value|
40
+ next unless value
41
+
42
+ case key
43
+ when :auth_methods then opt_auth_methods(value)
44
+ when :bind_address then opt('BindAddress', value)
45
+ when :compression then opt('Compression', value ? 'yes' : 'no')
46
+ when :compression_level then opt('CompressionLevel', value.to_i)
47
+ when :config then "-F '#{value}'"
48
+ when :encryption then opt('Ciphers', [value].flatten.join(','))
49
+ when :forward_agent then opt('ForwardAgent', value)
50
+ when :global_known_hosts_file then opt('GlobalKnownHostsFile', value)
51
+ when :hmac then opt('MACs', [value].flatten.join(','))
52
+ when :host_key then opt('HostKeyAlgorithms', [value].flatten.join(','))
53
+ when :host_key_alias then opt('HostKeyAlias', value)
54
+ when :host_name then opt('HostName', value)
55
+ when :kex then opt('KexAlgorithms', [value].flatten.join(','))
56
+ when :key_data then nil # not supported
57
+ when :keys then [value].flatten.select { |k| File.exist?(k) }.map { |k| "-i '#{k}'" }
58
+ when :keys_only then opt('IdentitiesOnly', value ? 'yes' : 'no')
59
+ when :languages then nil # not applicable
60
+ when :logger then nil # not applicable
61
+ when :paranoid then opt('StrictHostKeyChecking', value ? 'yes' : 'no')
62
+ when :passphrase then nil # not supported
63
+ when :password then nil # not supported
64
+ when :port then "-p #{value.to_i}"
65
+ when :properties then nil # not applicable
66
+ when :proxy then nil # not applicable
67
+ when :rekey_blocks_limit then nil # not supported
68
+ when :rekey_limit then opt('RekeyLimit', reverse_interpret_size(value))
69
+ when :rekey_packet_limit then nil # not supported
70
+ when :timeout then opt('ConnectTimeout', value.to_i)
71
+ when :user then "-l #{value}"
72
+ when :user_known_hosts_file then multi_opt('UserKnownHostsFile', value)
73
+ when :verbose then opt('LogLevel', interpret_log_level(value))
74
+ end
75
+ end.compact
76
+
77
+ %[-e "ssh #{mapped_options.join(' ')}"] unless mapped_options.empty?
78
+ end
79
+
80
+ private
81
+
82
+ def opt(option_name, option_value)
83
+ "-o #{option_name}='#{option_value}'"
84
+ end
85
+
86
+ def multi_opt(option_name, option_values)
87
+ [option_values].flatten.map do |value|
88
+ opt(option_name, value)
89
+ end.join(' ')
90
+ end
91
+
92
+ #
93
+ # In OpenSSH, password and pubkey default to 'yes', hostbased defaults to 'no'.
94
+ # Regardless, if :auth_method is configured, then we explicitly disable methods not included.
95
+ #
96
+ def opt_auth_methods(value)
97
+ value = [value].flatten
98
+ opts = []
99
+ if value.any?
100
+ if value.include? 'password'
101
+ opts << opt('PasswordAuthentication', 'yes')
102
+ else
103
+ opts << opt('PasswordAuthentication', 'no')
104
+ end
105
+ if value.include? 'publickey'
106
+ opts << opt('PubkeyAuthentication', 'yes')
107
+ else
108
+ opts << opt('PubkeyAuthentication', 'no')
109
+ end
110
+ if value.include? 'hostbased'
111
+ opts << opt('HostbasedAuthentication', 'yes')
112
+ else
113
+ opts << opt('HostbasedAuthentication', 'no')
114
+ end
115
+ end
116
+ if opts.any?
117
+ return opts.join(' ')
118
+ else
119
+ nil
120
+ end
121
+ end
122
+
123
+ #
124
+ # Converts the given integer size in bytes into a string with 'K', 'M', 'G' suffix, as appropriate.
125
+ #
126
+ # reverse of interpret_size in https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/config.rb
127
+ #
128
+ def reverse_interpret_size(size)
129
+ size = size.to_i
130
+ if size < 1024
131
+ "#{size}"
132
+ elsif size < 1024 * 1024
133
+ "#{size/1024}K"
134
+ elsif size < 1024 * 1024 * 1024
135
+ "#{size/(1024*1024)}M"
136
+ else
137
+ "#{size/(1024*1024*1024)}G"
138
+ end
139
+ end
140
+
141
+ def interpret_log_level(level)
142
+ if level.is_a? Symbol
143
+ case level
144
+ when :debug then "DEBUG"
145
+ when :info then "INFO"
146
+ when :warn then "ERROR"
147
+ when :error then "ERROR"
148
+ when :fatal then "FATAL"
149
+ else "INFO"
150
+ end
151
+ elsif level.is_a? Integer
152
+ case level
153
+ when Logger::DEBUG then "DEBUG"
154
+ when Logger::INFO then "INFO"
155
+ when Logger::WARN then "ERROR"
156
+ when Logger::ERROR then "ERROR"
157
+ when Logger::FATAL then "FATAL"
158
+ else "INFO"
159
+ end
160
+ else
161
+ "INFO"
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,181 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ Capistrano::Configuration.instance.load do
5
+
6
+ set :chef_source, '.'
7
+ set :chef_destination, '/tmp/chef'
8
+ set :chef_cookbooks, %w(cookbooks site-cookbooks)
9
+ set :chef_log_level, 'info'
10
+ set :chef_command, '/opt/chef/embedded/bin/ruby /opt/chef/bin/chef-solo -c /tmp/chef/solo.rb'
11
+ set :chef_parameters, '--color'
12
+ set :chef_excludes, %w(.git .svn nodes)
13
+ set :chef_stream_output, false
14
+ set :chef_parallel_rsync, true
15
+ set :chef_parallel_rsync_pool_size, 10
16
+ set :chef_syntax_check, false
17
+ set :chef_write_to_file, nil
18
+ set :chef_runner, nil
19
+ set :chef_lock_file, '/tmp/chef.lock'
20
+ set :chef_nodes_dir, 'nodes'
21
+ set :chef_data_bags_dir, 'data_bags'
22
+ set :chef_environment_dir, 'environments'
23
+
24
+ namespace :chef do
25
+
26
+ desc "Pushes the current chef configuration to the server."
27
+ task :update_code, :except => { :nochef => true } do
28
+ iron_chef.rsync
29
+ end
30
+
31
+ desc "Runs chef with --why-run flag to to understand the decisions it makes."
32
+ task :why_run, :except => { :nochef => true } do
33
+ iron_chef.lock
34
+ transaction do
35
+ on_rollback { iron_chef.unlock }
36
+ iron_chef.prepare
37
+ update_code
38
+ iron_chef.why_run
39
+ iron_chef.unlock
40
+ end
41
+ end
42
+
43
+ desc "Applies the current chef config to the server."
44
+ task :apply, :except => { :nochef => true } do
45
+ iron_chef.lock
46
+ transaction do
47
+ on_rollback { iron_chef.unlock }
48
+ iron_chef.prepare
49
+ update_code
50
+ iron_chef.apply
51
+ iron_chef.unlock
52
+ end
53
+ end
54
+
55
+ desc "Clears the chef lockfile on the server."
56
+ task :unlock, :except => { :nochef => true } do
57
+ iron_chef.unlock
58
+ end
59
+
60
+ desc "Clears the chef destination folder on the server."
61
+ task :clear, :except => { :nochef => true } do
62
+ run iron_chef.prepare_sudo_cmd("rm -rf #{chef_destination}/*")
63
+ iron_chef.unlock
64
+ end
65
+
66
+ desc "Shows all nodes available for chef config."
67
+ task :nodes, :except => { :nochef => true } do
68
+ puts iron_chef.nodes_list
69
+ end
70
+
71
+ desc "Dump each node's dynamically generated node.json file to local ./tmp/#{chef_nodes_dir} directory."
72
+ task :dump_nodes_json, :except => { :nochef => true } do
73
+ iron_chef.dump_nodes_json
74
+ end
75
+
76
+ end ## end chef namespace ##
77
+
78
+ ## begin env tasks
79
+
80
+ unless exists?(:chef_environments)
81
+ set :chef_environments, Dir["./#{chef_environment_dir}/*.rb"].map { |f| File.basename(f, ".rb") }
82
+ end
83
+
84
+ desc "Target individual nodes."
85
+ task :nodes, :except => { :nochef => true } do
86
+
87
+ nodes_available = iron_chef.nodes_list
88
+
89
+ iron_chef.tasks_for_env(nodes_available)
90
+
91
+ end
92
+
93
+ chef_environments.each do |name|
94
+ desc "Set the target chef environment to '#{name}'."
95
+ task name, :except => { :nochef => true } do
96
+ set :chef_environment, name.to_sym
97
+ if File.exist?(File.join(chef_environment_dir, "#{chef_environment}.rb"))
98
+ load "./#{chef_environment_dir}/#{chef_environment}"
99
+
100
+ iron_chef.tasks_for_env(iron_chef.env_nodes_list)
101
+
102
+ end
103
+ end
104
+ end
105
+
106
+ on :load do
107
+ if chef_environments.include?(ARGV.first)
108
+ # Execute the specified chef environment so that recipes required in environment can contribute to task list
109
+ find_and_execute_task(ARGV.first) if ARGV.any?{ |option| option =~ /-T|--tasks|-e|--explain/ }
110
+ else
111
+ # Execute the default chef environment so that recipes required in environment can contribute tasks
112
+ find_and_execute_task('nodes')
113
+ end
114
+ end
115
+
116
+ namespace :env do
117
+
118
+ desc "Stub out the chef environment config files."
119
+ task :prepare, :except => { :nochef => true } do
120
+ FileUtils.mkdir_p(chef_environment_dir)
121
+ chef_environments.each do |name|
122
+ rb_env_file = File.join(chef_environment_dir, "#{name}.rb")
123
+ unless File.exists?(rb_env_file)
124
+ File.open(rb_env_file, "w") do |f|
125
+ f.puts "# #{name.upcase}-specific chef environment configuration"
126
+ f.puts "# please put general chef environment config in config/deploy.rb"
127
+ end
128
+ yml_env_node_file = File.join(chef_nodes_dir, "#{name}-server1.yml")
129
+ unless File.exists?(yml_env_node_file)
130
+ File.open(yml_env_node_file, "w") do |f|
131
+ f.puts "json:\n chef_environment: #{name}\n\nroles:\n - app_server\n\nrecipes:\n - commons\n\nserver:\n host: ec2-xxx-xxx-xxx-xxx.us-west-2.compute.amazonaws.com"
132
+ end
133
+ end
134
+ json_env_data_bag_file = File.join("#{chef_data_bags_dir}/environments", "#{name}.json")
135
+ unless File.exists?(json_env_data_bag_file)
136
+ File.open(json_env_data_bag_file, "w") do |f|
137
+ f.puts "{\n\"id\": \"#{name}\",\n\"description\": \"environments #{name} data_bag_item\"\n}"
138
+ end
139
+ end
140
+ puts "Created chef environment config files for: #{name}"
141
+ end
142
+ end
143
+ end
144
+
145
+ desc "Shows chef environment nodes available for chef apply config."
146
+ task :nodes, :except => { :nochef => true } do
147
+ if chef_environments.include?(ARGV.first)
148
+ puts iron_chef.env_nodes_list
149
+ else
150
+ puts iron_chef.nodes_list
151
+ end
152
+ end
153
+
154
+ end
155
+
156
+ ## end env tasks
157
+
158
+ ## begin bootstrap namespace ##
159
+
160
+ namespace :bootstrap do
161
+
162
+ desc "Installs chef via omnibus on host."
163
+ task :chef, :except => { :nochef => true } do
164
+ run "mkdir -p #{chef_destination}"
165
+
166
+ script = <<-BASH
167
+ #!/bin/sh
168
+
169
+ curl -s -o /tmp/chef-omnibus-install.sh https://www.opscode.com/chef/install.sh
170
+ chmod +x /tmp/chef-omnibus-install.sh
171
+ /tmp/chef-omnibus-install.sh > /tmp/chef-omnibus-install.log
172
+
173
+ BASH
174
+
175
+ put script, "/tmp/chef-install.sh", :via => :scp
176
+ run iron_chef.prepare_sudo_cmd("chmod +x /tmp/chef-install.sh")
177
+ run iron_chef.prepare_sudo_cmd("/tmp/chef-install.sh > /tmp/chef-install.log")
178
+ end
179
+
180
+ end ## end bootstrap namespace ##
181
+ end
@@ -0,0 +1,39 @@
1
+ require 'thread'
2
+
3
+ module IronChef
4
+ class ThreadPool
5
+ def initialize(size)
6
+ @size = size
7
+ @jobs = Queue.new
8
+ @retvals = []
9
+
10
+ @pool = Array.new(@size) do |i|
11
+ Thread.new do
12
+ Thread.current[:id] = i
13
+
14
+ catch(:exit) do
15
+ loop do
16
+ job, args = @jobs.pop
17
+ @retvals << job.call(*args)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+
25
+ def schedule(*args, &block)
26
+ @jobs << [block, args]
27
+ end
28
+
29
+
30
+ def shutdown
31
+ @size.times do
32
+ schedule { throw :exit }
33
+ end
34
+
35
+ @pool.map(&:join)
36
+ @retvals
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ module IronChef
2
+ module Util
3
+ DEFAULT_THREAD_POOL_SIZE = 10
4
+
5
+ def self.thread_pool_size
6
+ @thread_pool_size ||= DEFAULT_THREAD_POOL_SIZE
7
+ end
8
+
9
+ def self.thread_pool_size=(size)
10
+ @thread_pool_size = size
11
+ end
12
+
13
+ def self.optionally_async(collection, async)
14
+ if async
15
+ async_collection = collection.clone
16
+ async_collection.extend IronChef::AsyncEnumerable
17
+ async_collection
18
+ else
19
+ collection
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module IronChef
2
+ VERSION = "0.0.10"
3
+ end
@@ -0,0 +1,22 @@
1
+ module IronChef
2
+ module Writer
3
+ class Batched
4
+ def initialize(logger)
5
+ @outputs = {}
6
+ @logger = logger
7
+ end
8
+
9
+ def collect_output(host, data)
10
+ @outputs[host] ||= ""
11
+ @outputs[host] << data
12
+ end
13
+
14
+ def all_output_collected
15
+ @outputs.keys.sort.each do |host|
16
+ @logger.info "Chef output for #{host}"
17
+ @logger.debug @outputs[host], host
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module IronChef
2
+ module Writer
3
+ class File
4
+ def initialize(writer, file)
5
+ @wrapped_writer = writer
6
+ @logger = Capistrano::Logger.new(:output => file)
7
+ @logger.level = Capistrano::Logger::TRACE
8
+ @file_writer = Batched.new(@logger)
9
+ end
10
+
11
+ def collect_output(host, data)
12
+ @wrapped_writer.collect_output(host, data)
13
+ @file_writer.collect_output(host, data)
14
+ end
15
+
16
+ def all_output_collected
17
+ @wrapped_writer.all_output_collected
18
+ @file_writer.all_output_collected
19
+ @logger.close
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ module IronChef
2
+ module Writer
3
+ class Streaming
4
+ def initialize(logger)
5
+ @logger = logger
6
+ end
7
+
8
+ def collect_output(host, data)
9
+ @logger.debug data, host
10
+ end
11
+
12
+ def all_output_collected
13
+ end
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: iron_chef
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.10
5
+ platform: ruby
6
+ authors:
7
+ - Scott Rosenthal
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-07-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: capistrano
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 2.15.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 2.15.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Iron Chef is a lightweight Chef Solo wrapper built as a capistrano plugin
42
+ email:
43
+ - sr7575@gmail.com
44
+ executables:
45
+ - ironchef
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - Gemfile
50
+ - Gemfile.lock
51
+ - Rakefile
52
+ - bin/ironchef
53
+ - iron_chef.gemspec
54
+ - lib/iron_chef.rb
55
+ - lib/iron_chef/async_enumerable.rb
56
+ - lib/iron_chef/erb.rb
57
+ - lib/iron_chef/plugin.rb
58
+ - lib/iron_chef/rsync.rb
59
+ - lib/iron_chef/tasks.rb
60
+ - lib/iron_chef/thread_pool.rb
61
+ - lib/iron_chef/util.rb
62
+ - lib/iron_chef/version.rb
63
+ - lib/iron_chef/writer/batched.rb
64
+ - lib/iron_chef/writer/file.rb
65
+ - lib/iron_chef/writer/streaming.rb
66
+ homepage: https://github.com/scottvrosenthal/iron_chef
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: 1.9.3
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: 1.8.11
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.0.3
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Iron Chef makes cloud server provisioning with Chef Solo powerful and easy.
90
+ test_files: []