vmc 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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