Class: Onering::API

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

Defined Under Namespace

Modules: Actions, Errors Classes: Assets, Auth, AutomationJobs, AutomationRequests, AutomationTasks, Configuration

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.



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

def initialize(options={})
  @_plugins = {}
  options = {} if options.nil?
  @_connection_options = options

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

  if options.get('config.nosslverify', false) == true
  # deliberately break SSL verification
    Onering::Logger.warn("Disabling SSL peer verification for #{options.get('config.url')}")
    OpenSSL::SSL.send(:const_set, :OLD_VERIFY_PEER, OpenSSL::SSL::VERIFY_PEER)
    OpenSSL::SSL.send(:remove_const, :VERIFY_PEER)
    OpenSSL::SSL.send(:const_set, :VERIFY_PEER, OpenSSL::SSL::VERIFY_NONE)
  else
  # restore SSL verification if it's currently broken
    if defined?(OpenSSL::SSL::OLD_VERIFY_PEER)
      if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE and OpenSSL::SSL::OLD_VERIFY_PEER != OpenSSL::SSL::VERIFY_NONE
        OpenSSL::SSL.send(:remove_const, :VERIFY_PEER)
        OpenSSL::SSL.send(:const_set, :VERIFY_PEER, OpenSSL::SSL::OLD_VERIFY_PEER)
      end
    end
  end

  if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE
    Onering::Logger.warn("Disabling SSL peer verification for #{options.get('config.url')}")
  end

  # 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

  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.”



205
206
207
208
209
210
211
212
213
214
# File 'lib/onering/api.rb', line 205

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.



32
33
34
# File 'lib/onering/api.rb', line 32

def url
  @url
end

Instance Method Details

#_default_param(key, value) ⇒ Object




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

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

#_setup_authObject




221
222
223
224
# File 'lib/onering/api.rb', line 221

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

#_setup_auth_tokenObject




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
316
317
318
319
320
# File 'lib/onering/api.rb', line 234

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

# get first keyfile found
  key = Onering::Config.get('authentication.key', Onering::Config.get('authentication.keyfile'))

  if key.nil?
    if Onering::Config.get('authentication.bootstrap.enabled', true)
      Onering::Logger.warn("Authentication token not found, attempting to autoregister client", "Onering::API")

      if not (bootstrap = Onering::Config.get('authentication.bootstrap.key')).nil?
        if bootstrap.to_s =~ /[0-9a-f]{32,64}/
        # attempt to create key.yml 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], 'key.yml')

          # 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)

            self.class.headers({
              'X-Auth-Bootstrap-Token' => bootstrap
            })

          # attempt to create/download the keyfile
            Onering::Logger.debug("Requesting authentication token for #{client[:name].strip}; #{bootstrap}", "Onering::API")
            response = self.class.get("/api/users/#{client[:name].strip}/tokens/#{client[:keyname]}")

          # if successful, write the file
            if response.code < 400 and response.body
              File.open(keyfile, 'w').puts(YAML.dump({
                'authentication' => {
                  'key' => response.body.strip.chomp
                }
              }))

              key = response.body.strip.chomp

            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

            self.class.headers({})

          # we're done here...
            break
          end
        else
          raise Errors::AuthenticationMissing.new("Autoregistration failed: invalid bootstrap token specified")
        end
     
      else
        raise Errors::AuthenticationMissing.new("Autoregistration failed: no bootstrap token specified")
      end

    else
      raise Errors::AuthenticationMissing.new("Authentication token not found, and autoregistration disabled")
    end
  end

  raise Errors::AuthenticationMissing.new("Token authentication specified, but cannot find a token config or as a command line argument") if key.nil?

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

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

#connect(options = {}) ⇒ Object



116
117
118
119
120
121
122
# File 'lib/onering/api.rb', line 116

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

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

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



196
197
198
# File 'lib/onering/api.rb', line 196

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

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



172
173
174
# File 'lib/onering/api.rb', line 172

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

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



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

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



186
187
188
189
190
191
192
193
194
# File 'lib/onering/api.rb', line 186

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



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

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



216
217
218
# File 'lib/onering/api.rb', line 216

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