lib/knife-instance/zestknife.rb
require 'chef/knife'
require 'knife-instance/aws'
require 'knife-instance/bootstrap_generator'
class ZestKnife < Chef::Knife
attr_accessor :base_domain
attr_accessor :internal_domain
def msg_pair(label, value, color=:cyan)
if value && !value.to_s.empty?
puts "#{ui.color(label, color)}: #{value}"
end
end
def self.aws_for_region(region)
Zest::AWS.new(Chef::Config[:knife][:aws_access_key_id], Chef::Config[:knife][:aws_secret_access_key], region)
end
def self.AWS_REGIONS
[]
end
def self.in_all_aws_regions
self.AWS_REGIONS.each do |region|
yield self.aws_for_region(region)
end
end
def find_item(klass, name)
begin
object = klass.load(name)
return [object]
rescue Net::HTTPServerException
return []
end
end
def find_ec2(name)
nodes = {}
self.class.in_all_aws_regions do |zest_aws|
nodes = nodes.merge(zest_aws.compute.servers.group_by { |s| s.tags["Name"] })
end
nodes[name].nil? ? [] : nodes[name]
end
def find_r53 name
in_zone = zone_from_name(name)
if in_zone.nil?
in_zone = zone
name = fqdn(name)
end
name = "#{name}." unless name[-1] == "."
recs = in_zone.records.select {|r| r.name == name }.to_a
recs.empty? ? [in_zone.records.get(name)].compact : recs
end
def zone
unless @zone
self.class.in_all_aws_regions do |zest_aws|
@zone ||= zest_aws.dns.zones.detect { |z| z.domain.downcase == domain }
end
raise "Could not find DNS zone" unless @zone
end
@zone
end
def zone_from_name dns_name
name, tld = dns_name.split(".")[-2..-1]
if name && tld
dns_domain = "#{name}.#{tld}"
zone = nil
self.class.in_all_aws_regions do |zest_aws|
zone1 = zest_aws.dns.zones.select {|x| x.domain =~ /^#{dns_domain}/ }.first
zone = zone1 if zone1
end
zone
end
end
def fqdn(name)
return '' if name.nil? || name.empty?
"#{name}.#{domain}"
end
def domain
@internal_domain || ""
end
def generate_hostname env
name = nil
5.times do |i|
name = random_hostname env
break if check_services(name).empty?
name = nil
srand # re-seed rand so we don't get stuck in a sequence
end
errors << "Unable to find available hostname in 5 tries" if name.nil?
name
end
def validate_hostname hostname
errors << "hostname can't be blank" and return if (hostname.nil? || hostname.empty?)
check_services(hostname).each do |service|
errors << "#{hostname} in #{service.class} already exists. Delete first."
end
end
def check_services hostname
find_item(Chef::Node, hostname) +
find_item(Chef::ApiClient, hostname) +
find_ec2(hostname) +
find_r53(hostname)
end
def random_hostname env
"#{domain_prefix}#{environment_prefix env}#{random_three_digit_number}"
end
def domain_prefix
base_domain[0]
end
def environment_prefix env
env[0]
end
def random_three_digit_number
sprintf("%03d", rand(1000))
end
def self.with_opts(*args)
invalid_args = args.select {|arg| !OPTS.keys.include? arg }
raise "Invalid option(s) passed to with_opts: #{invalid_args.join(", ")}" unless invalid_args.empty?
args.each do |arg|
option arg, OPTS[arg]
end
end
class << self; attr_accessor :validated_opts end
def self.with_validated_opts(*args)
with_opts(*args)
validates(*args)
end
def self.validates(*args)
raise "Invalid argument(s) passed to validates: #{args - VALIDATORS.keys}" unless (args - VALIDATORS.keys).empty?
self.validated_opts ||= []
self.validated_opts.concat args
end
def setup_config(keys=[:aws_access_key_id, :aws_secret_access_key])
keys.each do |k|
Chef::Config[:knife][k] = ENV[k.to_s] if Chef::Config[:knife][k].nil? && ENV[k.to_s]
end
end
def errors
@errors ||= []
end
def errors?
!errors.empty?
end
def validate!(keys=[:aws_access_key_id, :aws_secret_access_key])
keys.each do |k|
if Chef::Config[:knife][k].nil? & config[k].nil?
errors << "You did not provide a valid '#{k}' value."
end
end
self.class.validated_opts.each do |opt|
send VALIDATORS[opt]
end if self.class.validated_opts
if errors.each { |e| ui.error(e) }.any?
exit 1
end
end
def validate_env
end
def validate_domain
end
def validate_region
end
def validate_force_deploy
end
def validate_color
unless @color
errors << "You must provide a cluster_tag with the -t option"
end
end
def validate_prod
end
OPTS = {
:aws_access_key_id => {
:short => "-A ID",
:long => "--aws-access-key-id KEY",
:description => "Your AWS Access Key ID",
:proc => Proc.new { |key| Chef::Config[:knife][:aws_access_key_id] = key }
},
:aws_secret_access_key => {
:short => "-K SECRET",
:long => "--aws-secret-access-key SECRET",
:description => "Your AWS API Secret Access Key",
:proc => Proc.new { |key| Chef::Config[:knife][:aws_secret_access_key] = key }
},
:cluster_tag => {
:short => "-t TAG",
:long => "--cluster-tag TAG",
:description => "Tag that identifies this node as part of the <TAG> cluster"
},
:environment => {
:short => "-E CHEF_ENV",
:long => "--environment CHEF_ENV",
:description => "Chef environment"
},
:region => {
:long => "--region REGION",
:short => '-R REGION',
:description => "Your AWS region",
:default => ENV['AWS_REGION'],
:proc => Proc.new { |key| Chef::Config[:knife][:region] = key }
},
:encrypted_data_bag_secret => {
:short => "-B FILE",
:long => "--encrypted_data_bag_secret FILE",
:description => "Path to the secret key to unlock encrypted chef data bags",
:proc => Proc.new { |file| File.expand_path(file) },
:default => File.expand_path(ENV['DATABAG_KEY_PATH'])
},
:aws_ssh_key_id => {
:short => "-S KEY",
:long => "--aws-ssh-key KEY",
:description => "AWS EC2 SSH Key Pair Name",
:default => ENV["aws_ssh_key"]
},
:base_domain => {
:long => "--base-domain DOMAIN",
:description => "The domain to be used for this node.",
:default => ENV["default_base_domain"] || ""
},
:wait_for_it => {
:short => "-W",
:long => "--wait-for-it",
:description => "Wait for EC2 to return extended details about the host and register DNS",
:boolean => true,
:default => false
},
:prod => {
:long => "--prod",
:description => "If the environment for your command is production, you must also pass this parameter. This is to make it slightly harder to do something unintentionally to production."
}
}
VALIDATORS = {
:environment => :validate_env,
:base_domain => :validate_domain,
:cluster_tag => :validate_color,
:prod => :validate_prod,
:force_deploy => :validate_force_deploy,
:region => :validate_region
}
end