stacker_bee 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.rubocop.yml +7 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +112 -0
- data/Rakefile +6 -0
- data/bin/stacker_bee +97 -0
- data/config.default.yml +3 -0
- data/config/4.2.json +67126 -0
- data/lib/stacker_bee.rb +16 -0
- data/lib/stacker_bee/api.rb +44 -0
- data/lib/stacker_bee/body_parser.rb +23 -0
- data/lib/stacker_bee/client.rb +105 -0
- data/lib/stacker_bee/configuration.rb +6 -0
- data/lib/stacker_bee/connection.rb +31 -0
- data/lib/stacker_bee/middleware/logger.rb +48 -0
- data/lib/stacker_bee/middleware/signed_query.rb +29 -0
- data/lib/stacker_bee/rash.rb +95 -0
- data/lib/stacker_bee/request.rb +41 -0
- data/lib/stacker_bee/request_error.rb +39 -0
- data/lib/stacker_bee/response.rb +29 -0
- data/lib/stacker_bee/utilities.rb +18 -0
- data/lib/stacker_bee/version.rb +3 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_nil_request_parameter/properly_executes_the_request.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_request_parameter_with_an_Array/.yml +35 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/a_request_parameter_with_and_empty_string/properly_executes_the_request.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/containing_an_error/.yml +37 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/containing_an_error/should_log_response_as_error.yml +37 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first/.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/_account_type_/.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/first_item/_accounttype_/.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/should_log_request.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/should_not_log_response_as_error.yml +33 -0
- data/spec/cassettes/A_response_to_a_request_sent_to_the_CloudStack_API/space_character_in_a_request_parameter/properly_signs_the_request.yml +32 -0
- data/spec/fixtures/4.2.json +67126 -0
- data/spec/fixtures/simple.json +871 -0
- data/spec/integration/request_spec.rb +116 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/units/stacker_bee/api_spec.rb +24 -0
- data/spec/units/stacker_bee/client_spec.rb +181 -0
- data/spec/units/stacker_bee/configuration_spec.rb +7 -0
- data/spec/units/stacker_bee/connection_spec.rb +45 -0
- data/spec/units/stacker_bee/middleware/logger_spec.rb +55 -0
- data/spec/units/stacker_bee/rash_spec.rb +87 -0
- data/spec/units/stacker_bee/request_error_spec.rb +44 -0
- data/spec/units/stacker_bee/request_spec.rb +49 -0
- data/spec/units/stacker_bee/response_spec.rb +79 -0
- data/spec/units/stacker_bee/utilities_spec.rb +25 -0
- data/spec/units/stacker_bee_spec.rb +6 -0
- data/stacker_bee.gemspec +30 -0
- metadata +225 -0
data/lib/stacker_bee.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require_stacker_bee = if defined?(require_relative)
|
2
|
+
lambda do |path|
|
3
|
+
require_relative path
|
4
|
+
end
|
5
|
+
else # for 1.8.7
|
6
|
+
lambda do |path|
|
7
|
+
require "stacker_bee/#{path}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
%w(
|
12
|
+
version
|
13
|
+
client
|
14
|
+
).each do |file_name|
|
15
|
+
require_stacker_bee["stacker_bee/#{file_name}"]
|
16
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "multi_json"
|
2
|
+
require "stacker_bee/utilities"
|
3
|
+
|
4
|
+
module StackerBee
|
5
|
+
class API
|
6
|
+
include Utilities
|
7
|
+
|
8
|
+
attr_accessor :api_path
|
9
|
+
|
10
|
+
def initialize(attrs = {})
|
11
|
+
attrs.each_pair do |key, value|
|
12
|
+
setter = "#{key}="
|
13
|
+
send(setter, value)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
endpoints[uncase(key)]
|
19
|
+
end
|
20
|
+
|
21
|
+
def key?(key)
|
22
|
+
endpoints.key? uncase(key)
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def endpoints
|
28
|
+
@endpoints ||= read_endpoints
|
29
|
+
end
|
30
|
+
|
31
|
+
def read_endpoints
|
32
|
+
return unless api_path
|
33
|
+
json = File.read(api_path)
|
34
|
+
response = MultiJson.load(json)
|
35
|
+
apis_by_endpoint(response)
|
36
|
+
end
|
37
|
+
|
38
|
+
def apis_by_endpoint(response)
|
39
|
+
response["listapisresponse"]["api"].each_with_object({}) do |api, memo|
|
40
|
+
memo[uncase(api["name"])] = api
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "multi_json"
|
2
|
+
require "stacker_bee/rash"
|
3
|
+
|
4
|
+
module StackerBee
|
5
|
+
module BodyParser
|
6
|
+
attr_reader :body
|
7
|
+
|
8
|
+
def body=(raw_response)
|
9
|
+
@body = parse(raw_response.body)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(json)
|
13
|
+
parsed = MultiJson.load(json)
|
14
|
+
fail "Cannot determine response key in #{parsed.keys}" if parsed.size > 1
|
15
|
+
case value = parsed.values.first
|
16
|
+
when Hash then Rash.new(value)
|
17
|
+
when Array then value.map { |item| Rash.new(item) }
|
18
|
+
else
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "stacker_bee/configuration"
|
3
|
+
require "stacker_bee/api"
|
4
|
+
require "stacker_bee/connection"
|
5
|
+
require "stacker_bee/request"
|
6
|
+
require "stacker_bee/response"
|
7
|
+
|
8
|
+
module StackerBee
|
9
|
+
class Client
|
10
|
+
DEFAULT_API_PATH = File.join(
|
11
|
+
File.dirname(__FILE__), '../../config/4.2.json'
|
12
|
+
)
|
13
|
+
|
14
|
+
extend Forwardable
|
15
|
+
def_delegators :configuration,
|
16
|
+
:logger,
|
17
|
+
:logger=,
|
18
|
+
:url,
|
19
|
+
:url=,
|
20
|
+
:api_key,
|
21
|
+
:api_key=,
|
22
|
+
:secret_key,
|
23
|
+
:secret_key=
|
24
|
+
|
25
|
+
class << self
|
26
|
+
|
27
|
+
def reset!
|
28
|
+
@api, @api_path, @default_config = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_config
|
32
|
+
@default_config ||= {
|
33
|
+
allow_empty_string_params: false
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def configuration=(config_hash)
|
38
|
+
default_config.merge!(config_hash)
|
39
|
+
end
|
40
|
+
|
41
|
+
def api_path
|
42
|
+
@api_path ||= DEFAULT_API_PATH
|
43
|
+
end
|
44
|
+
|
45
|
+
def api_path=(new_api_path)
|
46
|
+
return @api_path if @api_path == new_api_path
|
47
|
+
@api = nil
|
48
|
+
@api_path = new_api_path
|
49
|
+
end
|
50
|
+
|
51
|
+
def api
|
52
|
+
@api ||= API.new(api_path: api_path)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(config = {})
|
57
|
+
self.configuration = config
|
58
|
+
end
|
59
|
+
|
60
|
+
def configuration=(config)
|
61
|
+
@configuration = configuration_with_defaults(config)
|
62
|
+
end
|
63
|
+
|
64
|
+
def configuration
|
65
|
+
@configuration ||= configuration_with_defaults
|
66
|
+
end
|
67
|
+
|
68
|
+
def request(endpoint_name, params = {})
|
69
|
+
request = Request.new(endpoint_for(endpoint_name), api_key, params)
|
70
|
+
request.allow_empty_string_params =
|
71
|
+
configuration.allow_empty_string_params
|
72
|
+
raw_response = connection.get(request)
|
73
|
+
Response.new(raw_response)
|
74
|
+
end
|
75
|
+
|
76
|
+
def endpoint_for(name)
|
77
|
+
api = self.class.api[name]
|
78
|
+
api && api["name"]
|
79
|
+
end
|
80
|
+
|
81
|
+
def method_missing(name, *args, &block)
|
82
|
+
endpoint = endpoint_for(name)
|
83
|
+
if endpoint
|
84
|
+
request(endpoint, *args, &block)
|
85
|
+
else
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def respond_to?(name, include_private = false)
|
91
|
+
self.class.api.key?(name) || super
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
def connection
|
97
|
+
@connection ||= Connection.new(configuration)
|
98
|
+
end
|
99
|
+
|
100
|
+
def configuration_with_defaults(config = {})
|
101
|
+
config_hash = self.class.default_config.merge(config)
|
102
|
+
Configuration.new(config_hash)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "faraday"
|
2
|
+
require "uri"
|
3
|
+
require "stacker_bee/middleware/signed_query"
|
4
|
+
require "stacker_bee/middleware/logger"
|
5
|
+
|
6
|
+
module StackerBee
|
7
|
+
class ConnectionError < Exception
|
8
|
+
end
|
9
|
+
|
10
|
+
class Connection
|
11
|
+
attr_accessor :configuration
|
12
|
+
def initialize(configuration)
|
13
|
+
@configuration = configuration
|
14
|
+
uri = URI.parse(self.configuration.url)
|
15
|
+
@path = uri.path
|
16
|
+
uri.path = ''
|
17
|
+
fail ConnectionError, "no protocol specified" unless uri.scheme
|
18
|
+
@faraday = Faraday.new(url: uri.to_s) do |faraday|
|
19
|
+
faraday.use Middleware::SignedQuery, self.configuration.secret_key
|
20
|
+
faraday.use Middleware::Logger, self.configuration.logger
|
21
|
+
faraday.adapter Faraday.default_adapter # Net::HTTP
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(request)
|
26
|
+
@faraday.get(@path, request.query_params)
|
27
|
+
rescue Faraday::Error::ConnectionFailed
|
28
|
+
raise ConnectionError, "Failed to connect to #{configuration.url}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'logger'
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
module StackerBee
|
6
|
+
module Middleware
|
7
|
+
class Logger < Faraday::Response::Middleware
|
8
|
+
extend Forwardable
|
9
|
+
PROGNAME = "StackerBee"
|
10
|
+
|
11
|
+
attr_accessor :logger
|
12
|
+
|
13
|
+
def initialize(app, _logger = nil)
|
14
|
+
super(app)
|
15
|
+
self.logger = _logger
|
16
|
+
logger.progname ||= PROGNAME
|
17
|
+
end
|
18
|
+
|
19
|
+
def logger
|
20
|
+
@logger ||= ::Logger.new($stdout)
|
21
|
+
end
|
22
|
+
|
23
|
+
def_delegators :logger, :debug, :info, :warn, :error, :fatal
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
log_request(env)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_complete(env)
|
31
|
+
log_response(env)
|
32
|
+
end
|
33
|
+
|
34
|
+
def log_request(env)
|
35
|
+
info "#{env[:method]} #{env[:url]}"
|
36
|
+
debug env[:request_headers].pretty_inspect
|
37
|
+
end
|
38
|
+
|
39
|
+
def log_response(env)
|
40
|
+
status_message = "Status: #{env[:status]}"
|
41
|
+
env[:status] < 400 ? info(status_message) : error(status_message)
|
42
|
+
debug env[:response_headers].pretty_inspect
|
43
|
+
debug env[:body]
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "faraday"
|
2
|
+
require "base64"
|
3
|
+
|
4
|
+
module StackerBee
|
5
|
+
module Middleware
|
6
|
+
class SignedQuery < Faraday::Middleware
|
7
|
+
def initialize(app, key)
|
8
|
+
@key = key
|
9
|
+
fail "Key cannot be nil" unless @key
|
10
|
+
super(app)
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
sign_uri(env[:url])
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
def sign_uri(uri)
|
19
|
+
downcased = uri.query.downcase
|
20
|
+
nonplussed = downcased.gsub('+', '%20')
|
21
|
+
signed = OpenSSL::HMAC.digest 'sha1', @key, nonplussed
|
22
|
+
encoded = Base64.encode64(signed).chomp
|
23
|
+
escaped = CGI.escape(encoded)
|
24
|
+
|
25
|
+
uri.query << "&signature=#{escaped}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require "stacker_bee/utilities"
|
3
|
+
|
4
|
+
module StackerBee
|
5
|
+
class Rash
|
6
|
+
extend Forwardable
|
7
|
+
include Utilities
|
8
|
+
|
9
|
+
def_delegators :@hash, *[
|
10
|
+
:default, :default_proc, :each_value, :empty?, :has_value?, :hash,
|
11
|
+
:length, :size, :value?, :values, :assoc, :each, :each_key, :each_pair,
|
12
|
+
:flatten, :invert, :keys, :key, :merge, :rassoc, :to_a, :to_h, :to_hash
|
13
|
+
]
|
14
|
+
|
15
|
+
def initialize(hash = {})
|
16
|
+
@hash = {}
|
17
|
+
hash.each_pair do |key, value|
|
18
|
+
@hash[convert_key(key)] = convert_value(value)
|
19
|
+
end
|
20
|
+
@hash.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
case other
|
25
|
+
when Rash
|
26
|
+
super || @hash == other.to_hash
|
27
|
+
when Hash
|
28
|
+
self == Rash.new(other)
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def select(*args, &block)
|
35
|
+
Rash.new @hash.select(*args, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def reject(*args, &block)
|
39
|
+
Rash.new @hash.reject(*args, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def values_at(*keys)
|
43
|
+
@hash.values_at(*keys.map { |key| convert_key(key) })
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch(key, *args, &block)
|
47
|
+
@hash.fetch(convert_key(key), *args, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](key)
|
51
|
+
@hash[convert_key(key)]
|
52
|
+
end
|
53
|
+
|
54
|
+
def key?(key)
|
55
|
+
@hash.key?(convert_key(key))
|
56
|
+
end
|
57
|
+
alias_method :include?, :key?
|
58
|
+
alias_method :has_key?, :key?
|
59
|
+
alias_method :member?, :key?
|
60
|
+
|
61
|
+
def to_hash
|
62
|
+
self.class.deep_dup(@hash)
|
63
|
+
end
|
64
|
+
|
65
|
+
def inspect
|
66
|
+
"#<#{self.class} #{@hash}>"
|
67
|
+
end
|
68
|
+
alias_method :to_s, :inspect
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def self.deep_dup(hash)
|
73
|
+
hash.dup.tap do |duplicate|
|
74
|
+
duplicate.each_pair do |key, value|
|
75
|
+
duplicate[key] = deep_dup(value) if value.is_a?(Hash)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def convert_key(key)
|
81
|
+
key.kind_of?(Numeric) ? key : uncase(key)
|
82
|
+
end
|
83
|
+
|
84
|
+
def convert_value(value)
|
85
|
+
case value
|
86
|
+
when Hash
|
87
|
+
Rash.new(value)
|
88
|
+
when Array
|
89
|
+
value.map { |item| convert_value(item) }
|
90
|
+
else
|
91
|
+
value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "stacker_bee/utilities"
|
2
|
+
|
3
|
+
module StackerBee
|
4
|
+
class Request
|
5
|
+
include Utilities
|
6
|
+
|
7
|
+
RESPONSE_TYPE = "json"
|
8
|
+
|
9
|
+
attr_accessor :params
|
10
|
+
attr_writer :allow_empty_string_params
|
11
|
+
|
12
|
+
def initialize(endpoint, api_key, params = {})
|
13
|
+
params[:api_key] = api_key
|
14
|
+
params[:command] = endpoint
|
15
|
+
params[:response] = RESPONSE_TYPE
|
16
|
+
self.params = params
|
17
|
+
end
|
18
|
+
|
19
|
+
def query_params
|
20
|
+
params
|
21
|
+
.reject { |key, val| val.nil? }
|
22
|
+
.reject { |key, val| !allow_empty_string_params && val == '' }
|
23
|
+
.sort
|
24
|
+
.map { |(key, val)| [cloud_stack_key(key), cloud_stack_value(val)] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def allow_empty_string_params
|
28
|
+
@allow_empty_string_params ||= false
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def cloud_stack_key(key)
|
34
|
+
camel_case(key, true)
|
35
|
+
end
|
36
|
+
|
37
|
+
def cloud_stack_value(value)
|
38
|
+
value.respond_to?(:join) ? value.join(',') : value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|