summaryrefslogtreecommitdiff
path: root/lib/chef/knife/search.rb
blob: 324c439ddcee204f8a10748ad91b07bd2c10ed74 (plain)
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
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
190
191
192
193
194
#
# Author:: Adam Jacob (<adam@chef.io>)
# Copyright:: Copyright 2009-2017, 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/knife"
require "chef/knife/core/node_presenter"
require "addressable/uri"

class Chef
  class Knife
    class Search < Knife

      include Knife::Core::MultiAttributeReturnOption

      deps do
        require "chef/node"
        require "chef/environment"
        require "chef/api_client"
        require "chef/search/query"
      end

      include Knife::Core::NodeFormattingOptions

      banner "knife search INDEX QUERY (options)"

      option :start,
        short: "-b ROW",
        long: "--start ROW",
        description: "The row to start returning results at.",
        default: 0,
        proc: lambda { |i| i.to_i }

      option :rows,
        short: "-R INT",
        long: "--rows INT",
        description: "The number of rows to return.",
        default: nil,
        proc: lambda { |i| i.to_i }

      option :run_list,
        short: "-r",
        long: "--run-list",
        description: "Show only the run list."

      option :id_only,
        short: "-i",
        long: "--id-only",
        description: "Show only the ID of matching objects."

      option :query,
        short: "-q QUERY",
        long: "--query QUERY",
        description: "The search query; useful to protect queries starting with -."

      option :filter_result,
        short: "-f FILTER",
        long: "--filter-result FILTER",
        description: "Only return specific attributes of the matching objects; for example: \"ServerName=name, Kernel=kernel.version\"."

      def run
        read_cli_args

        if @type == "node"
          ui.use_presenter Knife::Core::NodePresenter
        end

        q = Chef::Search::Query.new

        result_items = []
        result_count = 0

        search_args = Hash.new
        search_args[:fuzz] = true
        search_args[:start] = config[:start] if config[:start]
        search_args[:rows] = config[:rows] if config[:rows]
        if config[:filter_result]
          search_args[:filter_result] = create_result_filter(config[:filter_result])
        elsif (not ui.config[:attribute].nil?) && (not ui.config[:attribute].empty?)
          search_args[:filter_result] = create_result_filter_from_attributes(ui.config[:attribute])
        elsif config[:id_only]
          search_args[:filter_result] = create_result_filter_from_attributes([])
        end

        begin
          q.search(@type, @query, search_args) do |item|
            formatted_item = Hash.new
            if config[:id_only]
              formatted_item = format_for_display({ "id" => item["__display_name"] })
            elsif item.is_a?(Hash)
              # doing a little magic here to set the correct name
              formatted_item[item["__display_name"]] = item.reject { |k| k == "__display_name" }
            else
              formatted_item = format_for_display(item)
            end
            result_items << formatted_item
            result_count += 1
          end
        rescue Net::HTTPServerException => e
          msg = Chef::JSONCompat.from_json(e.response.body)["error"].first
          ui.error("knife search failed: #{msg}")
          exit 99
        end

        if ui.interchange?
          output({ results: result_count, rows: result_items })
        else
          ui.log "#{result_count} items found"
          ui.log("\n")
          result_items.each do |item|
            output(item)
            unless config[:id_only]
              ui.msg("\n")
            end
          end
        end

        # return a "failure" code to the shell so that knife search can be used in pipes similar to grep
        exit 1 if result_count == 0
      end

      def read_cli_args
        if config[:query]
          if @name_args[1]
            ui.error "Please specify query as an argument or an option via -q, not both"
            ui.msg opt_parser
            exit 1
          end
          @type = name_args[0]
          @query = config[:query]
        else
          case name_args.size
          when 0
            ui.error "No query specified"
            ui.msg opt_parser
            exit 1
          when 1
            @type = "node"
            @query = name_args[0]
          when 2
            @type = name_args[0]
            @query = name_args[1]
          end
        end
      end

      # This method turns a set of key value pairs in a string into the appropriate data structure that the
      # chef-server search api is expecting.
      # expected input is in the form of:
      # -f "return_var1=path.to.attribute, return_var2=shorter.path"
      #
      # a more concrete example might be:
      # -f "env=chef_environment, ruby_platform=languages.ruby.platform"
      #
      # The end result is a hash where the key is a symbol in the hash (the return variable)
      # and the path is an array with the path elements as strings (in order)
      # See lib/chef/search/query.rb for more examples of this.
      def create_result_filter(filter_string)
        final_filter = Hash.new
        filter_string.delete!(" ")
        filters = filter_string.split(",")
        filters.each do |f|
          return_id, attr_path = f.split("=")
          final_filter[return_id.to_sym] = attr_path.split(".")
        end
        final_filter
      end

      def create_result_filter_from_attributes(filter_array)
        final_filter = Hash.new
        filter_array.each do |f|
          final_filter[f] = f.split(".")
        end
        # adding magic filter so we can actually pull the name as before
        final_filter["__display_name"] = [ "name" ]
        final_filter
      end

    end
  end
end