miasma-azure 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ * Initial release
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.
@@ -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
@@ -0,0 +1,2 @@
1
+ require 'miasma'
2
+ require 'miasma-azure/version'
@@ -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,4 @@
1
+ module MiasmaAzure
2
+ # Current library version
3
+ VERSION = Gem::Version.new('0.1.0')
4
+ 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