vmc 0.0.4 → 0.0.5

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.
data/lib/vmc.rb CHANGED
@@ -0,0 +1,1588 @@
1
+ # Copyright 2010, VMware, Inc. Licensed under the
2
+ # MIT license, please see the LICENSE file. All rights reserved
3
+
4
+ require 'rubygems'
5
+ require 'pp'
6
+ require 'vmc_base'
7
+ require 'highline/import'
8
+
9
+ module VMC
10
+
11
+ class Client < VMC::BaseClient
12
+
13
+ VERSION = 0.999
14
+
15
+ attr_reader :host, :base_uri, :droplets_uri, :services_uri, :resources_uri, :token, :args
16
+
17
+ attr_accessor :puser
18
+
19
+ def setup_target_uris
20
+ get_host_target
21
+ @base_uri = "http://#{@host}"
22
+ @droplets_uri = "#{base_uri}/apps"
23
+ @services_uri = "#{base_uri}/services"
24
+ @resources_uri = "#{base_uri}/resources"
25
+ end
26
+
27
+ def check_target
28
+ get_token # Just load it if its around
29
+ @check = HTTPClient.get "#{base_uri}/info", nil, auth_hdr
30
+ error "\nERROR: Unable to contact target server: [#{@base_uri}]\n\n" if @check.status != 200
31
+ #display "\n[#{@base_uri}]\n\n"
32
+ check_puser if @puser
33
+ rescue
34
+ error "\nERROR: Unable to contact target server: [#{@base_uri}]\n\n"
35
+ end
36
+
37
+ def set_puser(puser)
38
+ @puser = puser
39
+ end
40
+
41
+ def check_puser
42
+ info_json = JSON.parse(@check.content)
43
+ username = info_json['user']
44
+ error "ERROR: Proxying to #{@puser} failed." unless (username && (username.downcase == @puser.downcase))
45
+ end
46
+
47
+ # ------- commands --------
48
+
49
+ # show version string
50
+ def version
51
+ display VERSION
52
+ exit 0
53
+ end
54
+
55
+ # Define a new cloud controller target
56
+ def target(host=nil)
57
+ unless host
58
+ display "\n[#{@base_uri}]\n\n"
59
+ return
60
+ end
61
+
62
+ target_host = host.gsub(/^http(s*):\/\//i, '')
63
+ try_host = "http://#{target_host}/info"
64
+ target_host_display = "http://#{target_host}"
65
+ display ''
66
+ begin
67
+ response = HTTPClient.get try_host
68
+ if response.status != 200
69
+ display "New Target host is not valid: '#{target_host_display}'"
70
+ show_response = ask "Would you like to use one of these (y/n)? "
71
+ display response.content if show_response.upcase == 'Y'
72
+ exit(1)
73
+ end
74
+ response_json = JSON.parse(response.content)
75
+ error "New Target host is not valid: '#{target_host_display}'" unless
76
+ response_json['name'] && response_json['version'] && response_json['support'] && response_json['description']
77
+ rescue => e
78
+ display e
79
+ error "New Target host is not valid: '#{target_host_display}'"
80
+ end
81
+ write_host_target(target_host)
82
+ display "Succesfully targeted to [#{target_host_display}]\n\n"
83
+ rescue => e
84
+ error "Problem executing command, #{e}"
85
+ end
86
+
87
+ # get information about this cloud
88
+ def info
89
+ info_json = JSON.parse(@check.content)
90
+
91
+ display "\n#{info_json['description']}"
92
+ display "For support visit #{info_json['support']}"
93
+ display ""
94
+ display "Target: #{@base_uri} (v#{info_json['version']})"
95
+ display "Client: v#{VERSION}"
96
+ if info_json['user']
97
+ display ''
98
+ display "User: #{info_json['user']}"
99
+ end
100
+ if usage = info_json['usage'] and limits = info_json['limits']
101
+ tmem = pretty_size(limits['memory']*1024*1024)
102
+ mem = pretty_size(usage['memory']*1024*1024)
103
+ tser = limits['services']
104
+ ser = usage['services']
105
+ tapps = limits['apps'] || 0
106
+ apps = usage['apps'] || 0
107
+
108
+ display "Usage: Memory (#{mem} of #{tmem} total)"
109
+ display " Services (#{ser} of #{tser} total)"
110
+ display " Apps (#{apps} of #{tapps} total)" if limits['apps']
111
+ end
112
+ display ''
113
+ rescue => e
114
+ error "Problem executing command, #{e}"
115
+ end
116
+
117
+ # register me as a user of this cloud
118
+ def register(email=nil,password=nil)
119
+ if not email and not password
120
+ puts "Register your account with an email account and password."
121
+ email = ask("Email: ")
122
+ password = ask("Password: ") {|q| q.echo = '*'}
123
+ password2 = ask("Verify Password: ") {|q| q.echo = '*'}
124
+ error "Passwords did not match, try again" if password != password2
125
+ end
126
+ get_token # Just load it if its around
127
+ register_internal(@base_uri, email, password, auth_hdr)
128
+ display "Registration completed"
129
+ # do an autologin also to setup token, avoiding login on next VMC command.
130
+ login_save_token(email, password) unless @token
131
+ rescue => e
132
+ error "Problem registering, #{e}"
133
+ end
134
+
135
+ def login(email=nil,password=nil)
136
+ if email and not password
137
+ tries = 0
138
+ begin
139
+ password = ask("Password: ") {|q| q.echo = '*'}
140
+ login_save_token(email, password)
141
+ display ""
142
+ rescue => e
143
+ display "Problem with login, #{e}, try again or register for an account."
144
+ retry if (tries += 1) < 3
145
+ exit 1
146
+ end
147
+ elsif !(email && password)
148
+ tries = 0
149
+ begin
150
+ email = ask("Email: ")
151
+ password = ask("Password: ") {|q| q.echo = '*'}
152
+ login_save_token(email, password)
153
+ display ""
154
+ rescue => e
155
+ display "Problem with login, #{e}, try again or register for an account."
156
+ retry if (tries += 1) < 3
157
+ exit 1
158
+ end
159
+ else
160
+ begin
161
+ login_save_token(email, password)
162
+ display ""
163
+ rescue => e
164
+ error "Problem with login, #{e}, try again or register for an account."
165
+ end
166
+ end
167
+ end
168
+
169
+ def logout
170
+ FileUtils.rm_f(token_file)
171
+ display "Succesfully logged out."
172
+ end
173
+
174
+ # Change the current users passwd
175
+ def passwd(password=nil)
176
+ check_for_token
177
+ user_info = JSON.parse(@check.content)
178
+ email = user_info['user']
179
+ puts "Changing password for #{email}\n\n"
180
+ if !password
181
+ password = ask("Password: ") {|q| q.echo = '*'}
182
+ password2 = ask("Verify Password: ") {|q| q.echo = '*'}
183
+ error "Passwords did not match, try again" if password != password2
184
+ end
185
+
186
+ response = get_user_internal(@base_uri, email, auth_hdr)
187
+ user_info = JSON.parse(response.content)
188
+ user_info['password'] = password
189
+ change_passwd_internal(@base_uri, user_info, auth_hdr)
190
+
191
+ display "Password succesfully changed."
192
+ # do an autologin also to setup token, avoiding login on next VMC command.
193
+ # only if not proxying
194
+ login_save_token(email, password) unless @puser
195
+ rescue => e
196
+ error "Problem changing password, #{e}"
197
+ end
198
+
199
+
200
+ def login_save_token(email, password)
201
+ # TODO: We should be url encoding email
202
+ @token = login_internal(@base_uri, email, password)
203
+ write_token
204
+ end
205
+
206
+ def target_file
207
+ "#{ENV['HOME']}/.vmc_target"
208
+ end
209
+
210
+ def get_host_target
211
+ return if @host
212
+ if File.exists? target_file
213
+ @host = File.read(target_file).strip!
214
+ ha = @host.split('.')
215
+ ha.shift
216
+ @suggest_url = ha.join('.')
217
+ @suggest_url = 'b29.me' if @suggest_url.empty?
218
+ else
219
+ @host = 'api.b29.me'
220
+ @suggest_url = 'b29.me'
221
+ end
222
+ end
223
+
224
+ def write_host_target(target_host)
225
+ File.open(target_file, 'w+') { |f| f.puts target_host }
226
+ FileUtils.chmod 0600, target_file
227
+ end
228
+
229
+ def token_file
230
+ "#{ENV['HOME']}/.vmc_token"
231
+ end
232
+
233
+ def instance_file
234
+ "#{ENV['HOME']}/.vmc_instances"
235
+ end
236
+
237
+ def get_token
238
+ return @token if @token
239
+ if File.exists?(token_file)
240
+ contents = File.read(token_file).strip
241
+ begin
242
+ tokens = JSON.parse(contents)
243
+ @token = tokens[@base_uri]
244
+ rescue
245
+ @token = contents
246
+ write_token
247
+ end
248
+ end
249
+ end
250
+
251
+ def check_for_token
252
+ return if get_token
253
+ display "Please Login:\n\n"
254
+ login
255
+ check_for_token
256
+ end
257
+
258
+ def write_token
259
+ contents = File.exists?(token_file) ? File.read(token_file).strip : nil
260
+ tokens = {}
261
+
262
+ if contents
263
+ begin
264
+ tokens = JSON.parse(contents)
265
+ rescue
266
+ end
267
+ end
268
+
269
+ tokens[@base_uri] = @token
270
+
271
+ File.open(token_file, 'w+') { |f| f.write(tokens.to_json) }
272
+ FileUtils.chmod 0600, token_file
273
+ end
274
+
275
+ def auth_hdr
276
+ auth = { 'AUTHORIZATION' => "#{@token}" }
277
+ auth['PROXY-USER'] = @puser if @puser
278
+ return auth
279
+ end
280
+
281
+ def display_services_banner
282
+ #display "#{'Name'.ljust(15)} #{'Service'.ljust(10)} #{'Version'.ljust(10)} #{'Plans'}"
283
+ #display "#{'----'.ljust(15)} #{'-------'.ljust(10)} #{'-------'.ljust(10)} #{'-----'}"
284
+ display "#{'Name'.ljust(15)} #{'Service'.ljust(10)}"
285
+ display "#{'----'.ljust(15)} #{'-------'.ljust(10)}"
286
+
287
+ end
288
+
289
+ def list_services(service_names, banner)
290
+ services = []
291
+ service_names.each { |service_name|
292
+ sn = URI.escape("#{services_uri}/#{service_name}")
293
+ response = HTTPClient.get(sn, nil, auth_hdr)
294
+ error "Problem getting services list" if response.status != 200
295
+ services << JSON.parse(response.content)
296
+ }
297
+
298
+ display(banner)
299
+
300
+ if services.empty?
301
+ display "None provisioned yet."
302
+ else
303
+ display_services_banner
304
+ services.each { |h|
305
+ display "#{h['name'].to_s.ljust(15)} ", false
306
+ display "#{h['vendor'].ljust(10)} "
307
+ #display "#{h['vendor'].ljust(10)} ", false
308
+ #display "#{h['version'].ljust(10)} ", false
309
+ #display "#{h['tier']}"
310
+ }
311
+ end
312
+ end
313
+
314
+ def app_list_services(appname=nil)
315
+ error "Application name required\nvmc app services <appname>" unless appname
316
+
317
+ response = HTTPClient.get "#{droplets_uri}/#{appname}", nil, auth_hdr
318
+ error "Application does not exist." if response.status == 404
319
+ return '' if response.status != 200
320
+ service_names = JSON.parse(response.content)['services']
321
+ service_names.empty? ? "None" : service_names.join(", ")
322
+ end
323
+
324
+ def user_list_services_helper
325
+ service_names = get_user_service_names
326
+ #list_services(service_names, "Provisioned Services:\n\n")
327
+ list_services(service_names, "\n======= Provisioned Services ========\n\n")
328
+ end
329
+
330
+ def get_user_service_names
331
+ service_names = []
332
+ response = HTTPClient.get "#{services_uri}", nil, auth_hdr
333
+ response_json = JSON.parse(response.content)
334
+ response_json.each { |service_desc| service_names << service_desc['name'] }
335
+ service_names
336
+ end
337
+
338
+ def services
339
+ check_for_token
340
+ response = HTTPClient.get "#{base_uri}/info/services", nil, auth_hdr
341
+ handle_response(response)
342
+ display "\n========== System Services ==========\n\n"
343
+ services = JSON.parse(response.content)
344
+ display_services_directory_banner
345
+ services.each { |service_type, value|
346
+ value.each { |vendor, version|
347
+ version.each { |version_str, service|
348
+ display "#{vendor.ljust(10)} ", false
349
+ display "#{version_str.ljust(5)} ", false
350
+ display "#{service['description']}"
351
+ }
352
+ }
353
+ }
354
+ display ""
355
+ user_list_services_helper
356
+ display ""
357
+ rescue => e
358
+ error "Problem executing command, #{e}"
359
+ end
360
+
361
+ def bind_service(service_name, appname)
362
+ display "Creating new service binding to '#{service_name}' for '#{appname}'."
363
+
364
+ # Now get the app and update it with the provisioned service
365
+ response = get_app_internal(droplets_uri, appname, auth_hdr)
366
+ appinfo = JSON.parse(response.content)
367
+ provisioned_service = appinfo['services']
368
+ provisioned_service = [] unless provisioned_service
369
+ provisioned_service << service_name
370
+ appinfo['services'] = provisioned_service
371
+ response = update_app_state_internal droplets_uri, appname, appinfo, auth_hdr
372
+ error "Problem updating application with new services" if response.status >= 400
373
+ display "Application '#{appname}' updated."
374
+ restart appname if appinfo['state'] == 'STARTED'
375
+ rescue => e
376
+ error "Problem executing command, #{e}"
377
+ end
378
+
379
+ def unbind_service(service_name, appname)
380
+ app_remove_service(appname, service_name)
381
+ end
382
+
383
+ def create_service(service, opts={})
384
+ check_for_token
385
+ response = HTTPClient.get "#{base_uri}/info/services", nil, auth_hdr
386
+ handle_response(response)
387
+ services = JSON.parse(response.content)
388
+ types = []
389
+ services.each {|k,v| types << v }
390
+ selection = nil
391
+ types.each { |t| selection = t if t[service] }
392
+ if ! selection
393
+ options = []
394
+ types.each {|t| options << t.keys }
395
+ options.flatten!
396
+ error("Service type must be one of: #{options.join(',')}") unless options.empty?
397
+ error("No selectable services at this time") if options.empty?
398
+ else
399
+ info = selection[service][selection[service].keys.first]
400
+ service_name = opts[:name] || "#{service}-#{fast_uuid[0..6]}"
401
+ service_type = info['type']
402
+ service_vendor = info['vendor']
403
+ service_tier = 'free'
404
+ service_version = info['version']
405
+
406
+ services = {
407
+ :name => service_name,
408
+ :type => service_type,
409
+ :vendor => service_vendor,
410
+ :tier => service_tier,
411
+ :version => service_version,
412
+ #:options => option_values
413
+ }
414
+
415
+ check_for_token
416
+ response = add_service_internal @services_uri, services, auth_hdr
417
+ handle_response(response)
418
+ display "Service '#{service_name}' provisioned."
419
+
420
+ if appname = opts[:bind]
421
+ display "Creating new service binding to '#{service_name}' for '#{appname}'."
422
+
423
+ # Now get the app and update it with the provisioned service
424
+ response = get_app_internal(droplets_uri, appname, auth_hdr)
425
+ handle_response(response)
426
+ appinfo = JSON.parse(response.content)
427
+ provisioned_service = appinfo['services']
428
+ provisioned_service = [] unless provisioned_service
429
+ provisioned_service << service_name
430
+ appinfo['services'] = provisioned_service
431
+ response = update_app_state_internal droplets_uri, appname, appinfo, auth_hdr
432
+ handle_response(response)
433
+ display "Application '#{appname}' updated."
434
+ restart appname if appinfo['state'] == 'STARTED'
435
+ end
436
+ end
437
+ rescue => e
438
+ error "Problem executing command, #{e}"
439
+ end
440
+
441
+ def delete_service(service)
442
+ check_for_token
443
+ user_remove_service_helper(service)
444
+ end
445
+
446
+ def user_remove_service_helper(service_name=nil)
447
+ available_services = get_user_service_names
448
+ error "No services to delete" if available_services.empty?
449
+
450
+ unless service_name
451
+ choose do |menu|
452
+ #menu.header = "The following services are provisioned"
453
+ menu.prompt = 'Please select the service you wish to delete: '
454
+ menu.select_by = :index
455
+ available_services.each { |key| menu.choice(key) { service_name = key } }
456
+ end
457
+ end
458
+
459
+ error "Error: Invalid service" unless available_services.include? service_name
460
+
461
+ remove_service_internal(services_uri, service_name, auth_hdr)
462
+ display("Successfully removed service: #{service_name}")
463
+ end
464
+
465
+ def user_add_service_helper(service_name_prefix, opts={})
466
+ service_type = opts[:type]
467
+ service_vendor = opts[:vendor]
468
+ service_version = opts[:version]
469
+ service_tier = opts[:tier]
470
+ service_name = opts[:name]
471
+ service_size = opts[:size]
472
+ response = HTTPClient.get "#{base_uri}/info/services", nil, auth_hdr
473
+ handle_response(response)
474
+
475
+ last_print = :none
476
+
477
+ services = JSON.parse(response.content)
478
+
479
+ available_services = {}
480
+ services.each do |type, vendors|
481
+ vendors.each do |vendor, versions|
482
+ versions.each do |version, entry|
483
+ tiers = entry["tiers"]
484
+ if tiers.size > 1
485
+ tier = tiers.has_key?("free") ? "free" : tiers.keys.first
486
+ entry["tiers"] = {tier => tiers[tier]}
487
+ end
488
+ available_services[vendor] = {:type => type, :version => version} if (service_type.nil? or service_type == type)
489
+ end
490
+ end
491
+ end
492
+
493
+ return nil if available_services.empty?
494
+
495
+ service_type_hash = nil
496
+ service_vendor = nil if (service_vendor and service_vendor.empty?)
497
+ unless service_vendor
498
+
499
+ if available_services.length == 1
500
+ service_vendor = available_services.keys.first
501
+ service_type_hash = services[available_services[service_vendor][:type]]
502
+
503
+ service_type = available_services[service_vendor][:type]
504
+ say("Single #{service_type} service available: #{service_vendor}")
505
+ last_print = :auto
506
+ else
507
+ puts "" if last_print == :auto
508
+ choose do |menu|
509
+ menu.header = "The following #{service_type} services are available"
510
+ menu.prompt = 'Please select one you wish to provision: '
511
+ menu.select_by = :index
512
+
513
+ available_services.each_key do |key|
514
+ menu.choice(key) do
515
+ service_type = available_services[key][:type]
516
+ service_type_hash = services[service_type]
517
+ service_vendor = key
518
+ end
519
+ end
520
+ end
521
+ last_print = :menu
522
+ end
523
+ end
524
+ error "Could not find vendor '#{service_vendor}' for #{service_type} services." unless service_type_hash[service_vendor]
525
+
526
+ service_vendor_hash = service_type_hash[service_vendor]
527
+
528
+ unless service_version
529
+ puts "" if last_print == :menu
530
+ if service_vendor_hash.length == 1
531
+ service_version = service_vendor_hash.first[0]
532
+ say("Single version available: #{service_version}")
533
+ last_print = :auto
534
+ else
535
+ puts "" if last_print == :auto
536
+ choose do |menu|
537
+ menu.header = "The following #{service_vendor} #{service_type} versions are available"
538
+ menu.prompt = 'Please select one you wish to provision: '
539
+ menu.select_by = :index
540
+
541
+ service_vendor_hash.each_key do |key|
542
+ menu.choice(key) { service_version = key }
543
+ end
544
+ end
545
+ last_print = :menu
546
+ end
547
+ end
548
+ error "Could not find version '#{service_version}' for #{service_vendor} #{service_type}." unless service_vendor_hash[service_version]
549
+
550
+ service_version_hash = service_vendor_hash[service_version]
551
+ service_tier = service_version_hash['tiers'].keys.first
552
+
553
+ service_tier_hash = service_version_hash['tiers'][service_tier]
554
+ options = service_tier_hash['options']
555
+
556
+ option_values = {}
557
+ if options
558
+ options.each do |k, v|
559
+
560
+ if service_size
561
+ option_values['size'] = service_size
562
+ else
563
+ puts "" if last_print == :menu
564
+ if v['type'] == 'value'
565
+ puts "" if last_print == :auto
566
+ choose do |menu|
567
+ menu.prompt = "#{k} (#{v['description']}): "
568
+ menu.select_by = :index
569
+
570
+
571
+ v['values'].each do |option|
572
+ option_value = option
573
+ menu.choice("#{option}") { option_values[k] = option_value }
574
+ end
575
+ end
576
+ last_print = :menu
577
+ end
578
+ end
579
+ end
580
+ end
581
+
582
+ default_service_name = "#{service_vendor}-#{fast_uuid[0..6]}"
583
+ if service_name.nil?
584
+ puts ""
585
+ service_name = ask("Specify the name of the service [#{default_service_name}]: ")
586
+ service_name = default_service_name if service_name.empty?
587
+ end
588
+
589
+ services = {
590
+ :name => service_name,
591
+ :type => service_type,
592
+ :vendor => service_vendor,
593
+ :tier => service_tier,
594
+ :version => service_version,
595
+ :options => option_values
596
+ }
597
+
598
+ check_for_token
599
+ response = add_service_internal @services_uri, services, auth_hdr
600
+ handle_response(response)
601
+ display "Service '#{service_vendor}' provisioned."
602
+
603
+ service_name
604
+ end
605
+
606
+
607
+ def app_add_service(opts={})
608
+ service_type = opts[:service]
609
+ service_vendor = opts[:vendor]
610
+ service_version = opts[:version]
611
+ service_tier = opts[:tier]
612
+ appname = opts[:appname]
613
+
614
+ error "Application name required\nvmc apps add-service <appname> [service] [vendor] [version] [tier]" unless appname
615
+
616
+ app_add_service_helper(appname, service_type, service_vendor, service_version, service_tier)
617
+
618
+ @appname = appname
619
+ restart
620
+ end
621
+
622
+ def app_add_service_helper(appname, service_type=nil, service_vendor=nil, service_version=nil, service_tier=nil)
623
+ check_for_token
624
+
625
+ # If we have a single arg, let's assume its a service name..
626
+ if service_type && !service_vendor && !service_version && !service_tier
627
+ service_name = service_type
628
+ service_type = nil
629
+ end
630
+
631
+ service_names = get_user_service_names
632
+
633
+ if service_name && (service_names.empty? || !service_names.include?(service_name))
634
+ service_name = nil
635
+ end
636
+
637
+ if !service_names.empty? && !service_name
638
+ list_services(get_user_service_names, "The following services have been provisioned for you:")
639
+ display ''
640
+ use_existing = ask "Would you like to use one of these (y/n)? "
641
+ if use_existing.upcase == 'Y'
642
+ begin
643
+ service_name = ask "Which service (name)? "
644
+ if !service_names.include?(service_name)
645
+ display "Incorrect service name, please type again"
646
+ service_name = nil
647
+ end
648
+ end while !service_name
649
+ end
650
+ end
651
+
652
+ if !service_name
653
+ display "Let's provision a new service"
654
+ opts = {
655
+ :type => service_type,
656
+ :vendor => service_vendor,
657
+ :version => service_version,
658
+ :tier => service_tier
659
+ }
660
+ service_name = user_add_service_helper(appname, opts)
661
+ end
662
+
663
+ unless service_name
664
+ display "No services available to provision"
665
+ # Should we ask if they want to continue?
666
+ return
667
+ end
668
+
669
+ display "Creating new service binding to '#{service_name}' for '#{appname}'."
670
+
671
+ # Now get the app and update it with the provisioned service
672
+ response = get_app_internal(droplets_uri, appname, auth_hdr)
673
+ appinfo = JSON.parse(response.content)
674
+ provisioned_service = appinfo['services']
675
+ provisioned_service = [] unless provisioned_service
676
+ provisioned_service << service_name
677
+ appinfo['services'] = provisioned_service
678
+ response = update_app_state_internal droplets_uri, appname, appinfo, auth_hdr
679
+ handle_response(response)
680
+ display "Application '#{appname}' updated."
681
+ end
682
+
683
+ def format_price(amount, type, *rest)
684
+ case type
685
+ when 'flat'
686
+ return "$#{amount}/#{rest[0]}"
687
+ when 'metered'
688
+ raise "Not Implemented"
689
+ end
690
+ end
691
+
692
+ def app_remove_service(appname=nil, service_name=nil)
693
+ check_for_token
694
+
695
+ app_response = get_app_internal(droplets_uri, appname, auth_hdr)
696
+ appinfo = JSON.parse(app_response.content)
697
+ provisioned_service = appinfo['services']
698
+ provisioned_service = [] unless provisioned_service
699
+ provisioned_service.delete(service_name)
700
+ appinfo['services'] = provisioned_service
701
+ response = update_app_state_internal droplets_uri, appname, appinfo, auth_hdr
702
+ handle_response(response)
703
+ display "Application '#{appname}' updated"
704
+ display "Service #{service_name} removed."
705
+ @appname = appname
706
+ restart
707
+ rescue => e
708
+ error "Problem executing command, #{e}"
709
+ end
710
+
711
+ def list(first_arg=nil)
712
+ apps(first_arg)
713
+ end
714
+
715
+ def apps(first_arg=nil, opts={})
716
+ check_for_token
717
+
718
+ case first_arg
719
+ when "list" then list_apps
720
+ when nil then list_apps
721
+ when 'add-service' then app_add_service(opts)
722
+ when 'remove-service' then app_remove_service(opts[:appname], opts[:service_name])
723
+ when 'list-services' then app_list_services(opts[:appname])
724
+ else "Incorrect option #{first_arg}. Must be 'list' (default), 'services', 'add-service', or 'remove-service"
725
+ end
726
+ end
727
+
728
+ def health(d)
729
+ return 'N/A' unless (d and d['state'])
730
+
731
+ healthy_instances = d['runningInstances']
732
+ expected_instance = d['instances']
733
+ health = nil
734
+
735
+ if d['state'] == "STARTED" && expected_instance > 0 && healthy_instances
736
+ health = format("%.3f", healthy_instances.to_f / expected_instance).to_f
737
+ end
738
+
739
+ if health
740
+ if health == 1.0
741
+ health = "RUNNING"
742
+ else
743
+ health = "#{(health * 100).round}%"
744
+ end
745
+ else
746
+ if d['state'] == 'STOPPED'
747
+ health = 'STOPPED'
748
+ else
749
+ health = 'N/A'
750
+ end
751
+ end
752
+
753
+ health
754
+ end
755
+
756
+ def list_apps
757
+ droplets_full = get_apps_internal @droplets_uri, auth_hdr
758
+ if droplets_full.empty?
759
+ display "No applications available."
760
+ return
761
+ end
762
+
763
+ display "#{'APPNAME'.ljust 15} #{'HEALTH'.ljust 8} #{'INSTANCES'.ljust 10} #{'SERVICES'.ljust 25} #{'URL'.ljust 15}\n"
764
+ display "#{'-------'.ljust 15} #{'------'.ljust 8} #{'---------'.ljust 10} #{'--------'.ljust 25} #{'---'.ljust 15}\n"
765
+ droplets_full.each { |d|
766
+ display "#{d['name'].ljust 15} ", false
767
+ display "#{health(d).ljust 8} ", false
768
+ display "#{d['instances'].to_s.ljust 10} ", false
769
+ display "#{d['services'].join(", ").ljust(25)} ", false
770
+ #display "#{app_list_services(d['name']).ljust(25)} ", false
771
+ display "#{d['uris'].join(', ').ljust(15)}"
772
+ }
773
+ rescue => e
774
+ error "Problem executing command, #{e}"
775
+ end
776
+
777
+ # opts = {:instances => [optional], :exec => [optional],
778
+ # :ignore_framework => [true/false/optional],
779
+ # :noframework => [true/false/optional],
780
+ # :appname => [optional],
781
+ # :url => [optional]}
782
+
783
+ def push(opts={})
784
+ instances = opts.delete(:instances) || 1
785
+ exec = opts.delete(:exec) || 'thin start'
786
+ ignore_framework = opts.delete(:noframework)
787
+ no_start = opts[:no_start]
788
+
789
+ path = opts[:path] || '.'
790
+ appname = opts[:appname]
791
+ url = opts[:url]
792
+ memswitch = opts[:mem]
793
+
794
+ check_for_token
795
+
796
+ # check if we have hit our app limit
797
+ check_app_limit
798
+
799
+ # check memsize here for capacity
800
+ check_has_capacity_for(mem_choice_to_quota(memswitch)) if memswitch
801
+
802
+ unless (appname && url)
803
+ unless opts[:path]
804
+ proceed = ask("Would you like to deploy from the current directory? [Yn]: ")
805
+ error "Push aborted" if proceed == 'n' || proceed == 'N'
806
+ end
807
+ appname = ask("Application Name: ") unless appname
808
+ error "Push aborted: Application Name required." if appname.empty?
809
+ url = ask("Application Deployed URL: '#{appname}.#{@suggest_url}'? ") unless url
810
+ url = "#{appname}.#{@suggest_url}" if url.empty?
811
+ end
812
+
813
+ response = get_app_internal(@droplets_uri, appname, auth_hdr)
814
+ error "Application #{appname} already exists, use update or delete." if response.status == 200
815
+
816
+ Dir.chdir(path) do
817
+ # Detect the appropriate framework.
818
+ framework = "http://b20nine.com/unknown"
819
+ mem = '256M'
820
+ unless ignore_framework
821
+ if File.exist?('config/environment.rb')
822
+ display "Rails application detected."
823
+ framework = "rails/1.0"
824
+ elsif Dir.glob('*.war').first
825
+ opt_war_file = Dir.glob('*.war').first
826
+ display "Java war file found, detecting framework..."
827
+
828
+ entries = []
829
+ Zip::ZipFile.foreach(opt_war_file) { |zentry| entries << zentry }
830
+ contents = entries.join("\n")
831
+
832
+ if contents =~ /WEB-INF\/grails-app/
833
+ display "Grails application detected."
834
+ framework = "grails/1.0"
835
+ mem = '512M'
836
+ elsif contents =~ /WEB-INF\/classes\/org\/springframework/
837
+ display "SpringSource application detected."
838
+ framework = "spring_web/1.0"
839
+ mem = '512M'
840
+ elsif contents =~ /WEB-INF\/lib\/spring-core.*\.jar/
841
+ display "SpringSource application detected."
842
+ framework = "spring_web/1.0"
843
+ mem = '512M'
844
+ else
845
+ display "Unknown J2EE Web Application"
846
+ framework = "spring_web/1.0"
847
+ end
848
+ elsif File.exist?('web.config')
849
+ display "ASP.NET application detected."
850
+ framework = "asp_web/1.0"
851
+ elsif !Dir.glob('*.rb').empty?
852
+ matched_file = nil
853
+ Dir.glob('*.rb').each do |fname|
854
+ next if matched_file
855
+ File.open(fname, 'r') do |f|
856
+ str = f.read # This might want to be limited
857
+ matched_file = fname if (str && str.match(/^\s*require\s*'sinatra'/i))
858
+ end
859
+ end
860
+ if matched_file && !File.exist?('config.ru')
861
+ display "Simple Sinatra application detected in #{matched_file}."
862
+ exec = "ruby #{matched_file}"
863
+ end
864
+ mem = '128M'
865
+ elsif !Dir.glob('*.js').empty?
866
+ # Fixme, make other files work too..
867
+ if File.exist?('app.js')
868
+ display "Node.js application detected."
869
+ framework = "nodejs/1.0"
870
+ mem = '64M'
871
+ end
872
+ end
873
+ end
874
+
875
+ mem = memswitch if memswitch
876
+ unless memswitch
877
+ choose do |menu|
878
+ menu.layout = :one_line
879
+ menu.prompt = "Memory Reservation [Default:#{mem}] "
880
+ menu.default = mem
881
+ mem_choices.each { |choice| menu.choice(choice) { mem = choice } }
882
+ end
883
+ end
884
+
885
+ # Set to MB number
886
+ mem_quota = mem_choice_to_quota(mem)
887
+
888
+ # check memsize here for capacity
889
+ check_has_capacity_for(mem_quota)
890
+
891
+ manifest = {
892
+ :name => "#{appname}",
893
+ :staging => {
894
+ :model => framework,
895
+ :stack => exec
896
+ },
897
+ :uris => [url],
898
+ :instances => instances,
899
+ :resources => {
900
+ :memory => mem_quota
901
+ },
902
+ }
903
+
904
+ display "Uploading Application Information."
905
+
906
+ # Send the manifest to the cloud controller
907
+ response = create_app_internal(@droplets_uri, manifest, auth_hdr)
908
+ handle_response(response)
909
+
910
+ # Provision database here if needed.
911
+ if framework_needs_db?(framework)
912
+ proceed = ask("This framework usually needs a database, would you like to provision it? [Yn]: ")
913
+ if proceed != 'n' && proceed != 'N'
914
+ app_add_service_helper(appname, 'database', '')
915
+ provisioned_db = true
916
+ end
917
+ end
918
+
919
+ # Stage and upload the app bits.
920
+ display "Uploading Application."
921
+ upload_size = upload_app_bits(@resources_uri, @droplets_uri, appname, auth_hdr, opt_war_file, provisioned_db)
922
+ if upload_size > 1024*1024
923
+ upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M'
924
+ elsif upload_size > 0
925
+ upload_size = (upload_size/1024.0).round.to_s + 'K'
926
+ end
927
+ display "\nUploaded Application '#{appname}' (#{upload_size})."
928
+
929
+ display "Push completed."
930
+ @push = true
931
+ @appname = appname
932
+ start unless no_start
933
+ end
934
+ rescue => e
935
+ error "Problem executing command, #{e}"
936
+ end
937
+
938
+ def delete(appname=nil, force=false)
939
+ error "Application name required, vmc delete <appname || --all> [--force]." unless appname
940
+
941
+ check_for_token
942
+
943
+ # wildcard behavior
944
+ apps = []
945
+ if appname == '--all'
946
+ droplets_full = get_apps_internal @droplets_uri, auth_hdr
947
+ apps = droplets_full.collect { |d| "#{d['name']}" }
948
+ apps.each { |app| delete_app(app, force) }
949
+ else
950
+ delete_app(appname, force)
951
+ end
952
+ rescue => e
953
+ error "Problem executing command, #{e}"
954
+ end
955
+
956
+ def delete_app(appname, force)
957
+ response = get_app_internal @droplets_uri, appname, auth_hdr
958
+ if response.status != 200
959
+ display "Application '#{appname}' does not exist."
960
+ return
961
+ end
962
+ appinfo = JSON.parse(response.content)
963
+ services_to_delete = []
964
+ app_services = appinfo['services']
965
+ app_services.each { |service|
966
+ del_service = 'Y' if force
967
+ del_service = ask("Application '#{appname}' uses '#{service}' service, would you like to delete it? [Yn]: ") unless force
968
+ services_to_delete << service unless (del_service == 'n' || del_service == 'N')
969
+ }
970
+ delete_app_internal @droplets_uri, appname, services_to_delete, auth_hdr
971
+ services_to_delete.each { |s| display "Service '#{s}' deleted."}
972
+ display "Application '#{appname}' deleted."
973
+ end
974
+
975
+ def stop(appname=nil)
976
+ appname ||= @appname
977
+ error "Application name required, vmc stop <appname>." unless appname
978
+
979
+ check_for_token
980
+ response = get_app_internal @droplets_uri, appname, auth_hdr
981
+ if response.status != 200
982
+ display "Application '#{appname}' does not exist, use push first."
983
+ return
984
+ end
985
+ appinfo = JSON.parse(response.content)
986
+ appinfo['state'] = 'STOPPED'
987
+ #display JSON.pretty_generate(appinfo)
988
+
989
+ hdrs = auth_hdr.merge({'content-type' => 'application/json'})
990
+ update_app_state_internal @droplets_uri, appname, appinfo, auth_hdr
991
+ display "Application '#{appname}' stopped."
992
+ rescue => e
993
+ error "Problem executing command, #{e}"
994
+ end
995
+
996
+ def start(appname=nil)
997
+ appname ||= @appname
998
+ error "Application name required, vmc start <appname>." unless appname
999
+
1000
+ check_for_token
1001
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1002
+ if response.status != 200
1003
+ display"Application #{appname} does not exist, use push first."
1004
+ return
1005
+ end
1006
+ appinfo = JSON.parse(response.content)
1007
+ if (appinfo['state'] == 'STARTED')
1008
+ display "Application '#{appname}' is already running."
1009
+ return
1010
+ end
1011
+
1012
+ appinfo['state'] = 'STARTED'
1013
+
1014
+ response = update_app_state_internal @droplets_uri, appname, appinfo, auth_hdr
1015
+ handle_response(response)
1016
+
1017
+ count = 0
1018
+ log_lines_displayed = 0
1019
+ failed = false
1020
+ display "Trying to start Application: '#{appname}'."
1021
+ loop do
1022
+ display ".", false unless count > 9
1023
+ sleep 1
1024
+ begin
1025
+ if app_started_properly(appname)
1026
+ display ''
1027
+ break;
1028
+ elsif !crashes(appname, false).empty?
1029
+ # Check for the existance of crashes
1030
+ display "\n\nERROR - Application '#{appname}' failed to start, logs information below.\n\n"
1031
+ grab_crash_logs(appname, '0', true)
1032
+ if @push
1033
+ display ''
1034
+ should_delete = ask "Should I delete the application? (y/n)? "
1035
+ delete_app(appname, false) if should_delete == 'Y' || should_delete == 'y'
1036
+ end
1037
+ failed = true
1038
+ break
1039
+ elsif count > 9
1040
+ log_lines_displayed = grab_startup_tail(appname, log_lines_displayed)
1041
+ end
1042
+ rescue => e
1043
+ display e.message, false
1044
+ end
1045
+ count += 1
1046
+ if count > 300 # 5 minutes
1047
+ error "\n\nApplication is taking too long to start, check your logs"
1048
+ break
1049
+ end
1050
+ end
1051
+
1052
+ display("Application '#{appname}' started.") unless failed
1053
+ rescue => e
1054
+ error "Problem executing command, #{e}"
1055
+ end
1056
+
1057
+ def restart(appname=nil)
1058
+ appname ||= @appname
1059
+ error "Application name required, vmc start <appname>." unless appname
1060
+
1061
+ stop(appname)
1062
+ start(appname)
1063
+ rescue => e
1064
+ error "Problem executing command, #{e}"
1065
+ end
1066
+
1067
+ def mem(appname, memsize=nil)
1068
+ check_for_token
1069
+
1070
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1071
+ handle_response(response)
1072
+
1073
+ appinfo = JSON.parse(response.content)
1074
+
1075
+ mem = current_mem = mem_quota_to_choice(appinfo['resources']['memory'])
1076
+
1077
+ if ! memsize
1078
+ choose do |menu|
1079
+ menu.layout = :one_line
1080
+ menu.prompt = "Update Memory Reservation? [Current:#{current_mem}] "
1081
+ menu.default = current_mem
1082
+ mem_choices.each { |choice| menu.choice(choice) { memsize = choice } }
1083
+ end
1084
+ end
1085
+
1086
+ memsize = mem_choice_to_quota(memsize)
1087
+ mem = mem_choice_to_quota(mem)
1088
+
1089
+ # check memsize here for capacity
1090
+ check_has_capacity_for(memsize - mem)
1091
+
1092
+ mem = memsize
1093
+
1094
+ if (mem != current_mem)
1095
+ appinfo['resources']['memory'] = mem
1096
+ response = update_app_state_internal @droplets_uri, appname, appinfo, auth_hdr
1097
+ handle_response(response)
1098
+ display "Updated memory reservation to '#{mem_quota_to_choice(mem)}'."
1099
+ restart appname if appinfo['state'] == 'STARTED'
1100
+ end
1101
+ rescue => e
1102
+ error "Problem executing command, #{e}"
1103
+ end
1104
+
1105
+ def update(appname=nil, canary=false)
1106
+ error "Application name required, vmc update <appname>." unless appname
1107
+
1108
+ check_for_token
1109
+
1110
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1111
+ if response.status != 200
1112
+ display "Application '#{appname}' does not exist, use push first."
1113
+ return
1114
+ end
1115
+ appinfo = JSON.parse(response.content)
1116
+
1117
+ display "Uploading Application."
1118
+ upload_app_bits(@resources_uri, @droplets_uri, appname, auth_hdr, Dir.glob('*.war').first)
1119
+
1120
+ unless canary
1121
+ @appname = appname
1122
+ display "Application updated."
1123
+ restart appname if appinfo['state'] == 'STARTED'
1124
+ return
1125
+ end
1126
+
1127
+ response = update_app_internal @droplets_uri, appname, auth_hdr
1128
+ handle_response(response)
1129
+
1130
+ last_state = 'NONE'
1131
+ begin
1132
+ response = get_update_app_status @droplets_uri, appname, auth_hdr
1133
+
1134
+ update_info = JSON.parse(response.content)
1135
+ update_state = update_info['state']
1136
+ if update_state != last_state
1137
+ display('') unless last_state == 'NONE'
1138
+ display("#{update_state.ljust(15)}", false)
1139
+ else
1140
+ display('.', false)
1141
+ end
1142
+
1143
+ if update_state == 'SUCCEEDED' || update_state == 'CANARY_FAILED'
1144
+ display('')
1145
+ if update_state == 'CANARY_FAILED' && update_info['canary']
1146
+ begin
1147
+ map = File.open(instance_file, 'r') { |f| JSON.parse(f.read) }
1148
+ rescue
1149
+ map = {}
1150
+ end
1151
+
1152
+ map["#{appname}-canary"] = update_info['canary']
1153
+
1154
+ File.open(instance_file, 'w') {|f| f.write(map.to_json)}
1155
+ display("Debug the canary using 'vmc files #{appname} --instance #{appname}-canary'")
1156
+ end
1157
+ break
1158
+ else
1159
+ last_state = update_state
1160
+ end
1161
+ sleep(0.5)
1162
+ end while true
1163
+ rescue => e
1164
+ error "Problem executing command, #{e}"
1165
+ end
1166
+
1167
+ def change_instances(appinfo, appname, instances)
1168
+ match = instances.match(/([+-])?\d+/)
1169
+ error "Invalid number of instances '#{instances}', vmc instances <appname> <num>." unless match
1170
+
1171
+ instances = instances.to_i
1172
+ current_instances = appinfo['instances']
1173
+ new_instances = match.captures[0] ? current_instances + instances : instances
1174
+ error "There must be at least 1 instance." if new_instances < 1
1175
+
1176
+ if current_instances == new_instances
1177
+ display "Application '#{appname}' is already running #{new_instances} instance#{'s' if new_instances > 1}."
1178
+ return
1179
+ end
1180
+
1181
+ appinfo['instances'] = new_instances
1182
+ response = update_app_state_internal @droplets_uri, appname, appinfo, auth_hdr
1183
+
1184
+ handle_response(response)
1185
+
1186
+ display "Scaled '#{appname}' #{new_instances > current_instances ? 'up' : 'down'} to " +
1187
+ "#{new_instances} instance#{'s' if new_instances > 1}."
1188
+ end
1189
+
1190
+ def get_instances(appname)
1191
+ instances_info_envelope = get_app_instances_internal(@droplets_uri, appname, auth_hdr)
1192
+
1193
+ # Empty array is returned if there are no instances running.
1194
+ error "No running instances for '#{appname}'" if instances_info_envelope.is_a?(Array)
1195
+
1196
+ instances_info = instances_info_envelope['instances']
1197
+ display "#{'Index'.ljust 5} #{'State'.ljust 15} #{'Since'.ljust 20}\n"
1198
+ display "#{'--'.ljust 5} #{'--------'.ljust 15} #{'-------------'.ljust 20}\n"
1199
+
1200
+ instances_info.each {|entry| entry[0] = entry[0].to_i}
1201
+ instances_info = instances_info.sort {|a,b| a['index'] - b['index']}
1202
+ instances_info.each do |entry|
1203
+ display "#{entry['index'].to_s.ljust 5} #{entry['state'].ljust 15} #{Time.at(entry['since']).strftime("%m/%d/%Y %I:%M%p").ljust 20}\n"
1204
+ end
1205
+ end
1206
+
1207
+ def instances(appname=nil, instances=nil)
1208
+ appname ||= @appname
1209
+ error "Application name required, vmc instances <appname> [num | delta]." unless appname
1210
+
1211
+ check_for_token
1212
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1213
+ handle_response(response)
1214
+ appinfo = JSON.parse(response.content)
1215
+
1216
+ if instances
1217
+ change_instances(appinfo, appname, instances) if instances
1218
+ elsif
1219
+ get_instances(appname)
1220
+ end
1221
+
1222
+ rescue => e
1223
+ error "Problem executing command, #{e}"
1224
+ end
1225
+
1226
+ def crashes(appname=nil, print_results=true)
1227
+ error "Application name required, vmc crashes <appname>." unless appname
1228
+
1229
+ check_for_token
1230
+ response = get_app_crashes_internal @droplets_uri, appname, auth_hdr
1231
+ if response.status == 404
1232
+ display"Application #{appname} does not exist, use push first."
1233
+ return []
1234
+ elsif response.status != 200
1235
+ display"Could not fetch application crashes."
1236
+ return []
1237
+ end
1238
+ crashes = JSON.parse(response.content)['crashes']
1239
+
1240
+ instance_map = {}
1241
+
1242
+ if print_results
1243
+ display "#{'Name'.ljust 10} #{'Id'.ljust 40} #{'Since'.ljust 20}\n"
1244
+ display "#{'--'.ljust 10} #{'--------'.ljust 40} #{'-------------'.ljust 20}\n"
1245
+ end
1246
+
1247
+ counter = 1
1248
+
1249
+ crashes = crashes.to_a.sort {|a,b| a['since'] - b['since']}
1250
+ crashes.each do |crash|
1251
+ name = "#{appname}-#{counter}"
1252
+ display "#{name.ljust 10} #{crash['instance'].ljust 40} #{Time.at(crash['since']).strftime("%m/%d/%Y %I:%M%p").ljust 20}\n" if print_results
1253
+ instance_map[name] = crash['instance']
1254
+ counter +=1
1255
+ end
1256
+
1257
+ File.open(instance_file, 'w') {|f| f.write(instance_map.to_json)}
1258
+ crashes
1259
+ rescue => e
1260
+ error "Problem executing command, #{e}"
1261
+ end
1262
+
1263
+ def map(appname=nil, url=nil)
1264
+ error "Application name and url required, vmc map <appname> <url>." unless appname && url
1265
+
1266
+ check_for_token
1267
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1268
+ error "Application #{appname} does not exist, use push first." if response.status != 200
1269
+
1270
+ appinfo = JSON.parse(response.content)
1271
+
1272
+ appinfo['uris'] << url
1273
+
1274
+ #display JSON.pretty_generate(appinfo)
1275
+ response = update_app_state_internal @droplets_uri, appname, appinfo, auth_hdr
1276
+ handle_response(response)
1277
+ display "Map completed."
1278
+ rescue => e
1279
+ error "Problem executing command, #{e}"
1280
+ end
1281
+
1282
+ def unmap(appname=nil, url=nil)
1283
+ error "Application name and url required, vmc unmap <appname> <url>." unless appname && url
1284
+
1285
+ check_for_token
1286
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1287
+ handle_response(response)
1288
+
1289
+ url = url.gsub(/^http(s*):\/\//i, '')
1290
+ appinfo = JSON.parse(response.content)
1291
+
1292
+ if appinfo['uris'].delete(url) == nil
1293
+ error "Error: You can only unmap a previously registered URL."
1294
+ end
1295
+
1296
+ #display JSON.pretty_generate(appinfo)
1297
+ update_app_state_internal @droplets_uri, appname, appinfo, auth_hdr
1298
+
1299
+ display "Unmap completed."
1300
+ rescue => e
1301
+ error "Problem executing command, #{e}"
1302
+ end
1303
+
1304
+ def files(appname=nil, path=nil, instance=nil)
1305
+ path ||= '/'
1306
+ error "Application name required, vmc files <appname> <pathinfo> [--instance <instance>]." unless appname
1307
+ check_for_token
1308
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1309
+ if response.status != 200
1310
+ display"Application #{appname} does not exist, use push first."
1311
+ return
1312
+ end
1313
+ instance ||= '0'
1314
+
1315
+ begin
1316
+ map = File.open(instance_file) {|f| JSON.parse(f.read)}
1317
+ instance = map[instance] if map[instance]
1318
+ rescue
1319
+ end
1320
+
1321
+ response = get_app_files_internal @droplets_uri, appname, instance, path, auth_hdr
1322
+ if response.status != 200 && response.status == 400
1323
+ error "Information not available, either pathinfo is incorrect or instance index is out of bounds."
1324
+ end
1325
+ display response.content
1326
+ rescue => e
1327
+ error "Problem executing command, #{e}"
1328
+ end
1329
+
1330
+ def log_file_paths
1331
+ %w[logs/stderr.log logs/stdout.log logs/startup.log]
1332
+ end
1333
+
1334
+ def grab_logs(appname, instance)
1335
+ display "No information available" and return unless appname
1336
+ instance ||= '0'
1337
+ log_file_paths.each do |path|
1338
+ response = get_app_files_internal @droplets_uri, appname, instance, path, auth_hdr
1339
+ unless response.status != 200 || response.content.empty?
1340
+ display "==== #{path} ====\n\n"
1341
+ display response.content
1342
+ display ''
1343
+ end
1344
+ end
1345
+ end
1346
+
1347
+ def grab_crash_logs(appname, instance, was_staged=false)
1348
+ display "No information available" and return unless appname
1349
+
1350
+ # stage crash info
1351
+ crashes(appname, false) unless was_staged
1352
+
1353
+ instance ||= '0'
1354
+ begin
1355
+ map = File.open(instance_file) {|f| JSON.parse(f.read)}
1356
+ instance = map[instance] if map[instance]
1357
+ rescue
1358
+ end
1359
+
1360
+ ['/logs/err.log', 'logs/stderr.log', 'logs/stdout.log', 'logs/startup.log'].each do |path|
1361
+ response = get_app_files_internal @droplets_uri, appname, instance, path, auth_hdr
1362
+ unless response.status != 200 || response.content.empty?
1363
+ display "==== #{path} ====\n\n"
1364
+ display response.content
1365
+ display ''
1366
+ end
1367
+ end
1368
+ end
1369
+
1370
+ def grab_startup_tail(appname, since = 0)
1371
+ new_lines = 0
1372
+ path = "logs/startup.log"
1373
+ response = get_app_files_internal @droplets_uri, appname, '0', path, auth_hdr
1374
+ unless response.status != 200 || response.content.empty?
1375
+ display "\n==== displaying startup log ====\n\n" if since == 0
1376
+ response_lines = response.content.split("\n")
1377
+ lines = response_lines.size
1378
+ tail = response_lines[since, lines] || []
1379
+ new_lines = tail.size
1380
+ display tail.join("\n") if new_lines > 0
1381
+ end
1382
+ since + new_lines
1383
+ end
1384
+
1385
+ def app_started_properly(appname)
1386
+ check_for_token
1387
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1388
+ if response.status != 200
1389
+ error "Application '#{appname}'s state is undetermined, not enough information available at this time."
1390
+ return
1391
+ end
1392
+
1393
+ droplet = JSON.parse(response.content)
1394
+
1395
+ case health(droplet)
1396
+ when 'N/A'
1397
+ # Health manager not running.
1398
+ error "Application '#{appname}'s state is undetermined, not enough information available at this time."
1399
+ when 'RUNNING'
1400
+ return true
1401
+ else
1402
+ return false
1403
+ end
1404
+ end
1405
+
1406
+ # Get the current logged in user
1407
+ def user
1408
+ info_json = JSON.parse(@check.content)
1409
+ username = info_json['user'] || 'N/A'
1410
+ display "[#{username}]"
1411
+ end
1412
+
1413
+ def display_services_directory_banner
1414
+ display "#{'Service'.ljust(10)} #{'Ver'.ljust(5)} #{'Description'}"
1415
+ display "#{'-------'.ljust(10)} #{'---'.ljust(5)} #{'-----------'}"
1416
+ end
1417
+
1418
+ # Get stats for application
1419
+ def stats(appname=nil)
1420
+ error "Application name required, vmc stats <appname>." unless appname
1421
+ check_for_token
1422
+ response = get_app_internal @droplets_uri, appname, auth_hdr
1423
+ if response.status != 200
1424
+ display"Application #{appname} does not exist, use push first."
1425
+ return
1426
+ end
1427
+ response = get_app_stats_internal @droplets_uri, appname, auth_hdr
1428
+ if response.status != 200 && response.status == 400
1429
+ error "Information not available, is instance index out of bounds?"
1430
+ end
1431
+
1432
+ display " #{'Instance '.ljust(10)} #{'CPU (Cores)'.ljust(15)} #{'Memory (limit)'.ljust(15)} #{'Disk (limit)'.ljust(15)} #{'Uptime '.ljust(5)}"
1433
+ display " #{'---------'.ljust(10)} #{'-----------'.ljust(15)} #{'--------------'.ljust(15)} #{'------------'.ljust(15)} #{'------ '.ljust(5)}"
1434
+
1435
+ stats = JSON.parse(response.content).to_a
1436
+
1437
+ stats.each {|entry| entry[0] = entry[0].to_i}
1438
+ stats = stats.sort {|a,b| a[0] - b[0]}
1439
+ stats.each do |entry|
1440
+ index, index_entry = entry
1441
+ stat = index_entry['stats']
1442
+ next unless stat
1443
+ hp = "#{stat['host']}:#{stat['port']}"
1444
+ uptime = uptime_string(stat['uptime'])
1445
+ usage = stat['usage']
1446
+ if usage
1447
+ cpu = usage['cpu']
1448
+ mem = (usage['mem'] * 1024) # mem comes in K's
1449
+ disk = usage['disk']
1450
+ end
1451
+
1452
+ mem_quota = stat['mem_quota']
1453
+ disk_quota = stat['disk_quota']
1454
+
1455
+ mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})"
1456
+ disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})"
1457
+ cpu = cpu ? cpu.to_s : 'NA'
1458
+ cpu = "#{cpu}% (#{stat['cores']})"
1459
+
1460
+ display " #{index.to_s.ljust(10)} #{cpu.ljust(15)} #{mem.ljust(15)} #{disk.ljust(15)} #{uptime.ljust(5)}"
1461
+
1462
+ end
1463
+ rescue => e
1464
+ error "Problem executing command, #{e}"
1465
+ end
1466
+
1467
+
1468
+ ##################################################################
1469
+ # Non vmc Commands
1470
+ ##################################################################
1471
+
1472
+ def handle_response(response)
1473
+ return unless response
1474
+ return if (response and response.status < 400)
1475
+ error "Error: Unknown error in response from server." unless response.content
1476
+ begin
1477
+ error_json = JSON.parse(response.content)
1478
+ error "Error: #{error_json['description']}"
1479
+ rescue
1480
+ response.content ||= "System error has occurred."
1481
+ error "Error: #{response.content}"
1482
+ end
1483
+ end
1484
+
1485
+ def framework_needs_db?(framework)
1486
+ return true if (framework == 'rails/1.0')
1487
+ return true if (framework == 'grails/1.0')
1488
+ return true if (framework == 'spring_web/1.0')
1489
+ return true if (framework == "asp_web/1.0")
1490
+ return false
1491
+ end
1492
+
1493
+ def uptime_string(delta)
1494
+ num_seconds = delta.to_i
1495
+ days = num_seconds / (60 * 60 * 24);
1496
+ num_seconds -= days * (60 * 60 * 24);
1497
+ hours = num_seconds / (60 * 60);
1498
+ num_seconds -= hours * (60 * 60);
1499
+ minutes = num_seconds / 60;
1500
+ num_seconds -= minutes * 60;
1501
+ "#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s"
1502
+ end
1503
+
1504
+ def pretty_size(size, prec=1)
1505
+ return 'NA' unless size
1506
+ return "#{size}B" if size < 1024
1507
+ return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024)
1508
+ return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024)
1509
+ return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0))
1510
+ end
1511
+
1512
+ def display(msg, nl=true)
1513
+ if nl
1514
+ puts(msg)
1515
+ else
1516
+ print(msg)
1517
+ STDOUT.flush
1518
+ end
1519
+ end
1520
+
1521
+ def error(msg)
1522
+ STDERR.puts(msg)
1523
+ STDERR.puts('')
1524
+ exit 1
1525
+ end
1526
+
1527
+ def check_app_limit
1528
+ info_json = JSON.parse(@check.content)
1529
+ usage = info_json['usage']
1530
+ limits = info_json['limits']
1531
+ return unless limits['apps']
1532
+ if limits['apps'] == usage['apps']
1533
+ display "Not enough capacity for operation."
1534
+ tapps = limits['apps'] || 0
1535
+ apps = usage['apps'] || 0
1536
+ error "Current Usage: (#{apps} of #{tapps} total apps already in use)"
1537
+ end
1538
+ end
1539
+
1540
+ def check_has_capacity_for(mem_wanted)
1541
+ info_json = JSON.parse(@check.content)
1542
+ usage = info_json['usage']
1543
+ limits = info_json['limits']
1544
+ available_for_use = limits['memory'].to_i - usage['memory'].to_i
1545
+ if mem_wanted > available_for_use
1546
+ info_json = JSON.parse(@check.content)
1547
+ tmem = pretty_size(limits['memory']*1024*1024)
1548
+ mem = pretty_size(usage['memory']*1024*1024)
1549
+ display "Not enough capacity for operation."
1550
+ available = pretty_size(available_for_use * 1024 * 1024)
1551
+ error "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
1552
+ end
1553
+ end
1554
+
1555
+ def mem_choices
1556
+ default = ['64M', '128M', '256M', '512M', '1G', '2G']
1557
+ info_json = JSON.parse(@check.content)
1558
+
1559
+ default unless info_json
1560
+ default unless (usage = info_json['usage'] and limits = info_json['limits'])
1561
+
1562
+ available_for_use = limits['memory'].to_i - usage['memory'].to_i
1563
+ check_has_capacity_for(64) if available_for_use < 64
1564
+ return ['64M'] if available_for_use < 128
1565
+ return ['64M', '128M'] if available_for_use < 256
1566
+ return ['64M', '128M', '256M'] if available_for_use < 512
1567
+ return ['64M', '128M', '256M', '512M'] if available_for_use < 1024
1568
+ return ['64M', '128M', '256M', '512M', '1G'] if available_for_use < 2048
1569
+ return ['64M', '128M', '256M', '512M', '1G', '2G']
1570
+ end
1571
+
1572
+ def mem_choice_to_quota(mem_choice)
1573
+ (mem_choice =~ /(\d+)M/i) ? mem_quota = $1.to_i : mem_quota = mem_choice.to_i * 1024
1574
+ mem_quota
1575
+ end
1576
+
1577
+ def mem_quota_to_choice(mem)
1578
+ if mem < 1024
1579
+ mem_choice = "#{mem}M"
1580
+ else
1581
+ mem_choice = "#{(mem/1024).to_i}G"
1582
+ end
1583
+ mem_choice
1584
+ end
1585
+
1586
+ end
1587
+
1588
+ end