1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
|
#
# 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
def delete(key)
@map.delete(key)
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
|