Class: Chef::REST

Inherits:
Object show all
Defined in:
lib/chef/rest.rb

Defined Under Namespace

Classes: CookieJar

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(url, client_name = Chef::Config[:node_name], signing_key_filename = Chef::Config[:client_key]) ⇒ REST

Returns a new instance of REST.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/chef/rest.rb', line 42

def initialize(url, client_name=Chef::Config[:node_name], signing_key_filename=Chef::Config[:client_key])
  @url = url
  @cookies = CookieJar.instance
  @client_name = client_name
  if signing_key_filename
    @signing_key_filename = signing_key_filename
    @signing_key = load_signing_key(signing_key_filename) 
    @sign_request = true
  else
    @signing_key = nil
    @sign_request = false
  end
  @sign_on_redirect = true
end

Instance Attribute Details

#client_nameObject

Returns the value of attribute client_name.



40
41
42
# File 'lib/chef/rest.rb', line 40

def client_name
  @client_name
end

#cookiesObject

Returns the value of attribute cookies.



40
41
42
# File 'lib/chef/rest.rb', line 40

def cookies
  @cookies
end

#sign_on_redirectObject

Returns the value of attribute sign_on_redirect.



40
41
42
# File 'lib/chef/rest.rb', line 40

def sign_on_redirect
  @sign_on_redirect
end

#sign_request(http_method, private_key, user_id, body = "", host = "localhost") ⇒ Object

Returns the value of attribute sign_request.



40
41
42
# File 'lib/chef/rest.rb', line 40

def sign_request
  @sign_request
end

#signing_keyObject

Returns the value of attribute signing_key.



40
41
42
# File 'lib/chef/rest.rb', line 40

def signing_key
  @signing_key
end

#signing_key_filenameObject

Returns the value of attribute signing_key_filename.



40
41
42
# File 'lib/chef/rest.rb', line 40

def signing_key_filename
  @signing_key_filename
end

#urlObject

Returns the value of attribute url.



40
41
42
# File 'lib/chef/rest.rb', line 40

def url
  @url
end

Instance Method Details

#create_url(path) ⇒ Object



117
118
119
120
121
122
123
# File 'lib/chef/rest.rb', line 117

def create_url(path)
  if path =~ /^(http|https):\/\//
    URI.parse(path)
  else
    URI.parse("#{@url}/#{path}")
  end
end

#delete_rest(path, headers = {}) ⇒ Object

Send an HTTP DELETE request to the path



103
104
105
# File 'lib/chef/rest.rb', line 103

def delete_rest(path, headers={}) 
  run_request(:DELETE, create_url(path), headers)       
end

#get_rest(path, raw = false, headers = {}) ⇒ Object

Send an HTTP GET request to the path

Parameters

path

The path to GET

raw

Whether you want the raw body returned, or JSON inflated. Defaults

to JSON inflated.


98
99
100
# File 'lib/chef/rest.rb', line 98

def get_rest(path, raw=false, headers={})
  run_request(:GET, create_url(path), headers, false, 10, raw)    
end

#load_signing_key(key) ⇒ Object



57
58
59
60
61
62
63
# File 'lib/chef/rest.rb', line 57

def load_signing_key(key)
  if File.exists?(key) && File.readable?(key)
    IO.read(key)
  else
    raise Chef::Exceptions::PrivateKeyMissing, "I cannot find #{key}, which you told me to use to sign requests!"
  end
end

#post_rest(path, json, headers = {}) ⇒ Object

Send an HTTP POST request to the path



108
109
110
# File 'lib/chef/rest.rb', line 108

def post_rest(path, json, headers={})
  run_request(:POST, create_url(path), headers, json)    
end

#put_rest(path, json, headers = {}) ⇒ Object

Send an HTTP PUT request to the path



113
114
115
# File 'lib/chef/rest.rb', line 113

def put_rest(path, json, headers={})
  run_request(:PUT, create_url(path), headers, json)
end

#register(name = Chef::Config[:node_name], destination = Chef::Config[:client_key]) ⇒ Object

Register the client



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
# File 'lib/chef/rest.rb', line 66

def register(name=Chef::Config[:node_name], destination=Chef::Config[:client_key])

  if File.exists?(destination)
    raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination} - check permissions?" unless File.writable?(destination)
  end

  nc = Chef::ApiClient.new
  nc.name(name)
  response = nc.save(true, true)

  Chef::Log.debug("Registration response: #{response.inspect}")

  raise Chef::Exceptions::CannotWritePrivateKey, "The response from the server did not include a private key!" unless response.has_key?("private_key")

  begin
    # Write out the private key
    file = File.open(destination, "w")
    file.print(response["private_key"])
    file.close
  rescue 
    raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination}"
  end

  true
end

#run_request(method, url, headers = {}, data = false, limit = 10, raw = false) ⇒ Object

Actually run an HTTP request. First argument is the HTTP method, which should be one of :GET, :PUT, :POST or :DELETE. Next is the URL, then an object to include in the body (which will be converted with .to_json) and finally, the limit of HTTP Redirects to follow (10).

Typically, you won’t use this method – instead, you’ll use one of the helper methods (get_rest, post_rest, etc.)

Will return the body of the response on success.

Raises:

  • (ArgumentError)


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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/chef/rest.rb', line 146

def run_request(method, url, headers={}, data=false, limit=10, raw=false)
  
  http_retry_delay = Chef::Config[:http_retry_delay] 
  http_retry_count = Chef::Config[:http_retry_count]

  raise ArgumentError, 'HTTP redirect too deep' if limit == 0 

  http = Net::HTTP.new(url.host, url.port)
  if url.scheme == "https"
    http.use_ssl = true 
    if Chef::Config[:ssl_verify_mode] == :verify_none
      http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    elsif Chef::Config[:ssl_verify_mode] == :verify_peer
      http.verify_mode = OpenSSL::SSL::VERIFY_PEER
    end
    if Chef::Config[:ssl_ca_path] and File.exists?(Chef::Config[:ssl_ca_path])
      http.ca_path = Chef::Config[:ssl_ca_path]
    elsif Chef::Config[:ssl_ca_file] and File.exists?(Chef::Config[:ssl_ca_file])
      http.ca_file = Chef::Config[:ssl_ca_file]
    end
    if Chef::Config[:ssl_client_cert] && File.exists?(Chef::Config[:ssl_client_cert])
      http.cert = OpenSSL::X509::Certificate.new(File.read(Chef::Config[:ssl_client_cert]))
      http.key = OpenSSL::PKey::RSA.new(File.read(Chef::Config[:ssl_client_key]))
    end
  end

  http.read_timeout = Chef::Config[:rest_timeout]

  unless raw
    headers = headers.merge({ 
      'Accept' => "application/json",
    })
  end
  
  if @cookies.has_key?("#{url.host}:#{url.port}")
    headers['Cookie'] = @cookies["#{url.host}:#{url.port}"]
  end

  json_body = data ? data.to_json : nil 

  if @sign_request
    Chef::Log.debug("Signing the request as #{@client_name}")
    if json_body
      headers.merge!(sign_request(method, OpenSSL::PKey::RSA.new(@signing_key), @client_name, json_body, "#{url.host}:#{url.port}"))
    else
      headers.merge!(sign_request(method, OpenSSL::PKey::RSA.new(@signing_key), @client_name, "", "#{url.host}:#{url.port}"))
    end
  end
 
  req = nil
  case method
  when :GET
    req_path = "#{url.path}"
    req_path << "?#{url.query}" if url.query
    req = Net::HTTP::Get.new(req_path, headers)
  when :POST
    headers["Content-Type"] = 'application/json' if data
    req_path = "#{url.path}"
    req_path << "?#{url.query}" if url.query
    req = Net::HTTP::Post.new(req_path, headers)          
    req.body = json_body if json_body 
  when :PUT
    headers["Content-Type"] = 'application/json' if data
    req_path = "#{url.path}"
    req_path << "?#{url.query}" if url.query
    req = Net::HTTP::Put.new(req_path, headers)
    req.body = json_body if json_body 
  when :DELETE
    req_path = "#{url.path}"
    req_path << "?#{url.query}" if url.query
    req = Net::HTTP::Delete.new(req_path, headers)
  else
    raise ArgumentError, "You must provide :GET, :PUT, :POST or :DELETE as the method"
  end

  Chef::Log.debug("Sending HTTP Request via #{req.method} to #{url.host}:#{url.port}#{req.path}")
  
  # Optionally handle HTTP Basic Authentication
  req.basic_auth(url.user, url.password) if url.user

  res = nil
  tf = nil
  http_retries = 1

  begin
    res = http.request(req) do |response|
      if raw
        tf = Tempfile.new("chef-rest") 
        # Stolen from http://www.ruby-forum.com/topic/166423
        # Kudos to _why!
        size, total = 0, response.header['Content-Length'].to_i
        response.read_body do |chunk|
          tf.write(chunk) 
          size += chunk.size
          if size == 0
            Chef::Log.debug("#{req.path} done (0 length file)")
          elsif total == 0
            Chef::Log.debug("#{req.path} (zero content length)")
          else
            Chef::Log.debug("#{req.path}" + " %d%% done (%d of %d)" % [(size * 100) / total, size, total])
          end
        end
        tf.close 
        tf
      else
        response.read_body
      end
      response
    end

  rescue Errno::ECONNREFUSED => e
    Chef::Log.error("Connection refused connecting to #{url.host}:#{url.port} for #{req.path} #{http_retries}/#{http_retry_count}")
    sleep(http_retry_delay)
    retry if (http_retries += 1) < http_retry_count
    raise Errno::ECONNREFUSED, "Connection refused connecting to #{url.host}:#{url.port} for #{req.path}, giving up"
  rescue Timeout::Error
    Chef::Log.error("Timeout connecting to #{url.host}:#{url.port} for #{req.path}, retry #{http_retries}/#{http_retry_count}")
    sleep(http_retry_delay)
    retry if (http_retries += 1) < http_retry_count
    raise Timeout::Error, "Timeout connecting to #{url.host}:#{url.port} for #{req.path}, giving up"
  end
  
  if res.kind_of?(Net::HTTPSuccess)
    if res['set-cookie']
      @cookies["#{url.host}:#{url.port}"] = res['set-cookie']
    end
    if res['content-type'] =~ /json/
      response_body = res.body.chomp
      JSON.parse(response_body)
    else
      if raw
        tf
      else
        res.body
      end
    end
  elsif res.kind_of?(Net::HTTPFound) or res.kind_of?(Net::HTTPMovedPermanently)
    if res['set-cookie']
      @cookies["#{url.host}:#{url.port}"] = res['set-cookie']
    end
    @sign_request = false if @sign_on_redirect == false
    run_request(:GET, create_url(res['location']), {}, false, limit - 1, raw)
  else
    if res['content-type'] =~ /json/
      exception = JSON.parse(res.body)
      Chef::Log.warn("HTTP Request Returned #{res.code} #{res.message}: #{exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"]}")
    end
    res.error!
  end
end