# frozen_string_literal: true require 'bolt/inventory/group2' module Bolt class Inventory class Inventory2 def initialize(data, config = nil, target_vars: {}, target_facts: {}, target_features: {}) @logger = Logging.logger[self] # Config is saved to add config options to targets @config = config || Bolt::Config.default @data = data ||= {} @groups = Group2.new(data.merge('name' => 'all')) @group_lookup = {} @target_vars = target_vars @target_facts = target_facts @target_features = target_features @groups.resolve_aliases(@groups.node_aliases, @groups.node_names) collect_groups end def validate @groups.validate end def collect_groups # Provide a lookup map for finding a group by name @group_lookup = @groups.collect_groups end def group_names @group_lookup.keys end def node_names @groups.node_names end def get_targets(targets) targets = expand_targets(targets) targets = if targets.is_a? Array targets.flatten.uniq(&:name) else [targets] end targets.map { |t| update_target(t) } end def add_to_group(targets, desired_group) if group_names.include?(desired_group) targets.each do |target| if group_names.include?(target.name) raise ValidationError.new("Group #{target.name} conflicts with node of the same name", target.name) end add_node(@groups, target, desired_group) end else raise ValidationError.new("Group #{desired_group} does not exist in inventory", nil) end end def set_var(target, key, value) data = { key => value } set_vars_from_hash(target.name, data) end def vars(target) @target_vars[target.name] || {} end def add_facts(target, new_facts = {}) @logger.warn("No facts to add") if new_facts.empty? set_facts(target.name, new_facts) end def facts(target) @target_facts[target.name] || {} end def set_feature(target, feature, value = true) @target_features[target.name] ||= Set.new if value @target_features[target.name] << feature else @target_features[target.name].delete(feature) end end def features(target) @target_features[target.name] || Set.new end def data_hash { data: @data, target_hash: { target_vars: @target_vars, target_facts: @target_facts, target_features: @target_features }, config: @config.transport_data_get } end #### PRIVATE #### # # For debugging only now def groups_in(node_name) @groups.data_for(node_name)['groups'] || {} end private :groups_in # Pass a target to get_targets for a public version of this # Should this reconfigure configured targets? def update_target(target) data = @groups.data_for(target.name) data ||= {} unless data['config'] @logger.debug("Did not find config for #{target.name} in inventory") data['config'] = {} end data = Bolt::Inventory.localhost_defaults(data) if target.name == 'localhost' # These should only get set from the inventory if they have not yet # been instantiated set_vars_from_hash(target.name, data['vars']) unless @target_vars[target.name] set_facts(target.name, data['facts']) unless @target_facts[target.name] data['features']&.each { |feature| set_feature(target, feature) } unless @target_features[target.name] # Use Config object to ensure config section is treated consistently with config file conf = @config.deep_clone conf.update_from_inventory(data['config']) conf.validate target.update_conf(conf.transport_conf) unless target.transport.nil? || Bolt::TRANSPORTS.include?(target.transport.to_sym) raise Bolt::UnknownTransportError.new(target.transport, target.uri) end target end private :update_target # If target is a group name, expand it to the members of that group. # Else match against nodes in inventory by name or alias. # If a wildcard string, error if no matches are found. # Else fall back to [target] if no matches are found. def resolve_name(target) if (group = @group_lookup[target]) group.node_names else # Try to wildcard match nodes in inventory # Ignore case because hostnames are generally case-insensitive regexp = Regexp.new("^#{Regexp.escape(target).gsub('\*', '.*?')}$", Regexp::IGNORECASE) nodes = @groups.node_names.select { |node| node =~ regexp } nodes += @groups.node_aliases.select { |target_alias, _node| target_alias =~ regexp }.values if nodes.empty? raise(WildcardError, target) if target.include?('*') [target] else nodes end end end private :resolve_name def expand_targets(targets) if targets.is_a? Bolt::Target targets.inventory = self targets elsif targets.is_a? Array targets.map { |tish| expand_targets(tish) } elsif targets.is_a? String # Expand a comma-separated list targets.split(/[[:space:],]+/).reject(&:empty?).map do |name| ts = resolve_name(name) ts.map do |t| target = create_target(t) target.inventory = self target end end end end private :expand_targets def set_vars_from_hash(tname, data) if data # Instantiate empty vars hash in case no vars are defined @target_vars[tname] ||= {} # Assign target new merged vars hash # This is essentially a copy-on-write to maintain the immutability of @target_vars @target_vars[tname] = @target_vars[tname].merge(data).freeze end end private :set_vars_from_hash def set_facts(tname, hash) if hash @target_facts[tname] ||= {} @target_facts[tname] = Bolt::Util.deep_merge(@target_facts[tname], hash).freeze end end private :set_facts def add_node(current_group, target, desired_group, track = { 'all' => nil }) if current_group.name == desired_group # Group to add to is found t_name = target.name # Add target to nodes hash current_group.nodes[t_name] = { 'name' => t_name }.merge(target.options) # Inherit facts, vars, and features from hierarchy current_group_data = { facts: current_group.facts, vars: current_group.vars, features: current_group.features } data = inherit_data(track, current_group.name, current_group_data) set_facts(t_name, @target_facts[t_name] ? data[:facts].merge(@target_facts[t_name]) : data[:facts]) set_vars_from_hash(t_name, @target_vars[t_name] ? data[:vars].merge(@target_vars[t_name]) : data[:vars]) data[:features].each do |feature| set_feature(target, feature) end return true end # Recurse on children Groups if not desired_group current_group.groups.each do |child_group| track[child_group.name] = current_group add_node(child_group, target, desired_group, track) end end private :add_node def inherit_data(track, name, data) unless track[name].nil? data[:facts] = track[name].facts.merge(data[:facts]) data[:vars] = track[name].vars.merge(data[:vars]) data[:features].concat(track[name].features) inherit_data(track, track[name].name, data) end data end private :inherit_data def create_target(target_name) data = @groups.data_for(target_name) || {} name_opt = {} name_opt['name'] = data['name'] if data['name'] # If there is no name then this node was only referred to as a string. uri = data['uri'] uri ||= target_name unless data['name'] Target.new(uri, name_opt) end end end end