stacker_bee 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|