# encoding: UTF-8 require 'yaml' require 'json' require 'tmpdir' require 'chake/config' require 'chake/version' require 'chake/readline' require 'chake/tmpdir' desc "Initializes current directory with sample structure" task :init do if File.exists?('nodes.yaml') puts '[exists] nodes.yaml' else File.open('nodes.yaml', 'w') do |f| sample_nodes = < :connect_common desc 'Executed before uploading' task :upload_common => :connect_common desc 'Executed before uploading' task :converge_common => :connect_common desc 'Executed before connecting to any host' task :connect_common Chake.nodes.each do |node| hostname = node.hostname bootstrap_script = File.join(Chake.tmpdir, 'bootstrap-' + hostname) file bootstrap_script => bootstrap_steps do |t| mkdir_p(File.dirname(bootstrap_script)) File.open(t.name, 'w') do |f| f.puts '#!/bin/sh' f.puts 'set -eu' bootstrap_steps.each do |platform| f.puts(File.read(platform)) end end chmod 0755, t.name end desc "bootstrap #{hostname}" task "bootstrap:#{hostname}" => [:bootstrap_common, bootstrap_script] do config = File.join(Chake.tmpdir, hostname + '.json') if File.exists?(config) # already bootstrapped, just overwrite write_json_file(config, node.data) else # copy bootstrap script over scp = node.scp target = "/tmp/.chake-bootstrap.#{Etc.getpwuid.name}" sh *scp, bootstrap_script, node.scp_dest + target # run bootstrap script node.run_as_root("#{target} #{hostname}") # overwrite config with current contents mkdir_p File.dirname(config) write_json_file(config, node.data) end end desc "upload data to #{hostname}" task "upload:#{hostname}" => :upload_common do encrypted = encrypted_for(hostname) rsync_excludes = (encrypted.values + encrypted.keys).map { |f| ["--exclude", f] }.flatten rsync_excludes << "--exclude" << ".git/" rsync_excludes << "--exclude" << "cache/" rsync_excludes << "--exclude" << "nodes/" rsync_excludes << "--exclude" << "local-mode-cache/" rsync = node.rsync + ["-avp"] + ENV.fetch('CHAKE_RSYNC_OPTIONS', '').split rsync_logging = Rake.application.options.silent && '--quiet' || '--verbose' hash_files = Dir.glob(File.join(Chake.tmpdir, '*.sha1sum')) files = Dir.glob("**/*").select { |f| !File.directory?(f) } - encrypted.keys - encrypted.values - hash_files if_files_changed(hostname, 'plain', files) do sh *rsync, '--delete', rsync_logging, *rsync_excludes, './', node.rsync_dest end if_files_changed(hostname, 'enc', encrypted.keys) do Dir.mktmpdir do |tmpdir| encrypted.each do |encrypted_file, target_file| target = File.join(tmpdir, target_file) mkdir_p(File.dirname(target)) rm_f target File.open(target, 'w', 0400) do |output| IO.popen(['gpg', '--quiet', '--batch', '--use-agent', '--decrypt', encrypted_file]) do |data| output.write(data.read) end end puts "#{target} (decrypted)" end sh *rsync, rsync_logging, tmpdir + '/', node.rsync_dest end end end converge_dependencies = [:converge_common, "bootstrap:#{hostname}", "upload:#{hostname}"] desc "converge #{hostname}" task "converge:#{hostname}" => converge_dependencies do chef_logging = Rake.application.options.silent && '-l fatal' || '' node.run_as_root "rm -f #{node.path}/nodes/*.json && chef-solo -c #{node.path}/#{Chake.chef_config} #{chef_logging} -j #{node.path}/#{Chake.tmpdir}/#{hostname}.json" end desc 'apply on #{hostname}' task "apply:#{hostname}", [:recipe] => [:recipe_input, :connect_common] do |task, args| chef_logging = Rake.application.options.silent && '-l fatal' || '' node.run_as_root "rm -f #{node.path}/nodes/*.json && chef-solo -c #{node.path}/#{Chake.chef_config} #{chef_logging} -j #{node.path}/#{Chake.tmpdir}/#{hostname}.json --override-runlist recipe[#{$recipe_to_apply}]" end task "apply:#{hostname}" => converge_dependencies desc "run a command on #{hostname}" task "run:#{hostname}", [:command] => [:run_input, :connect_common] do node.run($cmd_to_run) end desc "Logs in to a shell on #{hostname}" task "login:#{hostname}" => :connect_common do node.run_shell end desc 'checks connectivity and setup on all nodes' task "check:#{hostname}" => :connect_common do node.run('sudo echo OK') end end task :run_input, :command do |task,args| $cmd_to_run = args[:command] if !$cmd_to_run puts "# Enter command to run (use arrow keys for history):" $cmd_to_run = Chake::Readline::Commands.readline end if !$cmd_to_run || $cmd_to_run.strip == '' puts puts "I: no command provided, operation aborted." exit(1) end end task :recipe_input, :recipe do |task,args| $recipe_to_apply = args[:recipe] if !$recipe_to_apply recipes = Dir['**/*/recipes/*.rb'].map do |f| f =~ %r{(.*/)?(.*)/recipes/(.*).rb$} cookbook = $2 recipe = $3 recipe = nil if recipe == 'default' [cookbook,recipe].compact.join('::') end.sort puts 'Available recipes:' IO.popen('column', 'w') do |column| column.puts(recipes) end $recipe_to_apply = Chake::Readline::Recipes.readline if !$recipe_to_apply || $recipe_to_apply.empty? puts puts "I: no recipe provided, operation aborted." exit(1) end if !recipes.include?($recipe_to_apply) abort "E: no such recipe: #{$recipe_to_apply}" end end end desc "upload to all nodes" multitask :upload => Chake.nodes.map { |node| "upload:#{node.hostname}" } desc "bootstrap all nodes" multitask :bootstrap => Chake.nodes.map { |node| "bootstrap:#{node.hostname}" } desc "converge all nodes (default)" multitask "converge" => Chake.nodes.map { |node| "converge:#{node.hostname}" } desc "Apply on all nodes" multitask "apply", [:recipe] => Chake.nodes.map { |node| "apply:#{node.hostname}" } desc "run on all nodes" multitask :run, [:command] => Chake.nodes.map { |node| "run:#{node.hostname}" } task :default => :converge desc 'checks connectivity and setup on all nodes' multitask :check => (Chake.nodes.map { |node| "check:#{node.hostname}" }) do puts "✓ all hosts OK" puts " - ssh connection works" puts " - password-less sudo works" end desc 'runs a Ruby console in the chake environment' task :console do require 'irb' IRB.setup(eval("__FILE__"), argv: []) workspace = IRB::WorkSpace.new(self) puts 'chake - interactive console' puts '---------------------------' puts 'all node data in available in Chake.nodes' puts IRB::Irb.new(workspace).run(IRB.conf) end