Skip to content

Instantly share code, notes, and snippets.

@dhoer
Last active January 2, 2016 02:59
Show Gist options
  • Save dhoer/8240561 to your computer and use it in GitHub Desktop.
Save dhoer/8240561 to your computer and use it in GitHub Desktop.
Resolves Chef cookbook dependencies for runlist using Berkshelf, and allows diff against a server.
require 'json'
require 'oliver/logging'
require 'chef/knife/bootstrap'
class Dep
class << self
include Logging
def list(deployment_name, knife_config_path, instance_details, options)
@berkshelf_path = "#{ENV['HOME']}/.oliver/berkshelf"
@berks_config_path = "#{@berkshelf_path}/config.json"
log.debug "berkshelf_path=#{@berkshelf_path}"
log.debug "berks_config_path=#{@berks_config_path}"
rm_berkshelf_cookbooks(options[:clean])
inst = {}
search_results = search(deployment_name, knife_config_path)
remote_servers = search_results['rows'] if options[:diff]
instance_details.each do |instance|
inst[instance[:title]] = {}
instance_run_list = instance[:run_list].split(',').sort
log.debug "instance_run_list=#{instance_run_list}"
if options[:diff]
log.debug "remote_servers=#{remote_servers}"
remote_servers.each do |server|
server_run_list = server['run_list'].sort
log.debug "server_run_list=#{server_run_list}"
if instance_run_list == server_run_list # only way to match instance is by run_list
hostname = hostname(server)
if options[:format] == 'human'
puts instance[:title] if inst[instance[:title]] == {}
puts " #{hostname}" if options[:diff]
end
cookbooks = cookbooks(server_run_list)
deps = berks_deps(cookbooks, knife_config_path, options, server)
inst[instance[:title]][hostname] = deps
end
end
else
puts instance[:title] if options[:format] == 'human'
cookbooks = cookbooks(instance_run_list)
deps = berks_deps(cookbooks, knife_config_path, options)
inst[instance[:title]] = deps
end
end
format(inst, options)
end
# extract cookbook name from recipe
def cookbook(recipe)
recipe.gsub(/recipe\[(.*)\]/, '\1').gsub(/(.*)::.*/, '\1')
end
def hostname(server)
hostname = server['automatic']['cloud']['public_hostname']
hostname = server['automatic']['cloud']['local_ipv4'] if hostname.nil?
hostname
end
# returns hash of running instances based on deployment_name
def search(deployment_name, knife_config_path)
search = knife_search(deployment_name, knife_config_path)
rows = JSON.parse(search)
log.debug "search=#{rows}"
rows['rows'].map do |server|
user = server['automatic']['current_user']
hostname = hostname(server)
log.debug "#{user}@#{hostname}"
out = server_cookbooks(user, hostname)
log.debug "server_cookbooks=#{out}"
actual_cookbooks = format_cookbook_deps(out)
server['actual_cookbooks'] = actual_cookbooks
log.debug "actual_cookbooks=#{actual_cookbooks}"
end.compact
rows
end
def knife_search(deployment_name, knife_config_path)
`knife search "chef_environment:#{deployment_name}" -Fj --config #{knife_config_path}`
end
# format cookbook deps to [{cookbook: version}]
def format_cookbook_deps(deps)
hash = {}
deps.each_line do |line|
key, value = format_cookbook_dep(line)
hash[key] = value
end
hash
end
# format cookbook deps to [{cookbook: version}]
def format_cookbook_dep(line)
line.match(/\s*(.*)\s\((.*)\)/)
return $1, $2
end
# returns cookbook dependencies on server
def server_cookbooks(user, hostname)
`ssh -q -o StrictHostKeyChecking=no #{user}@#{hostname} /bin/bash <<EOF
cd /var/chef/cache/cookbooks
egrep '^\s*version' */metadata.rb | awk -F / '{print $2 $3}' | sed 's/metadata.rb//' | sed "s/\\/\:version\\s*[\\"']/ (/g" | sed "s/[\\"']/)/g"
EOF`
end
# puts human, pretty json format (json) and compact json format (j)
# puts legend to stdout for human format
def format(instances_cookbooks, options)
case options[:format]
when 'human'
puts '*=role/run_list items'
puts 'x=diff between (expected) and [actual] cookbook version' if options[:diff]
when 'json'
puts JSON.pretty_generate(instances_cookbooks)
when 'j'
puts instances_cookbooks.to_json
else
raise "You gave me format: #{options[:format]} -- I have no idea what to do with that."
end
end
# return array of cookbooks from run_list
def cookbooks(run_list)
cookbooks = []
run_list.each do |recipe|
cookbooks << cookbook(recipe)
end
cookbooks.sort
end
def berks_deps(cookbooks, knife_config_path, options, server=nil)
berks_config(knife_config_path)
berksfile = Tempfile.new('Berksfile')
begin
cookbooks = [cookbooks] unless cookbooks.respond_to?(:to_ary)
create_berksfile(berksfile, cookbooks, options)
cmd = berks_cmd(berksfile)
deps = berks_exec(cmd, server, cookbooks, options)
log.debug "berks_deps=#{deps}"
deps
ensure
berksfile.close! # bang deletes the temp file
end
end
# remove berkshelf cookbooks from cache
def rm_berkshelf_cookbooks(clean)
if clean
log.debug "Clean #{@berkshelf_path}/cookbooks/"
`rm -fr #{@berkshelf_path}/cookbooks/`
end
end
def create_berksfile(berksfile, cookbooks, options)
berksfile.write("site :opscode\n") if options[:opscode]
berksfile.write("chef_api :config\n")
cookbooks.uniq.each do |cookbook|
berksfile.write("cookbook '#{cookbook}'\n")
end
berksfile.close
end
def berks_cmd(berksfile)
cmd = "BERKSHELF_PATH=#{@berkshelf_path} berks install -c #{@berks_config_path} -b #{File.absolute_path(berksfile)}"
log.debug "berks_cmd=#{cmd}"
cmd
end
def berks_exec(cmd, server, cookbooks, options)
deps = {}
IO.popen(cmd) do |io|
while line = io.gets
process_berks_line(line, deps, server, cookbooks, options)
end
io.close
raise "Berks exit code: #{$?.to_i}" unless $?.to_i == 0
end
deps
end
def process_berks_line(line, deps, server, cookbooks, options)
line.match(/[Installing|Using] (.*) (\(.*\))/)
cookbook = $1
version = $2.gsub(/\((.*)\)/, '\1')
server_version = options[:diff] ? server['actual_cookbooks'][cookbook] : version
run_list_item = cookbooks.include?(cookbook) ? '*' : ' '
indent = options[:diff] ? ' ' : ''
if server_version == version
puts "#{indent} #{run_list_item}#{cookbook} (#{version})"
else
puts "#{indent} x#{run_list_item}#{cookbook} (#{version}) [#{server_version}]"
end if options[:format] == 'human'
deps[cookbook] = {'run_list_item' => cookbooks.include?(cookbook) ? true : false, 'version' => version}
if options[:diff]
deps[cookbook]['server_version'] = server_version
deps[cookbook]['diff'] = (server_version == version) ? false : true
end
end
def berks_config(knife_config_path)
FileUtils.mkpath @berkshelf_path
Chef::Config.from_file(knife_config_path)
config = <<EOF
{
"chef": {
"chef_server_url": "#{Chef::Config[:chef_server_url]}",
"validation_client_name": "#{Chef::Config[:validation_client_name]}",
"validation_key_path": "#{Chef::Config[:validation_key]}",
"client_key": "#{Chef::Config[:client_key]}",
"node_name": "#{Chef::Config[:node_name]}"
}
}
EOF
log.debug "berks_config=#{config}"
File.write(@berks_config_path, config)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment