#-- # Author:: Adam Jacob () # Author:: AJ Christensen () # Copyright:: Copyright (c) 2008 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef/node/immutable_collections' require 'chef/node/attribute_collections' require 'chef/mixin/deep_merge' require 'chef/log' class Chef class Node # == Attribute # Attribute implements a nested key-value (Hash) and flat collection # (Array) data structure supporting multiple levels of precedence, such # that a given key may have multiple values internally, but will only # return the highest precedence value when reading. class Attribute < Mash include Immutablize include Enumerable # List of the component attribute hashes, in order of precedence, low to # high. COMPONENTS = [ :@default, :@env_default, :@role_default, :@force_default, :@normal, :@override, :@role_override, :@env_override, :@force_override, :@automatic ].freeze DEFAULT_COMPONENTS = [ :@default, :@env_default, :@role_default, :@force_default ] OVERRIDE_COMPONENTS = [ :@override, :@role_override, :@env_override, :@force_override ] [:all?, :any?, :assoc, :chunk, :collect, :collect_concat, :compare_by_identity, :compare_by_identity?, :count, :cycle, :detect, :drop, :drop_while, :each, :each_cons, :each_entry, :each_key, :each_pair, :each_slice, :each_value, :each_with_index, :each_with_object, :empty?, :entries, :except, :fetch, :find, :find_all, :find_index, :first, :flat_map, :flatten, :grep, :group_by, :has_value?, :include?, :index, :inject, :invert, :key, :keys, :length, :map, :max, :max_by, :merge, :min, :min_by, :minmax, :minmax_by, :none?, :one?, :partition, :rassoc, :reduce, :reject, :reverse_each, :select, :size, :slice_before, :sort, :sort_by, :store, :symbolize_keys, :take, :take_while, :to_a, :to_hash, :to_set, :value?, :values, :values_at, :zip].each do |delegated_method| class_eval(<<-METHOD_DEFN) def #{delegated_method}(*args, &block) merged_attributes.send(:#{delegated_method}, *args, &block) end METHOD_DEFN end # return the cookbook level default attribute component attr_reader :default # return the role level default attribute component attr_reader :role_default # return the environment level default attribute component attr_reader :env_default # return the force_default level attribute component attr_reader :force_default # default! is the "advertised" method for force_default, but is # implemented as an alias because instance variables can't (easily) have # +!+ characters. alias :default! :force_default # return the "normal" level attribute component attr_reader :normal # return the cookbook level override attribute component attr_reader :override # return the role level override attribute component attr_reader :role_override # return the enviroment level override attribute component attr_reader :env_override # return the force override level attribute component attr_reader :force_override # +override!+ is the "advertised" method for +force_override+ but is # implemented as an alias because instance variables can't easily have # +!+ characters. alias :override! :force_override # return the automatic level attribute component attr_reader :automatic def initialize(normal, default, override, automatic) @set_unless_present = false @default = VividMash.new(self, default) @env_default = VividMash.new(self, {}) @role_default = VividMash.new(self, {}) @force_default = VividMash.new(self, {}) @normal = VividMash.new(self, normal) @override = VividMash.new(self, override) @role_override = VividMash.new(self, {}) @env_override = VividMash.new(self, {}) @force_override = VividMash.new(self, {}) @automatic = VividMash.new(self, automatic) @merged_attributes = nil @combined_override = nil @combined_default = nil end # Debug what's going on with an attribute. +args+ is a path spec to the # attribute you're interested in. For example, to debug where the value # of `node[:network][:default_interface]` is coming from, use: # debug_value(:network, :default_interface). # The return value is an Array of Arrays. The first element is # `["set_unless_enabled?", Boolean]`, which describes whether the # attribute collection is in "set_unless" mode. The rest of the Arrays # are pairs of `["precedence_level", value]`, where precedence level is # the component, such as role default, normal, etc. and value is the # attribute value set at that precedence level. If there is no value at # that precedence level, +value+ will be the symbol +:not_present+. def debug_value(*args) components = COMPONENTS.map do |component| ivar = instance_variable_get(component) value = args.inject(ivar) do |so_far, key| if so_far == :not_present :not_present elsif so_far.has_key?(key) so_far[key] else :not_present end end [component.to_s.sub(/^@/,""), value] end [["set_unless_enabled?", @set_unless_present]] + components end # Enables or disables `||=`-like attribute setting. See, e.g., Node#set_unless def set_unless_value_present=(setting) @set_unless_present = setting end # Clears merged_attributes, which will cause it to be recomputed on the # next access. def reset_cache @merged_attributes = nil @combined_default = nil @combined_override = nil @set_unless_present = false end alias :reset :reset_cache # Set the cookbook level default attribute component to +new_data+. def default=(new_data) reset @default = VividMash.new(self, new_data) end # Set the role level default attribute component to +new_data+ def role_default=(new_data) reset @role_default = VividMash.new(self, new_data) end # Set the environment level default attribute component to +new_data+ def env_default=(new_data) reset @env_default = VividMash.new(self, new_data) end # Set the force_default (+default!+) level attributes to +new_data+ def force_default=(new_data) reset @force_default = VividMash.new(self, new_data) end # Set the normal level attribute component to +new_data+ def normal=(new_data) reset @normal = VividMash.new(self, new_data) end # Set the cookbook level override attribute component to +new_data+ def override=(new_data) reset @override = VividMash.new(self, new_data) end # Set the role level override attribute component to +new_data+ def role_override=(new_data) reset @role_override = VividMash.new(self, new_data) end # Set the environment level override attribute component to +new_data+ def env_override=(new_data) reset @env_override = VividMash.new(self, new_data) end def force_override=(new_data) reset @force_override = VividMash.new(self, new_data) end def automatic=(new_data) reset @automatic = VividMash.new(self, new_data) end def merged_attributes @merged_attributes ||= begin components = [merge_defaults, @normal, merge_overrides, @automatic] resolved_attrs = components.inject(Mash.new) do |merged, component| Chef::Mixin::DeepMerge.hash_only_merge(merged, component) end immutablize(resolved_attrs) end end def combined_override @combined_override ||= immutablize(merge_overrides) end def combined_default @combined_default ||= immutablize(merge_defaults) end def [](key) merged_attributes[key] end def []=(key, value) merged_attributes[key] = value end def has_key?(key) COMPONENTS.any? do |component_ivar| instance_variable_get(component_ivar).has_key?(key) end end alias :attribute? :has_key? alias :member? :has_key? alias :include? :has_key? alias :key? :has_key? alias :each_attribute :each def method_missing(symbol, *args) if args.empty? if key?(symbol) self[symbol] else raise NoMethodError, "Undefined method or attribute `#{symbol}' on `node'" end elsif symbol.to_s =~ /=$/ key_to_set = symbol.to_s[/^(.+)=$/, 1] self[key_to_set] = (args.length == 1 ? args[0] : args) else raise NoMethodError, "Undefined node attribute or method `#{symbol}' on `node'" end end def inspect "#<#{self.class} " << (COMPONENTS + [:@merged_attributes, :@properties]).map{|iv| "#{iv}=#{instance_variable_get(iv).inspect}" }.join(', ') << ">" end def set_unless? @set_unless_present end private def merge_defaults DEFAULT_COMPONENTS.inject(Mash.new) do |merged, component_ivar| component_value = instance_variable_get(component_ivar) Chef::Mixin::DeepMerge.merge(merged, component_value) end end def merge_overrides OVERRIDE_COMPONENTS.inject(Mash.new) do |merged, component_ivar| component_value = instance_variable_get(component_ivar) Chef::Mixin::DeepMerge.merge(merged, component_value) end end end end end