miasma 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -0
- data/README.md +179 -0
- data/lib/miasma.rb +52 -0
- data/lib/miasma/contrib/aws.rb +390 -0
- data/lib/miasma/contrib/aws/auto_scale.rb +85 -0
- data/lib/miasma/contrib/aws/compute.rb +112 -0
- data/lib/miasma/contrib/aws/load_balancer.rb +185 -0
- data/lib/miasma/contrib/aws/orchestration.rb +338 -0
- data/lib/miasma/contrib/rackspace.rb +164 -0
- data/lib/miasma/contrib/rackspace/auto_scale.rb +84 -0
- data/lib/miasma/contrib/rackspace/compute.rb +104 -0
- data/lib/miasma/contrib/rackspace/load_balancer.rb +117 -0
- data/lib/miasma/contrib/rackspace/orchestration.rb +255 -0
- data/lib/miasma/error.rb +89 -0
- data/lib/miasma/models.rb +14 -0
- data/lib/miasma/models/auto_scale.rb +55 -0
- data/lib/miasma/models/auto_scale/group.rb +64 -0
- data/lib/miasma/models/auto_scale/groups.rb +34 -0
- data/lib/miasma/models/block_storage.rb +0 -0
- data/lib/miasma/models/compute.rb +70 -0
- data/lib/miasma/models/compute/server.rb +71 -0
- data/lib/miasma/models/compute/servers.rb +35 -0
- data/lib/miasma/models/dns.rb +0 -0
- data/lib/miasma/models/load_balancer.rb +55 -0
- data/lib/miasma/models/load_balancer/balancer.rb +72 -0
- data/lib/miasma/models/load_balancer/balancers.rb +34 -0
- data/lib/miasma/models/monitoring.rb +0 -0
- data/lib/miasma/models/orchestration.rb +127 -0
- data/lib/miasma/models/orchestration/event.rb +38 -0
- data/lib/miasma/models/orchestration/events.rb +64 -0
- data/lib/miasma/models/orchestration/resource.rb +79 -0
- data/lib/miasma/models/orchestration/resources.rb +55 -0
- data/lib/miasma/models/orchestration/stack.rb +144 -0
- data/lib/miasma/models/orchestration/stacks.rb +46 -0
- data/lib/miasma/models/queues.rb +0 -0
- data/lib/miasma/models/storage.rb +60 -0
- data/lib/miasma/models/storage/bucket.rb +36 -0
- data/lib/miasma/models/storage/buckets.rb +41 -0
- data/lib/miasma/models/storage/file.rb +45 -0
- data/lib/miasma/models/storage/files.rb +52 -0
- data/lib/miasma/types.rb +13 -0
- data/lib/miasma/types/api.rb +145 -0
- data/lib/miasma/types/collection.rb +116 -0
- data/lib/miasma/types/data.rb +53 -0
- data/lib/miasma/types/model.rb +118 -0
- data/lib/miasma/types/thin_model.rb +76 -0
- data/lib/miasma/utils.rb +12 -0
- data/lib/miasma/utils/animal_strings.rb +29 -0
- data/lib/miasma/utils/immutable.rb +36 -0
- data/lib/miasma/utils/lazy.rb +231 -0
- data/lib/miasma/utils/memoization.rb +55 -0
- data/lib/miasma/utils/smash.rb +149 -0
- data/lib/miasma/version.rb +4 -0
- data/miasma.gemspec +18 -0
- metadata +57 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f4c2fc901adb9940a31de3c3379646e1225eea6
|
4
|
+
data.tar.gz: c45a82a7de2a1004a51a6c1a3b5fb10c60028088
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e0147bc6672e477a407ff627ea6e37839cae93bc16c6f26499cd5f57a4069358a2e6849062d153bfd1b9940b23fe0568f0e5c6cf40b561be0bdfcd4476ff461
|
7
|
+
data.tar.gz: cdec65223d5ede929aad74fdf520b1a99c9a67a5d63a4559fff2cf414c5bad11eba6342da21fe9a5f27b89ff2f2b71488452e65123da99cac6a7add616902e74
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
```
|
2
|
+
███▄ ▄███▓ ██▓ ▄▄▄ ██████ ███▄ ▄███▓ ▄▄▄
|
3
|
+
▓██▒▀█▀ ██▒▓██▒▒████▄ ▒██ ▒ ▓██▒▀█▀ ██▒▒████▄
|
4
|
+
▓██ ▓██░▒██▒▒██ ▀█▄ ░ ▓██▄ ▓██ ▓██░▒██ ▀█▄
|
5
|
+
▒██ ▒██ ░██░░██▄▄▄▄██ ▒ ██▒▒██ ▒██ ░██▄▄▄▄██
|
6
|
+
▒██▒ ░██▒░██░ ▓█ ▓██▒▒██████▒▒▒██▒ ░██▒ ▓█ ▓██▒
|
7
|
+
░ ▒░ ░ ░░▓ ▒▒ ▓▒█░▒ ▒▓▒ ▒ ░░ ▒░ ░ ░ ▒▒ ▓▒█░
|
8
|
+
░ ░ ░ ▒ ░ ▒ ▒▒ ░░ ░▒ ░ ░░ ░ ░ ▒ ▒▒ ░
|
9
|
+
░ ░ ▒ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ▒
|
10
|
+
░ ░ ░ ░ ░ ░ ░ ░
|
11
|
+
```
|
12
|
+
|
13
|
+
## Overview
|
14
|
+
|
15
|
+
Miasma is YACACL (Yet Another Cloud API Client Library). Instead of
|
16
|
+
attempting to cover all the functionalities of all the different
|
17
|
+
cloud and virt APIs, miasma is focused on providing a common modeling
|
18
|
+
that will "just work". This means there may be many things that seem
|
19
|
+
to be missing, but that's probably okay. Consistency is more important
|
20
|
+
than overall completeness. Miasma isn't trying to be a replacement
|
21
|
+
for libraries like fog, rather it's attempting to supplement those
|
22
|
+
libraries by providing a consistent modeling API.
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
### Example
|
27
|
+
|
28
|
+
Lets have a look at using the compute model:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
compute = Miasma.api(
|
32
|
+
:type => :compute,
|
33
|
+
:provider => :aws,
|
34
|
+
:credentials => {
|
35
|
+
:aws_secret_access_key => 'SECRET',
|
36
|
+
:aws_access_key_id => 'KEY_ID',
|
37
|
+
:aws_region => 'us-west-2'
|
38
|
+
}
|
39
|
+
)
|
40
|
+
```
|
41
|
+
|
42
|
+
With this we can now list existing servers:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
compute.servers.all
|
46
|
+
```
|
47
|
+
|
48
|
+
This will provide an array of `Miasma::Models::Compute::Server`
|
49
|
+
instances. It will also cache the result so subsequent calls
|
50
|
+
will not require another API call. The list can be refreshed
|
51
|
+
by requesting a reload:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
compute.servers.reload
|
55
|
+
```
|
56
|
+
|
57
|
+
### Switching providers
|
58
|
+
|
59
|
+
Switching providers requires modification to the API parameters:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
compute = Miasma.api(
|
63
|
+
:provider => :rackspace,
|
64
|
+
:credentials => {
|
65
|
+
:rackspace_username => 'USER',
|
66
|
+
:rackspace_api_key => 'KEY',
|
67
|
+
:rackspace_region => 'REGION'
|
68
|
+
}
|
69
|
+
)
|
70
|
+
```
|
71
|
+
|
72
|
+
The `compute` API will act exactly the same as before, now using
|
73
|
+
Rackspace instead of AWS.
|
74
|
+
|
75
|
+
## Design Objectives
|
76
|
+
|
77
|
+
This library is following a few simple objectives:
|
78
|
+
|
79
|
+
* Light weight
|
80
|
+
* Consistent API
|
81
|
+
|
82
|
+
The availabile API is defined first via the models,
|
83
|
+
then concrete implementations are built via available
|
84
|
+
provider interfaces. This means the provider code is
|
85
|
+
structured to support the models instead of the models
|
86
|
+
being built around specific providers. The result is
|
87
|
+
a clean model interface providing consistency regardless
|
88
|
+
of the provider backend.
|
89
|
+
|
90
|
+
The "weight" of the library is kept light by using a
|
91
|
+
few simple approaches. All code is lazy loaded, so nothing
|
92
|
+
will be loaded into the runtime until it is actually required.
|
93
|
+
Dependencies are also kept very light, to reduce the number
|
94
|
+
of required libraries needing to be loaded. Parser wrapping
|
95
|
+
libraries are also used (`multi_json` and `multi_xml`) allowing
|
96
|
+
a choice of actual parsing backends in use. This removes
|
97
|
+
dependencies on nokogiri unless it's actually desired and
|
98
|
+
increases installation speeds.
|
99
|
+
|
100
|
+
## Features
|
101
|
+
|
102
|
+
### Thin Models
|
103
|
+
|
104
|
+
Thin models are a stripped down model that provides a subset
|
105
|
+
of information that an actual instance of the model may
|
106
|
+
contain. For instance, an `AutoScale::Group` may contain
|
107
|
+
a list of `Compute::Server`s. The collection provided within
|
108
|
+
the `AutoScale::Group` will be created via the resulting
|
109
|
+
API information on the group itself. However, since
|
110
|
+
we can provide expected mappings to what API provides
|
111
|
+
`Compute` and know these instances will be within the
|
112
|
+
`servers` collection, we can use the `#expand` helper to
|
113
|
+
automatically load the full instance:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
auto_scale = Miasma.api(:type => :auto_scale, :provider ...)
|
117
|
+
group = auto_scale.groups.first
|
118
|
+
|
119
|
+
# this list will provide the `ThinModel` instances:
|
120
|
+
p group.servers.all
|
121
|
+
|
122
|
+
# this list will provide the full instances:
|
123
|
+
p group.servers.all.map(&:expand)
|
124
|
+
```
|
125
|
+
|
126
|
+
### Automatic API switching
|
127
|
+
|
128
|
+
Resources within a specific provider can span multiple
|
129
|
+
API endpoints. To deal with this, the provider API
|
130
|
+
implemenetations provide an `#api_for` method which
|
131
|
+
will automatically build a new API instance. This
|
132
|
+
allows Miasma to hop APIs under the hood to expand
|
133
|
+
`ThinModels` as shown above.
|
134
|
+
|
135
|
+
## Current status
|
136
|
+
|
137
|
+
Miasma is currently under active development and is
|
138
|
+
in a beta state. Models are still being implemented
|
139
|
+
and spec coverage is following closely behind the
|
140
|
+
model completions.
|
141
|
+
|
142
|
+
### Currently Supported Providers
|
143
|
+
|
144
|
+
* AWS
|
145
|
+
* Rackspace
|
146
|
+
|
147
|
+
### Models
|
148
|
+
|
149
|
+
#### AWS
|
150
|
+
|
151
|
+
|Model |Create|Read|Update|Delete|
|
152
|
+
|--------------|------|----|------|------|
|
153
|
+
|AutoScale | X | | | |
|
154
|
+
|BlockStorage | | | | |
|
155
|
+
|Compute | X | X | | X |
|
156
|
+
|DNS | | | | |
|
157
|
+
|LoadBalancer | X | X | X | X |
|
158
|
+
|Network | | | | |
|
159
|
+
|Orchestration | X | X | X | X |
|
160
|
+
|Queues | | | | |
|
161
|
+
|Storage | | | | |
|
162
|
+
|
163
|
+
#### Rackspace
|
164
|
+
|
165
|
+
|Model |Create|Read|Update|Delete|
|
166
|
+
|--------------|------|----|------|------|
|
167
|
+
|AutoScale | X | | | |
|
168
|
+
|BlockStorage | | | | |
|
169
|
+
|Compute | X | X | | X |
|
170
|
+
|DNS | | | | |
|
171
|
+
|LoadBalancer | | X | | |
|
172
|
+
|Network | | | | |
|
173
|
+
|Orchestration | X | X | X | X |
|
174
|
+
|Queues | | | | |
|
175
|
+
|Storage | | | | |
|
176
|
+
|
177
|
+
## Info
|
178
|
+
|
179
|
+
* Repository: https://github.com/chrisroberts/miasma
|
data/lib/miasma.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Load in dependencies
|
2
|
+
require 'http'
|
3
|
+
require 'multi_json'
|
4
|
+
require 'multi_xml'
|
5
|
+
|
6
|
+
# Make version available
|
7
|
+
require 'miasma/version'
|
8
|
+
|
9
|
+
module Miasma
|
10
|
+
autoload :Error, 'miasma/error'
|
11
|
+
autoload :Models, 'miasma/models'
|
12
|
+
autoload :Types, 'miasma/types'
|
13
|
+
autoload :Utils, 'miasma/utils'
|
14
|
+
|
15
|
+
# Generate and API connection
|
16
|
+
#
|
17
|
+
# @param args [Hash]
|
18
|
+
# @option args [String, Symbol] :type API type (:compute, :dns, etc)
|
19
|
+
# @option args [String, Symbol] :provider Service provider
|
20
|
+
# @option args [Hash] :credentials Service provider credentials
|
21
|
+
def self.api(args={})
|
22
|
+
args = Utils::Smash.new(args)
|
23
|
+
[:type, :provider, :credentials].each do |key|
|
24
|
+
unless(args[key])
|
25
|
+
raise ArgumentError.new "Missing required api argument `#{key.inspect}`!"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
args[:type] = Utils.camel(args[:type].to_s).to_sym
|
29
|
+
args[:provider] = Utils.camel(args[:provider].to_s).to_sym
|
30
|
+
begin
|
31
|
+
require "miasma/contrib/#{Utils.snake(args[:provider])}"
|
32
|
+
rescue LoadError
|
33
|
+
# just ignore
|
34
|
+
end
|
35
|
+
base_klass = Models.const_get(args[:type])
|
36
|
+
begin
|
37
|
+
if(base_klass)
|
38
|
+
api_klass = base_klass.const_get(args[:provider])
|
39
|
+
if(api_klass)
|
40
|
+
api_klass.new(args[:credentials].to_smash)
|
41
|
+
else
|
42
|
+
raise Error.new "Failed to locate #{args[:type]} API for #{args[:provider].inspect}"
|
43
|
+
end
|
44
|
+
else
|
45
|
+
raise Error.new "Failed to locate request API type #{args[:type].inspect}"
|
46
|
+
end
|
47
|
+
rescue NameError
|
48
|
+
raise Error.new "Failed to locate request API type #{args[:type].inspect}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,390 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
require 'miasma/utils/smash'
|
3
|
+
|
4
|
+
require 'time'
|
5
|
+
require 'openssl'
|
6
|
+
|
7
|
+
module Miasma
|
8
|
+
module Contrib
|
9
|
+
# Core API for AWS access
|
10
|
+
class AwsApiCore
|
11
|
+
|
12
|
+
module RequestUtils
|
13
|
+
|
14
|
+
# Fetch all results when tokens are being used
|
15
|
+
# for paging results
|
16
|
+
#
|
17
|
+
# @param next_token [String]
|
18
|
+
# @param result_key [Array<String, Symbol>] path to result
|
19
|
+
# @yield block to perform request
|
20
|
+
# @yieldparam options [Hash] request parameters (token information)
|
21
|
+
# @return [Array]
|
22
|
+
def all_result_pages(next_token, *result_key, &block)
|
23
|
+
list = []
|
24
|
+
options = next_token ? Smash.new('NextToken' => next_token) : Smash.new
|
25
|
+
result = block.call(options)
|
26
|
+
content = result.get(*result_key.dup)
|
27
|
+
if(content.is_a?(Array))
|
28
|
+
list += content
|
29
|
+
else
|
30
|
+
list << content
|
31
|
+
end
|
32
|
+
if(token = result.get(:body, 'NextToken'))
|
33
|
+
list += all_result_pages(token, *result_key, &block)
|
34
|
+
end
|
35
|
+
list.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [String] current time ISO8601 format
|
41
|
+
def self.time_iso8601
|
42
|
+
Time.now.utc.strftime('%Y%m%dT%H%M%SZ')
|
43
|
+
end
|
44
|
+
|
45
|
+
# HMAC helper class
|
46
|
+
class Hmac
|
47
|
+
|
48
|
+
# @return [OpenSSL::Digest]
|
49
|
+
attr_reader :digest
|
50
|
+
# @return [String] secret key
|
51
|
+
attr_reader :key
|
52
|
+
|
53
|
+
# Create new HMAC helper
|
54
|
+
#
|
55
|
+
# @param kind [String] digest type (sha1, sha256, sha512, etc)
|
56
|
+
# @param key [String] secret key
|
57
|
+
# @return [self]
|
58
|
+
def initialize(kind, key)
|
59
|
+
@digest = OpenSSL::Digest.new(kind)
|
60
|
+
@key = key
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [String]
|
64
|
+
def to_s
|
65
|
+
"Hmac#{digest.name}"
|
66
|
+
end
|
67
|
+
|
68
|
+
# Generate the hexdigest of the content
|
69
|
+
#
|
70
|
+
# @param content [String] content to digest
|
71
|
+
# @return [String] hashed result
|
72
|
+
def hexdigest_of(content)
|
73
|
+
digest << content
|
74
|
+
hash = digest.hexdigest
|
75
|
+
digest.reset
|
76
|
+
hash
|
77
|
+
end
|
78
|
+
|
79
|
+
# Sign the given data
|
80
|
+
#
|
81
|
+
# @param data [String]
|
82
|
+
# @param key_override [Object]
|
83
|
+
# @return [Object] signature
|
84
|
+
def sign(data, key_override=nil)
|
85
|
+
result = OpenSSL::HMAC.digest(digest, key_override || key, data)
|
86
|
+
digest.reset
|
87
|
+
result
|
88
|
+
end
|
89
|
+
|
90
|
+
# Sign the given data and return hexdigest
|
91
|
+
#
|
92
|
+
# @param data [String]
|
93
|
+
# @param key_override [Object]
|
94
|
+
# @return [String] hex encoded signature
|
95
|
+
def hex_sign(data, key_override=nil)
|
96
|
+
result = OpenSSL::HMAC.hexdigest(digest, key_override || key, data)
|
97
|
+
digest.reset
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
# Base signature class
|
104
|
+
class Signature
|
105
|
+
|
106
|
+
# Create new instance
|
107
|
+
def initialize(*args)
|
108
|
+
raise NotImplementedError.new 'This class should not be used directly!'
|
109
|
+
end
|
110
|
+
|
111
|
+
# Generate the signature
|
112
|
+
#
|
113
|
+
# @param http_method [Symbol] HTTP request method
|
114
|
+
# @param path [String] request path
|
115
|
+
# @param opts [Hash] request options
|
116
|
+
# @return [String] signature
|
117
|
+
def generate(http_method, path, opts={})
|
118
|
+
raise NotImplementedError
|
119
|
+
end
|
120
|
+
|
121
|
+
# URL string escape compatible with AWS requirements
|
122
|
+
#
|
123
|
+
# @param string [String] string to escape
|
124
|
+
# @return [String] escaped string
|
125
|
+
def safe_escape(string)
|
126
|
+
string.to_s.gsub(/([^a-zA-Z0-9_.\-~])/) do
|
127
|
+
'%' << $1.unpack('H2' * $1.bytesize).join('%').upcase
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
# AWS signature version 4
|
134
|
+
class SignatureV4 < Signature
|
135
|
+
|
136
|
+
# @return [Hmac]
|
137
|
+
attr_reader :hmac
|
138
|
+
# @return [String] access key
|
139
|
+
attr_reader :access_key
|
140
|
+
# @return [String] region
|
141
|
+
attr_reader :region
|
142
|
+
# @return [String] service
|
143
|
+
attr_reader :service
|
144
|
+
|
145
|
+
# Create new signature generator
|
146
|
+
#
|
147
|
+
# @param access_key [String]
|
148
|
+
# @param secret_key [String]
|
149
|
+
# @param region [String]
|
150
|
+
# @param service [String]
|
151
|
+
# @return [self]
|
152
|
+
def initialize(access_key, secret_key, region, service)
|
153
|
+
@hmac = Hmac.new('sha256', secret_key)
|
154
|
+
@access_key = access_key
|
155
|
+
@region = region
|
156
|
+
@service = service
|
157
|
+
end
|
158
|
+
|
159
|
+
# Generate the signature
|
160
|
+
#
|
161
|
+
# @param http_method [Symbol] HTTP request method
|
162
|
+
# @param path [String] request path
|
163
|
+
# @param opts [Hash] request options
|
164
|
+
# @return [String] signature
|
165
|
+
def generate(http_method, path, opts)
|
166
|
+
to_sign = [
|
167
|
+
algorithm,
|
168
|
+
AwsApiCore.time_iso8601,
|
169
|
+
credential_scope,
|
170
|
+
hashed_canonical_request(
|
171
|
+
build_canonical_request(http_method, path, opts)
|
172
|
+
)
|
173
|
+
].join("\n")
|
174
|
+
signature = sign_request(to_sign)
|
175
|
+
"#{algorithm} Credential=#{access_key}/#{credential_scope}, SignedHeaders=#{signed_headers(opts[:headers])}, Signature=#{signature}"
|
176
|
+
end
|
177
|
+
|
178
|
+
# Sign the request
|
179
|
+
#
|
180
|
+
# @param request [String] request to sign
|
181
|
+
# @return [String] signature
|
182
|
+
def sign_request(request)
|
183
|
+
key = hmac.sign(
|
184
|
+
'aws4_request',
|
185
|
+
hmac.sign(
|
186
|
+
service,
|
187
|
+
hmac.sign(
|
188
|
+
region,
|
189
|
+
hmac.sign(
|
190
|
+
Time.now.utc.strftime('%Y%m%d'),
|
191
|
+
"AWS4#{hmac.key}"
|
192
|
+
)
|
193
|
+
)
|
194
|
+
)
|
195
|
+
)
|
196
|
+
hmac.hex_sign(request, key)
|
197
|
+
end
|
198
|
+
|
199
|
+
# @return [String] signature algorithm
|
200
|
+
def algorithm
|
201
|
+
'AWS4-HMAC-SHA256'
|
202
|
+
end
|
203
|
+
|
204
|
+
# @return [String] credential scope for request
|
205
|
+
def credential_scope
|
206
|
+
[
|
207
|
+
Time.now.utc.strftime('%Y%m%d'),
|
208
|
+
region,
|
209
|
+
service,
|
210
|
+
'aws4_request'
|
211
|
+
].join('/')
|
212
|
+
end
|
213
|
+
|
214
|
+
# Generate the hash of the canonical request
|
215
|
+
#
|
216
|
+
# @param request [String] canonical request string
|
217
|
+
# @return [String] hashed canonical request
|
218
|
+
def hashed_canonical_request(request)
|
219
|
+
hmac.hexdigest_of(request)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Build the canonical request string used for signing
|
223
|
+
#
|
224
|
+
# @param http_method [Symbol] HTTP request method
|
225
|
+
# @param path [String] request path
|
226
|
+
# @param opts [Hash] request options
|
227
|
+
# @return [String] canonical request string
|
228
|
+
def build_canonical_request(http_method, path, opts)
|
229
|
+
[
|
230
|
+
http_method.to_s.upcase,
|
231
|
+
path,
|
232
|
+
canonical_query(opts[:params]),
|
233
|
+
canonical_headers(opts[:headers]),
|
234
|
+
signed_headers(opts[:headers]),
|
235
|
+
canonical_payload(opts)
|
236
|
+
].join("\n")
|
237
|
+
end
|
238
|
+
|
239
|
+
# Build the canonical query string used for signing
|
240
|
+
#
|
241
|
+
# @param params [Hash] query params
|
242
|
+
# @return [String] canonical query string
|
243
|
+
def canonical_query(params)
|
244
|
+
params ||= {}
|
245
|
+
params = Hash[params.sort_by(&:first)]
|
246
|
+
query = params.map do |key, value|
|
247
|
+
"#{safe_escape(key)}=#{safe_escape(value)}"
|
248
|
+
end.join('&')
|
249
|
+
end
|
250
|
+
|
251
|
+
# Build the canonical header string used for signing
|
252
|
+
#
|
253
|
+
# @param headers [Hash] request headers
|
254
|
+
# @return [String] canonical headers string
|
255
|
+
def canonical_headers(headers)
|
256
|
+
headers ||= {}
|
257
|
+
headers = Hash[headers.sort_by(&:first)]
|
258
|
+
headers.map do |key, value|
|
259
|
+
[key.downcase, value.chomp].join(':')
|
260
|
+
end.join("\n") << "\n"
|
261
|
+
end
|
262
|
+
|
263
|
+
# List of headers included in signature
|
264
|
+
#
|
265
|
+
# @param headers [Hash] request headers
|
266
|
+
# @return [String] header list
|
267
|
+
def signed_headers(headers)
|
268
|
+
headers ||= {}
|
269
|
+
headers.sort_by(&:first).map(&:first).
|
270
|
+
map(&:downcase).join(';')
|
271
|
+
end
|
272
|
+
|
273
|
+
# Build the canonical payload string used for signing
|
274
|
+
#
|
275
|
+
# @param options [Hash] request options
|
276
|
+
# @return [String] body checksum
|
277
|
+
def canonical_payload(options)
|
278
|
+
body = options.fetch(:body, '')
|
279
|
+
if(options[:json])
|
280
|
+
body = MultiJson.dump(options[:json])
|
281
|
+
elsif(options[:form])
|
282
|
+
body = URI.encode_www_form(options[:form])
|
283
|
+
end
|
284
|
+
hmac.hexdigest_of(body)
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
module ApiCommon
|
290
|
+
|
291
|
+
def self.included(klass)
|
292
|
+
klass.class_eval do
|
293
|
+
attribute :aws_access_key_id, String, :required => true
|
294
|
+
attribute :aws_secret_access_key, String, :required => true
|
295
|
+
attribute :aws_region, String, :required => true
|
296
|
+
attribute :aws_host, String
|
297
|
+
|
298
|
+
# @return [Contrib::AwsApiCore::SignatureV4]
|
299
|
+
attr_reader :signer
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Build new API for specified type using current provider / creds
|
304
|
+
#
|
305
|
+
# @param type [Symbol] api type
|
306
|
+
# @return [Api]
|
307
|
+
def api_for(type)
|
308
|
+
memoize(type) do
|
309
|
+
creds = attributes.dup
|
310
|
+
creds.delete(:aws_host)
|
311
|
+
Miasma.api(
|
312
|
+
Smash.new(
|
313
|
+
:type => type,
|
314
|
+
:provider => provider,
|
315
|
+
:credentials => creds
|
316
|
+
)
|
317
|
+
)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Setup for API connections
|
322
|
+
def connect
|
323
|
+
unless(aws_host)
|
324
|
+
self.aws_host = [
|
325
|
+
self.class::API_SERVICE.downcase,
|
326
|
+
aws_region,
|
327
|
+
'amazonaws.com'
|
328
|
+
].join('.')
|
329
|
+
end
|
330
|
+
@signer = Contrib::AwsApiCore::SignatureV4.new(
|
331
|
+
aws_access_key_id, aws_secret_access_key, aws_region, self.class::API_SERVICE
|
332
|
+
)
|
333
|
+
end
|
334
|
+
|
335
|
+
# @return [HTTP] connection for requests (forces headers)
|
336
|
+
def connection
|
337
|
+
super.with_headers(
|
338
|
+
'Host' => aws_host,
|
339
|
+
'X-Amz-Date' => Contrib::AwsApiCore.time_iso8601
|
340
|
+
)
|
341
|
+
end
|
342
|
+
|
343
|
+
# @return [String] endpoint for request
|
344
|
+
def endpoint
|
345
|
+
"https://#{aws_host}"
|
346
|
+
end
|
347
|
+
|
348
|
+
# Override to inject signature
|
349
|
+
#
|
350
|
+
# @param connection [HTTP]
|
351
|
+
# @param http_method [Symbol]
|
352
|
+
# @param request_args [Array]
|
353
|
+
# @return [HTTP::Response]
|
354
|
+
# @note if http_method is :post, params will be automatically
|
355
|
+
# removed and placed into :form
|
356
|
+
def make_request(connection, http_method, request_args)
|
357
|
+
dest, options = request_args
|
358
|
+
path = URI.parse(dest).path
|
359
|
+
options = options.to_smash
|
360
|
+
options[:params] = options.fetch(:params, Smash.new).to_smash.deep_merge('Version' => self.class::API_VERSION)
|
361
|
+
if(http_method.to_sym == :post)
|
362
|
+
if(options[:form])
|
363
|
+
options[:form].merge(options.delete(:params))
|
364
|
+
else
|
365
|
+
options[:form] = options.delete(:params)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
signature = signer.generate(
|
369
|
+
http_method, path, options.merge(
|
370
|
+
Smash.new(
|
371
|
+
:headers => Smash[
|
372
|
+
connection.default_headers.to_a
|
373
|
+
]
|
374
|
+
)
|
375
|
+
)
|
376
|
+
)
|
377
|
+
options = Hash[options.map{|k,v|[k.to_sym,v]}]
|
378
|
+
connection.auth(signature).send(http_method, dest, options)
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
Models::Compute.autoload :Aws, 'miasma/contrib/aws/compute'
|
387
|
+
Models::LoadBalancer.autoload :Aws, 'miasma/contrib/aws/load_balancer'
|
388
|
+
Models::AutoScale.autoload :Aws, 'miasma/contrib/aws/auto_scale'
|
389
|
+
Models::Orchestration.autoload :Aws, 'miasma/contrib/aws/orchestration'
|
390
|
+
end
|