iron_chef 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []