summaryrefslogtreecommitdiff
path: root/lib/chef/node_map.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/node_map.rb')
-rw-r--r--lib/chef/node_map.rb146
1 files changed, 146 insertions, 0 deletions
diff --git a/lib/chef/node_map.rb b/lib/chef/node_map.rb
new file mode 100644
index 0000000000..2ca6d9ba17
--- /dev/null
+++ b/lib/chef/node_map.rb
@@ -0,0 +1,146 @@
+#
+# Author:: Lamont Granquist (<lamont@chef.io>)
+# Copyright:: Copyright (c) 2014 Chef Software, 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.
+#
+
+class Chef
+ class NodeMap
+
+ VALID_OPTS = [
+ :on_platform,
+ :on_platforms,
+ :platform,
+ :os,
+ :platform_family,
+ ]
+
+ DEPRECATED_OPTS = [
+ :on_platform,
+ :on_platforms,
+ ]
+
+ # Create a new NodeMap
+ #
+ def initialize
+ @map = {}
+ end
+
+ # Set a key/value pair on the map with a filter. The filter must be true
+ # when applied to the node in order to retrieve the value.
+ #
+ # @param key [Object] Key to store
+ # @param value [Object] Value associated with the key
+ # @param filters [Hash] Node filter options to apply to key retrieval
+ # @yield [node] Arbitrary node filter as a block which takes a node argument
+ # @return [NodeMap] Returns self for possible chaining
+ #
+ def set(key, value, filters = {}, &block)
+ validate_filter!(filters)
+ deprecate_filter!(filters)
+ @map[key] ||= []
+ # we match on the first value we find, so we want to unshift so that the
+ # last setter wins
+ # FIXME: need a test for this behavior
+ @map[key].unshift({ filters: filters, block: block, value: value })
+ self
+ end
+
+ # Get a value from the NodeMap via applying the node to the filters that
+ # were set on the key.
+ #
+ # @param node [Chef::Node] The Chef::Node object for the run
+ # @param key [Object] Key to look up
+ # @return [Object] Value
+ #
+ def get(node, key)
+ # FIXME: real exception
+ raise "first argument must be a Chef::Node" unless node.is_a?(Chef::Node)
+ return nil unless @map.has_key?(key)
+ @map[key].each do |matcher|
+ if filters_match?(node, matcher[:filters]) &&
+ block_matches?(node, matcher[:block])
+ return matcher[:value]
+ end
+ end
+ nil
+ end
+
+ private
+
+ # only allow valid filter options
+ def validate_filter!(filters)
+ filters.each_key do |key|
+ # FIXME: real exception
+ raise "Bad key #{key} in Chef::NodeMap filter expression" unless VALID_OPTS.include?(key)
+ end
+ end
+
+ # warn on deprecated filter options
+ def deprecate_filter!(filters)
+ filters.each_key do |key|
+ Chef::Log.warn "The #{key} option to node_map has been deprecated" if DEPRECATED_OPTS.include?(key)
+ end
+ end
+
+ # @todo: this works fine, but is probably hard to understand
+ def negative_match(filter, param)
+ # We support strings prefaced by '!' to mean 'not'. In particular, this is most useful
+ # for os matching on '!windows'.
+ negative_matches = filter.select { |f| f[0] == '!' }
+ return true if !negative_matches.empty? && negative_matches.include?('!' + param)
+
+ # We support the symbol :all to match everything, for backcompat, but this can and should
+ # simply be ommitted.
+ positive_matches = filter.reject { |f| f[0] == '!' || f == :all }
+ return true if !positive_matches.empty? && !positive_matches.include?(param)
+
+ # sorry double-negative: this means we pass this filter.
+ false
+ end
+
+ def filters_match?(node, filters)
+ return true if filters.empty?
+
+ # each filter is applied in turn. if any fail, then it shortcuts and returns false.
+ # if it passes or does not exist it succeeds and continues on. so multiple filters are
+ # effectively joined by 'and'. all filters can be single strings, or arrays which are
+ # effectively joined by 'or'.
+
+ os_filter = [ filters[:os] ].flatten.compact
+ unless os_filter.empty?
+ return false if negative_match(os_filter, node[:os])
+ end
+
+ platform_family_filter = [ filters[:platform_family] ].flatten.compact
+ unless platform_family_filter.empty?
+ return false if negative_match(platform_family_filter, node[:platform_family])
+ end
+
+ # :on_platform and :on_platforms here are synonyms which are deprecated
+ platform_filter = [ filters[:platform] || filters[:on_platform] || filters[:on_platforms] ].flatten.compact
+ unless platform_filter.empty?
+ return false if negative_match(platform_filter, node[:platform])
+ end
+
+ return true
+ end
+
+ def block_matches?(node, block)
+ return true if block.nil?
+ block.call node
+ end
+ end
+end