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.
@@ -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