class LookupKey < ActiveRecord::Base include Authorization KEY_TYPES = %w( string boolean integer real array hash yaml json ) VALIDATOR_TYPES = %w( regexp list ) TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON', 'yes', 'YES', 'y', 'Y'].to_set FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF', 'no', 'NO', 'n', 'N'].to_set KEY_DELM = "," EQ_DELM = "=" serialize :default_value belongs_to :puppetclass has_many :environment_classes, :dependent => :destroy has_many :environments, :through => :environment_classes, :uniq => true has_many :param_classes, :through => :environment_classes, :source => :puppetclass def param_class param_classes.first end has_many :lookup_values, :dependent => :destroy, :inverse_of => :lookup_key accepts_nested_attributes_for :lookup_values, :reject_if => lambda { |a| a[:value].blank? }, :allow_destroy => true before_validation :validate_and_cast_default_value validates_uniqueness_of :key, :unless => Proc.new{|p| p.is_param?} validates_presence_of :key validates_presence_of :puppetclass_id, :unless => Proc.new {|k| k.is_param?} validates_inclusion_of :validator_type, :in => VALIDATOR_TYPES, :message => "invalid", :allow_blank => true, :allow_nil => true validates_inclusion_of :key_type, :in => KEY_TYPES, :message => "invalid", :allow_blank => true, :allow_nil => true validate :validate_list, :validate_regexp validates_associated :lookup_values validate :ensure_type before_save :sanitize_path scoped_search :on => :key, :complete_value => true, :default_order => true scoped_search :on => :override, :complete_value => {:true => true, :false => false} scoped_search :in => :param_class, :on => :name, :rename => :puppetclass, :complete_value => true scoped_search :in => :lookup_values, :on => :value, :rename => :value, :complete_value => true default_scope :order => 'lookup_keys.key' scope :override, where(:override => true) scope :parameters_for_class, lambda {|puppetclass_ids, environment_id| override.joins(:environment_classes).where(:environment_classes => {:puppetclass_id => puppetclass_ids, :environment_id => environment_id}) } def to_param "#{id}-#{key}" end def to_s key end # params: # +host: The considered Host instance. # +options+: A hash containing the following, optional keys: # +obs_matcher_block+: Callback to notify with extra information. # It is given a hash having the following structure: # +{ :host => #<Host>, :used_matched => "fact=value", :value => #<Value> }+ # +skip_fqdn+: Boolean value indicating whether to skip the fqdn matcher. Defaults to false. # Useful to give the previous value, prior to an eventual override. def value_for host, options = {} skip_fqdn = options[:skip_fqdn] || false obs_matcher_block = options[:obs_matcher_block] path2matches(host).each do |match| next if skip_fqdn and match =~ /^fqdn\s*=/ if (v = lookup_values.find_by_match(match)) obs_matcher_block.call({:host => host, :used_matcher => match, :value => v.value}) if obs_matcher_block return v.value end end if (!is_param || (is_param && override)) && lookup_values.any? default_value end def path path = read_attribute(:path) path.blank? ? array2path(Setting["Default_variables_Lookup_Path"]) : path end def path=(v) return unless v using_default = v.tr("\r","") == array2path(Setting["Default_variables_Lookup_Path"]) write_attribute(:path, using_default ? nil : v) end def default_value_before_type_cast value_before_type_cast default_value end def value_before_type_cast val case key_type.to_sym when :json val = JSON.dump val when :yaml, :hash val = YAML.dump val val.sub!(/\A---\s*$\n/, '') when :array val = val.inspect end unless key_type.blank? val end # Returns the casted value, or raises a TypeError def cast_validate_value value method = "cast_value_#{key_type}".to_sym return value unless self.respond_to? method, true self.send(method, value) rescue raise TypeError end def as_json(options={}) options ||= {} super({:only => [:key, :is_param, :required, :override, :description, :default_value, :id]}.merge(options)) end def path_elements path.split.map do |paths| paths.split(KEY_DELM).map do |element| element end end end private # Generate possible lookup values type matches to a given host def path2matches host raise ::Foreman::Exception.new(N_("Invalid Host")) unless host.class.model_name == "Host" matches = [] path_elements.each do |rule| match = [] rule.each do |element| match << "#{element}#{EQ_DELM}#{attr_to_value(host,element)}" end matches << match.join(KEY_DELM) end matches end # translates an element such as domain to its real value per host # tries to find the host attribute first, parameters and then fallback to a puppet fact. def attr_to_value host, element # direct host attribute return host.send(element) if host.respond_to?(element) # host parameter return host.host_params[element] if host.host_params.include?(element) # fact attribute if (fn = host.fact_names.first(:conditions => { :name => element })) return FactValue.where(:host_id => host.id, :fact_name_id => fn.id).first.value end end def sanitize_path self.path = path.tr("\s","").downcase unless path.blank? end def array2path array raise ::Foreman::Exception.new(N_("invalid path")) unless array.is_a?(Array) array.map do |sub_array| sub_array.is_a?(Array) ? sub_array.join(KEY_DELM) : sub_array end.join("\n") end def validate_and_cast_default_value return true if default_value.nil? begin self.default_value = cast_validate_value self.default_value true rescue errors.add(:default_value, _("is invalid")) false end end def cast_value_boolean value return true if TRUE_VALUES.include? value return false if FALSE_VALUES.include? value raise TypeError end def cast_value_integer value return value.to_i if value.is_a?(Numeric) if value.is_a?(String) if value =~ /^0x[0-9a-f]+$/i value.to_i(16) elsif value =~ /^0[0-7]+$/ value.to_i(8) elsif value =~ /^-?\d+$/ value.to_i else raise TypeError end end end def cast_value_real value return value if value.is_a? Numeric if value.is_a?(String) if value =~ /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?$/ value.to_f else cast_value_integer value end end end def load_yaml_or_json value return value unless value.is_a? String begin JSON.load value rescue YAML.load value end end def cast_value_array value return value if value.is_a? Array return value.to_a if not value.is_a? String and value.is_a? Enumerable value = load_yaml_or_json value raise TypeError unless value.is_a? Array value end def cast_value_hash value return value if value.is_a? Hash value = load_yaml_or_json value raise TypeError unless value.is_a? Hash value end def cast_value_yaml value value = YAML.load value end def cast_value_json value value = JSON.load value end def ensure_type if puppetclass_id.present? and is_param? self.errors.add(:base, _('Global variable or class Parameter, not both')) end end def validate_regexp return true unless (validator_type == 'regexp') errors.add(:default_value, _("is invalid")) and return false unless (default_value =~ /#{validator_rule}/) end def validate_list return true unless (validator_type == 'list') errors.add(:default_value, _("%{default_value} is not one of %{validator_rule}") % { :default_value => default_value, :validator_rule => validator_rule }) and return false unless validator_rule.split(KEY_DELM).map(&:strip).include?(default_value) end end