Last active
January 2, 2016 02:59
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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