# # Author:: Adam Jacob () # Copyright:: Copyright 2008-2016, 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. # require "chef/config" require "chef/exceptions" require "chef/server_api" require "uri" class Chef class Search class Query attr_accessor :rest attr_reader :config def initialize(url = nil, config:Chef::Config) @config = config @url = url end def rest @rest ||= Chef::ServerAPI.new(@url || @config[:chef_server_url]) end # Backwards compatability for cookbooks. # This can be removed in Chef > 12. def partial_search(type, query = "*:*", *args, &block) Chef::Log.warn(<<-WARNDEP) DEPRECATED: The 'partial_search' API is deprecated and will be removed in future releases. Please use 'search' with a :filter_result argument to get partial search data. WARNDEP if !args.empty? && args.first.is_a?(Hash) # partial_search uses :keys instead of :filter_result for # result filtering. args_h = args.first.dup args_h[:filter_result] = args_h[:keys] args_h.delete(:keys) search(type, query, args_h, &block) else search(type, query, *args, &block) end end # # New search input, designed to be backwards compatible with the old method signature # 'type' and 'query' are the same as before, args now will accept either a Hash of # search arguments with symbols as the keys (ie :sort, :start, :rows) and a :filter_result # option. # # :filter_result should be in the format of another Hash with the structure of: # { # :returned_name1 => ["path", "to", "variable"], # :returned_name2 => ["shorter", "path"] # } # a real world example might be something like: # { # :ip_address => ["ipaddress"], # :ruby_version => ["languages", "ruby", "version"] # } # this will bring back 2 variables 'ip_address' and 'ruby_version' with whatever value was found # an example of the returned json may be: # {"ip_address":"127.0.0.1", "ruby_version": "1.9.3"} # def search(type, query = "*:*", *args, &block) validate_type(type) args_h = hashify_args(*args) response = call_rest_service(type, query: query, **args_h) if block response["rows"].each { |row| block.call(row) if row } # # args_h[:rows] and args_h[:start] are the page size and # start position requested of the search index backing the # search API. # # The response may contain fewer rows than arg_h[:rows] if # the page of index results included deleted nodes which # have been filtered from the returned data. In this case, # we still want to start the next page at start + # args_h[:rows] to avoid asking the search backend for # overlapping pages (which could result in duplicates). # next_start = response["start"] + (args_h[:rows] || response["rows"].length) unless next_start >= response["total"] args_h[:start] = next_start search(type, query, args_h, &block) end true else [ response["rows"], response["start"], response["total"] ] end end private def validate_type(t) unless t.kind_of?(String) || t.kind_of?(Symbol) msg = "Invalid search object type #{t.inspect} (#{t.class}), must be a String or Symbol." + "Usage: search(:node, QUERY[, OPTIONAL_ARGS])" + " `knife search environment QUERY (options)`" raise Chef::Exceptions::InvalidSearchQuery, msg end end def hashify_args(*args) return Hash.new if args.empty? return args.first if args.first.is_a?(Hash) args_h = Hash.new args_h[:sort] = args[0] if args[0] args_h[:start] = args[1] if args[1] args_h[:rows] = args[2] args_h[:filter_result] = args[3] args_h end def escape(s) s && URI.escape(s.to_s) end def create_query_string(type, query, rows, start, sort) qstr = "search/#{type}?q=#{escape(query)}" qstr += "&sort=#{escape(sort)}" if sort qstr += "&start=#{escape(start)}" if start qstr += "&rows=#{escape(rows)}" if rows qstr end def call_rest_service(type, query:"*:*", rows:nil, start:0, sort:"X_CHEF_id_CHEF_X asc", filter_result:nil) query_string = create_query_string(type, query, rows, start, sort) if filter_result response = rest.post(query_string, filter_result) # response returns rows in the format of # { "url" => url_to_node, "data" => filter_result_hash } response["rows"].map! { |row| row["data"] } else response = rest.get(query_string) response["rows"].map! do |row| case type.to_s when "node" Chef::Node.from_hash(row) when "role" Chef::Role.from_hash(row) when "environment" Chef::Environment.from_hash(row) when "client" Chef::ApiClient.from_hash(row) else Chef::DataBagItem.from_hash(row) end end end response end end end end