File Manager
Viewing File: cookbook_uploader.rb
require "set"
require "chef/exceptions"
require "chef/knife/cookbook_metadata"
require "chef/digester"
require "chef/cookbook_manifest"
require "chef/cookbook_version"
require "chef/cookbook/syntax_check"
require "chef/cookbook/file_system_file_vendor"
require "chef/util/threaded_job_queue"
require "chef/sandbox"
require "chef/server_api"
class Chef
class CookbookUploader
attr_reader :cookbooks
attr_reader :path
attr_reader :opts
attr_reader :rest
attr_reader :concurrency
# Creates a new CookbookUploader.
# ===Arguments:
# * cookbooks::: A Chef::CookbookVersion or array of them describing the
# cookbook(s) to be uploaded
# * path::: A String or Array of Strings representing the base paths to the
# cookbook repositories.
# * opts::: (optional) An options Hash
# ===Options:
# * :force indicates that the uploader should set the force option when
# uploading the cookbook. This allows frozen CookbookVersion
# documents on the server to be overwritten (otherwise a 409 is
# returned by the server)
# * :rest A Chef::ServerAPI object that you have configured the way you like it.
# If you don't provide this, one will be created using the values
# in Chef::Config.
# * :concurrency An integer that decided how many threads will be used to
# perform concurrent uploads
def initialize(cookbooks, opts = {})
@opts = opts
@cookbooks = Array(cookbooks)
@rest = opts[:rest] || Chef::ServerAPI.new(Chef::Config[:chef_server_url])
@concurrency = opts[:concurrency] || 10
@policy_mode = opts[:policy_mode] || false
end
def upload_cookbooks
# Syntax Check
validate_cookbooks
# generate checksums of cookbook files and create a sandbox
checksum_files = {}
cookbooks.each do |cb|
Chef::Log.info("Saving #{cb.name}")
checksum_files.merge!(cb.checksums)
end
checksums = checksum_files.inject({}) { |memo, elt| memo[elt.first] = nil; memo }
new_sandbox = rest.post("sandboxes", { :checksums => checksums })
Chef::Log.info("Uploading files")
queue = Chef::Util::ThreadedJobQueue.new
checksums_to_upload = Set.new
# upload the new checksums and commit the sandbox
new_sandbox["checksums"].each do |checksum, info|
if info["needs_upload"] == true
checksums_to_upload << checksum
Chef::Log.info("Uploading #{checksum_files[checksum]} (checksum hex = #{checksum}) to #{info['url']}")
queue << uploader_function_for(checksum_files[checksum], checksum, info["url"], checksums_to_upload)
else
Chef::Log.debug("#{checksum_files[checksum]} has not changed")
end
end
queue.process(@concurrency)
sandbox_url = new_sandbox["uri"]
Chef::Log.debug("Committing sandbox")
# Retry if S3 is claims a checksum doesn't exist (the eventual
# in eventual consistency)
retries = 0
begin
rest.put(sandbox_url, { :is_completed => true })
rescue Net::HTTPServerException => e
if e.message =~ /^400/ && (retries += 1) <= 5
sleep 2
retry
else
raise
end
end
# files are uploaded, so save the manifest
cookbooks.each do |cb|
manifest = Chef::CookbookManifest.new(cb, policy_mode: policy_mode?)
save_url = opts[:force] ? manifest.force_save_url : manifest.save_url
begin
rest.put(save_url, manifest)
rescue Net::HTTPServerException => e
case e.response.code
when "409"
raise Chef::Exceptions::CookbookFrozen, "Version #{cb.version} of cookbook #{cb.name} is frozen. Use --force to override."
else
raise
end
end
end
Chef::Log.info("Upload complete!")
end
def uploader_function_for(file, checksum, url, checksums_to_upload)
lambda do
# Checksum is the hexadecimal representation of the md5,
# but we need the base64 encoding for the content-md5
# header
checksum64 = Base64.encode64([checksum].pack("H*")).strip
file_contents = File.open(file, "rb") { |f| f.read }
# Custom headers. 'content-type' disables JSON serialization of the request body.
headers = { "content-type" => "application/x-binary", "content-md5" => checksum64, "accept" => "application/json" }
begin
rest.put(url, file_contents, headers)
checksums_to_upload.delete(checksum)
rescue Net::HTTPServerException, Net::HTTPFatalError, Errno::ECONNREFUSED, Timeout::Error, Errno::ETIMEDOUT, SocketError => e
error_message = "Failed to upload #{file} (#{checksum}) to #{url} : #{e.message}"
error_message << "\n#{e.response.body}" if e.respond_to?(:response)
Chef::Knife.ui.error(error_message)
raise
end
end
end
def validate_cookbooks
cookbooks.each do |cb|
syntax_checker = Chef::Cookbook::SyntaxCheck.for_cookbook(cb.name)
Chef::Log.info("Validating ruby files")
exit(1) unless syntax_checker.validate_ruby_files
Chef::Log.info("Validating templates")
exit(1) unless syntax_checker.validate_templates
Chef::Log.info("Syntax OK")
true
end
end
def policy_mode?
@policy_mode
end
end
end