chake-0.17/0000755000004100000410000000000013156732012012544 5ustar www-datawww-datachake-0.17/Rakefile0000644000004100000410000000434213156732012014214 0ustar www-datawww-datanamespace :bundler do require "bundler/gem_tasks" end task :test do sh 'rspec', '--color' end pkg = Gem::Specification.load('chake.gemspec') task 'build:tarball' => 'bundler:build' do chdir 'pkg' do sh 'gem2tgz', "#{pkg.name}-#{pkg.version}.gem" end end desc 'Create Debian source package' task 'build:debsrc' => ['bundler:clobber', 'build:tarball'] do dirname = "#{pkg.name}-#{pkg.version}" chdir 'pkg' do sh 'gem2deb', '--no-wnpp-check', '-s', '-p', pkg.name, "#{dirname}.tar.gz" chdir dirname do ln 'man/Rakefile', 'debian/dh_ruby.rake' sh "sed -i -e 's/-1/-1~0/ ; s/\*.*/* Development snapshot/' debian/changelog" sh 'dpkg-buildpackage', '-S', '-us', '-uc' end end end desc 'Builds and installs Debian package' task 'deb:install' => 'build:debsrc'do chdir "pkg/#{pkg.name}-#{pkg.version}" do sh 'fakeroot debian/rules binary' end sh 'sudo', 'dpkg', '-i', "pkg/#{pkg.name}_#{pkg.version}-1~0_all.deb" end desc 'Create source RPM package' task 'build:rpmsrc' => ['build:tarball', 'pkg/chake.spec'] do chdir 'pkg' do sh 'rpmbuild --define "_topdir .rpmbuild" --define "_sourcedir $(pwd)" --define "_srcrpmdir $(pwd)" -bs chake.spec --nodeps' end end file 'pkg/chake.spec' => ['chake.spec.erb', 'lib/chake/version.rb'] do |t| require 'erb' pkg = Gem::Specification.load('chake.gemspec') template = ERB.new(File.read('chake.spec.erb')) File.open(t.name, 'w') do |f| f.puts(template.result(binding)) end puts "Generated #{t.name}" end task 'build:all' => ['build:debsrc', 'build:rpmsrc'] desc 'lists changes since last release' task :changelog do last_tag = `git tag | sort -V`.split.last sh 'git', 'shortlog', last_tag + '..' end task :check_tag do last_tag = `git tag | sort -V`.split.last if last_tag == "v#{pkg.version}" fail "Version #{pkg.version} was already released!" end end desc 'checks if the latest release is properly documented in ChangeLog.md' task :check_changelog do begin sh 'grep', '^#\s*' + pkg.version.to_s, 'ChangeLog.md' rescue puts "Version #{pkg.version} not documented in ChangeLog.md!" raise end end desc 'Makes a release' task :release => [:check_tag, :check_changelog, :test, 'bundler:release'] task :default => :test chake-0.17/bin/0000755000004100000410000000000013156732012013314 5ustar www-datawww-datachake-0.17/bin/chake0000755000004100000410000000103713156732012014316 0ustar www-datawww-data#!/usr/bin/env ruby require 'rake' rakefiles = %w[rakefile Rakefile rakefile.rb Rakefile.rb] if (!rakefiles.any? { |f| File.exist?(f) }) && !ARGV.include?('-f') && !ARGV.include?('--rakefile') require 'tmpdir' require 'fileutils' # syntethize a Rakefile tmpdir = Dir.mktmpdir rakefile = File.join(tmpdir, 'Rakefile') File.open(rakefile, 'w') do |f| f.puts 'require "chake"' end ARGV.unshift << '--rakefile' << rakefile # clenup after finishing at_exit do FileUtils.rm_rf tmpdir end end Rake.application.run chake-0.17/Gemfile0000644000004100000410000000013213156732012014033 0ustar www-datawww-datasource 'https://rubygems.org' # Specify your gem's dependencies in chake.gemspec gemspec chake-0.17/examples/0000755000004100000410000000000013156732012014362 5ustar www-datawww-datachake-0.17/examples/test/0000755000004100000410000000000013156732012015341 5ustar www-datawww-datachake-0.17/examples/test/Rakefile0000644000004100000410000000065313156732012017012 0ustar www-datawww-data$:.unshift '../../lib' # this shouldn't be needed when you have chake installed require 'chake' manifest = %w[ Rakefile cookbooks cookbooks/example cookbooks/example/recipes cookbooks/example/recipes/default.rb cookbooks/example/files cookbooks/example/files/host-homer cookbooks/example/files/host-homer/test.asc config.rb ] desc 'removes everything' task :clean do rm_rf Dir.glob('**/*') - manifest end chake-0.17/examples/test/config.rb0000644000004100000410000000024113156732012017130 0ustar www-datawww-dataroot = File.expand_path(File.dirname(__FILE__)) file_cache_path root + '/cache' cookbook_path root + '/cookbooks' role_path root + '/config/roles' chake-0.17/examples/test/cookbooks/0000755000004100000410000000000013156732012017332 5ustar www-datawww-datachake-0.17/examples/test/cookbooks/example/0000755000004100000410000000000013156732012020765 5ustar www-datawww-datachake-0.17/examples/test/cookbooks/example/files/0000755000004100000410000000000013156732012022067 5ustar www-datawww-datachake-0.17/examples/test/cookbooks/example/files/host-homer/0000755000004100000410000000000013156732012024154 5ustar www-datawww-datachake-0.17/examples/test/cookbooks/example/files/host-homer/test.asc0000644000004100000410000000156413156732012025631 0ustar www-datawww-data-----BEGIN PGP MESSAGE----- Version: GnuPG v1 hQIMA5A8ZkAWdYz7AQ//dgQhKXuGS0dY04TXa1cXEtOHYC64LLnoo+UZT/KgnkzI /IAgLFbAV0Fd9DGLK857qv9fWf/FB5b6loYLPOVEys0mb5aQrqPPFeKwTTXxTJdk Ts2NvSVfNZx95Igotm/vkKW6/dbGlDfQSOVQMzhvmKOMMpRP+ixqn+G/nwE/0wQG QXc3UPAe9gBFlI9GWLTafhftwrxXiYeNF8N03W9BUk1OiQqJUmDRK9ZjVe/vbEiZ iQ689MnF5l+/6gptU0j77QIqk5vEItkl7RISxOS8PDFI+926NV3fZUrRAlu2eDIy HNMGkcKTjGoPNrj/UzzmqjP3uNQtKoK559cul7uY44QmsEINoFP3HoAOHeVxm1pG 6IG5EY6znwVEYTIVPK21NAPFpfk9yAB53sv6GrtOaYp8FFp+lLHJlPQcOOP51UV9 GqiAuec7Lgr3iy1yaXIBHFlLG0lmZ1OI41zwqh+Z8EF1NC0gFPhGuOJqBGmBbOxy Wt1IFx1JUHi0f277us9pFbQlcUb8tgFZ0J69epBrod+xWaZQKFwLWsCTN66fKJfp rqBzRBcNDJwsKOd54v/Cmrws8bwnfB8iNZYuEQdEt9u5TGIZFl4R9k+/pydAQouP Z95g4vh2ST2ZEeblnbCc2TFY/7j2O+aXyBzX/4+/bM0kZbFjxCNuefU/v3ZnDwXS QAH+NPQ31IiqMmoETUqHzo1UI+hV9em81Llt2bsbgWULp84LEmzczbjAcMi2EWNt SzPyJzOLrqEvEYg2O/+bGho= =Y2iZ -----END PGP MESSAGE----- chake-0.17/examples/test/cookbooks/example/recipes/0000755000004100000410000000000013156732012022417 5ustar www-datawww-datachake-0.17/examples/test/cookbooks/example/recipes/default.rb0000644000004100000410000000040313156732012024365 0ustar www-datawww-datafile '/tmp/chake.test' do content "It works on #{node[:fqdn]}!\n" mode '0644' owner 'root' group 'root' end cookbook_file '/tmp/chake.test.unencrypted' do source 'test' mode '0600' owner 'root' group 'root' end chake-0.17/ChangeLog.md0000644000004100000410000001175413156732012014725 0ustar www-datawww-data# 0.17 * make rsync exclude extra directories who are created as root by chef-solo at the server side. This fixes the case where upload phase when the SSH user is not root. # 0.16 * make `run` also capture stderr, for now mixed together with stdout. In the future that may be improved for example to print stderr output in red when running on a TTY. # 0.15 * improve text in the parallel execution docs * add new hook: `connect_common`, which will run before any attempt to connect to any node. * make output of `check` target more explicit about what was tested # 0.14 * Fix typo in README.md * thanks to Luciano Prestes Cavalcanti * Turn "all hosts" tasks (converge, upload, bootstrap, run, apply) into multitasks. This will make them run in parallel. # 0.13 * transmit decrypted files with mode 0400 * Use the Omnibus packages from Chef upstream on platforms where we don't have proper Chef packages from the OS official repository. # 0.12 * Switch manpage build from ronn to asciidoctor * Add ability to override the Chef configuration file by setting `$CHAKE_CHEF_CONFIG` (default: `config.rb`) * bootstrap: ensure short hostname is in /etc/hosts # 0.11 * bootstrap: make sure FQDN matches hostname * Add `rake check` task to check SSH connectivity and sudo setup * Add tasks to apply a single recipe to nodes: `rake apply[recipe]` and `rake apply:$NODE[recipe]`. If `[recipe]` is not passed in the command line, the user is prompted for the recipe name. * run task changed to have the same interface and behavior as the new apply task: `rake run[command]`, or `rake run:$NODE[command]`. If `[command]` is not passed in the command line, the user is prompted for the command. # 0.10.2 * Fix check for modified files at the upload phase. Now chake will properly avoiding rsync calls when there is no changed files since the latest upload. * Fix generated RPM spec file. Will now properly build, install, and work under both CentOS 7 and Fedora 22+. * Collect test coverage statistics when running tests. * Added dependency on simplecov # 0.10.1 * actually implement support for custom ports in Node URL's. Despite being documented, that didn't actually work until now. # 0.10 * Add hook functionality. See README/manpage for documentation. * README.md: a few reviews # 0.9.1 * fix manpage installation path # 0.9 * fix build step for obs uploads * add infrastructure to build and install a manpage * Add support for a nodes.d/ directory; very useful when dealing with a larger amount of nodes. # 0.8 * gemspec: minor improvements in the long description * LICENSE.txt: fixed license name * run: print small message before prompting * Add history support for the `run` tasks * Abort `run` tasks if no command is provided # 0.7 * gemspec: improve summary and description * Also for encrypted files under $cookbook/files/, and not only under $cookbook/files/\*/. * Allow overriding tmpdir with `$CHAKE_TMPDIR` * Stop cloud-init from resetting the hostname # 0.6 * Support a ssh prefix command by setting `$CHAKE_SSH_PREFIX` in the environment. For example, `CHAKE_SSH_PREFIX=tsocks` will make all ssh invocations as `tocks ssh ...` instead of just `ssh ...`. # 0.5 * Add a task login:$host that you can use to easily log in to any of your hosts. # 0.4.3 * When running remote commands as root, run `sudo COMMAND` directly instead of `sudo sh -c "COMMAND"`. Under over-restrictive sudo setups (i.e. one in which you cannot run a shell as root), `sudo sh -c "FOO"` will not be allowed. # 0.4.2 * tmp/chake: create only when actually needed * Control nodes files with `$CHAKE_NODES` # 0.4.1 * Don't always assume the local username as the remote username for SSH connections: * `user@host`: connect with `user@host` * `host`: connect with `host` (username will be obtained by SSH itself from either its configuration files or the current username) # 0.4.0 * Redesign build of RPM package * Output of command run on nodes is now aligned * Change storage of temporary files from .tmp to tmp/chake * The JSON node attributes files generated in tmp/chake are not readable * SSH config file can now be controlled with the `$CHAKE_SSH_CONFIG` environment variable * Extra options for rsync can now be passed in the `$CHAKE_RSYNC_OPTIONS` environment variable * Chake::VERSION is now available in Rakefiles * update test suite to use new rspec syntax instead the old one which is obsolete in rspec 3. * Thanks to Athos Ribeiro. # 0.3.3 * rsync: exclude cache/ to work with the version of rsync in OSX # 0.3.2 * Now finally, hopefully, really fix RPM builds * chake init: rename 'myhost' → 'basics' * The official home is on gitlab * Completed basic documentation # 0.3.1 * Fix setting hostname when bootstrapping * Rakefile: do not allow releases without a changelog entry * Now *really* fix RPM builds, hopefully # 0.3 * Fix RPM build * bootstrap: set hostname # 0.2.3 * No functional changes * Small changes to make chake compatible with Debian 7, and most of the RPM-based distributions chake-0.17/LICENSE.txt0000644000004100000410000000210613156732012014366 0ustar www-datawww-dataCopyright (c) 2014 Antonio Terceiro The Expat License (a.k.a. "MIT") Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. chake-0.17/spec/0000755000004100000410000000000013156732012013476 5ustar www-datawww-datachake-0.17/spec/spec_helper.rb0000644000004100000410000000223013156732012016311 0ustar www-datawww-databegin require 'simplecov' SimpleCov.start rescue LoadError puts "W: simplecov not installed, we won't have a coverage report" end require 'chake/node' require 'chake/backend' require 'rspec/version' if RSpec::Version::STRING < '2.14' puts "Skipping tests, need RSpec >= 2.14" exit end shared_examples "Chake::Backend" do |backend_class| let(:backend) { backend_class.new(node) } it('runs commands') do io = StringIO.new("line 1\nline 2\n") expect(IO).to receive(:popen).with(backend.command_runner + ['something'], Hash).and_return(io) expect(backend).to receive(:printf).with(anything, "myhost", "something") expect(backend).to receive(:printf).with(anything, "myhost", "line 1") expect(backend).to receive(:printf).with(anything, "myhost", "line 2") backend.run('something') end it('runs as root') do expect(backend).to receive(:run).with('sudo something') backend.run_as_root('something') end it('does not use sudo if already root') do allow(backend.node).to receive(:remote_username).and_return('root') expect(backend).to receive(:run).with('something') backend.run_as_root('something') end end chake-0.17/spec/chake/0000755000004100000410000000000013156732012014551 5ustar www-datawww-datachake-0.17/spec/chake/backend_spec.rb0000644000004100000410000000003113156732012017471 0ustar www-datawww-datarequire 'chake/backend' chake-0.17/spec/chake/node_spec.rb0000644000004100000410000000375313156732012017045 0ustar www-datawww-datarequire 'chake/node' describe Chake::Node do before do ent = double allow(ent).to receive(:name).and_return('jonhdoe') allow(Etc).to receive(:getpwuid).and_return(ent) end let(:simple) { Chake::Node.new('hostname') } it('has a name') { expect(simple.hostname).to eq('hostname') } it('uses ssh by default') { expect(simple.backend).to be_an_instance_of(Chake::Backend::Ssh) } it('user current username by default') { expect(simple.username).to eq('jonhdoe') } it('writes to /var/tmp/chef.$username') { expect(simple.path).to eq('/var/tmp/chef.jonhdoe') } let(:with_username) { Chake::Node.new('username@hostname') } it('accepts username') { expect(with_username.username).to eq('username') } it('uses ssh') { expect(with_username.backend).to be_an_instance_of(Chake::Backend::Ssh) } let(:with_backend) { Chake::Node.new('local://hostname')} it('accepts backend as URI scheme') { expect(with_backend.backend).to be_an_instance_of(Chake::Backend::Local) } it('wont accept any backend') do expect { Chake::Node.new('foobar://bazqux').backend }.to raise_error(ArgumentError) end let(:with_data) { Chake::Node.new('local://localhost', 'run_list' => ['recipe[common]']) } it('takes data') do expect(with_data.data).to be_a(Hash) end let(:with_port) { Chake::Node.new('ssh://foo.bar.com:2222') } it('accepts a port specification') do expect(with_port.port).to eq(2222) end let(:with_port_but_no_scheme) { Chake::Node.new('foo.bar.com:2222') } it('accepts a port specification without a scheme') do expect(with_port_but_no_scheme.port).to eq(2222) expect(with_port_but_no_scheme.backend.to_s).to eq('ssh') end [:run, :run_as_root, :rsync_dest].each do |method| it("delegates #{method} to backend") do node = simple backend = double args = Object.new allow(node).to receive(:backend).and_return(backend) expect(backend).to receive(method).with(args) node.send(method, args) end end end chake-0.17/spec/chake/backend/0000755000004100000410000000000013156732012016140 5ustar www-datawww-datachake-0.17/spec/chake/backend/ssh_spec.rb0000644000004100000410000000213213156732012020272 0ustar www-datawww-datarequire 'spec_helper' describe Chake::Backend::Ssh do include_examples "Chake::Backend", Chake::Backend::Ssh let(:node) { Chake::Node.new('ssh://myuser@myhost/srv/chef') } it('runs commands with ssh') { expect(backend.command_runner).to eq(['ssh', 'myuser@myhost']) } it('rsyncs over ssh') { expect(backend.rsync_dest).to eq('myuser@myhost:/srv/chef/') } it 'uses no remote username if none was passed' do node = Chake::Node.new('theserver') expect(node.username).to eq(Etc.getpwuid.name) expect(node.remote_username).to be_nil end it 'uses username is passwd' do expect(node.username).to eq('myuser') expect(node.remote_username).to eq('myuser') end context 'with a custom port' do let(:node) { Chake::Node.new('ssh://myhost:2222') } it 'uses port with ssh' do expect(backend.command_runner).to eq(['ssh', '-p', '2222', 'myhost']) end it 'uses port with scp' do expect(backend.scp).to eq(['scp', '-P', '2222']) end it 'uses port with rsync' do expect(backend.send(:rsync_ssh)).to eq(['-e', 'ssh -p 2222']) end end end chake-0.17/spec/chake/backend/local_spec.rb0000644000004100000410000000101113156732012020562 0ustar www-datawww-datarequire 'spec_helper' describe Chake::Backend::Local do include_examples "Chake::Backend", Chake::Backend::Local let(:node) { Chake::Node.new('local://myusername@myhost/srv/chef') } it('runs commands with sh -c') { expect(backend.command_runner).to eq(['sh', '-c']) } it('rsyncs locally') { expect(backend.rsync_dest).to eq('/srv/chef/') } it('skips if hostname is not the local hostname') do allow(Socket).to receive(:gethostname).and_return('otherhost') expect(node.skip?).to eq(true) end end chake-0.17/lib/0000755000004100000410000000000013156732012013312 5ustar www-datawww-datachake-0.17/lib/chake.rb0000644000004100000410000002216513156732012014720 0ustar www-datawww-data# encoding: UTF-8 require 'yaml' require 'json' require 'tmpdir' require 'chake/version' require 'chake/node' require 'chake/readline' require 'chake/tmpdir' chef_config = ENV['CHAKE_CHEF_CONFIG'] || 'config.rb' nodes_file = ENV['CHAKE_NODES'] || 'nodes.yaml' nodes_directory = ENV['CHAKE_NODES_D'] || 'nodes.d' node_data = File.exists?(nodes_file) && YAML.load_file(nodes_file) || {} Dir.glob(File.join(nodes_directory, '*.yaml')).sort.each do |f| node_data.merge!(YAML.load_file(f)) end $nodes = node_data.map { |node,data| Chake::Node.new(node, data) }.reject(&:skip?).uniq(&:hostname) $chake_tmpdir = 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 $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 "chef-solo -c #{node.path}/#{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 "chef-solo -c #{node.path}/#{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 true') 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 => $nodes.map { |node| "upload:#{node.hostname}" } desc "bootstrap all nodes" multitask :bootstrap => $nodes.map { |node| "bootstrap:#{node.hostname}" } desc "converge all nodes (default)" multitask "converge" => $nodes.map { |node| "converge:#{node.hostname}" } desc "Apply on all nodes" multitask "apply", [:recipe] => $nodes.map { |node| "apply:#{node.hostname}" } desc "run on all nodes" multitask :run, [:command] => $nodes.map { |node| "run:#{node.hostname}" } task :default => :converge desc 'checks connectivity and setup on all nodes' multitask :check => ($nodes.map { |node| "check:#{node.hostname}" }) do puts "✓ all hosts OK" puts " - ssh connection works" puts " - password-less sudo works" end chake-0.17/lib/chake/0000755000004100000410000000000013156732012014365 5ustar www-datawww-datachake-0.17/lib/chake/tmpdir.rb0000644000004100000410000000012413156732012016206 0ustar www-datawww-datamodule Chake def self.tmpdir ENV.fetch('CHAKE_TMPDIR', 'tmp/chake') end end chake-0.17/lib/chake/bootstrap/0000755000004100000410000000000013156732012016402 5ustar www-datawww-datachake-0.17/lib/chake/bootstrap/99_unsupported.sh0000644000004100000410000000072413156732012021652 0ustar www-datawww-dataecho "---------------------" echo "Unsupported platform: Installing chef-solo with omnibus package" echo "---------------------" echo for file in /etc/os-release /etc/issue; do if [ -f $file ]; then cat $file break fi done if ! which chef-solo >/dev/null ; then # Install chef-solo via omnibus package that chef provides # This script should install chef-solo in any Linux distribution wget -O- https://opscode.com/chef/install.sh | bash exit fi chake-0.17/lib/chake/bootstrap/01_debian.sh0000644000004100000410000000030413156732012020455 0ustar www-datawww-dataif [ -x /usr/bin/apt-get ]; then apt-get update export DEBIAN_FRONTEND=noninteractive apt-get -q -y install rsync chef update-rc.d chef-client disable service chef-client stop exit fi chake-0.17/lib/chake/bootstrap/00_set_hostname.sh0000644000004100000410000000117613156732012021733 0ustar www-datawww-datahostname="$1" echo "$hostname" > /etc/hostname hostname --file /etc/hostname fqdn=$(hostname --fqdn || true) if [ "$fqdn" != "$hostname" ]; then # if hostname is bar.example.com, we also want `bar` to be in /etc/hosts short_hostname=$(echo "$hostname" | cut -d . -f 1) if [ "$short_hostname" != "$hostname" ] && ! grep -q "\s${short_hostname}" /etc/hosts; then hostname="$hostname $short_hostname" fi printf "127.0.1.1\t%s\n" "$hostname" >> /etc/hosts fi # Stop cloud-init from resetting the hostname if [ -f /etc/cloud/cloud.cfg ]; then sed -i -e '/^\s*-\s*\(set_hostname\|update_hostname\)/d' /etc/cloud/cloud.cfg fi chake-0.17/lib/chake/readline.rb0000644000004100000410000000272713156732012016505 0ustar www-datawww-datarequire 'etc' require 'readline' require 'chake/tmpdir' module Chake class Readline class << self def history_file raise NotImplementedError end def history @history ||= [] end def prompt raise NotImplementedError end def init return if !File.exists?(history_file) @history = File.readlines(history_file).map(&:strip) end def finish return if !File.writable?(File.dirname(history_file)) || history.empty? File.open(history_file, 'w') do |f| history.last(500).each do |line| f.puts(line) end end end def readline ::Readline::HISTORY.clear history.each do |cmd| ::Readline::HISTORY.push(cmd) end input = ::Readline.readline(prompt) if input && input.strip != '' && input != @last history.push(input) end input end end class Commands < Readline def self.history_file File.join(Chake.tmpdir, '.commands_history') end def self.prompt '$ ' end end class Recipes < Readline def self.history_file File.join(Chake.tmpdir, '.recipes_history') end def self.prompt '> ' end end end end Chake::Readline.constants.each do |subclass| subclass = Chake::Readline.const_get(subclass) subclass.init at_exit do subclass.finish end end chake-0.17/lib/chake/version.rb0000644000004100000410000000004413156732012016375 0ustar www-datawww-datamodule Chake VERSION = "0.17" end chake-0.17/lib/chake/backend.rb0000644000004100000410000000264213156732012016305 0ustar www-datawww-datamodule Chake class Backend < Struct.new(:node) class CommandFailed < Exception end def scp ['scp'] end def scp_dest '' end def rsync ['rsync'] end def rsync_dest node.path + '/' end def run(cmd) printf "%#{Node.max_node_name_length}s: $ %s\n", node.hostname, cmd output = IO.popen(command_runner + [cmd], err: [:child, :out]) output.each_line do |line| printf "%#{Node.max_node_name_length}s: %s\n", node.hostname, line.strip end output.close if $? status = $?.exitstatus if status != 0 raise CommandFailed.new([node.hostname, 'FAILED with exit status %d' % status].join(': ')) end end end def run_shell system(*shell_command) end def run_as_root(cmd) if node.remote_username == 'root' run(cmd) else run('sudo ' + cmd) end end def to_s self.class.backend_name end def skip? false end def self.backend_name name.split("::").last.downcase end def self.inherited(subclass) @backends ||= [] @backends << subclass end def self.get(name) backend = @backends.find { |b| b.backend_name == name } backend || raise(ArgumentError.new("Invalid backend name: #{name}")) end end end require 'chake/backend/ssh' require 'chake/backend/local' chake-0.17/lib/chake/node.rb0000644000004100000410000000243213156732012015640 0ustar www-datawww-datarequire 'uri' require 'etc' require 'forwardable' require 'chake/backend' module Chake class Node extend Forwardable attr_reader :hostname attr_reader :port attr_reader :username attr_reader :remote_username attr_reader :path attr_reader :data def self.max_node_name_length @max_node_name_length ||= 0 end def self.max_node_name_length=(value) @max_node_name_length = value end def initialize(hostname, data = {}) uri = URI.parse(hostname) if !uri.host && ((!uri.scheme && uri.path) || (uri.scheme && uri.opaque)) uri = URI.parse("ssh://#{hostname}") end if uri.path && uri.path.empty? uri.path = nil end @backend_name = uri.scheme @hostname = uri.host @port = uri.port @username = uri.user || Etc.getpwuid.name @remote_username = uri.user @path = uri.path || "/var/tmp/chef.#{username}" @data = data if @hostname.length > self.class.max_node_name_length self.class.max_node_name_length = @hostname.length end end def backend @backend ||= Chake::Backend.get(@backend_name).new(self) end def_delegators :backend, :run, :run_as_root, :run_shell, :rsync, :rsync_dest, :scp, :scp_dest, :skip? end end chake-0.17/lib/chake/backend/0000755000004100000410000000000013156732012015754 5ustar www-datawww-datachake-0.17/lib/chake/backend/ssh.rb0000644000004100000410000000305713156732012017103 0ustar www-datawww-datamodule Chake class Backend class Ssh < Backend def scp ['scp', ssh_config, scp_options].flatten.compact end def scp_dest ssh_target + ':' end def rsync [ssh_prefix, 'rsync', rsync_ssh].flatten.compact end def rsync_dest [ssh_target, node.path + '/'].join(':') end def command_runner [ssh_prefix, 'ssh', ssh_config, ssh_options, ssh_target].flatten.compact end def shell_command command_runner end private def rsync_ssh @rsync_ssh ||= begin ssh_command = 'ssh' if File.exist?(ssh_config_file) ssh_command += ' -F ' + ssh_config_file end if node.port ssh_command += ' -p ' + node.port.to_s end if ssh_command == 'ssh' [] else ['-e', ssh_command] end end end def ssh_config File.exist?(ssh_config_file) && ['-F', ssh_config_file] || [] end def ssh_config_file @ssh_config_file ||= ENV.fetch('CHAKE_SSH_CONFIG', '.ssh_config') end def ssh_prefix @ssh_prefix ||= ENV.fetch('CHAKE_SSH_PREFIX', '').split end def ssh_target [node.remote_username, node.hostname].compact.join('@') end def ssh_options node.port && ['-p', node.port.to_s] || [] end def scp_options node.port && ['-P', node.port.to_s] || [] end end end end chake-0.17/lib/chake/backend/local.rb0000644000004100000410000000046213156732012017375 0ustar www-datawww-datarequire 'socket' module Chake class Backend class Local < Backend def command_runner ['sh', '-c'] end def shell_command ENV.fetch('SHELL', Etc.getpwuid.shell) end def skip? node.hostname != Socket.gethostname end end end end chake-0.17/man/0000755000004100000410000000000013156732012013317 5ustar www-datawww-datachake-0.17/man/Rakefile0000644000004100000410000000152213156732012014764 0ustar www-datawww-datatask :default => 'man/chake.1' file 'man/chake.1' => ['man/chake.adoc'] do command = 'asciidoctor --backend manpage --out-file man/chake.1 man/chake.adoc' if ENV['SOURCE_DATE_EPOCH'] date = Time.at(ENV['SOURCE_DATE_EPOCH'].to_i) command << " --attribute docdate=#{date.strftime('%Y-%m-%d')}" end sh command end file 'man/chake.adoc' => ['README.md', 'man/readme2man.sed'] do |t| sh "sed -f man/readme2man.sed README.md > #{t.name} || (rm -f #{t.name}; false)" end task :install => 'man/chake.1' do prefix = ENV['PREFIX'] || File.exists?('debian/rules') && '/usr' || '/usr/local' target = [ENV["DESTDIR"], prefix , 'share/man/man1'].compact man = File.join(*target) sh 'install', '-d', '-m', '0755', man sh 'install', '-m', '0644', 'man/chake.1', man end task :clean do rm_f 'man/chake.1' rm_f 'man/chake.adoc' end chake-0.17/man/readme2man.sed0000644000004100000410000000013313156732012016024 0ustar www-datawww-data1a :doctype: manpage /^## Install/,/^[^#]/ d /^## Contributing/,$ d s/^##\(.*\)/## \U\1/ chake-0.17/man/.gitignore0000644000004100000410000000001113156732012015277 0ustar www-datawww-datachake.1* chake-0.17/.gitlab-ci.yml0000644000004100000410000000027413156732012015203 0ustar www-datawww-databefore_script: - pwd - echo $USER - bundle install --path .bundle tests: script: - bundle exec rake - bundle exec rake -f man/Rakefile cache: paths: - .bundle chake-0.17/.gitignore0000644000004100000410000000032013156732012014527 0ustar www-datawww-data*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp /examples/test/nodes.yaml /examples/test/.tmp .*.html chake-0.17/chake.spec.erb0000644000004100000410000000150413156732012015242 0ustar www-datawww-data%define gem_name <%= pkg.name %> Summary: <%= pkg.summary %> Name: <%= pkg.name %> Version: <%= pkg.version %> Release: 1 Source0: %{name}-%{version}.tar.gz License: <%= pkg.license %> Group: Development/Tools Prefix: %{_prefix} Vendor: <%= pkg.authors.first %> <<%= pkg.email.first %>> Url: <%= pkg.homepage %> BuildArch: noarch BuildRequires: ruby, rubygems-devel, rubygem-rake, rubygem-bundler Requires: ruby, rubygem-rake %description <%= pkg.description %> %prep %setup -n %{name}-%{version} %build %{__rm} -rf %{buildroot} sed -i -e 's#spec.files\s*=.*#spec.files = Dir.glob("**/*")#' %{name}.gemspec rake bundler:build %gem_install -n pkg/%{name}-%{version}.gem %install cp -a usr %{buildroot}/usr %clean rm -rf $RPM_BUILD_ROOT %files %{gem_instdir}/ %exclude %{gem_cache} %{gem_spec} %{_bindir}/chake %doc %{gem_docdir} chake-0.17/chake.gemspec0000644000004100000410000000254513156732012015172 0ustar www-datawww-data# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'chake/version' Gem::Specification.new do |spec| spec.name = "chake" spec.version = Chake::VERSION spec.authors = ["Antonio Terceiro"] spec.email = ["terceiro@softwarelivre.org"] spec.summary = %q{serverless configuration management tool for chef} spec.description = %q{chake allows one to manage a number of hosts via SSH by combining chef (solo) and rake. It doesn't require a chef server; all you need is a workstation from where you can SSH into all your hosts. chake automates copying the configuration management repository to the target host (including managing encrypted files), running chef on them, and running arbitrary commands on the hosts.} spec.homepage = "https://gitlab.com/terceiro/chake" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.add_development_dependency "bundler", "~> 1.5" spec.add_development_dependency "rspec" spec.add_development_dependency "simplecov" spec.add_development_dependency "asciidoctor", '>= 1.5.3' spec.add_dependency "rake" end chake-0.17/README.md0000644000004100000410000002342113156732012014025 0ustar www-datawww-data# chake(1) ## NAME chake - serverless configuration with chef ## Introduction chake is a tool that helps you manage multiple hosts with, without the need for a chef server. Configuration is managed in a local directory, which should probably be under version control with **git(1)** or anything else. Configuration is usually deployed via rsync over SSH, and applied by invoking **chef-solo(1)** over SSH on each host. ## Installation $ gem install chake ## Creating the repository ``` $ chake init [create] nodes.yaml [ mkdir] nodes.d/ [create] config.rb [ mkdir] config/roles [ mkdir] cookbooks/basics/recipes/ [create] cookbooks/basics/recipes/default.rb [create] Rakefile ``` A brief explanation of the created files: * `nodes.yaml`: where you will list the hosts you will be managing, and what recipes to apply to each of them. * `nodes.d`: a directory with multiple files in the same format as nodes.yaml. All files matching `*.yaml` in it will be added to the list of nodes. * `config.rb`: contains the chef-solo configuration. You can modify it, but usually you won't need to. * `config/roles`: directory is where you can put your role definitions. * `cookbooks`: directory where you will store your cookbooks. A sample cookbook called "basics" is created, but feel free to remove it and add actual cookbooks. * `Rakefile`: Contains just the `require 'chake'` line. You can augment it with other tasks specific to your intrastructure. After the repository is created, you can call either `chake` or `rake`, as they are completely equivalent. ## Managing nodes Just after you created your repository, the contents of `nodes.yaml` is the following: ```yaml host1.mycompany.com: run_list: - recipe[basics] ``` You can list your hosts with `rake nodes`: ``` $ rake nodes host1.mycompany.com ssh ``` To add more nodes, just append to `nodes.yaml`: ```yaml host1.mycompany.com: run_list: - recipe[basics] host2.mycompany.com: run_list: - recipes[basics] ``` And chake now knows about your new node: ``` $ rake nodes host1.mycompany.com ssh host2.mycompany.com ssh ``` ## Preparings nodes to be managed Nodes have very few initial requirements to be managed with `chake`: - The node must be accessible via SSH. - The user you connect to the node must either be `root`, or be allowed to run `sudo` (in which case `sudo` must be installed). **A note on password prompts:** every time chake calls ssh on a node, you may be required to type in your password; every time chake calls sudo on the node, you may be require to type in your password. For managing one or two nodes this is probably fine, but for larger numbers of nodes it is not practical. To avoid password prompts, you can: - Configure SSH key-based authentication. This is more secure than using passwords. While you are at it, you also probably want disable password authentication completely, and only allow key-based authentication - Configure passwordless `sudo` access for the user you use to connect to your nodes. ## Checking connectivity and initial host setup To check whether hosts are correcly configured, you can use the `check` task: ```bash $ rake check ``` That will run the the `sudo true` command on each host. If that pass without you having to passwords, you are sure that * you have SSH access to each host; and * the user you are connecting as has password-less sudo correctly setup. ```bash $ rake check ``` ## Applying cookbooks Note that by default all tasks that apply to all hosts will run in parallel, using rake's support for multitasks. If for some reason you need to prevent that, you can pass `-j1` (or --jobs=1`) in the rake invocation. Note that by default rake will only run N+4 tasks in parallel, where N is the number of cores on the machine you are running it. If you have more than N+4 hosts and want all of them to be handled in parallel, you might want o pass `-j` (or `--jobs`), without any number, as the last argument; with that rake will have no limit on the number of tasks to perform in parallel. To apply the configuration to all nodes, run ```bash $ rake converge ``` To apply the configuration to a single node, run ```bash $ rake converge:$NODE ``` To apply a single recipe on all nodes, run ```bash $ rake apply[myrecipe] ``` To apply a single recipe on a specific node, run ```bash $ rake apply:$NODE[myrecipe] ``` If you don't inform a recipe in the command line, you will be prompted for one. To run a shell command on all nodes, run ``` $ rake run[command] ``` If the `command` you want to run contains spaces, or other characters that are special do the shell, you have to quote them. To run a shell command on a specific node, run ``` $ rake run:$NODE[command] ``` If you don't inform a command in the command line, you will be prompted for one. To check the existing tasks, run ```bash $ rake -T ``` ## Writing cookbooks Since chake is actually a wrapper for Chef Solo, you should read the [chef documentation](https://docs.chef.io/). In special, look at the [Chef Solo Documentation](https://docs.chef.io/chef_solo.html). ## The node bootstrapping process When chake acts on a node for the first time, it has to bootstrap it. The bootstrapping process includes doing the following: - installing chef and rsync - disabling the chef client daemon - setting up the hostname ## Node URLs The keys in the hash that is represented in `nodes.yaml` is a node URL. All components of the URL but the hostname are optional, so just listing hostnames is the simplest form of specifying your nodes. Here are all the components of the node URLs: ``` [backend://][username@]hostname[:port][/path] ``` * `backend`: backend to use to connect to the host. `ssh` or `local` (default: `ssh`) * `username`: user name to connect with (default: the username on your local workstation) * `hostname`: the hostname to connect to (default: _none_) * `port`: port number to connect to (default: 22) * `/path`: where to store the cookbooks at the node (default: `/var/tmp/chef.$USERNAME`) ## Extra features ### Hooks You can define rake tasks that will be executed before bootstrapping nodes, before uploading configuration management content to nodes, and before converging. To do this, you just need to enhance the corresponding tasks: * `bootstrap_common`: executed before bootstrapping nodes (even if nodes have already been bootstrapped) * `upload_common`: executed before uploading content to the node * `converge_common`: executed before converging (i.e. running chef) * `connect_common`: executed before doing any action that connects to any of the hosts. This can be used for example to generate a ssh configuration file based on the contents of the nodes definition files. Example: ``` task :bootstrap_common do sh './scripts/pre-bootstrap-checks' end ``` ### Encrypted files Any files ending matching `*.gpg` and `*.asc` will be decrypted with GnuPG before being sent to the node. You can use them to store passwords and other sensitive information (SSL keys, etc) in the repository together with the rest of the configuration. ### repository-local SSH configuration If you need special SSH configuration parameters, you can create a file called `.ssh_config` (or whatever file name you have in the `$CHAKE_SSH_CONFIG` environment variable, see below for details) in at the root of your repository, and chake will use it when calling `ssh`. ### Logging in to a host To easily login to one of your host, just run `rake login:$HOSTNAME`. This will automatically use the repository-local SSH configuration as above so you don't have to type `-F .ssh_config` all the time. ### Running all SSH invocations with some prefix command Some times, you will also want or need to prefix your SSH invocations with some prefix command in order to e.g. tunnel it through some central exit node. You can do this by setting `$CHAKE_SSH_PREFIX` on your environment. Example: ``` CHAKE_SSH_PREFIX=tsocks rake converge ``` The above will make all SSH invocations to all hosts be called as `tsocks ssh [...]` ### Converging local host If you want to manage your local workstation with chake, you can declare a local node like this in `nodes.yaml`: ```yaml local://thunderbolt: run_list: - role[workstation] ``` To apply the configuration to the local host, you can use the conventional `rake converge:thunderbolt`, or the special target `rake local`. When converging all nodes, `chake` will skip nodes that are declared with the `local://` backend and whose hostname does not match the hostname in the declaration. For example: ```yaml local://desktop: run_list: - role[workstation] local://laptop: run_list: - role[workstation] ``` When you run `rake converge` on `desktop`, `laptop` will be skipped, and vice-versa. ## Environment variables * `$CHAKE_SSH_CONFIG`: Local SSH configuration file. Defaults to `.ssh_config`. * `$CHAKE_SSH_PREFIX`: Command to prefix SSH (and rsync over SSH) calls with. * `$CHAKE_RSYNC_OPTIONS`: extra options to pass to `rsync`. Useful to e.g. exclude large files from being upload to each server. * `$CHAKE_NODES`: File containing the list of servers to be managed. Default: `nodes.yaml`. * `$CHAKE_NODES_D`: Directory containing node definition files servers to be managed. Default: `nodes.d`. * `$CHAKE_TMPDIR`: Directory used to store temporary cache files. Default: `tmp/chake`. * `$CHAKE_CHEF_CONFIG`: Chef configuration file, relative to the root of the repository. Default: `config.rb`. ## See also * **rake(1)**, **chef-solo(1)** * Chef documentation: https://docs.chef.io/ ## Contributing 1. Fork it ( http://github.com/terceiro/chake/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request