miasma-azure 0.1.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/CHANGELOG.md +2 -0
- data/LICENSE +13 -0
- data/README.md +91 -0
- data/lib/miasma-azure.rb +2 -0
- data/lib/miasma-azure/api.rb +18 -0
- data/lib/miasma-azure/version.rb +4 -0
- data/lib/miasma/contrib/azure.rb +369 -0
- data/lib/miasma/contrib/azure/orchestration.rb +461 -0
- data/lib/miasma/contrib/azure/storage.rb +356 -0
- data/miasma-azure.gemspec +23 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: be83f458b27083e4429033147f913df0d7e4fb91
|
4
|
+
data.tar.gz: bec0131489f448f8a384f97144ff9f4f6c1aae27
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 340a89e131f1f5c59e2c26310ed5a526ba287d77ca638a4780fd165a8ffc0226b59887a8d1ab91151e32ef684d5967b605cb24ea53cd93df97d327534fa2546d
|
7
|
+
data.tar.gz: 2ec209dab7c0a261cb17debcf991385b89e568658a53f0b3e27b9d1bc0d72d7357af16fcd87127fdf0db0ec12946400bb24da503892d2ee29f0b49def8c89a25
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2016 Chris Roberts
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Miasma Azure
|
2
|
+
|
3
|
+
Azure API plugin for the miasma cloud library
|
4
|
+
|
5
|
+
## Setup
|
6
|
+
|
7
|
+
### Storage Credentials
|
8
|
+
|
9
|
+
Storage makes use of the Azure Blob Storage Service:
|
10
|
+
|
11
|
+
* `azure_blob_account_name` - Name of blob storage service account
|
12
|
+
* `azure_blob_secret_key` - Secret key for blob storage access
|
13
|
+
|
14
|
+
### Orchestration Credentials
|
15
|
+
|
16
|
+
Orchestration makes use of two services:
|
17
|
+
|
18
|
+
1. Azure Storage Services - Blob
|
19
|
+
2. Azure Resource Manager
|
20
|
+
|
21
|
+
> Credentials for the blob service are defined above.
|
22
|
+
|
23
|
+
Credentials for the Azure Resource Manager require some setup
|
24
|
+
within Azure due to the OAuth2 requirement. To setup an OAuth2
|
25
|
+
application allowing miasma to function properly, perform the
|
26
|
+
following steps:
|
27
|
+
|
28
|
+
Start at the Azure portal:
|
29
|
+
|
30
|
+
1. Click `Browse` to open available service list
|
31
|
+
2. Click `Active Directory` to open AD service
|
32
|
+
3. Choose desired directory and click `APPLICATIONS`
|
33
|
+
4. At the bottom of the page click `ADD`
|
34
|
+
5. Click `Add an application my organization is developing`
|
35
|
+
6. Enter a name for the application
|
36
|
+
7. Click the `WEB APPLICATION AND/OR WEB API` radio button
|
37
|
+
8. Click the next arrow `->`
|
38
|
+
9. Enter `http://localhost` for the `SIGN-ON URL`
|
39
|
+
10. Enter `https://management.azure.com/` for the `APP ID URL`
|
40
|
+
11. Click the check icon to complete the application setup
|
41
|
+
12. Click `CONFIGURE`
|
42
|
+
13. Locate the section named `keys`
|
43
|
+
14. Select `1 year` or `2 years` from the drop down
|
44
|
+
15. Click `SAVE` at the bottom of the screen
|
45
|
+
16. The key value will now be visible. Copy the key value (This is the `azure_client_secret`)
|
46
|
+
17. Go back to the Azure Portal
|
47
|
+
18. Click `Subscriptions`
|
48
|
+
19. Click desired subscription
|
49
|
+
20. Click `Settings`
|
50
|
+
21. Click `Users`
|
51
|
+
22. Click `Add`
|
52
|
+
23. `Select a role` -> Click `Owner`
|
53
|
+
24. `Add users` -> In the search box enter application name used above
|
54
|
+
25. Click the application entry and click `Select`
|
55
|
+
26. Click `OK`
|
56
|
+
|
57
|
+
#### Orchestration Credential Items
|
58
|
+
|
59
|
+
The following credential information is provided from Active Directory. After clicking
|
60
|
+
on the desired directory, the ID can be found within the URL (UUID value)
|
61
|
+
|
62
|
+
* `azure_tenant_id` - Active Directory ID
|
63
|
+
|
64
|
+
The following credential information is provided from the Active Directory application
|
65
|
+
entry created above. Under the `CONFIGURE` section:
|
66
|
+
|
67
|
+
* `azure_client_id` - Field `CLIENT ID`
|
68
|
+
* `azure_client_secret` - Field `keys` (can only be viewed when initially saved)
|
69
|
+
|
70
|
+
The following credential information is provided from the Azure portal. Click `Subscriptions`.
|
71
|
+
|
72
|
+
* `azure_subscription_id` - Azure subscription ID
|
73
|
+
|
74
|
+
* `azure_region` - Deployment region (`westus`, `eastus`, etc.)
|
75
|
+
|
76
|
+
## Current support matrix
|
77
|
+
|
78
|
+
|Model |Create|Read|Update|Delete|
|
79
|
+
|--------------|------|----|------|------|
|
80
|
+
|AutoScale | | | | |
|
81
|
+
|BlockStorage | | | | |
|
82
|
+
|Compute | | | | |
|
83
|
+
|DNS | | | | |
|
84
|
+
|LoadBalancer | | | | |
|
85
|
+
|Network | | | | |
|
86
|
+
|Orchestration | X | X | X | X |
|
87
|
+
|Queues | | | | |
|
88
|
+
|Storage | X | X | X | X |
|
89
|
+
|
90
|
+
## Info
|
91
|
+
* Repository: https://github.com/miasma-rb/miasma-azure
|
data/lib/miasma-azure.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
|
3
|
+
module Miasma
|
4
|
+
module Contrib
|
5
|
+
module Azure
|
6
|
+
class Api < Miasma::Types::Api
|
7
|
+
include Contrib::AzureApiCore::ApiCommon
|
8
|
+
|
9
|
+
attribute :api_endpoint, String, :required => true
|
10
|
+
|
11
|
+
def endpoint
|
12
|
+
api_endpoint
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,369 @@
|
|
1
|
+
require 'miasma'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Miasma
|
5
|
+
module Contrib
|
6
|
+
|
7
|
+
module Azure
|
8
|
+
autoload :Api, 'miasma-azure/api'
|
9
|
+
end
|
10
|
+
|
11
|
+
# Core API for Azure access
|
12
|
+
class AzureApiCore
|
13
|
+
|
14
|
+
# @return [String] time in RFC 1123 format
|
15
|
+
def self.time_rfc1123
|
16
|
+
Time.now.httpdate
|
17
|
+
end
|
18
|
+
|
19
|
+
# HMAC helper class
|
20
|
+
class Hmac
|
21
|
+
|
22
|
+
# @return [OpenSSL::Digest]
|
23
|
+
attr_reader :digest
|
24
|
+
# @return [String] secret key
|
25
|
+
attr_reader :key
|
26
|
+
|
27
|
+
# Create new HMAC helper
|
28
|
+
#
|
29
|
+
# @param kind [String] digest type (sha1, sha256, sha512, etc)
|
30
|
+
# @param key [String] secret key
|
31
|
+
# @return [self]
|
32
|
+
def initialize(kind, key)
|
33
|
+
@digest = OpenSSL::Digest.new(kind)
|
34
|
+
@key = key
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String]
|
38
|
+
def to_s
|
39
|
+
"Hmac#{digest.name}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Generate the hexdigest of the content
|
43
|
+
#
|
44
|
+
# @param content [String] content to digest
|
45
|
+
# @return [String] hashed result
|
46
|
+
def hexdigest_of(content)
|
47
|
+
digest << content
|
48
|
+
hash = digest.hexdigest
|
49
|
+
digest.reset
|
50
|
+
hash
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sign the given data
|
54
|
+
#
|
55
|
+
# @param data [String]
|
56
|
+
# @param key_override [Object]
|
57
|
+
# @return [Object] signature
|
58
|
+
def sign(data, key_override=nil)
|
59
|
+
result = OpenSSL::HMAC.digest(digest, key_override || key, data)
|
60
|
+
digest.reset
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sign the given data and return hexdigest
|
65
|
+
#
|
66
|
+
# @param data [String]
|
67
|
+
# @param key_override [Object]
|
68
|
+
# @return [String] hex encoded signature
|
69
|
+
def hex_sign(data, key_override=nil)
|
70
|
+
result = OpenSSL::HMAC.hexdigest(digest, key_override || key, data)
|
71
|
+
digest.reset
|
72
|
+
result
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Base signature class
|
79
|
+
class Signature
|
80
|
+
|
81
|
+
# Create new instance
|
82
|
+
def initialize(*args)
|
83
|
+
raise NotImplementedError.new 'This class should not be used directly!'
|
84
|
+
end
|
85
|
+
|
86
|
+
# Generate the signature
|
87
|
+
#
|
88
|
+
# @param http_method [Symbol] HTTP request method
|
89
|
+
# @param path [String] request path
|
90
|
+
# @param opts [Hash] request options
|
91
|
+
# @return [String] signature
|
92
|
+
def generate(http_method, path, opts={})
|
93
|
+
raise NotImplementedError
|
94
|
+
end
|
95
|
+
|
96
|
+
# URL string escape
|
97
|
+
#
|
98
|
+
# @param string [String] string to escape
|
99
|
+
# @return [String] escaped string
|
100
|
+
def safe_escape(string)
|
101
|
+
string.to_s.gsub(/([^a-zA-Z0-9_.\-~])/) do
|
102
|
+
'%' << $1.unpack('H2' * $1.bytesize).join('%').upcase
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
class SignatureAzure < Signature
|
109
|
+
|
110
|
+
# Required Header Items
|
111
|
+
SIGNATURE_HEADERS = [
|
112
|
+
'Content-Encoding',
|
113
|
+
'Content-Language',
|
114
|
+
'Content-Length',
|
115
|
+
'Content-MD5',
|
116
|
+
'Content-Type',
|
117
|
+
'Date',
|
118
|
+
'If-Modified-Since',
|
119
|
+
'If-Match',
|
120
|
+
'If-None-Match',
|
121
|
+
'If-Unmodified-Since',
|
122
|
+
'Range'
|
123
|
+
]
|
124
|
+
|
125
|
+
# @return [Hmac]
|
126
|
+
attr_reader :hmac
|
127
|
+
# @return [String] shared private key
|
128
|
+
attr_reader :shared_key
|
129
|
+
# @return [String] name of account
|
130
|
+
attr_reader :account_name
|
131
|
+
|
132
|
+
def initialize(shared_key, account_name)
|
133
|
+
shared_key = Base64.decode64(shared_key)
|
134
|
+
@hmac = Hmac.new('sha256', shared_key)
|
135
|
+
@shared_key = shared_key
|
136
|
+
@account_name = account_name
|
137
|
+
end
|
138
|
+
|
139
|
+
def generate(http_method, path, opts)
|
140
|
+
signature = generate_signature(
|
141
|
+
http_method,
|
142
|
+
opts[:headers],
|
143
|
+
opts.merge(:path => path)
|
144
|
+
)
|
145
|
+
"SharedKey #{account_name}:#{signature}"
|
146
|
+
end
|
147
|
+
|
148
|
+
def generate_signature(http_method, headers, resource)
|
149
|
+
headers = headers.to_smash
|
150
|
+
headers.delete('Content-Length') if headers['Content-Length'].to_s == '0'
|
151
|
+
to_sign = [
|
152
|
+
http_method.to_s.upcase,
|
153
|
+
*self.class.const_get(:SIGNATURE_HEADERS).map{|head_name|
|
154
|
+
headers.fetch(head_name, '')
|
155
|
+
},
|
156
|
+
build_canonical_headers(headers),
|
157
|
+
build_canonical_resource(resource)
|
158
|
+
].join("\n")
|
159
|
+
signature = sign_request(to_sign)
|
160
|
+
end
|
161
|
+
|
162
|
+
def sign_request(request)
|
163
|
+
result = hmac.sign(request)
|
164
|
+
Base64.encode64(result).strip
|
165
|
+
end
|
166
|
+
|
167
|
+
def build_canonical_headers(headers)
|
168
|
+
headers.map do |key, value|
|
169
|
+
key = key.to_s.downcase
|
170
|
+
if(key.start_with?('x-ms-'))
|
171
|
+
[key, value].map(&:strip).join(':')
|
172
|
+
end
|
173
|
+
end.compact.sort.join("\n")
|
174
|
+
end
|
175
|
+
|
176
|
+
def build_canonical_resource(resource)
|
177
|
+
[
|
178
|
+
"/#{account_name}#{resource[:path]}",
|
179
|
+
*resource.fetch(:params, {}).map{|key, value|
|
180
|
+
key = key.downcase.strip
|
181
|
+
value = value.is_a?(Array) ? value.map(&:strip).sort.join(',') : value
|
182
|
+
[key, value].join(':')
|
183
|
+
}.sort
|
184
|
+
].join("\n")
|
185
|
+
end
|
186
|
+
|
187
|
+
class SasBlob < SignatureAzure
|
188
|
+
|
189
|
+
SIGNATURE_HEADERS = [
|
190
|
+
'Cache-Control',
|
191
|
+
'Content-Disposition',
|
192
|
+
'Content-Encoding',
|
193
|
+
'Content-Language',
|
194
|
+
'Content-Type'
|
195
|
+
]
|
196
|
+
|
197
|
+
def generate(http_method, path, opts)
|
198
|
+
params = opts.fetch(:params, Smash.new)
|
199
|
+
headers = opts.fetch(:headers, Smash.new)
|
200
|
+
to_sign = [
|
201
|
+
params[:sp],
|
202
|
+
params[:st],
|
203
|
+
params[:se],
|
204
|
+
['/blob', account_name, path].join('/'),
|
205
|
+
params[:si],
|
206
|
+
params[:sip],
|
207
|
+
params[:spr],
|
208
|
+
params[:sv],
|
209
|
+
*self.class.const_get(:SIGNATURE_HEADERS).map{|head_name|
|
210
|
+
headers.fetch(head_name, '')
|
211
|
+
}
|
212
|
+
].map(&:to_s).join("\n")
|
213
|
+
sign_request(to_sign)
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
|
220
|
+
module ApiCommon
|
221
|
+
|
222
|
+
def self.included(klass)
|
223
|
+
klass.class_eval do
|
224
|
+
attribute :azure_tenant_id, String
|
225
|
+
attribute :azure_client_id, String
|
226
|
+
attribute :azure_subscription_id, String
|
227
|
+
attribute :azure_client_secret, String
|
228
|
+
attribute :azure_region, String
|
229
|
+
attribute :azure_resource, String, :default => 'https://management.azure.com/'
|
230
|
+
attribute :azure_login_url, String, :default => 'https://login.microsoftonline.com'
|
231
|
+
attribute :azure_blob_account_name, String
|
232
|
+
attribute :azure_blob_secret_key, String
|
233
|
+
attribute :azure_root_orchestration_container, String, :default => 'miasma-orchestration-templates'
|
234
|
+
|
235
|
+
attr_reader :signer
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Setup for API connections
|
240
|
+
def connect
|
241
|
+
@oauth_token_information = Smash.new
|
242
|
+
end
|
243
|
+
|
244
|
+
# @return [HTTP] connection for requests (forces headers)
|
245
|
+
def connection
|
246
|
+
unless(signer)
|
247
|
+
super.headers(
|
248
|
+
'Authorization' => "Bearer #{client_access_token}"
|
249
|
+
)
|
250
|
+
else
|
251
|
+
super
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Perform request
|
256
|
+
#
|
257
|
+
# @param connection [HTTP]
|
258
|
+
# @param http_method [Symbol]
|
259
|
+
# @param request_args [Array]
|
260
|
+
# @return [HTTP::Response]
|
261
|
+
def make_request(connection, http_method, request_args)
|
262
|
+
dest, options = request_args
|
263
|
+
options = options ? options.to_smash : Smash.new
|
264
|
+
options[:headers] = Smash[connection.default_options.headers.to_a].merge(options.fetch(:headers, Smash.new))
|
265
|
+
service = Bogo::Utility.snake(self.class.name.split('::')[-2,1].first)
|
266
|
+
if(signer)
|
267
|
+
options[:headers] ||= Smash.new
|
268
|
+
options[:headers]['x-ms-date'] = AzureApiCore.time_rfc1123
|
269
|
+
if(self.respond_to?(:api_version))
|
270
|
+
options[:headers]['x-ms-version'] = self.send(:api_version)
|
271
|
+
end
|
272
|
+
options[:headers]['Authorization'] = signer.generate(
|
273
|
+
http_method, URI.parse(dest).path, options
|
274
|
+
)
|
275
|
+
az_connection = connection.headers(options[:headers])
|
276
|
+
else
|
277
|
+
if(self.respond_to?(:api_version))
|
278
|
+
options[:params] ||= Smash.new
|
279
|
+
options[:params]['api-version'] = self.send(:api_version)
|
280
|
+
end
|
281
|
+
if(self.respond_to?(:root_path))
|
282
|
+
p_dest = URI.parse(dest)
|
283
|
+
dest = "#{p_dest.scheme}://#{p_dest.host}"
|
284
|
+
dest = File.join(dest, self.send(:root_path), p_dest.path)
|
285
|
+
end
|
286
|
+
az_connection = connection
|
287
|
+
end
|
288
|
+
az_connection.send(http_method, dest, options)
|
289
|
+
end
|
290
|
+
|
291
|
+
# @return [String] endpoint for request
|
292
|
+
def endpoint
|
293
|
+
azure_resource
|
294
|
+
end
|
295
|
+
|
296
|
+
def oauth_token_buffer_seconds
|
297
|
+
240
|
298
|
+
end
|
299
|
+
|
300
|
+
def access_token_expired?
|
301
|
+
if(oauth_token_information[:expires_on])
|
302
|
+
(oauth_token_information[:expires_on] + oauth_token_buffer_seconds) <
|
303
|
+
Time.now
|
304
|
+
else
|
305
|
+
true
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def client_access_token
|
310
|
+
request_client_token if access_token_expired?
|
311
|
+
oauth_token_information[:access_token]
|
312
|
+
end
|
313
|
+
|
314
|
+
def oauth_token_information
|
315
|
+
@oauth_token_information
|
316
|
+
end
|
317
|
+
|
318
|
+
def request_client_token
|
319
|
+
result = HTTP.post(
|
320
|
+
File.join(azure_login_url, azure_tenant_id, 'oauth2', 'token'),
|
321
|
+
:form => {
|
322
|
+
:grant_type => 'client_credentials',
|
323
|
+
:client_id => azure_client_id,
|
324
|
+
:client_secret => azure_client_secret,
|
325
|
+
:resource => azure_resource
|
326
|
+
}
|
327
|
+
)
|
328
|
+
unless(result.code == 200)
|
329
|
+
# TODO: Wrap this in custom exception to play nice
|
330
|
+
puts result.inspect
|
331
|
+
puts "FAIL: #{result.body.to_s}"
|
332
|
+
puts result.headers
|
333
|
+
raise 'ACK'
|
334
|
+
end
|
335
|
+
@oauth_token_information = MultiJson.load(
|
336
|
+
result.body.to_s
|
337
|
+
).to_smash
|
338
|
+
@oauth_token_information[:expires_on] = Time.at(@oauth_token_information[:expires_on].to_i)
|
339
|
+
@oauth_token_information[:not_before] = Time.at(@oauth_token_information[:not_before].to_i)
|
340
|
+
@oauth_token_information
|
341
|
+
end
|
342
|
+
|
343
|
+
def retryable_allowed?(*_)
|
344
|
+
if(ENV['DEBUG'])
|
345
|
+
false
|
346
|
+
else
|
347
|
+
super
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# @return [String] custom escape
|
352
|
+
def uri_escape(string)
|
353
|
+
signer.safe_escape(string)
|
354
|
+
end
|
355
|
+
|
356
|
+
end
|
357
|
+
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
Models::Orchestration.autoload :Azure, 'miasma/contrib/azure/orchestration'
|
362
|
+
Models::Storage.autoload :Azure, 'miasma/contrib/azure/storage'
|
363
|
+
|
364
|
+
# Models::Compute.autoload :Azure, 'miasma/contrib/azure/compute'
|
365
|
+
# Models::LoadBalancer.autoload :Azure, 'miasma/contrib/azure/load_balancer'
|
366
|
+
# Models::AutoScale.autoload :Azure, 'miasma/contrib/azure/auto_scale'
|
367
|
+
|
368
|
+
|
369
|
+
end
|