require 'ironfan/requirements' module Ironfan @@clusters ||= Hash.new @@realms ||= Hash.new # path to search for cluster definition files def self.cluster_path return Array(Chef::Config[:cluster_path]) if Chef::Config[:cluster_path] raise "Holy smokes, you have no cookbook_path or cluster_path set up. Follow chef's directions for creating a knife.rb." if Chef::Config[:cookbook_path].blank? cl_path = Chef::Config[:cookbook_path].map{|dir| File.expand_path('../clusters', dir) }.uniq ui.warn "No cluster path set. Taking a wild guess that #{cl_path.inspect} is \nreasonable based on your cookbook_path -- but please set cluster_path in your knife.rb" Chef::Config[:cluster_path] = cl_path end # # Delegates def self.clusters @@clusters end # # Delegates def self.realms @@realms end def self.ui=(ui) @ui = ui ; end def self.ui() @ui ; end def self.chef_config=(cc) @chef_config = cc ; end def self.chef_config() @chef_config ; end # execute against multiple targets in parallel def self.parallel(targets) raise 'missing block' unless block_given? results = [] [targets].flatten.each_with_index.map do |target, idx| sleep(0.25) # avoid hammering with simultaneous requests Thread.new(target) do |target| results[idx] = safely(target.inspect) do yield target end end end.each(&:join) # wait for all the blocks to return results end # # Defines a cluster with the given name. # # @example # Ironfan.cluster 'demosimple' do # cloud :ec2 do # availability_zones ['us-east-1d'] # flavor "t1.micro" # image_name "ubuntu-natty" # end # role :base_role # role :chef_client # # facet :sandbox do # instances 2 # role :nfs_client # end # end # # def self.cluster(name, attrs={}, &block) name = name.to_sym # If this is being called as Ironfan.cluster('foo') with no additional arguments, # return the cached cluster object if it exists if @@clusters[name] and attrs.empty? and not block_given? return @@clusters[name] else # Otherwise we're being asked to (re)initialize and cache a cluster definition cl = Ironfan::Dsl::Cluster.new(:name => name) cl.receive!(attrs, &block) @@clusters[name] = cl.resolve end end def self.realm(name, attrs={}, &block) name = name.to_sym if @@realms[name] and attrs.empty? and not block_given? return @@realms[name] else rlm = Ironfan::Dsl::Realm.new(:name => name) rlm.receive!(attrs, &block) rlm.clusters.keys.each{|k| @@clusters[k.to_sym] = rlm.clusters[k].resolve} @@realms[name] = rlm end end # # Return cluster if it's defined. Otherwise, search Ironfan.cluster_path # for an eponymous file, load it, and return the cluster it defines. # # Raises an error if a matching file isn't found, or if loading that file # doesn't define the requested cluster. # # @return [Ironfan::Cluster] the requested cluster def self.load_cluster(name) name = name.to_sym raise ArgumentError, "Please supply a cluster name" if name.to_s.empty? return @@clusters[name] if @@clusters[name] cluster_path.each do |cp_dir| Dir[ File.join(cp_dir, '*.rb') ].each do |filename| Chef::Log.info("Loading cluster file #{filename}") require filename end end unless @@clusters[name] then die("Couldn't find a cluster definition for #{name} in #{cluster_path}") end @@clusters[name] end # # Map from cluster name to file name # # @return [Hash] map from cluster name to file name def self.cluster_filenames return @cluster_filenames if @cluster_filenames @cluster_filenames = {} cluster_path.each do |cp_dir| Dir[ File.join(cp_dir, '*.rb') ].each do |filename| cluster_name = File.basename(filename).gsub(/\.rb$/, '') @cluster_filenames[cluster_name.to_sym] ||= filename end end @cluster_filenames end # # Utility to die with an error message. # If the last arg is an integer, use it as the exit code. # def self.die *strings exit_code = strings.last.is_a?(Integer) ? strings.pop : -1 strings.each{|str| ui.warn str } exit exit_code end # # Utility to turn an error into a warning # # @example # Ironfan.safely do # Ironfan.fog_connection.associate_address(self.fog_server.id, address) # end # def self.safely(info="") begin yield rescue StandardError => err ui.warn("Error running #{info}:") ui.warn(err) Chef::Log.error( err ) Chef::Log.error( err.backtrace.join("\n") ) return err end end # # Utility to retry a flaky operation three times, with ascending wait times # # FIXME: Add specs to test the rescue here. It's a PITA to debug naturally or # # Manual test: # bundle exec ruby -e "require 'chef'; require 'ironfan'; Ironfan.tell_you_thrice { p 'hah'; raise 'hell' }" def self.tell_you_thrice(options={}) options = { name: "problem", error_class: StandardError, retries: 3, multiplier: 3 }.merge!(options) try = 0 message = '' begin try += 1 yield rescue options[:error_class] => err raise unless try < options[:retries] pause_for = options[:multiplier] * try Chef::Log.debug "Caught error (was #{err.inspect}). Sleeping #{pause_for} seconds." sleep pause_for retry end end # # Utility to show a step of the overall process # def self.step(name, desc, *style) ui.info(" #{"%-15s" % (name.to_s+":")}\t#{ui.color(desc.to_s, *style)}") end def self.substep(name, desc, color = :gray) step(name, " - #{desc}", color) if (verbosity >= 1 or color != :gray) end def self.verbosity chef_config[:verbosity].to_i end # Output a TODO to the logs if you've switched on pestering def self.todo(*args) Chef::Log.debug(*args) if Chef::Config[:show_todo] end # # Utility to do mock out a step during a dry-run # def self.unless_dry_run if dry_run? ui.info(" ... but not really") return nil else yield end end def self.dry_run? chef_config[:dry_run] end # Intentionally skipping an implied step def self.noop(source,method,*params) # Chef::Log.debug("#{method} is a no-op for #{source} -- skipping (#{params.join(',')})") end end