Class: Bolt::Inventory::Group

Inherits:
Object
  • Object
show all
Defined in:
lib/bolt/inventory/group.rb

Overview

Group is a specific implementation of Inventory based on nested structured data.

Constant Summary collapse

NAME_REGEX =

Regex used to validate group names and target aliases.

/\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
DATA_KEYS =
%w[name config facts vars features plugin_hooks].freeze
NODE_KEYS =
DATA_KEYS + ['alias']
GROUP_KEYS =
DATA_KEYS + %w[groups nodes]
CONFIG_KEYS =
Bolt::TRANSPORTS.keys.map(&:to_s) + ['transport']

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ Group

Returns a new instance of Group.

Raises:



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
# File 'lib/bolt/inventory/group.rb', line 19

def initialize(data)
  @logger = Logging.logger[self]

  raise ValidationError.new("Expected group to be a Hash, not #{data.class}", nil) unless data.is_a?(Hash)
  raise ValidationError.new("Group does not have a name", nil) unless data.key?('name')

  @name = data['name']
  raise ValidationError.new("Group name must be a String, not #{@name.inspect}", nil) unless @name.is_a?(String)
  raise ValidationError.new("Invalid group name #{@name}", @name) unless @name =~ NAME_REGEX

  unless (unexpected_keys = data.keys - GROUP_KEYS).empty?
    msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in group #{@name}"
    @logger.warn(msg)
  end

  @vars = fetch_value(data, 'vars', Hash)
  @facts = fetch_value(data, 'facts', Hash)
  @features = fetch_value(data, 'features', Array)
  @plugin_hooks = fetch_value(data, 'plugin_hooks', Hash)
  @config = fetch_value(data, 'config', Hash)

  unless (unexpected_keys = @config.keys - CONFIG_KEYS).empty?
    msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for group #{@name}"
    @logger.warn(msg)
  end

  nodes = fetch_value(data, 'nodes', Array)
  groups = fetch_value(data, 'groups', Array)

  @nodes = {}
  @aliases = {}
  nodes.reject { |node| node.is_a?(String) }.each do |node|
    unless node.is_a?(Hash)
      raise ValidationError.new("Node entry must be a String or Hash, not #{node.class}", @name)
    end

    if @nodes.include?(node['name'])
      @logger.warn("Ignoring duplicate node in #{@name}: #{node}")
      next
    end

    raise ValidationError.new("Node #{node} does not have a name", @name) unless node['name']
    @nodes[node['name']] = node

    unless (unexpected_keys = node.keys - NODE_KEYS).empty?
      msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in node #{node['name']}"
      @logger.warn(msg)
    end

    unless node['config'].nil? || node['config'].is_a?(Hash)
      raise ValidationError.new("Invalid configuration for node: #{node['name']}", @name)
    end

    config_keys = node['config']&.keys || []
    unless (unexpected_keys = config_keys - CONFIG_KEYS).empty?
      msg = "Found unexpected key(s) #{unexpected_keys.join(', ')} in config for node #{node['name']}"
      @logger.warn(msg)
    end

    next unless node.include?('alias')

    aliases = node['alias']
    aliases = [aliases] if aliases.is_a?(String)
    unless aliases.is_a?(Array)
      msg = "Alias entry on #{node['name']} must be a String or Array, not #{aliases.class}"
      raise ValidationError.new(msg, @name)
    end

    aliases.each do |alia|
      raise ValidationError.new("Invalid alias #{alia}", @name) unless alia =~ NAME_REGEX

      if (found = @aliases[alia])
        raise ValidationError.new(alias_conflict(alia, found, node['name']), @name)
      end
      @aliases[alia] = node['name']
    end
  end

  # If node is a string, it can refer to either a node name or alias. Which can't be determined
  # until all groups have been resolved, and requires a depth-first traversal to categorize them.
  @name_or_alias = nodes.select { |node| node.is_a?(String) }

  @groups = groups.map { |g| Group.new(g) }
end

Instance Attribute Details

#aliasesObject

Returns the value of attribute aliases.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def aliases
  @aliases
end

#configObject

Returns the value of attribute config.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def config
  @config
end

#factsObject

Returns the value of attribute facts.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def facts
  @facts
end

#featuresObject

Returns the value of attribute features.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def features
  @features
end

#groupsObject

Returns the value of attribute groups.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def groups
  @groups
end

#nameObject

Returns the value of attribute name.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def name
  @name
end

#name_or_aliasObject

Returns the value of attribute name_or_alias.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def name_or_alias
  @name_or_alias
end

#nodesObject

Returns the value of attribute nodes.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def nodes
  @nodes
end

#plugin_hooksObject

Returns the value of attribute plugin_hooks.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def plugin_hooks
  @plugin_hooks
end

#restObject

Returns the value of attribute rest.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def rest
  @rest
end

#varsObject

Returns the value of attribute vars.



8
9
10
# File 'lib/bolt/inventory/group.rb', line 8

def vars
  @vars
end

Instance Method Details

#collect_groupsObject

Return a mapping of group names to group.



260
261
262
263
264
# File 'lib/bolt/inventory/group.rb', line 260

def collect_groups
  @groups.inject(name => self) do |acc, g|
    acc.merge(g.collect_groups)
  end
end

#data_for(node_name) ⇒ Object

The data functions below expect and return nil or a hash of the schema { ‘config’ => Hash , ‘vars’ => Hash, ‘facts’ => Hash, ‘features’ => Array, groups => Array }



193
194
195
# File 'lib/bolt/inventory/group.rb', line 193

def data_for(node_name)
  data_merge(group_collect(node_name), node_collect(node_name))
end

#data_merge(data1, data2) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/bolt/inventory/group.rb', line 227

def data_merge(data1, data2)
  if data2.nil? || data1.nil?
    return data2 || data1
  end

  {
    'config' => Bolt::Util.deep_merge(data1['config'], data2['config']),
    # Shallow merge instead of deep merge so that vars with a hash value
    # are assigned a new hash, rather than merging the existing value
    # with the value meant to replace it
    'vars' => data1['vars'].merge(data2['vars']),
    'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
    'features' => data1['features'] | data2['features'],
    'plugin_hooks' => data1['plugin_hooks'].merge(data2['plugin_hooks']),
    'groups' => data2['groups'] + data1['groups']
  }
end

#empty_dataObject



218
219
220
221
222
223
224
225
# File 'lib/bolt/inventory/group.rb', line 218

def empty_data
  { 'config' => {},
    'vars' => {},
    'facts' => {},
    'features' => [],
    'plugin_hooks' => {},
    'groups' => [] }
end

#group_collect(node_name) ⇒ Object



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/bolt/inventory/group.rb', line 282

def group_collect(node_name)
  data = @groups.inject(nil) do |acc, g|
    if (d = g.data_for(node_name))
      data_merge(d, acc)
    else
      acc
    end
  end

  if data
    data_merge(group_data, data)
  elsif @nodes.include?(node_name)
    group_data
  end
end

#group_dataObject



209
210
211
212
213
214
215
216
# File 'lib/bolt/inventory/group.rb', line 209

def group_data
  { 'config' => @config,
    'vars' => @vars,
    'facts' => @facts,
    'features' => @features,
    'plugin_hooks' => @plugin_hooks,
    'groups' => [@name] }
end

#node_aliasesObject

Returns a mapping of aliases to nodes contained within the group, which includes subgroups.



253
254
255
256
257
# File 'lib/bolt/inventory/group.rb', line 253

def node_aliases
  @groups.inject(@aliases) do |acc, g|
    acc.merge(g.node_aliases)
  end
end

#node_collect(node_name) ⇒ Object



271
272
273
274
275
276
277
278
279
280
# File 'lib/bolt/inventory/group.rb', line 271

def node_collect(node_name)
  data = @groups.inject(nil) do |acc, g|
    if (d = g.node_collect(node_name))
      data_merge(d, acc)
    else
      acc
    end
  end
  data_merge(node_data(node_name), data)
end

#node_data(node_name) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
# File 'lib/bolt/inventory/group.rb', line 197

def node_data(node_name)
  if (data = @nodes[node_name])
    { 'config' => data['config'] || {},
      'vars' => data['vars'] || {},
      'facts' => data['facts'] || {},
      'features' => data['features'] || [],
      'plugin_hooks' => data['plugin_hooks'] || {},
      # groups come from group_data
      'groups' => [] }
  end
end

#node_namesObject

Returns all nodes contained within the group, which includes nodes from subgroups.



246
247
248
249
250
# File 'lib/bolt/inventory/group.rb', line 246

def node_names
  @groups.inject(local_node_names) do |acc, g|
    acc.merge(g.node_names)
  end
end

#resolve_aliases(aliases) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/bolt/inventory/group.rb', line 112

def resolve_aliases(aliases)
  @name_or_alias.each do |name_or_alias|
    # If an alias is found, insert the name into this group. Otherwise use the name as a new node.
    node_name = aliases[name_or_alias] || name_or_alias

    if @nodes.include?(node_name)
      @logger.warn("Ignoring duplicate node in #{@name}: #{node_name}")
    else
      @nodes[node_name] = { 'name' => node_name }
    end
  end

  @groups.each { |g| g.resolve_aliases(aliases) }
end

#validate(used_names = Set.new, node_names = Set.new, aliased = {}, depth = 0) ⇒ Object

Raises:



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/bolt/inventory/group.rb', line 143

def validate(used_names = Set.new, node_names = Set.new, aliased = {}, depth = 0)
  # Test if this group name conflicts with anything used before.
  raise ValidationError.new("Tried to redefine group #{@name}", @name) if used_names.include?(@name)
  raise ValidationError.new(group_node_conflict(@name), @name) if node_names.include?(@name)
  raise ValidationError.new(group_alias_conflict(@name), @name) if aliased.include?(@name)

  used_names << @name

  # Collect node names and aliases into a list used to validate that subgroups don't conflict.
  # Used names validate that previously used group names don't conflict with new node names/aliases.
  @nodes.each_key do |n|
    # Require nodes to be parseable as a Target.
    begin
      Bolt::Target.new(n)
    rescue Bolt::ParseError => e
      @logger.debug(e)
      raise ValidationError.new("Invalid node name #{n}", @name)
    end

    raise ValidationError.new(group_node_conflict(n), @name) if used_names.include?(n)
    raise ValidationError.new(alias_node_conflict(n), @name) if aliased.include?(n)

    node_names << n
  end

  @aliases.each do |n, target|
    raise ValidationError.new(group_alias_conflict(n), @name) if used_names.include?(n)
    raise ValidationError.new(alias_node_conflict(n), @name) if node_names.include?(n)

    if aliased.include?(n) && aliased[n] != target
      raise ValidationError.new(alias_conflict(n, target, aliased[n]), @name)
    end

    aliased[n] = target
  end

  @groups.each do |g|
    begin
      g.validate(used_names, node_names, aliased, depth + 1)
    rescue ValidationError => e
      e.add_parent(@name)
      raise e
    end
  end

  nil
end