waz-cmd 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in waz-cmd.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ Microsoft Public License (Ms-PL)
2
+
3
+ This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software.
4
+
5
+ 1. Definitions
6
+
7
+ The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law.
8
+
9
+ A "contribution" is the original software, or any additions or changes to the software.
10
+
11
+ A "contributor" is any person that distributes its contribution under this license.
12
+
13
+ "Licensed patents" are a contributor's patent claims that read directly on its contribution.
14
+
15
+ 2. Grant of Rights
16
+
17
+ (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create.
18
+
19
+ (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software.
20
+
21
+ 3. Conditions and Limitations
22
+
23
+ (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks.
24
+
25
+ (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically.
26
+
27
+ (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software.
28
+
29
+ (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license.
30
+
31
+ (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ Waz-Cmd
2
+ =======
3
+
4
+ Installation
5
+ ------------
6
+
7
+ To install, just `gem install waz-cmd`
8
+
9
+ Basic usage
10
+ -----------
11
+
12
+ c:\>waz generate certificates
13
+ Writing certificate to 'c:\users\smarx/.waz/cert.pem'
14
+ Writing certificate in .cer form to 'c:\users\smarx/.waz/cert.cer'
15
+ Writing key to 'c:\users\smarx/.waz/key.pem'
16
+
17
+ To use the new certificate, upload 'c:\users\smarx/.waz/cert.cer' as a management certificate in the Windows Azure portal (https://windows.azure.com)
18
+
19
+ c:\>waz set subscriptionId XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
20
+
21
+ c:\>waz deploy blobedit staging c:\repositories\smarxrole\packages\ExtraSmall.cspkg c:\repositories\smarxrole\packages\ServiceConfiguration.blobedit.cscfg
22
+ Waiting for operation to complete...
23
+ Operation succeeded (200)
24
+
25
+ Documentation
26
+ -------------
27
+
28
+ Just run `waz help` for full documentation.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/bin/waz ADDED
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env ruby
2
+ require 'waz-cmd'
3
+ require 'commander/import'
4
+ require 'waz-blobs'
5
+ require 'uri'
6
+ require 'uuidtools'
7
+ require 'fileutils'
8
+
9
+ include Waz::Cmd
10
+
11
+ program :name, 'waz'
12
+ program :version, '1.0'
13
+ program :description, 'Windows Azure Command-line Tool'
14
+ program :help, 'Author', 'Steve Marx (http://blog.smarx.com, Steve.Marx@microsoft.com, @smarx)'
15
+
16
+ global_option '--pem FILE', 'Name of the .pem file containing the management certificate, deafault $HOME/.waz/cert.pem'
17
+ global_option '--key FILE', 'Name of the .pem file containing the private key, default $HOME/.waz/key.pem'
18
+ global_option '--subscriptionId STRING', 'Subscription ID (a GUID) to operate on'
19
+ global_option '--expand', 'Expand details where possible (like individual role instances). Default is false'
20
+
21
+ begin
22
+ $config = YAML::load(File.read("#{ENV['HOME']}/.wazrc"))
23
+ rescue
24
+ $config = {}
25
+ end
26
+
27
+ def template(name)
28
+ File.join(File.dirname(__FILE__), '../lib/waz-cmd/templates', name)
29
+ end
30
+
31
+ command 'set' do |c|
32
+ c.syntax = 'waz set <variable> <value>'
33
+ c.description = 'Set a default value for a variable (like "subscriptionId") in $HOME/.wazrc'
34
+ c.action do |args, options|
35
+ $config[args[0]] = args[1]
36
+ File.open("#{ENV['HOME']}/.wazrc", 'w') do |f|
37
+ f.write($config.to_yaml)
38
+ end
39
+ end
40
+ end
41
+
42
+ command 'get' do |c|
43
+ c.syntax = 'waz get <variable>'
44
+ c.description = 'Get the default value for a variable (like "subscriptionId") in $HOME/.wazrc'
45
+ c.action do |args, options|
46
+ puts $config[args[0]]
47
+ end
48
+ end
49
+
50
+ command 'get-all' do |c|
51
+ c.syntax = 'waz get-all'
52
+ c.description = 'Get the default values for all variables (like "subscriptionId") in $HOME/.wazrc'
53
+ c.action do |args, options|
54
+ $config.each do |k,v|
55
+ puts "#{k} = #{v}"
56
+ end
57
+ end
58
+ end
59
+
60
+ command 'generate certificate' do |c|
61
+ c.syntax = 'waz generate certificate [options]'
62
+ c.description = 'Generate a public/private key pair to use with the Windows Azure Service Management API'
63
+ c.option '--cer FILE', 'Name of the .cer file to write the public certificate, default $HOME/.waz/cert.cer'
64
+ c.action do |args, options|
65
+ set_defaults options
66
+ rsa = OpenSSL::PKey::RSA.new 1024
67
+ cert = OpenSSL::X509::Certificate.new
68
+ cert.version = 3
69
+ cert.serial = 0
70
+ name = OpenSSL::X509::Name.new([['CN', 'waz']])
71
+ cert.subject = name
72
+ cert.issuer = name
73
+ cert.not_before = 0
74
+ cert.not_after = 2147483647 #LONG_MAX
75
+ cert.public_key = rsa.public_key
76
+
77
+ ef = OpenSSL::X509::ExtensionFactory.new(nil,cert)
78
+ ef.issuer_certificate = cert
79
+ cert.extensions = [
80
+ ef.create_extension("basicConstraints","CA:FALSE"),
81
+ ef.create_extension("keyUsage", "keyEncipherment"),
82
+ ef.create_extension("subjectKeyIdentifier", "hash"),
83
+ ef.create_extension("extendedKeyUsage", "serverAuth"),
84
+ ]
85
+ aki = ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
86
+ cert.add_extension(aki)
87
+ cert.sign(rsa, OpenSSL::Digest::SHA1.new)
88
+
89
+ puts "Writing certificate to '#{options.pem}'"
90
+ FileUtils.makedirs(File.dirname(options.pem))
91
+ File.open(options.pem, "w") {|f| f.puts cert}
92
+ puts "Writing certificate in .cer form to '#{options.cer}'"
93
+ FileUtils.makedirs(File.dirname(options.cer))
94
+ File.open(options.cer, "wb") {|f| f.write cert.to_der}
95
+ puts "Writing key to '#{options.key}'"
96
+ FileUtils.makedirs(File.dirname(options.key))
97
+ File.open(options.key, "w") {|f| f.puts rsa}
98
+ puts
99
+ puts "To use the new certificate, upload '#{options.cer}' as a management certificate in the Windows Azure portal (https://windows.azure.com)"
100
+ end
101
+ end
102
+
103
+ command 'list applications' do |c|
104
+ c.syntax = 'waz list applications [options]'
105
+ c.description = 'List all Windows Azure applications under the given subscription'
106
+ c.action do |args, options|
107
+ set_defaults options
108
+ raise 'Subscription ID is required' unless options.subscriptionId
109
+ apps = arrayize(make_call("services/hostedservices", :get, options)['HostedServices']['HostedService'])
110
+ puts Tilt.new(template('list_applications.erb'), 1, :trim => '%').render(nil, :apps => apps)
111
+ end
112
+ end
113
+
114
+ command 'list storage' do |c|
115
+ c.syntax = 'waz list storage'
116
+ c.description = 'List all Windows Azure storage accounts under the given subscription'
117
+ c.action do |args, options|
118
+ set_defaults options
119
+ raise 'Subscription ID is required' unless options.subscriptionId
120
+ apps = arrayize(make_call("services/storageservices", :get, options)['StorageServices']['StorageService'])
121
+ puts Tilt.new(template('list_applications.erb'), 1, :trim => '%').render(nil, :apps => apps)
122
+ end
123
+ end
124
+
125
+ command 'show application' do |c|
126
+ c.syntax = 'waz show application <name> [options]'
127
+ c.description = 'Show the details of an application'
128
+ c.action do |args, options|
129
+ set_defaults options
130
+ raise 'Subscription ID is required' unless options.subscriptionId
131
+ raise 'Application name is required' unless args.first
132
+ app = make_call("services/hostedservices/#{args.first}?embed-detail=true", :get, options)['HostedService']
133
+ puts Tilt.new(template('show_application.erb'), 1, :trim => '%').render(nil, :app => app, :expand => options.expand)
134
+ end
135
+ end
136
+
137
+ command 'show storage' do |c|
138
+ c.syntax = 'waz show storage <name> [options]'
139
+ c.description = 'Show the details of a storage account'
140
+ c.action do |args, options|
141
+ set_defaults options
142
+ raise 'Subscription ID is required' unless options.subscriptionId
143
+ raise 'Storage account name is required' unless args.first
144
+ account = make_call("services/storageservices/#{args.first}", :get, options)['StorageService']
145
+ puts Tilt.new(template('show_storage.erb'), 1, :trim => '%').render(nil, :account => account)
146
+ end
147
+ end
148
+
149
+ command 'delete storage' do |c|
150
+ c.syntax = 'was delete storage <name> [options]'
151
+ c.description = 'Delete a storage account'
152
+ c.action do |args, options|
153
+ set_defaults options
154
+ raise 'Subscription ID is required' unless options.subscriptionId
155
+ raise 'Storage account name is required' unless args.first
156
+ make_call("services/storageservices/#{args.first}", :delete, options) do |response|
157
+ wait_for_completion options, response.headers[:x_ms_request_id]
158
+ end
159
+ end
160
+ end
161
+
162
+ command 'get keys' do |c|
163
+ c.syntax = 'waz get keys <name> [options]'
164
+ c.description = 'Get the keys for a storage account'
165
+ c.action do |args, options|
166
+ set_defaults options
167
+ raise 'Subscription ID is required' unless options.subscriptionId
168
+ raise 'Storage account name is required' unless args.first
169
+ keys = make_call("services/storageservices/#{args.first}/keys", :get, options)['StorageService']['StorageServiceKeys']
170
+ puts Tilt.new(template('get_keys.erb'), 1, :trim => '%').render(nil, :keys => keys)
171
+ end
172
+ end
173
+
174
+ command 'connection string' do |c|
175
+ c.syntax = 'waz get connection string <name> [options]'
176
+ c.description = 'Get a connection string for a storage account'
177
+ c.option '--http', 'Use HTTP in the connection string instead of (the default) HTTPS'
178
+ c.action do |args, options|
179
+ set_defaults options
180
+ raise 'Subscription ID is required' unless options.subscriptionId
181
+ raise 'Storage account name is required' unless args.first
182
+ keys = make_call("services/storageservices/#{args.first}/keys", :get, options)['StorageService']['StorageServiceKeys']
183
+ puts "DefaultEndpointsProtocol=#{options.http ? 'http' : 'https'};AccountName=#{args.first};AccountKey=#{keys['Primary']}"
184
+ end
185
+ end
186
+
187
+ alias_command 'cs', 'connection string'
188
+
189
+ command 'create storage' do |c|
190
+ c.syntax = 'waz create storage <name> <location|affinity group> [options]'
191
+ c.description = 'Create a storage account'
192
+ c.action do |args, options|
193
+ set_defaults options
194
+ raise 'Subscription ID is required' unless options.subscriptionId
195
+ raise 'Storage account name is required' unless args.first
196
+ raise 'Location or affinity group is required' unless args[1]
197
+ make_call("services/storageservices", :post, options,
198
+ Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml|
199
+ xml.CreateStorageServiceInput('xmlns' => 'http://schemas.microsoft.com/windowsazure') {
200
+ xml.ServiceName args[0]
201
+ xml.Description args[0]
202
+ xml.Label Base64.encode64(args[0]).rstrip
203
+ if args[1].length > 20
204
+ xml.AffinityGroup args[1]
205
+ else
206
+ xml.Location args[1]
207
+ end
208
+ }
209
+ }.to_xml) do |response|
210
+ wait_for_completion options, response.headers[:x_ms_request_id]
211
+ end
212
+ end
213
+ end
214
+
215
+ command 'create application' do |c|
216
+ c.syntax = 'waz create application <name> <location|affinity group> [options]'
217
+ c.description = 'Create a storage account'
218
+ c.action do |args, options|
219
+ set_defaults options
220
+ raise 'Subscription ID is required' unless options.subscriptionId
221
+ raise 'Application name is required' unless args.first
222
+ raise 'Location or affinity group is required' unless args[1]
223
+ make_call("services/hostedservices", :post, options,
224
+ Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml|
225
+ xml.CreateHostedService(:xmlns => 'http://schemas.microsoft.com/windowsazure') {
226
+ xml.ServiceName args[0]
227
+ xml.Label Base64.encode64(args[0]).rstrip
228
+ if args[1].length > 20
229
+ xml.AffinityGroup args[1]
230
+ else
231
+ xml.Location args[1]
232
+ end
233
+ }
234
+ }.to_xml) do |response|
235
+ wait_for_completion options, response.headers[:x_ms_request_id]
236
+ end
237
+ end
238
+ end
239
+
240
+ command 'delete application' do |c|
241
+ c.syntax = 'waz delete application <name> [options]'
242
+ c.description = 'Delete an application'
243
+ c.action do |args, options|
244
+ set_defaults options
245
+ raise 'Subscription ID is required' unless options.subscriptionId
246
+ raise 'Application name is required' unless args.first
247
+ make_call("services/hostedservices/#{args.first}", :delete, options) do |response|
248
+ wait_for_completion options, response.headers[:x_ms_request_id]
249
+ end
250
+ end
251
+ end
252
+
253
+ command 'list locations' do |c|
254
+ c.syntax = 'waz list locations [options]'
255
+ c.description = 'List data center locations'
256
+ c.action do |args, options|
257
+ set_defaults options
258
+ raise 'Subscription ID is required' unless options.subscriptionId
259
+ locations = arrayize(make_call("locations", :get, options)['Locations']['Location'])
260
+ puts Tilt.new(template('list_locations.erb'), 1, :trim => '%').render(nil, :locations => locations)
261
+ end
262
+ end
263
+
264
+ command 'list affinity groups' do |c|
265
+ c.syntax = 'waz list affinity groups [options]'
266
+ c.description = 'List affinity groups'
267
+ c.action do |args, options|
268
+ set_defaults options
269
+ raise 'Subscription ID is required' unless options.subscriptionId
270
+ groups = arrayize(make_call("affinitygroups", :get, options)['AffinityGroups']['AffinityGroup'])
271
+ puts Tilt.new(template('list_affinity_groups.erb'), 1, :trim => '%').render(nil, :groups => groups)
272
+ end
273
+ end
274
+
275
+ command 'create affinity group' do |c|
276
+ c.syntax = 'waz create affinity group <name> <location> [options]'
277
+ c.description = 'Create an affinity group'
278
+ c.action do |args, options|
279
+ set_defaults options
280
+ raise 'Subscription ID is required' unless options.subscriptionId
281
+ raise 'Affinity group name is required' unless args.first
282
+ raise 'Location is required' unless args[1]
283
+ make_call("affinitygroups", :post, options,
284
+ Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml|
285
+ xml.CreateAffinityGroup(:xmlns => 'http://schemas.microsoft.com/windowsazure') {
286
+ xml.Name args[0]
287
+ xml.Label Base64.encode64(args[0]).rstrip
288
+ xml.Location args[1]
289
+ }
290
+ }.to_xml) do |response|
291
+ wait_for_completion options, response.headers[:x_ms_request_id]
292
+ end
293
+ end
294
+ end
295
+
296
+ command 'show affinity group' do |c|
297
+ c.syntax = 'waz show affinity group <name> [options]'
298
+ c.description = 'Show the details of an affinity group'
299
+ c.action do |args, options|
300
+ set_defaults options
301
+ raise 'Subscription ID is required' unless options.subscriptionId
302
+ raise 'Affinity group name is required' unless args.first
303
+ group = make_call("affinitygroups/#{args[0]}", :get, options)['AffinityGroup']
304
+ puts Tilt.new(template('show_affinity_group.erb'), 1, :trim => '%').render(nil, :group => group)
305
+ end
306
+ end
307
+
308
+ command 'delete affinity group' do |c|
309
+ c.syntax = 'waz delete affinity group <name> [options]'
310
+ c.description = 'Delete an affinity group'
311
+ c.action do |args, options|
312
+ set_defaults options
313
+ raise 'Subscription ID is required' unless options.subscriptionId
314
+ raise 'Affinity group name is required' unless args.first
315
+ make_call("affinitygroups/#{args[0]}", :delete, options) do |response|
316
+ wait_for_completion options, response.headers[:x_ms_request_id]
317
+ end
318
+ end
319
+ end
320
+
321
+ command 'show deployment' do |c|
322
+ c.syntax = 'waz show deployment <application> <staging|production> [options]'
323
+ c.description = 'Show the details of a deployment'
324
+ c.action do |args, options|
325
+ set_defaults options
326
+ raise 'Subscription ID is required' unless options.subscriptionId
327
+ raise 'Application name is required' unless args.first
328
+ raise 'Deployment slot is required' unless args[1]
329
+ deployment = make_call("services/hostedservices/#{args[0]}/deploymentslots/#{args[1]}", :get, options)['Deployment']
330
+ puts Tilt.new(template('show_deployment.erb'), 1, :trim => '%').render(nil, :expand => options.expand, :deployment => deployment)
331
+ end
332
+ end
333
+
334
+ command 'show history' do |c|
335
+ c.syntax = 'waz show history <application> <deployment-name> [options]'
336
+ c.description = 'Show the history of operations on a deployment. Note that deployment name is the GUID of the deployment (find it with something like "waz show deployment <application> production")'
337
+ c.option '--startTime STRING', 'Earliest time to retrieve history, in a form like YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ (defaults to yesterday)'
338
+ c.option '--endTime STRING', 'Latest time to retrieve history, in a form like YYYY-MM-DD or YYYY-MM-DDTHH:MM:SSZ (defaults to infinity)'
339
+ c.action do |args, options|
340
+ set_defaults options
341
+ options.default :startTime => (Time.now-24*60*60).strftime("%Y-%m-%d"), :endTime => Time.at(2147483647).strftime("%Y-%m-%d") #LONG_MAX
342
+ raise 'Subscription ID is required' unless options.subscriptionId
343
+ raise 'Application name is required' unless args.first
344
+ raise 'Deployment name is required' unless args[1]
345
+ history = make_call("operations?StartTime=#{options.startTime}&EndTime=#{options.endTime}&ObjectIdFilter=/#{options.subscriptionId}/services/hostedservices/#{args[0]}/deployments/#{args[1]}", :get, options)['SubscriptionOperationCollection']['SubscriptionOperations']
346
+ puts Tilt.new(template('show_history.erb'), 1, :trim => '%').render(nil, :history => history)
347
+ end
348
+ end
349
+
350
+ restar_instance 'boot'
351
+ restar_instance 'image'
352
+
353
+ command 'swap' do |c|
354
+ c.syntax = 'waz swap <application> [options]'
355
+ c.description = 'Perform a VIP swap between staging and production of the specified application'
356
+ c.action do |args, options|
357
+ set_defaults options
358
+ raise 'Subscription ID is required' unless options.subscriptionId
359
+ raise 'Application name is required' unless args[0]
360
+ deployments = arrayize(make_call("services/hostedservices/#{args[0]}?embed-detail=true", :get, options)['HostedService']['Deployments']['Deployment'])
361
+ prodidx = deployments.index{|d| d['DeploymentSlot'] == 'Production'}
362
+ stageidx = deployments.index{|d| d['DeploymentSlot'] == 'Staging'}
363
+ raise 'No staging deployment to swap.' unless stageidx
364
+ production = prodidx ? deployments[prodidx]['Name'] : nil
365
+ staging = stageidx ? deployments[stageidx]['Name'] : nil
366
+ make_call("services/hostedservices/#{args[0]}", :post, options,
367
+ Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml|
368
+ xml.Swap('xmlns' => 'http://schemas.microsoft.com/windowsazure') {
369
+ if production
370
+ xml.Production production
371
+ end
372
+ xml.SourceDeployment staging
373
+ }
374
+ }.to_xml) do |response|
375
+ wait_for_completion options, response.headers[:x_ms_request_id]
376
+ end
377
+ end
378
+ end
379
+
380
+ startstop('start', 'Running')
381
+ startstop('stop', 'Suspended')
382
+
383
+ command 'delete deployment' do |c|
384
+ c.syntax = 'waz delete deployment <application> <production|staging> [options]'
385
+ c.description = 'Delete a deployment'
386
+ c.action do |args, options|
387
+ set_defaults options
388
+ raise 'Subscription ID is required' unless options.subscriptionId
389
+ raise 'Application name is required' unless args[0]
390
+ raise 'Deployment slot is required' unless args[1]
391
+
392
+ make_call("services/hostedservices/#{args[0]}/deploymentslots/#{args[1]}", :delete, options) do |response|
393
+ wait_for_completion options, response.headers[:x_ms_request_id]
394
+ end
395
+ end
396
+ end
397
+
398
+ command 'show configuration' do |c|
399
+ c.syntax = 'waz show configuration <application> <production|staging> [options]'
400
+ c.description = 'Show deployment configuration'
401
+ c.action do |args, options|
402
+ set_defaults options
403
+ raise 'Subscription ID is required' unless options.subscriptionId
404
+ raise 'Application name is required' unless args[0]
405
+ raise 'Deployment slot is required' unless args[1]
406
+
407
+ roles = arrayize(Crack::XML.parse(Iconv.conv('utf-16', 'utf-8', Base64.decode64(make_call("services/hostedservices/#{args[0]}/deploymentslots/#{args[1]}", :get, options)['Deployment']['Configuration'])))['ServiceConfiguration']['Role'])
408
+
409
+ puts Tilt.new(template('show_configuration.erb'), 1, :trim => '%').render(nil, :roles => roles)
410
+ end
411
+ end
412
+
413
+ command 'configure' do |c|
414
+ c.syntax = 'waz configure <application> <production|staging> <role> <setting-name> <setting-value> [options]'
415
+ c.description = 'Change a configuration setting for a role of a deployment'
416
+ c.action do |args, options|
417
+ set_defaults options
418
+ raise 'Subscription ID is required' unless options.subscriptionId
419
+ raise 'Application name is required' unless args[0]
420
+ raise 'Deployment slot is required' unless args[1]
421
+ raise 'Role name is required' unless args[2]
422
+ raise 'Setting name is required' unless args[3]
423
+ raise 'Setting value is required' unless args[4]
424
+
425
+ doc = REXML::Document.new(Iconv.conv('utf-16', 'utf-8', Base64.decode64(make_call("services/hostedservices/#{args[0]}/deploymentslots/#{args[1]}", :get, options)['Deployment']['Configuration'])))
426
+ role = REXML::XPath.first(doc, "//Role[@name='#{args[2]}']")
427
+ raise "Couldn't find role '#{args[2]}'" if role.nil?
428
+ setting = REXML::XPath.first(role, "//Setting[@name='#{args[3]}']")
429
+ raise "Couldn't find setting '#{args[3]}'" if setting.nil?
430
+ setting.attributes['value'] = args[4]
431
+
432
+ make_call("services/hostedservices/#{args[0]}/deploymentslots/#{args[1]}/?comp=config", :post, options,
433
+ Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml|
434
+ xml.ChangeConfiguration('xmlns' => 'http://schemas.microsoft.com/windowsazure') {
435
+ xml.Configuration Base64.encode64(Iconv.conv('utf-8', 'utf-16', doc.to_s)).rstrip
436
+ }
437
+ }.to_xml) do |response|
438
+ wait_for_completion options, response.headers[:x_ms_request_id]
439
+ end
440
+ end
441
+ end
442
+
443
+ command 'deploy' do |c|
444
+ c.syntax = 'waz deploy <application> <production|staging> <path/to/.cspkg> <path/to/.cscfg> [options]'
445
+ c.description = 'Deploy to an existing Windows Azure application'
446
+ c.option '--deploymentAccount STRING', 'Name of the storage account to deploy through (temporarily hold the .cspkg), defaults to the first storage account in the subscription'
447
+ c.action do |args, options|
448
+ set_defaults options
449
+ raise 'Subscription ID is required' unless options.subscriptionId
450
+ raise 'Application name is required' unless args[0]
451
+ raise 'Deployment slot is required' unless args[1]
452
+ raise 'Package (.cspkg) is required' unless args[2]
453
+ raise 'Configuration file (.cscfg) is required' unless args[3]
454
+
455
+ if options.account.nil?
456
+ accounts = arrayize(make_call("services/storageservices", :get, options)['StorageServices']['StorageService'])
457
+ raise 'Must have at least one storage account' unless accounts.length > 0
458
+ options.account = accounts[0]['ServiceName']
459
+ end
460
+
461
+ key = make_call("services/storageservices/#{options.account}/keys", :get, options)['StorageService']['StorageServiceKeys']['Primary']
462
+
463
+ WAZ::Storage::Base.establish_connection!(:account_name => options.account, :access_key => key)
464
+ begin
465
+ container = WAZ::Blobs::Container.create('deployments')
466
+ rescue
467
+ container = WAZ::Blobs::Container.find('deployments')
468
+ end
469
+
470
+ name = File.basename(args[2]).gsub(' ', '') + Time.new.utc.strftime('%Y-%m-%d%H:%M:%S')
471
+ blob = nil
472
+ open(args[2]) do |f|
473
+ blob = container.upload(URI::escape(name), f, 'application/octet-stream')
474
+ end
475
+
476
+ make_call("services/hostedservices/#{args[0]}/deploymentslots/#{args[1]}", :post, options,
477
+ Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml|
478
+ xml.CreateDeployment('xmlns' => 'http://schemas.microsoft.com/windowsazure') {
479
+ xml.Name UUIDTools::UUID.random_create.to_s.gsub '-', ''
480
+ xml.PackageUrl URI::unescape blob.url
481
+ xml.Label Base64.encode64 name
482
+ xml.Configuration Base64.encode64 File.read args[3]
483
+ xml.StartDeployment 'true'
484
+ xml.TreatWarningsAsError 'false'
485
+ }
486
+ }.to_xml) do |response|
487
+ wait_for_completion options, response.headers[:x_ms_request_id]
488
+ end
489
+
490
+ blob.destroy!
491
+ end
492
+ end
@@ -0,0 +1,2 @@
1
+ Primary: <%= keys['Primary'] %>
2
+ Secondary: <%= keys['Secondary'] %>
@@ -0,0 +1,3 @@
1
+ % groups.each do |group|
2
+ <%= Tilt.new('show_affinity_group.erb', 1, :trim => '%').render(nil, :group => group) %>
3
+ % end
@@ -0,0 +1,3 @@
1
+ % apps.each do |app|
2
+ <%= app['ServiceName'] %>
3
+ % end
@@ -0,0 +1,3 @@
1
+ % locations.each do |location|
2
+ <%= location['Name'] %>
3
+ % end
@@ -0,0 +1,3 @@
1
+ <%= Base64.decode64 group['Label'] %>
2
+ Name: <%= group['Name'] %>
3
+ Location: <%= group['Location'] %>
@@ -0,0 +1,11 @@
1
+ <%= app['ServiceName'] %>
2
+ % props = app['HostedServiceProperties']
3
+ % unless app['Deployments']['Deployment'].nil?
4
+ % deployments = app['Deployments']['Deployment']
5
+ % deployments = [deployments] unless deployments.kind_of? Array
6
+ % deployments.each do |deployment|
7
+ <%= Tilt.new(template('show_deployment.erb'), 1, :trim => '%').render(nil, :deployment => deployment, :expand => expand).gsub(/^/, ' ') %>
8
+ % end
9
+ % else
10
+ Nothing deployed.
11
+ % end
@@ -0,0 +1,6 @@
1
+ % roles.each do |role|
2
+ <%= $terminal.color role['name'], :bold %>
3
+ % arrayize(role['ConfigurationSettings']['Setting']).each do |setting|
4
+ <%= setting['name'] %>: <%= setting['value'] %>
5
+ % end
6
+ % end
@@ -0,0 +1,33 @@
1
+ <%= $terminal.color deployment['DeploymentSlot'].upcase, :bold %>
2
+ Label: <%= Base64.decode64 deployment['Label'] %>
3
+ Name: <%= deployment['Name'] %>
4
+ Status: <%= deployment['Status'] %>
5
+ Url: <%= deployment['Url'] %>
6
+ % if deployment['SdkVersion']
7
+ SDK version: <%= deployment['SdkVersion'] %>
8
+ % end
9
+ % if deployment['UpgradeStatus']
10
+ Upgrade status: <%= deployment['UpgradeStatus']['UpgradeType'] %> upgrade in progress, on upgrade domain <%= deployment['UpgradeStatus'][0]['CurrentUpgradeDomain'] %>/<%= deployment['UpgradeDomainCount'] %>
11
+ % end
12
+ % instances = deployment['RoleInstanceList']['RoleInstance']
13
+ % instances = [instances] unless instances.kind_of? Array
14
+ % instances = instances.group_by {|i| i['RoleName']}
15
+ <%= $terminal.color "ROLES", :bold %>
16
+ % roles = deployment['RoleList']['Role']
17
+ % roles = [roles] unless roles.kind_of? Array
18
+ % roles.each do |role|
19
+ <%= role['RoleName'] %> (<%= role['OsVersion'] %>)
20
+ % if not expand
21
+ <%= instances[role['RoleName']].group_by{|i| i['InstanceStatus']}.map{|key, value| "#{value.count} #{key}"}.join(', ') %> (use --expand to see details)
22
+ % else
23
+ % instances[role['RoleName']].each do |instance|
24
+ <%= instance['InstanceName'] %> (<%= instance['InstanceStatus'] %>)
25
+ % end
26
+ % end
27
+ % end
28
+ <%= $terminal.color "ENDPOINTS", :bold %>
29
+ % endpoints = deployment['InputEndpointList']['InputEndpoint']
30
+ % endpoints = [endpoints] unless endpoints.kind_of? Array
31
+ % endpoints.each do |endpoint|
32
+ <%= endpoint['Vip'] %>:<%= endpoint['Port'] %> on <%= endpoint['RoleName'] %>
33
+ % end
@@ -0,0 +1,21 @@
1
+ % if history['SubscriptionOperation'].nil?
2
+ No history to display.
3
+ % else
4
+ % operations = history['SubscriptionOperation']
5
+ % operations = [operations] unless operations.kind_of? Array
6
+ % operations.each do |operation|
7
+ <%= $terminal.color operation['OperationName'], :bold %>
8
+ ID: <%= operation['OperationId'] %>
9
+ Parameters: <%= operation['OperationParameters']['OperationParameter'].sort{|a,b| a['a:Name'] <=> b['a:Name']}.map{|p| "#{p['a:Name']}=#{p['a:Value'].length > 30 ? '...' : p['a:Value']}"}.join ', ' %>
10
+ Status: <%= operation['OperationStatus']['Status'] %> (<%= operation['OperationStatus']['HttpStatusCode'] %>)
11
+ Start: <%= operation['OperationStartedTime'] %>
12
+ % if operation['OperationCompletedTime']
13
+ Completed: <%= operation['OperationCompletedTime'] %>
14
+ % end
15
+ % if operation['OperationCaller']['UsedServiceManagementApi'] == 'true'
16
+ Caller: <%= operation['OperationCaller']['ClientIP'] %> (<%= operation['OperationCaller']['SubscriptionCertificateThumbprint'] %>)
17
+ % else
18
+ Caller: <%= operation['OperationCaller']['UserEmailAddress'] %>
19
+ % end
20
+ % end
21
+ % end
@@ -0,0 +1,8 @@
1
+ <%= account['ServiceName'] %>
2
+ % props = account['StorageServiceProperties']
3
+ % unless props['Location'].nil?
4
+ Location: <%= props['Location'] %>
5
+ % end
6
+ % unless props['AffinityGroup'].nil?
7
+ Affinity Group: <%= props['AffinityGroup'] %>
8
+ % end
@@ -0,0 +1,5 @@
1
+ module Waz
2
+ module Cmd
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
data/lib/waz-cmd.rb ADDED
@@ -0,0 +1,105 @@
1
+ module Waz
2
+ module Cmd
3
+ require 'faster_require'
4
+ require 'rexml/document'
5
+ require 'rexml/xpath'
6
+ require 'iconv'
7
+ begin
8
+ require 'Win32/Console/ANSI' if RUBY_PLATFORM =~ /win32|mingw32/
9
+ rescue
10
+ puts 'WARNING: Output will look weird on Windows unless you install the "win32console" gem.'
11
+ end
12
+ require 'openssl'
13
+ require 'RestClient'
14
+ require 'tilt'
15
+ require 'base64'
16
+ require 'yaml'
17
+ require 'crack'
18
+ require 'nokogiri'
19
+
20
+ def set_defaults(options)
21
+ options.default :pem => "#{ENV['HOME']}/.waz/cert.pem", :cer => "#{ENV['HOME']}/.waz/cert.cer", :key => "#{ENV['HOME']}/.waz/key.pem"
22
+ options.default :subscriptionId => $config['subscriptionId'] if $config['subscriptionId']
23
+ end
24
+
25
+ def make_call(path, method, options, body=nil)
26
+ headers = { 'x-ms-version' => '2011-06-01' }
27
+ headers['Content-Type'] = 'application/xml' unless body.nil?
28
+ options = {
29
+ :url => "https://management.core.windows.net/#{options.subscriptionId}/#{path}",
30
+ :method => method,
31
+ :headers => headers,
32
+ :ssl_client_cert => OpenSSL::X509::Certificate.new(File.read(options.pem)),
33
+ :ssl_client_key => OpenSSL::PKey::RSA.new(File.read(options.key))
34
+ }
35
+ options[:payload] = body unless body.nil?
36
+ if block_given?
37
+ yield RestClient::Request.execute options
38
+ else
39
+ Crack::XML.parse RestClient::Request.execute options
40
+ end
41
+ end
42
+
43
+ def arrayize(x)
44
+ return [] if x.nil?
45
+ return x if x.kind_of?(Array)
46
+ return [x]
47
+ end
48
+
49
+ def restar_instance(star)
50
+ command "re#{star} instance" do |c|
51
+ c.syntax = "waz re#{star} instance <application> <production|staging> <instance> [options]"
52
+ c.description = "Re#{star} the specified instance"
53
+ c.action do |args, options|
54
+ set_defaults options
55
+ raise 'Subscription ID is required' unless options.subscriptionId
56
+ raise 'Application name is required' unless args[0]
57
+ raise 'Deployment name is required' unless args[1]
58
+ raise 'Instance name is required' unless args[2]
59
+ make_call("services/hostedservices/#{args[0]}/deploymentslots/#{args[1]}/roleinstances/#{args[2]}?comp=re#{star}", :post, options, '') do |response|
60
+ wait_for_completion options, response.headers[:x_ms_request_id]
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def startstop(verb, status)
67
+ command verb do |c|
68
+ c.syntax = "waz #{verb} <application> <production|staging> [options]"
69
+ c.description = "#{verb.capitalize} the deployment in the specified slot of the specified application"
70
+ c.action do |args, options|
71
+ set_defaults options
72
+ raise 'Subscription ID is required' unless options.subscriptionId
73
+ raise 'Application name is required' unless args[0]
74
+ raise 'Deployment slot is required' unless args[1]
75
+
76
+ make_call("services/hostedservices/#{args[0]}/deploymentslots/#{args[1]}/?comp=status", :post, options,
77
+ Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml|
78
+ xml.UpdateDeploymentStatus('xmlns' => 'http://schemas.microsoft.com/windowsazure') {
79
+ xml.Status status
80
+ }
81
+ }.to_xml) do |response|
82
+ wait_for_completion options, response.headers[:x_ms_request_id]
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def wait_for_completion(options, requestId)
89
+ puts 'Waiting for operation to complete...'
90
+ done = false
91
+ while not done
92
+ status = make_call("operations/#{requestId}", :get, options)['Operation']
93
+ done = status['Status'] != 'InProgress'
94
+ if done
95
+ puts "Operation #{status['Status'].downcase} (#{status['HttpStatusCode']})"
96
+ if status['Error']
97
+ puts "#{status['Error']['Code']}: #{status['Error']['Message']}"
98
+ end
99
+ else
100
+ sleep 10
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
data/waz-cmd.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "waz-cmd/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "waz-cmd"
7
+ s.version = Waz::Cmd::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Steve Marx"]
10
+ s.email = ["Steve.Marx@microsoft.com"]
11
+ s.homepage = "http://blog.smarx.com"
12
+ s.summary = %q{Command-line tool to manage Windows Azure applications and storage accounts.}
13
+ s.description = %q{This gem allows you to perform most of the available operations in the Windows Azure Service Management API from a friendly commandline tool.}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency('commander')
21
+ s.add_dependency('faster_require')
22
+ s.add_dependency('tilt')
23
+ s.add_dependency('crack')
24
+ s.add_dependency('nokogiri')
25
+ s.add_dependency('waz-storage')
26
+ s.add_dependency('uuidtools')
27
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: waz-cmd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steve Marx
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-07-01 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: commander
16
+ requirement: &25037928 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *25037928
25
+ - !ruby/object:Gem::Dependency
26
+ name: faster_require
27
+ requirement: &25037316 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *25037316
36
+ - !ruby/object:Gem::Dependency
37
+ name: tilt
38
+ requirement: &25036848 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *25036848
47
+ - !ruby/object:Gem::Dependency
48
+ name: crack
49
+ requirement: &25036296 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *25036296
58
+ - !ruby/object:Gem::Dependency
59
+ name: nokogiri
60
+ requirement: &25035756 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *25035756
69
+ - !ruby/object:Gem::Dependency
70
+ name: waz-storage
71
+ requirement: &25035240 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *25035240
80
+ - !ruby/object:Gem::Dependency
81
+ name: uuidtools
82
+ requirement: &21821088 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: *21821088
91
+ description: This gem allows you to perform most of the available operations in the
92
+ Windows Azure Service Management API from a friendly commandline tool.
93
+ email:
94
+ - Steve.Marx@microsoft.com
95
+ executables:
96
+ - waz
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - .gitignore
101
+ - Gemfile
102
+ - LICENSE
103
+ - README.md
104
+ - Rakefile
105
+ - bin/waz
106
+ - lib/waz-cmd.rb
107
+ - lib/waz-cmd/templates/get_keys.erb
108
+ - lib/waz-cmd/templates/list_affinity_groups.erb
109
+ - lib/waz-cmd/templates/list_applications.erb
110
+ - lib/waz-cmd/templates/list_locations.erb
111
+ - lib/waz-cmd/templates/show_affinity_group.erb
112
+ - lib/waz-cmd/templates/show_application.erb
113
+ - lib/waz-cmd/templates/show_configuration.erb
114
+ - lib/waz-cmd/templates/show_deployment.erb
115
+ - lib/waz-cmd/templates/show_history.erb
116
+ - lib/waz-cmd/templates/show_storage.erb
117
+ - lib/waz-cmd/version.rb
118
+ - waz-cmd.gemspec
119
+ homepage: http://blog.smarx.com
120
+ licenses: []
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.5
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: Command-line tool to manage Windows Azure applications and storage accounts.
143
+ test_files: []