lib/capistrano-chef-solo.rb in yyuu-capistrano-chef-solo-0.0.9 vs lib/capistrano-chef-solo.rb in yyuu-capistrano-chef-solo-0.1.0
- old
+ new
@@ -8,248 +8,407 @@
module Capistrano
module ChefSolo
def self.extended(configuration)
configuration.load {
namespace(:"chef-solo") {
- _cset(:chef_solo_home) {
- capture('echo $HOME').strip
+ desc("Setup chef-solo. (an alias of chef_solo:setup)")
+ task(:setup, :except => { :no_release => true }) {
+ find_and_execute_task("chef_solo:setup")
}
- _cset(:chef_solo_version, "10.16.4")
- _cset(:chef_solo_path) { File.join(chef_solo_home, 'chef') }
+
+ desc("Run chef-solo. (an alias of chef_solo)")
+ task(:default, :except => { :no_release => true }) {
+ find_and_execute_task("chef_solo:default")
+ }
+
+ desc("Show chef-solo version. (an alias of chef_solo:version)")
+ task(:version, :except => { :no_release => true }) {
+ find_and_execute_task("chef_solo:version")
+ }
+
+ desc("Show chef-solo attributes. (an alias of chef_solo:attributes)")
+ task(:attributes, :except => { :no_release => true }) {
+ find_and_execute_task("chef_solo:attributes")
+ }
+ }
+
+ namespace(:chef_solo) {
+ _cset(:chef_solo_version, "11.4.0")
+ _cset(:chef_solo_path) { capture("echo $HOME/chef").strip }
_cset(:chef_solo_path_children, %w(bundle cache config cookbooks))
+ _cset(:chef_solo_config_file) { File.join(chef_solo_path, "config", "solo.rb") }
+ _cset(:chef_solo_attributes_file) { File.join(chef_solo_path, "config", "solo.json") }
- def connect_with_settings()
- # preserve original :user and :ssh_options
- set(:_chef_solo_user, user)
- set(:_chef_solo_ssh_options, ssh_options)
- set(:_chef_solo_rbenv_ruby_version, rbenv_ruby_version)
- begin
- # login as chef user if specified
- set(:user, fetch(:chef_solo_user, user))
- set(:ssh_options, fetch(:chef_solo_ssh_options, ssh_options))
- set(:rbenv_ruby_version, fetch(:chef_solo_ruby_version, rbenv_ruby_version))
+ _cset(:chef_solo_bootstrap_user) {
+ if variables.key?(:chef_solo_user)
+ logger.info(":chef_solo_user has been deprecated. use :chef_solo_bootstrap_user instead.")
+ fetch(:chef_solo_user, user)
+ else
+ user
+ end
+ }
+ _cset(:chef_solo_bootstrap_password) { password }
+ _cset(:chef_solo_bootstrap_ssh_options) {
+ if variables.key?(:chef_solo_ssh_options)
+ logger.info(":chef_solo_ssh_options has been deprecated. use :chef_solo_bootstrap_ssh_options instead.")
+ fetch(:chef_solo_ssh_options, ssh_options)
+ else
+ ssh_options
+ end
+ }
+ _cset(:chef_solo_use_password) {
+ auth_methods = ssh_options.fetch(:auth_methods, []).map { |m| m.to_sym }
+ auth_methods.include?(:password) or auth_methods.empty?
+ }
+ def _bootstrap_settings(&block)
+ if fetch(:_chef_solo_bootstrapped, false)
yield
- ensure
- # restore original :user and :ssh_options
- set(:user, _chef_solo_user)
- set(:ssh_options, _chef_solo_ssh_options)
- set(:rbenv_ruby_version, _chef_solo_rbenv_ruby_version)
+ else
+ # preserve original :user and :ssh_options
+ set(:_chef_solo_bootstrap_user, fetch(:user))
+ set(:_chef_solo_bootstrap_password, fetch(:password)) if chef_solo_use_password
+ set(:_chef_solo_bootstrap_ssh_options, fetch(:ssh_options))
+ servers = find_servers
+ begin
+ # we have to establish connections before teardown.
+ # https://github.com/capistrano/capistrano/pull/416
+ establish_connections_to(servers)
+ logger.info("entering chef-solo bootstrap mode. reconnect to servers as `#{chef_solo_bootstrap_user}'.")
+ # drop connection which is connected as standard :user.
+ teardown_connections_to(servers)
+ set(:user, chef_solo_bootstrap_user)
+ set(:password, chef_solo_bootstrap_password) if chef_solo_use_password
+ set(:ssh_options, chef_solo_bootstrap_ssh_options)
+ set(:_chef_solo_bootstrapped, true)
+ yield
+ ensure
+ set(:user, _chef_solo_bootstrap_user)
+ set(:password, _chef_solo_bootstrap_password) if chef_solo_use_password
+ set(:ssh_options, _chef_solo_bootstrap_ssh_options)
+ set(:_chef_solo_bootstrapped, false)
+ # we have to establish connections before teardown.
+ # https://github.com/capistrano/capistrano/pull/416
+ establish_connections_to(servers)
+ logger.info("leaving chef-solo bootstrap mode. reconnect to servers as `#{user}'.")
+ # drop connection which is connected as bootstrap :user.
+ teardown_connections_to(servers)
+ end
end
end
+ _cset(:chef_solo_bootstrap, false)
+ def connect_with_settings(&block)
+ if chef_solo_bootstrap
+ _bootstrap_settings do
+ yield
+ end
+ else
+ yield
+ end
+ end
desc("Setup chef-solo.")
- task(:setup) {
- connect_with_settings {
- transaction {
- bootstrap
- }
- }
+ task(:setup, :except => { :no_release => true }) {
+ connect_with_settings do
+ transaction do
+ install
+ end
+ end
}
desc("Run chef-solo.")
- task(:default) {
- connect_with_settings {
- transaction {
- bootstrap
+ task(:default, :except => { :no_release => true }) {
+ connect_with_settings do
+ setup
+ transaction do
update
- }
- }
+ invoke
+ end
+ end
}
- desc("Show version.")
- task(:version) {
- connect_with_settings {
- run("cd #{chef_solo_path} && #{bundle_cmd} exec chef-solo --version")
- }
+ # 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
+ end
+ end
+ end
+
+ 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 --version")
+ end
}
- task(:bootstrap) {
+ desc("Show chef-solo attributes.")
+ task(:attributes, :except => { :no_release => true }) {
+ hosts = ENV.fetch("HOST", "").split(/\s*,\s*/)
+ roles = ENV.fetch("ROLE", "").split(/\s*,\s*/).map { |role| role.to_sym }
+ 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) {
- set(:rbenv_use_bundler, true)
- find_and_execute_task('rbenv:setup')
+ task(:install_ruby, :except => { :no_release => true }) {
+ set(:rbenv_install_bundler, true)
+ find_and_execute_task("rbenv:setup")
}
_cset(:chef_solo_gemfile) {
- (<<-EOS).gsub(/^\s*/, '')
+ (<<-EOS).gsub(/^\s*/, "")
source "https://rubygems.org"
gem "chef", #{chef_solo_version.to_s.dump}
EOS
}
- task(:install_chef) {
- dirs = chef_solo_path_children.map { |dir| File.join(chef_solo_path, dir) }
- run("mkdir -p #{dirs.join(' ')}")
- put(chef_solo_gemfile, "#{File.join(chef_solo_path, 'Gemfile')}")
- run("cd #{chef_solo_path} && #{bundle_cmd} install --path=#{chef_solo_path}/bundle --quiet")
+ task(:install_chef, :except => { :no_release => true }) {
+ begin
+ version = capture("cd #{chef_solo_path.dump} && #{bundle_cmd} exec chef-solo --version")
+ installed = Regexp.new(Regexp.escape(chef_solo_version)) =~ version
+ rescue
+ installed = false
+ end
+ unless installed
+ dirs = chef_solo_path_children.map { |dir| File.join(chef_solo_path, dir) }
+ 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(" ")}")
+ end
}
- task(:update) {
- update_cookbooks
- update_config
- update_attributes
- invoke
- }
+ def update(options={})
+ update_cookbooks(options)
+ update_config(options)
+ update_attributes(options)
+ end
- task(:update_cookbooks) {
- tmpdir = `mktemp -d /tmp/capistrano-chef-solo.XXXXXXXXXX`.chomp
- remote_tmpdir = capture("mktemp -d /tmp/capistrano-chef-solo.XXXXXXXXXX").chomp
- destination = File.join(tmpdir, 'cookbooks')
- remote_destination = File.join(chef_solo_path, 'cookbooks')
- filename = File.join(tmpdir, 'cookbooks.tar.gz')
- remote_filename = File.join(remote_tmpdir, 'cookbooks.tar.gz')
+ def update_cookbooks(options={})
+ tmpdir = run_locally("mktemp -d /tmp/chef-solo.XXXXXXXXXX").strip
+ remote_tmpdir = capture("mktemp -d /tmp/chef-solo.XXXXXXXXXX").strip
+ destination = File.join(tmpdir, "cookbooks")
+ remote_destination = File.join(chef_solo_path, "cookbooks")
+ filename = File.join(tmpdir, "cookbooks.tar.gz")
+ remote_filename = File.join(remote_tmpdir, "cookbooks.tar.gz")
begin
bundle_cookbooks(filename, destination)
- run("mkdir -p #{remote_tmpdir}")
+ run("mkdir -p #{remote_tmpdir.dump}")
distribute_cookbooks(filename, remote_filename, remote_destination)
ensure
- run("rm -rf #{remote_tmpdir}") rescue nil
- run_locally("rm -rf #{tmpdir}") rescue nil
+ run("rm -rf #{remote_tmpdir.dump}") rescue nil
+ run_locally("rm -rf #{tmpdir.dump}") rescue nil
end
- }
+ end
- # s/cookbook/&s/g for backward compatibility with releases older than 0.0.2.
- # they will be removed in future releases.
- _cset(:chef_solo_cookbook_repository) {
- logger.info("WARNING: `chef_solo_cookbook_repository' has been deprecated. use `chef_solo_cookbooks_repository' instead.")
- abort("chef_solo_cookbook_repository not set")
- }
- _cset(:chef_solo_cookbook_revision) {
- logger.info("WARNING: `chef_solo_cookbook_revision' has been deprecated. use `chef_solo_cookbooks_revision' instead.")
- "HEAD"
- }
- _cset(:chef_solo_cookbook_subdir) {
- logger.info("WARNING: `chef_solo_cookbook_subdir' has been deprecated. use `chef_solo_cookbooks_subdir' instead.")
- "/"
- }
- _cset(:chef_solo_cookbooks_exclude, %w(.hg .git .svn))
-
- # special variable to set multiple cookbooks repositories.
- # by default, it will build from :chef_solo_cookbooks_* variables.
+ #
+ # The definition of cookbooks.
+ # By default, load cookbooks from local path of "config/cookbooks".
+ #
+ _cset(:chef_solo_cookbooks_name) { application }
+ _cset(:chef_solo_cookbooks_scm, :none)
_cset(:chef_solo_cookbooks) {
- repository = fetch(:chef_solo_cookbooks_repository, nil)
- repository = fetch(:chef_solo_cookbook_repository, nil) unless repository # for backward compatibility
- name = File.basename(repository, File.extname(repository))
- options = { :repository => repository, :cookbooks_exclude => chef_solo_cookbooks_exclude }
- options[:revision] = fetch(:chef_solo_cookbooks_revision, nil)
- options[:revision] = fetch(:chef_solo_cookbook_revision, nil) unless options[:revision] # for backward compatibility
- options[:cookbooks] = fetch(:chef_solo_cookbooks_subdir, nil)
- options[:cookbooks] = fetch(:chef_solo_cookbook_subdir, nil) unless options[:cookbooks] # for backward compatibility
- { name => options }
+ cookbooks = {}
+ cookbooks[chef_solo_cookbooks_name] = {}
+ cookbooks[chef_solo_cookbooks_name][:cookbooks] = fetch(:chef_solo_cookbooks_subdir, "config/cookbooks")
+ cookbooks[chef_solo_cookbooks_name][:repository] = fetch(:chef_solo_cookbooks_repository) if exists?(:chef_solo_cookbooks_repository)
+ cookbooks[chef_solo_cookbooks_name][:revision] = fetch(:chef_solo_cookbooks_revision) if exists?(:chef_solo_cookbooks_revision)
+ cookbooks
}
- _cset(:chef_solo_repository_cache) { File.expand_path('./tmp/cookbooks-cache') }
+ _cset(:chef_solo_cookbooks_exclude, %w(.hg .git .svn))
+ def _normalize_cookbooks(cookbooks)
+ xs = cookbooks.map { |name, options|
+ options[:scm] ||= chef_solo_cookbooks_scm
+ options[:cookbooks_exclude] ||= chef_solo_cookbooks_exclude
+ [name, options]
+ }
+ Hash[xs]
+ end
+
+ _cset(:chef_solo_repository_cache) { File.expand_path("./tmp/cookbooks-cache") }
def bundle_cookbooks(filename, destination)
dirs = [ File.dirname(filename), destination ].uniq
- run_locally("mkdir -p #{dirs.join(' ')}")
- chef_solo_cookbooks.each do |name, options|
- configuration = Capistrano::Configuration.new()
- # refreshing just :source, :revision and :real_revision is enough?
- options = {
- :source => proc { Capistrano::Deploy::SCM.new(configuration[:scm], configuration) },
- :revision => proc { configuration[:source].head },
- :real_revision => proc {
- configuration[:source].local.query_revision(configuration[:revision]) { |cmd| with_env("LC_ALL", "C") { run_locally(cmd) } }
- },
- }.merge(options)
- variables.merge(options).each do |key, val|
- configuration.set(key, val)
- end
- repository_cache = File.join(chef_solo_repository_cache, name)
- if File.exist?(repository_cache)
- run_locally(configuration[:source].sync(configuration[:real_revision], repository_cache))
+ run_locally("mkdir -p #{dirs.map { |x| x.dump }.join(" ")}")
+ cookbooks = _normalize_cookbooks(chef_solo_cookbooks)
+ cookbooks.each do |name, options|
+ case options[:scm].to_sym
+ when :none
+ fetch_cookbooks_none(name, destination, options)
else
- run_locally(configuration[:source].checkout(configuration[:real_revision], repository_cache))
+ fetch_cookbooks_repository(name, destination, options)
end
+ end
+ run_locally("cd #{File.dirname(destination).dump} && tar chzf #{filename.dump} #{File.basename(destination).dump}")
+ end
- cookbooks = [ options.fetch(:cookbooks, '/') ].flatten.compact
- execute = cookbooks.map { |c|
- repository_cache_subdir = File.join(repository_cache, c)
- exclusions = options.fetch(:cookbooks_exclude, []).map { |e| "--exclude=\"#{e}\"" }.join(' ')
- "rsync -lrpt #{exclusions} #{repository_cache_subdir}/ #{destination}"
- }
- run_locally(execute.join(' && '))
+ def _fetch_cookbook(source, destination, options)
+ exclusions = options.fetch(:cookbooks_exclude, []).map { |e| "--exclude=#{e.dump}" }.join(" ")
+ run_locally("rsync -lrpt #{exclusions} #{source}/ #{destination}")
+ end
+
+ def _fetch_cookbooks(source, destination, options)
+ cookbooks = [ options.fetch(:cookbooks, "/") ].flatten.compact
+ cookbooks.each do |cookbook|
+ _fetch_cookbook(File.join(source, cookbook), destination, options)
end
- run_locally("cd #{File.dirname(destination)} && tar chzf #{filename} #{File.basename(destination)}")
end
+ def fetch_cookbooks_none(name, destination, options={})
+ _fetch_cookbooks(options.fetch(:repository, "."), destination, options)
+ end
+
+ def fetch_cookbooks_repository(name, destination, options={})
+ configuration = Capistrano::Configuration.new
+ # refreshing just :source, :revision and :real_revision is enough?
+ options = {
+ :source => lambda { Capistrano::Deploy::SCM.new(configuration[:scm], configuration) },
+ :revision => lambda { configuration[:source].head },
+ :real_revision => lambda {
+ configuration[:source].local.query_revision(configuration[:revision]) { |cmd| with_env("LC_ALL", "C") { run_locally(cmd) } }
+ },
+ }.merge(options)
+ variables.merge(options).each do |key, val|
+ configuration.set(key, val)
+ end
+ repository_cache = File.join(chef_solo_repository_cache, name)
+ if File.exist?(repository_cache)
+ run_locally(configuration[:source].sync(configuration[:real_revision], repository_cache))
+ else
+ run_locally(configuration[:source].checkout(configuration[:real_revision], repository_cache))
+ end
+ _fetch_cookbooks(repository_cache, destination, options)
+ end
+
def distribute_cookbooks(filename, remote_filename, remote_destination)
upload(filename, remote_filename)
- run("rm -rf #{remote_destination}")
- run("cd #{File.dirname(remote_destination)} && tar xzf #{remote_filename}")
+ run("rm -rf #{remote_destination.dump}")
+ run("cd #{File.dirname(remote_destination).dump} && tar xzf #{remote_filename.dump}")
end
_cset(:chef_solo_config) {
- (<<-EOS).gsub(/^\s*/, '')
- file_cache_path #{File.join(chef_solo_path, 'cache').dump}
- cookbook_path #{File.join(chef_solo_path, 'cookbooks').dump}
+ (<<-EOS).gsub(/^\s*/, "")
+ file_cache_path #{File.join(chef_solo_path, "cache").dump}
+ cookbook_path #{File.join(chef_solo_path, "cookbooks").dump}
EOS
}
- task(:update_config) {
- put(chef_solo_config, File.join(chef_solo_path, 'config', 'solo.rb'))
- }
+ def update_config(options={})
+ top.put(chef_solo_config, chef_solo_config_file)
+ end
# merge nested hashes
- def deep_merge(a, b)
- f = lambda { |key, val1, val2| Hash === val1 && Hash === val2 ? val1.merge(val2, &f) : val2 }
- a.merge(b, &f)
+ def _merge_attributes!(a, b)
+ f = lambda { |key, val1, val2|
+ case val1
+ when Array
+ val1 + val2
+ when Hash
+ val1.merge(val2, &f)
+ else
+ val2
+ end
+ }
+ a.merge!(b, &f)
end
- def json(x)
- if fetch(:chef_solo_pretty_json, true)
- JSON.pretty_generate(x)
- else
- JSON.generate(x)
- end
+ def _json_attributes(x)
+ JSON.send(fetch(:chef_solo_pretty_json, true) ? :pretty_generate : :generate, x)
end
_cset(:chef_solo_capistrano_attributes) {
- # reject lazy variables since they might have side-effects.
- Hash[variables.reject { |key, value| value.respond_to?(:call) }]
+ #
+ # The rule of generating chef attributes from Capistrano variables
+ #
+ # 1. Reject variables if it is in exclude list.
+ # 2. Reject variables if it is lazy and not in include list.
+ # (lazy variables might have any side-effects)
+ #
+ attributes = variables.reject { |key, value|
+ excluded = chef_solo_capistrano_attributes_exclude.include?(key)
+ included = chef_solo_capistrano_attributes_include.include?(key)
+ excluded or (not included and value.respond_to?(:call))
+ }
+ Hash[attributes.map { |key, value| [key, fetch(key, nil)] }]
}
+ _cset(:chef_solo_capistrano_attributes_include, [
+ :application, :deploy_to, :rails_env, :latest_release,
+ :releases_path, :shared_path, :current_path, :release_path,
+ ])
+ _cset(:chef_solo_capistrano_attributes_exclude, [:logger, :password])
_cset(:chef_solo_attributes, {})
_cset(:chef_solo_host_attributes, {})
+ _cset(:chef_solo_role_attributes, {})
_cset(:chef_solo_run_list, [])
_cset(:chef_solo_host_run_list, {})
+ _cset(:chef_solo_role_run_list, {})
- def generate_attributes(options={})
- attributes = deep_merge(chef_solo_capistrano_attributes, chef_solo_attributes)
- attributes = deep_merge(attributes, {"run_list" => chef_solo_run_list})
- if options.has_key?(:host)
- attributes = deep_merge(attributes, chef_solo_host_attributes.fetch(options[:host], {}))
- attributes = deep_merge(attributes, {"run_list" => chef_solo_host_run_list.fetch(options[:host], [])})
+ 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`.
+ # 3. Use attributes defined in `:chef_solo_role_attributes` for target role.
+ # 4. Use attributes defined in `:chef_solo_host_attributes` for target host.
+ #
+ attributes = chef_solo_capistrano_attributes.dup
+ _merge_attributes!(attributes, chef_solo_attributes)
+ roles.each do |role|
+ _merge_attributes!(attributes, chef_solo_role_attributes.fetch(role, {}))
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.
+ #
+ # * 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})
+ end
attributes
end
- desc("Show chef-solo attributes.")
- task(:show_attributes) {
- STDOUT.puts(json(generate_attributes))
- }
-
- task(:update_attributes) {
- to = File.join(chef_solo_path, "config", "solo.json")
- if chef_solo_host_attributes.empty? and chef_solo_host_run_list.empty?
- put(json(generate_attributes), to)
- else
- execute_on_servers { |servers|
- servers.each { |server|
- put(json(generate_attributes(:host => server.host), to, :hosts => server.host))
- }
- }
+ def update_attributes(options={})
+ run_list = options.delete(:run_list)
+ servers = find_servers_for_task(current_task)
+ servers.each do |server|
+ attributes = _generate_attributes(:hosts => server.host, :roles => role_names_for_host(server), :run_list => run_list)
+ top.put(_json_attributes(attributes), chef_solo_attributes_file, options.merge(:hosts => server.host))
end
- }
+ end
- task(:invoke) {
- execute = []
- execute << "cd #{chef_solo_path}"
- execute << "#{sudo} #{bundle_cmd} exec chef-solo " + \
- "-c #{File.join(chef_solo_path, 'config', 'solo.rb')} " + \
- "-j #{File.join(chef_solo_path, 'config', 'solo.json')}"
- run(execute.join(' && '))
- }
+ def invoke(options={})
+ bin = fetch(:chef_solo_executable, "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 #{bin.dump} #{args.join(" ")}", options)
+ end
}
}
end
end
end