Class: Onering::API

Inherits:
Object
  • Object
show all
Includes:
HTTParty, Util
Defined in:
lib/onering/api.rb,
lib/onering/plugins/devices.rb,
lib/onering/plugins/automation.rb,
lib/onering/plugins/authentication.rb

Defined Under Namespace

Modules: Actions, Errors Classes: Auth, AutomationJobs, AutomationRequests, AutomationTasks, Devices

Constant Summary collapse

DEFAULT_BASE =
"https://onering"
DEFAULT_PATH =
"/api"
DEFAULT_CLIENT_PEM =
["~/.onering/client.pem", "/etc/onering/client.pem"]
DEFAULT_CLIENT_KEY =
["~/.onering/client.key", "/etc/onering/client.key"]
DEFAULT_VALIDATION_PEM =
"/etc/onering/validation.pem"

Constants included from Util

Util::HTTP_STATUS_CODES

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Util

#fact, #gem_path, #http_status, #make_filter

Constructor Details

#initialize(options = {}) ⇒ API

Returns a new instance of API.



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
# File 'lib/onering/api.rb', line 54

def initialize(options={})
  @_plugins = {}
  @_connection_options = options

# load and merge all config file sources
  Onering::Config.load(@_connection_options[:configfile], @_connection_options.get(:config, {}))

# source interface specified
# !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !! HAX !!
#   Due to certain versions of Ruby's Net::HTTP not allowing you explicitly
#   specify the source IP/interface to use, this horrific monkey patch is
#   necessary, if not right.
#
#   If at least some of your code doesn't make you feel bottomless shame
#   then you aren't coding hard enough.
#
if options.get('config.source').is_a?(String)
  if options.get('config.source') =~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/
  # insert firing pin into the hack
    TCPSocket.instance_eval do
      (class << self; self; end).instance_eval do
        alias_method :_stock_open, :open
        attr_writer  :_hack_local_ip

        define_method(:open) do |conn_address, conn_port|
          _stock_open(conn_address, conn_port, @_hack_local_ip)
        end
      end
    end

  # arm the hack
    TCPSocket._hack_local_ip = options.get('config.source')

  # sound the siren
    Onering::Logger.info("Using local interface #{options.get('config.source')} to connect", "Onering::API")

  else
    raise "Invalid source IP address #{options.get('config.source')}"
  end
end

# set API connectivity details
  Onering::API.base_uri(options.get('config.url', Onering::Config.get(:url, DEFAULT_BASE)))
  Onering::Logger.info("Server URL is #{Onering::API.base_uri}", "Onering::API")

# add default parameters
  options.get('config.params',{}).each do |k,v|
    _default_param(k,v)
  end

  Onering::Reporter.setup()
  connect(options) if options.get(:autoconnect, true)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object

I’m not a huge fan of what’s happening here, but metaprogramming is hard…

“Don’t let the perfect be the enemy of the good.”



197
198
199
200
201
202
203
204
205
206
# File 'lib/onering/api.rb', line 197

def method_missing(method, *args, &block)
  modname = method.to_s.split('_').map(&:capitalize).join

  if not (plugin = (Onering::API.const_get(modname) rescue nil)).nil?
    @_plugins[method] ||= plugin.new.connect(@_connection_options)
    return @_plugins[method]
  else
    super
  end
end

Instance Attribute Details

#urlObject

Returns the value of attribute url.



44
45
46
# File 'lib/onering/api.rb', line 44

def url
  @url
end

Instance Method Details

#_default_param(key, value) ⇒ Object




226
227
228
229
230
# File 'lib/onering/api.rb', line 226

def _default_param(key, value)
  @_default_params ||= {}
  @_default_params[key] = value
  Onering::API.default_params(@_default_params)
end

#_setup_authObject




213
214
215
216
217
218
219
220
221
222
223
# File 'lib/onering/api.rb', line 213

def _setup_auth()
  type = Onering::Config.get('authentication.type', :auto)

  case type.to_sym
  when :token
    _setup_auth_token()

  else
    _setup_auth_ssl()
  end
end

#_setup_auth_sslObject




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
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/onering/api.rb', line 233

def _setup_auth_ssl()
  begin
    Onering::Logger.info("Using SSL authentication mechanism", "Onering::API")

  # get first keyfile found
    key = (([Onering::Config.get('authentication.keyfile')] + DEFAULT_CLIENT_PEM).compact.select{|i|
      rv = (File.readable?(File.expand_path(i)) rescue false)
      Onering::Logger.debug("SSL keyfile found at #{File.expand_path(i)}", "Onering::API") if rv === true
      rv
    }).first

  # SSL client key not found, attempt autoregistration...
    if key.nil?
      if Onering::Config.get('authentication.autoregister', true)
        Onering::Logger.warn("SSL keyfile not found, attempting to autoregister client", "Onering::API")

        validation_key = Onering::Config.get('authentication.validation_keyfile', DEFAULT_VALIDATION_PEM)
        validation_key = (File.expand_path(validation_key) rescue validation_key)

      # if validation key exists, autoregister
        if File.size?(validation_key)
          Onering::Logger.debug("Using validation key at #{validation_key}", "Onering::API")

        # set the authentication PEM to validation.pem
          Onering::API.pem(File.read(validation_key))

        # attempt to create client.pem from least-specific to most, first writable path wins
          clients = [{
            :path       => "/etc/onering",
            :name       => fact('hardwareid'),
            :keyname    => 'system',
            :autodelete => true
          },{
            :path       => "~/.onering",
            :name       => ENV['USER'],
            :keyname    => 'cli',
            :autodelete => false
          }]

        # for each client attempt...
          clients.each do |client|
          # expand and assemble path
            client[:path] = (File.expand_path(client[:path]) rescue client[:path])
            keyfile = File.join(client[:path], 'client.pem')

          # skip this if we can't write to the parent directory
            next unless File.writable?(client[:path])
            Dir.mkdir(client[:path]) unless File.directory?(client[:path])
            next if File.exists?(keyfile)

          # attempt to create/download the keyfile
            Onering::Logger.debug("Requesting SSL keyfile as client #{client[:name].strip}, key #{client[:keyname]}", "Onering::API")
            response = self.class.get("/api/users/#{client[:name].strip}/keys/#{client[:keyname]}")

          # if successful, write the file
            if response.code < 400 and response.body
              File.open(keyfile, 'w').puts(response.body)
              raise Actions::Retry.new
            else
          # all errors are fatal at this stage
              Onering::Logger.fatal!("Cannot autoregister client: HTTP #{response.code} - #{(response.parsed_response || {}).get('error.message', 'Unknown error')}", "Onering::API")
            end
          end

        # it is an error to not have created a client.pem by now
          raise Errors::AuthenticationMissing.new("Cannot autoregister client: keyfile not created")

        else
        # cannot autoregister without a validation.pem
          raise Errors::AuthenticationMissing.new("Cannot autoregister client: validation keyfile is missing")
        end
      else
        raise Errors::AuthenticationMissing.new("Cannot find SSL key and autoregistration is disabled")
      end
    else
      Onering::API.pem(File.read((File.expand_path(key) rescue key)))
      Onering::Logger.debug("Using SSL keyfile #{File.expand_path(key) rescue key}", "Onering::API")
    end

  rescue Actions::Retry
    retry
  end
end

#_setup_auth_tokenObject




319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/onering/api.rb', line 319

def _setup_auth_token()
  Onering::Logger.info("Using token authentication mechanism", "Onering::API")

# get first keyfile found
  key = Onering::Config.get('authentication.key')
  raise Errors::AuthenticationMissing.new("Cannot find an API token") if key.nil?

# set auth mechanism
  Onering::API.headers({
    'X-Auth-Mechanism' => 'token'
  })

# set default parameters
  _default_param(:token, key)
end

#connect(options = {}) ⇒ Object



108
109
110
111
112
113
114
# File 'lib/onering/api.rb', line 108

def connect(options={})
# setup authentication
  _setup_auth()

  Onering::Logger.debug("Connection setup complete", "Onering::API")
  return self
end

#delete(endpoint, options = {}) ⇒ Object



188
189
190
# File 'lib/onering/api.rb', line 188

def delete(endpoint, options={})
  request(:delete, endpoint, options)
end

#get(endpoint, options = {}) ⇒ Object



164
165
166
# File 'lib/onering/api.rb', line 164

def get(endpoint, options={})
  request(:get, endpoint, options)
end

#post(endpoint, options = {}, &block) ⇒ Object



168
169
170
171
172
173
174
175
176
# File 'lib/onering/api.rb', line 168

def post(endpoint, options={}, &block)
  if block_given?
    request(:post, endpoint, options.merge({
      :body => yield
    }))
  else
    request(:post, endpoint, options)
  end
end

#put(endpoint, options = {}, &block) ⇒ Object



178
179
180
181
182
183
184
185
186
# File 'lib/onering/api.rb', line 178

def put(endpoint, options={}, &block)
  if block_given?
    request(:put, endpoint, options.merge({
      :body => yield
    }))
  else
    request(:put, endpoint, options)
  end
end

#request(method, endpoint, options = {}) ⇒ Object



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
# File 'lib/onering/api.rb', line 117

def request(method, endpoint, options={})
  endpoint = [Onering::Config.get(:path, DEFAULT_PATH).strip, endpoint.sub(/^\//,'')].join('/')

  Onering::Logger.debug("#{method.to_s.upcase} #{endpoint}#{(options[:query] || {}).empty? ? '' : '?'+options[:query].join('=', '&')}", "Onering::API")
  options.get(:headers,[]).each do |name, value|
    next if name == 'Content-Type' and value == 'application/json'
    Onering::Logger.debug("+#{name}: #{value}", "Onering::API")
  end

  begin
    case (method.to_sym rescue method)
    when :post
      rv = Onering::API.post(endpoint, options)
    when :put
      rv = Onering::API.put(endpoint, options)
    when :delete
      rv = Onering::API.delete(endpoint, options)
    when :head
      rv = Onering::API.head(endpoint, options)
    else
      rv = Onering::API.get(endpoint, options)
    end
  rescue SocketError => e
    Onering::Logger.fatal!("Unable to connect to #{Onering::API.base_uri}", "Onering::API")
  end

  if rv.code >= 500
    raise Errors::ServerError.new("HTTP #{rv.code} - #{Onering::Util.http_status(rv.code)} #{rv.parsed_response.get('error.message','') rescue ''}")
  elsif rv.code >= 400
    message = "HTTP #{rv.code} - #{Onering::Util.http_status(rv.code)} #{rv.parsed_response.get('error.message', '') rescue ''}"

    case rv.code
    when 401
      raise Errors::Unauthorized.new(message)
    when 403
      raise Errors::Forbidden.new(message)
    when 404
      raise Errors::NotFound.new(message)
    else
      raise Errors::ClientError.new(message)
    end
  else
    rv
  end
end

#statusObject



208
209
210
# File 'lib/onering/api.rb', line 208

def status()
  Onering::API.get("/").parsed_response
end