lib/capistrano-chef-solo.rb in yyuu-capistrano-chef-solo-0.1.3 vs lib/capistrano-chef-solo.rb in yyuu-capistrano-chef-solo-0.1.4

- old
+ new

@@ -13,10 +13,15 @@ desc("Setup chef-solo. (an alias of chef_solo:setup)") task(:setup, :except => { :no_release => true }) { find_and_execute_task("chef_solo:setup") } + desc("Uninstall chef-solo. (an alias of chef_solo:purge)") + task(:purge, :except => { :no_release => true }) { + find_and_execute_task("chef_solo:purge") + } + desc("Run chef-solo. (an alias of chef_solo)") task(:default, :except => { :no_release => true }) { find_and_execute_task("chef_solo:default") } @@ -30,15 +35,17 @@ find_and_execute_task("chef_solo:attributes") } } namespace(:chef_solo) { + _cset(:chef_solo_use_bundler, true) _cset(:chef_solo_version, "11.4.0") _cset(:chef_solo_path) { capture("echo $HOME/chef").strip } _cset(:chef_solo_cache_path) { File.join(chef_solo_path, "cache") } _cset(:chef_solo_config_path) { File.join(chef_solo_path, "config") } _cset(:chef_solo_cookbooks_path) { File.join(chef_solo_path, "cookbooks") } + _cset(:chef_solo_data_bags_path) { File.join(chef_solo_path, "data_bags") } _cset(:chef_solo_config_file) { File.join(chef_solo_config_path, "solo.rb") } _cset(:chef_solo_attributes_file) { File.join(chef_solo_config_path, "solo.json") } _cset(:chef_solo_bootstrap_user) { if variables.key?(:chef_solo_user) @@ -128,21 +135,34 @@ # FIXME: # Some variables (such like :default_environment set by capistrano-rbenv) may be # initialized without bootstrap settings during `on :load`. # Is there any way to avoid this without setting `:rbenv_setup_default_environment` # as false? - set(:rbenv_setup_default_environment, false) + on(:load) do + before("rbenv:setup_default_environment") do + set(:rbenv_setup_default_environment, false) if chef_solo_bootstrap + end + end desc("Setup chef-solo.") task(:setup, :except => { :no_release => true }) { connect_with_settings do transaction do install end end } + desc("Uninstall chef-solo.") + task(:purge, :except => { :no_release => true }) { + connect_with_settings do + transaction do + uninstall + end + end + } + desc("Run chef-solo.") task(:default, :except => { :no_release => true }) { connect_with_settings do setup transaction do @@ -155,22 +175,22 @@ # Acts like `default`, but will apply specified recipes only. def run_list(*recipes) connect_with_settings do setup transaction do - update(:run_list => recipes) - invoke + update + invoke(:run_list => recipes) end end end _cset(:chef_solo_cmd, "chef-solo") desc("Show chef-solo version.") task(:version, :except => { :no_release => true }) { connect_with_settings do - run("cd #{chef_solo_path.dump} && #{bundle_cmd} exec #{chef_solo_cmd} --version") + execute("--version") end } desc("Show chef-solo attributes.") task(:attributes, :except => { :no_release => true }) { @@ -179,110 +199,186 @@ roles += hosts.map { |host| role_names_for_host(ServerDefinition.new(host)) } attributes = _generate_attributes(:hosts => hosts, :roles => roles) STDOUT.puts(_json_attributes(attributes)) } - task(:install, :except => { :no_release => true }) { - install_ruby - install_chef - } - - task(:install_ruby, :except => { :no_release => true }) { - set(:rbenv_install_bundler, true) - find_and_execute_task("rbenv:setup") - } - + _cset(:chef_solo_gem_dependencies) {{ + fetch(:chef_solo_gem, "chef") => chef_solo_version, + }} _cset(:chef_solo_gemfile) { - (<<-EOS).gsub(/^\s*/, "") - source "https://rubygems.org" - gem "chef", #{chef_solo_version.to_s.dump} - EOS + gemfile = [] + gemfile << %{source "https://rubygems.org"} + chef_solo_gem_dependencies.each do |name, options| + if options.nil? + gemfile << %{gem #{name.dump}} + else + gemfile << %{gem #{name.dump}, #{options.inspect}} + end + end + gemfile.join("\n") } - task(:install_chef, :except => { :no_release => true }) { + task(:install, :except => { :no_release => true }) { + set(:rbenv_install_bundler, true) if chef_solo_use_bundler + find_and_execute_task("rbenv:setup") begin - version = capture("cd #{chef_solo_path.dump} && #{bundle_cmd} exec #{chef_solo_cmd} --version") + version = execute("--version", :via => :capture) installed = Regexp.new(Regexp.escape(chef_solo_version)) =~ version rescue installed = false end unless installed - dirs = [ chef_solo_path, chef_solo_cache_path, chef_solo_config_path, chef_solo_cookbooks_path ].uniq + dirs = [ chef_solo_path, chef_solo_cache_path, chef_solo_config_path ].uniq run("mkdir -p #{dirs.map { |x| x.dump }.join(" ")}") - top.put(chef_solo_gemfile, File.join(chef_solo_path, "Gemfile")) - args = fetch(:chef_solo_bundle_options, []) - args << "--path=#{File.join(chef_solo_path, "bundle").dump}" - args << "--quiet" - run("cd #{chef_solo_path.dump} && #{bundle_cmd} install #{args.join(" ")}") + if chef_solo_use_bundler + top.put(chef_solo_gemfile, File.join(chef_solo_path, "Gemfile")) + args = fetch(:chef_solo_bundle_options, []) + args << "--path=#{File.join(chef_solo_path, "bundle").dump}" + args << "--quiet" + run("cd #{chef_solo_path.dump} && #{bundle_cmd} install #{args.join(" ")}") + else + chef_solo_gem_dependencies.each do |name, options| + args = String === options ? "-v #{options.dump}" : "" # options must be a version string + rbenv.exec("gem install #{args} #{name.dump}", :path => chef_solo_path) + end + rbenv.rehash + end end } + task(:uninstall, :except => { :no_release => true }) { + if chef_solo_use_bundler + run("rm -f #{File.join(chef_solo_path, "Gemfile").dump} #{File.join(chef_solo_path, "Gemfile.lock").dump}") + run("rm -rf #{File.join(chef_solo_path, "bundle").dump}") + else + chef_solo_gem_dependencies.each do |name, options| + args = String === options ? "-v #{options.dump}" : "" # options must be a version string + rbenv.exec("gem uninstall -I -x #{args} #{name.dump}", :path => chef_solo_path) + end + end + } + def update(options={}) update_cookbooks(options) + update_data_bags(options) update_attributes(options) update_config(options) end def update_cookbooks(options={}) - _normalize_cookbooks(chef_solo_cookbooks).each do |name, variables| + repos = _normalize_cookbooks(chef_solo_cookbooks) + _install_repos(:cookbooks, repos, chef_solo_cookbooks_path, options) do |name, tmpdir, variables| + deploy_cookbooks(name, tmpdir, variables, options) + end + end + + def update_data_bags(options={}) + repos = _normalize_data_bags(chef_solo_data_bags) + _install_repos(:data_bags, repos, chef_solo_data_bags_path, options) do |name, tmpdir, variables| + deploy_data_bags(name, tmpdir, variables, options) + end + end + + def _install_repos(t, repos, destination, options={}, &block) + # (0) remove existing old data + run("rm -rf #{destination.dump} && mkdir -p #{destination.dump}", options) + repos.each do |name, variables| begin - tmpdir = capture("mktemp -d /tmp/cookbooks.XXXXXXXXXX", options).strip + tmpdir = capture("mktemp -d #{File.join("/tmp", "#{t}.XXXXXXXXXX").dump}", options).strip run("rm -rf #{tmpdir.dump} && mkdir -p #{tmpdir.dump}", options) - deploy_cookbooks(name, tmpdir, variables, options) - install_cookbooks(name, tmpdir, chef_solo_cookbooks_path, options) + # (1) caller deploys the repository to tmpdir + yield name, tmpdir, variables + # (2) then deploy it to actual destination + logger.debug("installing #{t} `#{name}' from #{tmpdir} to #{destination}.") + run("rsync -lrpt #{(tmpdir + "/").dump} #{destination.dump}", options) ensure run("rm -rf #{tmpdir.dump}", options) end end end - # - # The definition of cookbooks. - # By default, load cookbooks from local path of "config/cookbooks". - # - _cset(:chef_solo_cookbooks_exclude, %w(.hg .git .svn)) - _cset(:chef_solo_cookbooks_default_variables) {{ + _cset(:chef_solo_repository_cache) { File.expand_path("tmp") } + _cset(:chef_solo_repository_exclude, %w(.hg .git .svn)) + _cset(:chef_solo_repository_variables) {{ :scm => :none, :deploy_via => :copy_subdir, :deploy_subdir => nil, :repository => ".", - :cookbooks_exclude => chef_solo_cookbooks_exclude, :copy_cache => nil, }} - _cset(:chef_solo_cookbooks) { - variables = chef_solo_cookbooks_default_variables.dup - variables[:scm] = fetch(:chef_solo_cookbooks_scm) if exists?(:chef_solo_cookbooks_scm) - variables[:deploy_subdir] = fetch(:chef_solo_cookbooks_subdir, "config/cookbooks") - variables[:repository] = fetch(:chef_solo_cookbooks_repository) if exists?("chef_solo_cookbooks_repository") - variables[:revision] = fetch(:chef_solo_cookbooks_revision) if exists?(:chef_solo_cookbooks_revision) - if exists?(:chef_solo_cookbook_name) - # deploy as single cookbook - name = fetch(:chef_solo_cookbook_name) - { name => variables.merge(:cookbook_name => name) } + + # + # The definition of cookbooks. + # By default, load cookbooks from local path of "config/cookbooks". + # + _cset(:chef_solo_cookbooks_exclude) { chef_solo_repository_exclude } + _cset(:chef_solo_cookbooks_variables) { chef_solo_repository_variables.merge(:copy_exclude => chef_solo_cookbooks_exclude) } + _cset(:chef_solo_cookbooks) { _default_repos(:cookbook, chef_solo_cookbooks_variables) } + _cset(:chef_solo_cookbooks_cache) { File.join(chef_solo_repository_cache, "cookbooks-cache") } + def _normalize_cookbooks(repos) + _normalize_repos(repos, chef_solo_cookbooks_cache, chef_solo_cookbooks_variables) { |name, variables| + variables[:deploy_subdir] ||= variables[:cookbooks] # use :cookbooks as :deploy_subdir for backward compatibility with prior than 0.1.2 + variables[:copy_exclude] ||= variables[:cookbooks_exclude] + } + end + + # + # The definition of data_bags. + # By default, load data_bags from local path of "config/data_bags". + # + _cset(:chef_solo_data_bags_exclude) { chef_solo_repository_exclude } + _cset(:chef_solo_data_bags_variables) { chef_solo_repository_variables.merge(:copy_exclude => chef_solo_data_bags_exclude) } + _cset(:chef_solo_data_bags) { _default_repos(:data_bag, chef_solo_data_bags_variables) } + _cset(:chef_solo_data_bags_cache) { File.join(chef_solo_repository_cache, "data_bags-cache") } + def _normalize_data_bags(repos) + _normalize_repos(repos, chef_solo_data_bags_cache, chef_solo_data_bags_variables) { |name, variables| + variables[:deploy_subdir] ||= variables[:data_bags] # use :data_bags as :deploy_subdir for backward compatibility with prior than 0.1.2 + variables[:copy_exclude] ||= variables[:data_bags_exclude] + } + end + + def _default_repos(singular, variables={}, &block) + plural = "#{singular}s" + variables = variables.dup + variables[:scm] = fetch("chef_solo_#{plural}_scm".to_sym) if exists?("chef_solo_#{plural}_scm".to_sym) + variables[:deploy_subdir] = fetch("chef_solo_#{plural}_subdir".to_sym, File.join("config", plural)) + variables[:repository] = fetch("chef_solo_#{plural}_repository".to_sym) if exists?("chef_solo_#{plural}_repository".to_sym) + variables[:revision] = fetch("chef_solo_#{plural}_revision".to_sym) if exists?("chef_solo_#{plural}_revision".to_sym) + if exists?("chef_solo_#{singular}_name".to_sym) + name = fetch("chef_solo_#{singular}_name".to_sym) # deploy as single cookbook + variables["#{singular}_name".to_sym] = name else - # deploy as multiple cookbooks - name = fetch(:chef_solo_cookbooks_name, application) - { name => variables } + name = fetch("chef_solo_#{plural}_name".to_sym, application) # deploy as multiple cookbooks end - } + { name => variables } + end - _cset(:chef_solo_repository_cache) { File.expand_path("tmp/cookbooks-cache") } - def _normalize_cookbooks(cookbooks) - xs = cookbooks.map { |name, variables| - variables = chef_solo_cookbooks_default_variables.merge(variables) + def _normalize_repos(repos, cache_path, default_variables={}, &block) + normalized = repos.map { |name, variables| + variables = default_variables.merge(variables) variables[:application] ||= name - # use :cookbooks as :deploy_subdir for backward compatibility with prior than 0.1.2 - variables[:deploy_subdir] ||= variables[:cookbooks] if variables[:scm] != :none - variables[:copy_cache] ||= File.expand_path(name, chef_solo_repository_cache) + variables[:copy_cache] ||= File.expand_path(name, cache_path) end + yield name, variables [name, variables] } - Hash[xs] + Hash[normalized] end def deploy_cookbooks(name, destination, variables={}, options={}) - logger.debug("retrieving cookbooks `#{name}' from #{variables[:repository]} via #{variables[:deploy_via]}.") + # deploy as single cookbook, or deploy as multiple cookbooks + final_destination = variables.key?(:cookbook_name) ? File.join(destination, variables[:cookbook_name]) : destination + _deploy_repo(:cookbooks, name, final_destination, variables, options) + end + + def deploy_data_bags(name, destination, variables={}, options={}) + # deploy as single data_bag, or deploy as multiple data_bags + final_destination = variables.key?(:data_bag_name) ? File.join(destination, variables[:data_bag_name]) : destination + _deploy_repo(:data_bags, name, final_destination, variables, options) + end + + def _deploy_repo(t, name, destination, variables={}, options={}) begin releases_path = capture("mktemp -d /tmp/releases.XXXXXXXXXX", options).strip release_path = File.join(releases_path, release_name) run("rm -rf #{releases_path.dump} && mkdir -p #{releases_path.dump}", options) c = _middle_copy(top) # create new configuration with separated @variables @@ -293,22 +389,22 @@ set(:revision) { source.head } set(:source) { ::Capistrano::Deploy::SCM.new(scm, self) } set(:real_revision) { source.local.query_revision(revision) { |cmd| with_env("LC_ALL", "C") { run_locally(cmd) } } } set(:strategy) { ::Capistrano::Deploy::Strategy.new(deploy_via, self) } variables.each do |key, val| - set(key, val) + if val.nil? + unset(key) + else + set(key, val) + end end + from = File.join(repository, fetch(:deploy_subdir, "/")) + to = destination + logger.debug("retrieving #{t} `#{name}' from #{from} (scm=#{scm}, via=#{deploy_via}) to #{to}.") strategy.deploy! end - if variables.key?(:cookbook_name) - # deploy as single cookbook - final_destination = File.join(destination, variables[:cookbook_name]) - else - # deploy as multiple cookbooks - final_destination = destination - end - run("rsync -lrpt #{(release_path + "/").dump} #{final_destination.dump}", options) + run("rsync -lrpt #{(release_path + "/").dump} #{destination.dump}", options) ensure run("rm -rf #{releases_path.dump}", options) end end @@ -319,20 +415,15 @@ o.instance_variable_set(k, v ? v.clone : v) end o end - def install_cookbooks(name, source, destination, options={}) - logger.debug("installing cookbooks `#{name}' to #{destination}.") - run("mkdir -p #{source.dump} #{destination.dump}", options) - run("rsync -lrpt #{(source + "/").dump} #{destination.dump}", options) - end - _cset(:chef_solo_config) { (<<-EOS).gsub(/^\s*/, "") file_cache_path #{chef_solo_cache_path.dump} cookbook_path #{chef_solo_cookbooks_path.dump} + data_bag_path #{chef_solo_data_bags_path.dump} EOS } def update_config(options={}) top.put(chef_solo_config, chef_solo_config_file, options) end @@ -384,11 +475,10 @@ _cset(:chef_solo_role_run_list, {}) def _generate_attributes(options={}) hosts = [ options.delete(:hosts) ].flatten.compact.uniq roles = [ options.delete(:roles) ].flatten.compact.uniq - run_list = [ options.delete(:run_list) ].flatten.compact.uniq # # By default, the Chef attributes will be generated by following order. # # 1. Use _non-lazy_ variables of Capistrano. # 2. Use attributes defined in `:chef_solo_attributes`. @@ -402,46 +492,49 @@ end hosts.each do |host| _merge_attributes!(attributes, chef_solo_host_attributes.fetch(host, {})) end # - # The Chef `run_list` will be generated by following rules. + # The Chef `run_list` will be generated from `:chef_solo_role_run_list` and + # `:chef_solo_host_run_list`. # - # * If `:run_list` was given as argument, just use it. - # * Otherwise, generate it from `:chef_solo_role_run_list`, `:chef_solo_role_run_list` - # and `:chef_solo_host_run_list`. - # - if run_list.empty? - _merge_attributes!(attributes, {"run_list" => chef_solo_run_list}) - roles.each do |role| - _merge_attributes!(attributes, {"run_list" => chef_solo_role_run_list.fetch(role, [])}) - end - hosts.each do |host| - _merge_attributes!(attributes, {"run_list" => chef_solo_host_run_list.fetch(host, [])}) - end - else - attributes["run_list"] = [] # ignore run_list not from argument - _merge_attributes!(attributes, {"run_list" => run_list}) + _merge_attributes!(attributes, {"run_list" => chef_solo_run_list}) + roles.each do |role| + _merge_attributes!(attributes, {"run_list" => chef_solo_role_run_list.fetch(role, [])}) end + hosts.each do |host| + _merge_attributes!(attributes, {"run_list" => chef_solo_host_run_list.fetch(host, [])}) + end attributes end def update_attributes(options={}) - run_list = options.delete(:run_list) servers = find_servers_for_task(current_task) servers.each do |server| logger.debug("updating chef-solo attributes for #{server.host}.") - attributes = _generate_attributes(:hosts => server.host, :roles => role_names_for_host(server), :run_list => run_list) + attributes = _generate_attributes(:hosts => server.host, :roles => role_names_for_host(server)) top.put(_json_attributes(attributes), chef_solo_attributes_file, options.merge(:hosts => server.host)) end end def invoke(options={}) + options = options.dup + run_list = [ options.delete(:run_list) ].flatten.compact logger.debug("invoking chef-solo.") args = fetch(:chef_solo_options, []) - args << "-c #{chef_solo_config_file.dump}" - args << "-j #{chef_solo_attributes_file.dump}" - run("cd #{chef_solo_path.dump} && #{sudo} #{bundle_cmd} exec #{chef_solo_cmd} #{args.join(" ")}", options) + args += ["-c", chef_solo_config_file] + args += ["-j", chef_solo_attributes_file] + args += ["-o", run_list.join(",")] unless run_list.empty? + execute(args, options.merge(:via => :sudo)) + end + + def execute(args, options={}) + if chef_solo_use_bundler + command = "bundle exec #{chef_solo_cmd}" + else + command = chef_solo_cmd + end + rbenv.exec("#{command} #{[ args ].flatten.compact.map { |x| x.dump }.join(" ")}", options.merge(:path => chef_solo_path)) end } } end end