require "net/http" require_relative "../util/choria" module MCollective class Discovery class Choria def self.discover(filter, timeout, limit=0, client=nil) Choria.new(filter, timeout, limit, client).discover end attr_reader :timeout, :limit, :client, :config attr_accessor :filter def initialize(filter, timeout, limit, client) @filter = filter @timeout = timeout @limit = limit @client = client @config = Config.instance end # Search for nodes # # @return [Array] list of certnames found def discover queries = [] if choria.proxied_discovery? Log.debug("Performing discovery against a PuppetDB Proxy") choria.proxy_discovery_query(proxy_request) else Log.debug("Performing direct discovery against PuppetDB") queries << discover_collective(filter["collective"]) if filter["collective"] queries << discover_nodes(filter["identity"]) unless filter["identity"].empty? queries << discover_classes(filter["cf_class"]) unless filter["cf_class"].empty? queries << discover_facts(filter["fact"]) unless filter["fact"].empty? queries << discover_agents(filter["agent"]) unless filter["agent"].empty? choria.pql_query(node_search_string(queries.compact), true) end end # Creates a request hash for the discovery proxy # # @return [Hash] def proxy_request request = {} request["collective"] = filter["collective"] if filter["collective"] request["identities"] = filter["identity"] unless filter["identity"].empty? request["classes"] = filter["cf_class"] unless filter["cf_class"].empty? request["facts"] = filter["fact"] unless filter["fact"].empty? request["agents"] = filter["agent"] unless filter["agent"].empty? request end # Discovers nodes in a specific collective # # @param filter [String] a collective name # @return [String] a query string def discover_collective(filter) 'certname in inventory[certname] { facts.mcollective.server.collectives.match("\d+") = "%s" }' % filter end # Searches for facts # # Nodes are searched using an `and` operator via the discover_classes method # # When the `rpcutil` or `scout` agent is required it will look for `Mcollective` class # otherwise `Mcollective_avent_agentname` thus it will only find plugins # installed using the `choria/mcollective` AIO plugin packager # # @param filter [Array] agent names # @return [Array] list of nodes found def discover_agents(filter) pql = filter.map do |agent| if ["rpcutil", "scout"].include?(agent) "(%s or %s)" % [discover_classes(["mcollective::service"]), discover_classes(["choria::service"])] elsif agent =~ /^\/(.+)\/$/ 'resources {type = "File" and tag ~ "mcollective_agent_.*?%s.*?_server"}' % [string_regexi($1)] else 'resources {type = "File" and tag = "mcollective_agent_%s_server"}' % [agent] end end pql.join(" and ") unless pql.empty? end # Turns a string into a case insensitive regex string # # @param value [String] # @return [String] def string_regexi(value) value =~ /^\/(.+)\/$/ ? derived_value = $1 : derived_value = value.dup derived_value.each_char.map do |char| if char =~ /[[:alpha:]]/ "[%s%s]" % [char.downcase, char.upcase] else char end end.join end # Capitalize a Puppet Resource # # foo::bar => Foo::Bar # # @param resource [String] a resource title # @return [String] def capitalize_resource(resource) resource.split("::").map(&:capitalize).join("::") end # Searches for facts # # Nodes are searched using an `and` operator # # @param filter [Array] hashes with :fact, :operator and :value # @return [Array] list of nodes found def discover_facts(filter) pql = filter.map do |q| fact = q[:fact] operator = q[:operator] value = q[:value] case operator when "=~" regex = string_regexi(value) 'inventory {facts.%s ~ "%s"}' % [fact, regex] when "==" if ["true", "false"].include?(value) || numeric?(value) 'inventory {facts.%s = %s or facts.%s = "%s"}' % [fact, value, fact, value] else 'inventory {facts.%s = "%s"}' % [fact, value] end when "!=" if ["true", "false"].include?(value) || numeric?(value) 'inventory {!(facts.%s = %s or facts.%s = "%s")}' % [fact, value, fact, value] else 'inventory {!(facts.%s = "%s")}' % [fact, value] end when ">=", ">", "<=", "<" raise("Do not know how to do string fact comparisons using the '%s' operator with PuppetDB" % operator) unless numeric?(value) "inventory {facts.%s %s %s}" % [fact, operator, value] else raise("Do not know how to do fact comparisons using the '%s' operator with PuppetDB" % operator) end end pql.join(" and ") unless pql.empty? end # Searches for classes # # Nodes are searched using an `and` operator # # @return [Array] list of nodes found def discover_classes(filter) pql = filter.map do |klass| if klass =~ /^\/(.+)\/$/ 'resources {type = "Class" and title ~ "%s"}' % [string_regexi($1)] else 'resources {type = "Class" and title = "%s"}' % [capitalize_resource(klass)] end end pql.join(" and ") unless pql.empty? end # Searches for nodes # # Nodes are searched using an `or` operator # # @return [Array] list of nodes found def discover_nodes(filter) if filter.empty? Log.debug("Empty node filter found, discovering all nodes") nil else pql = filter.map do |ident| case ident when /^pql:\s*(.+)$/ "certname in %s" % $1 when /^\/(.+)\/$/ 'certname ~ "%s"' % string_regexi($1) else 'certname = "%s"' % ident end end pql.join(" or ") unless pql.empty? end end # Produce a nodes query with the supplied sub query included # # @param queries [Array] PQL queries to be used as a sub query # @return [String] nodes search string def node_search_string(queries) filter_queries = queries.map {|q| "(%s)" % q}.join(" and ") "nodes[certname, deactivated] { %s }" % [filter_queries] end # Determines if a string is a number, either float or integer # # @param string [String] # @return [boolean] def numeric?(string) true if Float(string) rescue false end def choria @_choria ||= Util::Choria.new(false) end end end end