module Rudy # = Rudy::Huxtable # # Huxtable gives access to instances for config, global, and logger to any # class that includes it. # # class Rudy::Hello # include Rudy::Huxtable # # def print_config # p self.config.defaults # end # # end # module Huxtable # TODO: investigate @@debug bug. When this is true, Caesars.debug? returns true # too. It's possible this is intentional but probably not. @@debug = false @@config = Rudy::Config.new @@global = Rudy::Global.new @@logger = StringIO.new # BUG: memory-leak for long-running apps @@sacred_params = [:accesskey, :secretkey, :cert, :privatekey] # NOTE: These methods conflict with Drydock::Command classes. It's # probably a good idea to not expose these anyway since it can be # done via Rudy::Huxtable.update_global etc... #def config; @@config; end #def global; @@global; end #def logger; @@logger; end def self.update_config(path=nil) # nil or otherwise bad paths send to look_and_load are ignored @@config.look_and_load(path || nil) @@global.apply_config(@@config) end update_config def self.update_global(ghash={}) @@global.update(ghash) end def self.update_logger(logger) @@logger = logger end def self.create_domain @sdb = Rudy::AWS::SDB.new(@@global.accesskey, @@global.secretkey, @@global.region) @sdb.create_domain Rudy::DOMAIN end def self.domain_exists? @sdb = Rudy::AWS::SDB.new(@@global.accesskey, @@global.secretkey, @@global.region) (@sdb.list_domains || []).member? Rudy::DOMAIN end def self.domain Rudy::DOMAIN end def self.change_zone(v); @@global.zone = v; end def self.change_role(v); @@global.role = v; end def self.change_region(v); @@global.region = v; end def self.change_environment(v); @@global.environment = v; end def self.change_position(v); @@global.position = v; end def debug?; @@debug == true; end def check_keys raise "No EC2 .pem keys provided" unless has_pem_keys? raise "No SSH key provided for #{current_user}!" unless has_keypair? raise "No SSH key provided for root!" unless has_keypair?(:root) end def has_pem_keys? (@@global.cert && File.exists?(@@global.cert) && @@global.privatekey && File.exists?(@@global.privatekey)) end def has_keys? (@@global.accesskey && !@@global.accesskey.empty? && @@global.secretkey && !@@global.secretkey.empty?) end def config_dirname raise "No config paths defined" unless @@config.is_a?(Rudy::Config) && @@config.paths.is_a?(Array) base_dir = File.dirname @@config.paths.first raise "Config directory doesn't exist #{base_dir}" unless File.exists?(base_dir) base_dir end def has_keypair?(name=nil) kp = user_keypairpath(name) (!kp.nil? && File.exists?(kp)) end # Returns the name of the current keypair for the given user. # If there's a private key path in the config this will return # the basename (it's assumed the Amazon KeyPair has the same # name as the file). Otherwise this returns the Rudy style # name: key-ENV-ROLE-USER. Or if this the user is # root: key-ENV-ROLE def user_keypairname(user) kp = user_keypairpath(user) if kp kp = Huxtable.keypair_path_to_name(kp) else n = (user.to_s == 'root') ? '' : "-#{user}" "key-%s%s" % [current_machine_group, n] end end def root_keypairname user_keypairname :root end def user_keypairpath(name) raise "No user provided" unless name raise "No configuration" unless @@config raise "No machines configuration" unless @@config.machines zon, env, rol = @@global.zone, @@global.environment, @@global.role #Caesars.enable_debug path = @@config.machines.find_deferred(zon, env, rol, [:users, name, :keypair]) path ||= @@config.machines.find_deferred(env, rol, [:users, name, :keypair]) path ||= @@config.machines.find_deferred(rol, [:users, name, :keypair]) # EC2 Keypairs that were created are intended for starting the machine instances. # These are used as the root SSH keys. If we can find a user defined key, we'll # check the config path for a generated one. if !path && name.to_s == 'root' path = File.join(self.config_dirname, "key-#{current_machine_group}") end path = File.expand_path(path) if path && File.exists?(path) path end def root_keypairpath user_keypairpath :root end def has_root_keypair? path = user_keypairpath(:root) (!path.nil? && !path.empty?) end def current_user @@global.user end def current_user_keypairpath user_keypairpath(current_user) end def current_machine_hostname(group=nil) group ||= machine_group find_machine(group)[:dns_name] end def current_machine_group [@@global.environment, @@global.role].join(Rudy::DELIM) end def current_group_name "g-#{current_machine_group}" end def current_machine_count fetch_machine_param(:positions) || 1 end def current_machine_image fetch_machine_param(:ami) #zon, env, rol = @@global.zone, @@global.environment, @@global.role #ami = @@config.machines.find_deferred([zon, env, rol]) || {} #ami.merge!(@@config.machines.find_deferred(env, rol, :ami)) #ami.merge!(@@config.machines.find_deferred(rol, :ami)) ## I commented this out while cleaning (start of 0.6 branch) . It ## seems like a bad idea. I don't want Huxtables throwing exceptions. ##raise Rudy::NoMachineImage, current_machine_group unless ami #ami end def current_machine_size fetch_machine_param(:size) || 'm1.small' end def current_machine_address raise "No configuration" unless @@config raise "No machines configuration" unless @@config.machines @@config.machines.find_deferred(@@global.environment, @@global.role, :address) end # TODO: fix machine_group to include zone def current_machine_name [@@global.zone, current_machine_group, @@global.position].join(Rudy::DELIM) end # +name+ the name of the remote user to use for the remainder of the command # (or until switched again). If no name is provided, the user will be revert # to whatever it was before the previous switch. # TODO: deprecate def switch_user(name=nil) if name == nil && @switch_user_previous @@global.user = @switch_user_previous elsif @@global.user != name raise "No root keypair defined for #{name}!" unless has_keypair?(name) @@logger.puts "Remote commands will be run as #{name} user" @switch_user_previous = @@global.user @@global.user = name end end def group_metadata(env=@@global.environment, role=@@global.role) query = "['environment' = '#{env}'] intersection ['role' = '#{role}']" @sdb.query_with_attributes(Rudy::DOMAIN, query) end def self.keypair_path_to_name(kp) return nil unless kp name = File.basename kp #name.gsub(/key-/, '') # We keep the key- now end private # We grab the appropriate routines config and check the paths # against those defined for the matching machine group. # Disks that appear in a routine but not in a machine will be # removed and a warning printed. Otherwise, the routines config # is merged on top of the machine config and that's what we return. # # This means that all the disk info is returned so we know what # size they are and stuff. # Return a hash: # # :after: # - :root: pwd # - :rudy: pwd # :disks: # :create: # /rudy/example1: # :device: /dev/sdr # :size: 2 # /rudy/example2: # :device: /dev/sdm # :size: 1 # def fetch_routine_config(action) raise "No configuration" unless @@config raise "No routines configuration" unless @@config.routines raise "No globals" unless @@global zon, env, rol = @@global.zone, @@global.environment, @@global.role disk_defs = fetch_machine_param(:disks) routine = @@config.routines.find(@@global.environment, @@global.role, action) return nil unless routine routine.disks.each_pair do |raction,disks| disks.each_pair do |path, props| unless disk_defs.has_key?(path) @logger.puts "#{path} is not defined. Check your #{action} routines config.".color(:red) routine.disks[raction].delete(path) next end routine.disks[raction][path] = disk_defs[path].merge(props) end end routine end # Looks for ENV-ROLE configuration in machines. There must be # at least one definition in the config for this to return true # That's how Rudy knows the current group is defined. def known_machine_group? return false if !@@config && !@@global zon, env, rol = @@global.zone, @@global.environment, @@global.role conf = @@config.machines.find_deferred(@@global.region, zon, [env, rol]) conf ||= @@config.machines.find_deferred(zon, [env, rol]) !conf.nil? end def fetch_machine_param(parameter) raise "No configuration" unless @@config raise "No machines configuration" unless @@config.machines raise "No globals" unless @@global top_level = @@config.machines.find(parameter) mc = fetch_machine_config mc[parameter] || top_level || nil end def fetch_machine_config raise "No configuration" unless @@config raise "No machines configuration" unless @@config.machines raise "No globals" unless @@global zon, env, rol = @@global.zone, @@global.environment, @@global.role hashes = [] hashes << @@config.machines.find(env, rol) hashes << @@config.machines.find(zon, env, rol) hashes << @@config.machines.find(zon, [env, rol]) hashes << @@config.machines.find(zon, env) hashes << @@config.machines.find(env) hashes << @@config.machines.find(zon) compilation = {} hashes.reverse.each do |conf| compilation.merge! conf if conf end compilation = nil if compilation.empty? compilation end # Returns the appropriate config block from the machines config. # Also adds the following unless otherwise specified: # :region, :zone, :environment, :role, :position def fetch_script_config sconf = fetch_machine_param :config extras = { :region => @@global.region, :zone => @@global.zone, :environment => @@global.environment, :role => @@global.role, :position => @@global.position } sconf.merge! extras sconf end end end