lib/cli/commands/apps.rb in af-0.3.13.beta.5 vs lib/cli/commands/apps.rb in af-0.3.16.1

- old
+ new

@@ -1,15 +1,21 @@ require 'digest/sha1' require 'fileutils' +require 'pathname' require 'tempfile' require 'tmpdir' require 'set' +require "uuidtools" +require 'socket' module VMC::Cli::Command class Apps < Base include VMC::Cli::ServicesHelper + include VMC::Cli::ManifestHelper + include VMC::Cli::TunnelHelper + include VMC::Cli::ConsoleHelper def list apps = client.apps apps.sort! {|a, b| a[:name] <=> b[:name] } return display JSON.pretty_generate(apps || []) if @options[:json] @@ -34,121 +40,91 @@ # Numerators are in secs TICKER_TICKS = 25/SLEEP_TIME HEALTH_TICKS = 5/SLEEP_TIME TAIL_TICKS = 45/SLEEP_TIME GIVEUP_TICKS = 120/SLEEP_TIME - YES_SET = Set.new(["y", "Y", "yes", "YES"]) - def start(appname, push = false) - app = client.app_info(appname) + def info(what, default=nil) + @options[what] || (@app_info && @app_info[what.to_s]) || default + end - return display "Application '#{appname}' could not be found".red if app.nil? - return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED' + def console(appname, interactive=true) + unless defined? Caldecott + display "To use `vmc rails-console', you must first install Caldecott:" + display "" + display "\tgem install caldecott" + display "" + display "Note that you'll need a C compiler. If you're on OS X, Xcode" + display "will provide one. If you're on Windows, try DevKit." + display "" + display "This manual step will be removed in the future." + display "" + err "Caldecott is not installed." + end - if @options[:debug] - runtimes = client.runtimes_info - return display "Cannot get runtime information." unless runtimes + #Make sure there is a console we can connect to first + conn_info = console_connection_info appname - runtime = runtimes[app[:staging][:stack].to_sym] - return display "Unknown runtime." unless runtime + port = pick_tunnel_port(@options[:port] || 20000) - unless runtime[:debug_modes] and runtime[:debug_modes].include? @options[:debug] - modes = runtime[:debug_modes] || [] + raise VMC::Client::AuthError unless client.logged_in? - display "\nApplication '#{appname}' cannot start in '#{@options[:debug]}' mode" + if not tunnel_pushed? + display "Deploying tunnel application '#{tunnel_appname}'." + auth = UUIDTools::UUID.random_create.to_s + push_caldecott(auth) + start_caldecott + else + auth = tunnel_auth + end - if push - display "Try `vmc start' with one of the following modes: #{modes.inspect}" - else - display "Available modes: #{modes.inspect}" - end - - return - end + if not tunnel_healthy?(auth) + display "Redeploying tunnel application '#{tunnel_appname}'." + # We don't expect caldecott not to be running, so take the + # most aggressive restart method.. delete/re-push + client.delete_app(tunnel_appname) + invalidate_tunnel_app_info + push_caldecott(auth) + start_caldecott end - banner = 'Staging Application: ' - display banner, false + start_tunnel(port, conn_info, auth) + wait_for_tunnel_start(port) + start_local_console(port, appname) if interactive + port + end - t = Thread.new do - count = 0 - while count < TAIL_TICKS do - display '.', false - sleep SLEEP_TIME - count += 1 + def start(appname=nil, push=false) + if appname + do_start(appname, push) + else + each_app do |name| + do_start(name, push) end end + end - app[:state] = 'STARTED' - app[:debug] = @options[:debug] - client.update_app(appname, app) + def stop(appname=nil) + if appname + do_stop(appname) + else + reversed = [] + each_app do |name| + reversed.unshift name + end - Thread.kill(t) - clear(LINE_LENGTH) - display "#{banner}#{'OK'.green}" - - banner = 'Starting Application: ' - display banner, false - - count = log_lines_displayed = 0 - failed = false - start_time = Time.now.to_i - - loop do - display '.', false unless count > TICKER_TICKS - sleep SLEEP_TIME - begin - break if app_started_properly(appname, count > HEALTH_TICKS) - if !crashes(appname, false, start_time).empty? - # Check for the existance of crashes - display "\nError: Application [#{appname}] failed to start, logs information below.\n".red - grab_crash_logs(appname, '0', true) - if push and !no_prompt - display "\n" - delete_app(appname, false) if ask "Delete the application?", :default => true - end - failed = true - break - elsif count > TAIL_TICKS - log_lines_displayed = grab_startup_tail(appname, log_lines_displayed) - end - rescue => e - err(e.message, '') + reversed.each do |name| + do_stop(name) end - count += 1 - if count > GIVEUP_TICKS # 2 minutes - display "\nApplication is taking too long to start, check your logs".yellow - break - end end - exit(false) if failed - clear(LINE_LENGTH) - display "#{banner}#{'OK'.green}" end - def stop(appname) - app = client.app_info(appname) - return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED' - display 'Stopping Application: ', false - app[:state] = 'STOPPED' - client.update_app(appname, app) - display 'OK'.green - end - - def restart(appname) + def restart(appname=nil) stop(appname) start(appname) end - def rename(appname, newname) - app = client.app_info(appname) - app[:name] = newname - display 'Renaming Appliction: ' - client.update_app(newname, app) - display 'OK'.green - end - def mem(appname, memsize=nil) app = client.app_info(appname) mem = current_mem = mem_quota_to_choice(app[:resources][:memory]) memsize = normalize_mem(memsize) if memsize @@ -183,93 +159,49 @@ app = client.app_info(appname) uris = app[:uris] || [] uris << url app[:uris] = uris client.update_app(appname, app) - display "Succesfully mapped url".green + display "Successfully mapped url".green end def unmap(appname, url) app = client.app_info(appname) uris = app[:uris] || [] url = url.gsub(/^http(s*):\/\//i, '') deleted = uris.delete(url) err "Invalid url" unless deleted app[:uris] = uris client.update_app(appname, app) - display "Succesfully unmapped url".green - + display "Successfully unmapped url".green end def delete(appname=nil) force = @options[:force] if @options[:all] - if no_prompt || force || ask("Delete ALL applications and services?", :default => false) + if no_prompt || force || ask("Delete ALL applications?", :default => false) apps = client.apps apps.each { |app| delete_app(app[:name], force) } end else err 'No valid appname given' unless appname delete_app(appname, force) end end - def delete_app(appname, force) - app = client.app_info(appname) - services_to_delete = [] - app_services = app[:services] - services_apps_hash = provisioned_services_apps_hash - app_services.each { |service| - del_service = force && no_prompt - unless no_prompt || force - del_service = ask( - "Provisioned service [#{service}] detected, would you like to delete it?", - :default => false - ) - - if del_service - apps_using_service = services_apps_hash[service].reject!{ |app| app == appname} - if apps_using_service.size > 0 - del_service = ask( - "Provisioned service [#{service}] is also used by #{apps_using_service.size == 1 ? "app" : "apps"} #{apps_using_service.entries}, are you sure you want to delete it?", - :default => false - ) - end - end - end - services_to_delete << service if del_service - } - - display "Deleting application [#{appname}]: ", false - client.delete_app(appname) - display 'OK'.green - - services_to_delete.each do |s| - delete_service_banner(s) - end - end - - def all_files(appname, path) - instances_info_envelope = client.app_instances(appname) - return if instances_info_envelope.is_a?(Array) - instances_info = instances_info_envelope[:instances] || [] - instances_info.each do |entry| - content = client.app_files(appname, path, entry[:index]) - display_logfile(path, content, entry[:index], "====> [#{entry[:index]}: #{path}] <====\n".bold) - end - end - def files(appname, path='/') return all_files(appname, path) if @options[:all] && !@options[:instance] instance = @options[:instance] || '0' content = client.app_files(appname, path, instance) display content - rescue VMC::Client::NotFound => e + rescue VMC::Client::NotFound, VMC::Client::TargetError err 'No such file or directory' end def logs(appname) + # Check if we have an app before progressing further + client.app_info(appname) return grab_all_logs(appname) if @options[:all] && !@options[:instance] instance = @options[:instance] || '0' grab_logs(appname, instance) end @@ -312,199 +244,71 @@ instance = @options[:instance] || '0' grab_crash_logs(appname, instance) end def instances(appname, num=nil) - if (num) + if num change_instances(appname, num) else get_instances(appname) end end - def stats(appname) - stats = client.app_stats(appname) - return display JSON.pretty_generate(stats) if @options[:json] - - stats_table = table do |t| - t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime' - stats.each do |entry| - index = entry[:instance] - stat = entry[:stats] - hp = "#{stat[:host]}:#{stat[:port]}" - uptime = uptime_string(stat[:uptime]) - usage = stat[:usage] - if usage - cpu = usage[:cpu] - mem = (usage[:mem] * 1024) # mem comes in K's - disk = usage[:disk] - end - mem_quota = stat[:mem_quota] - disk_quota = stat[:disk_quota] - mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})" - disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})" - cpu = cpu ? cpu.to_s : 'NA' - cpu = "#{cpu}% (#{stat[:cores]})" - t << [index, cpu, mem, disk, uptime] + def stats(appname=nil) + if appname + display "\n", false + do_stats(appname) + else + each_app do |n| + display "\n#{n}:" + do_stats(n) end end - display "\n" - if stats.empty? - display "No running instances for [#{appname}]".yellow - else - display stats_table - end end - def update(appname) - app = client.app_info(appname) - if @options[:canary] - display "[--canary] is deprecated and will be removed in a future version".yellow - end - path = @options[:path] || '.' - upload_app_bits(appname, path) - restart appname if app[:state] == 'STARTED' - end - - def push(appname=nil) - instances = @options[:instances] || 1 - exec = @options[:exec] || 'thin start' - ignore_framework = @options[:noframework] - no_start = @options[:nostart] - - path = @options[:path] || '.' - appname ||= @options[:name] - mem, memswitch = nil, @options[:mem] - memswitch = normalize_mem(memswitch) if memswitch - url = @options[:url] - - # Check app existing upfront if we have appname - app_checked = false + def update(appname=nil) if appname - err "Application '#{appname}' already exists, use update" if app_exists?(appname) - app_checked = true + app = client.app_info(appname) + if @options[:canary] + display "[--canary] is deprecated and will be removed in a future version".yellow + end + upload_app_bits(appname, @path) + restart appname if app[:state] == 'STARTED' else - raise VMC::Client::AuthError unless client.logged_in? - end + each_app do |name| + display "Updating application '#{name}'..." - # check if we have hit our app limit - check_app_limit - - # check memsize here for capacity - if memswitch && !no_start - check_has_capacity_for(mem_choice_to_quota(memswitch) * instances) - end - - unless no_prompt || @options[:path] - unless ask('Would you like to deploy from the current directory?', :default => true) - path = ask('Please enter in the deployment path') + app = client.app_info(name) + upload_app_bits(name, @application) + restart name if app[:state] == 'STARTED' end end + end - path = File.expand_path(path) - check_deploy_directory(path) - - appname ||= ask("Application Name") unless no_prompt - err "Application Name required." if appname.nil? || appname.empty? - - if !app_checked and app_exists?(appname) - err "Application '#{appname}' already exists, use update or delete." - end - - default_url = "#{appname}.#{VMC::Cli::Config.suggest_url}" - - unless no_prompt || url - url = ask( - "Application Deployed URL", - :default => default_url + def push(appname=nil) + unless no_prompt || @options[:path] + proceed = ask( + 'Would you like to deploy from the current directory?', + :default => true ) - # common error case is for prompted users to answer y or Y or yes or - # YES to this ask() resulting in an unintended URL of y. Special case - # this common error - url = nil if YES_SET.member? url - end - - url ||= default_url - - # Detect the appropriate framework. - framework = nil - unless ignore_framework - framework = VMC::Cli::Framework.detect(path) - - if prompt_ok and framework - framework_correct = - ask("Detected a #{framework}, is this correct?", :default => true) + unless proceed + @path = ask('Deployment path') end - - if prompt_ok && (framework.nil? || !framework_correct) - display "#{"[WARNING]".yellow} Can't determine the Application Type." unless framework - framework = VMC::Cli::Framework.lookup( - ask( - "Select Application Type", - { :indexed => true, - :choices => VMC::Cli::Framework.known_frameworks - } - ) - ) - display "Selected #{framework}" - end - # Framework override, deprecated - exec = framework.exec if framework && framework.exec - else - framework = VMC::Cli::Framework.new end - err "Application Type undetermined for path '#{path}'" unless framework - - if memswitch - mem = memswitch - elsif prompt_ok - mem = ask("Memory Reservation", - :default => framework.memory, :choices => mem_choices) - else - mem = framework.memory + pushed = false + each_app(false) do |name| + display "Pushing application '#{name}'..." if name + do_push(name) + pushed = true end - # Set to MB number - mem_quota = mem_choice_to_quota(mem) - - # check memsize here for capacity - check_has_capacity_for(mem_quota * instances) unless no_start - - display 'Creating Application: ', false - - manifest = { - :name => "#{appname}", - :staging => { - :framework => framework.name, - :runtime => @options[:runtime] - }, - :uris => [url], - :instances => instances, - :resources => { - :memory => mem_quota - }, - } - - # Send the manifest to the cloud controller - client.create_app(appname, manifest) - display 'OK'.green - - # Services check - unless no_prompt || @options[:noservices] - services = client.services_info - unless services.empty? - proceed = ask("Would you like to bind any services to '#{appname}'?", :default => false) - bind_services(appname, services) if proceed - end + unless pushed + @application = @path + do_push(appname) end - - # Stage and upload the app bits. - upload_app_bits(appname, path) - - start(appname, true) unless no_start end def environment(appname) app = client.app_info(appname) env = app[:env] || [] @@ -565,185 +369,156 @@ false end def check_deploy_directory(path) err 'Deployment path does not exist' unless File.exists? path - err 'Deployment path is not a directory' unless File.directory? path return if File.expand_path(Dir.tmpdir) != File.expand_path(path) err "Can't deploy applications from staging directory: [#{Dir.tmpdir}]" end + def check_unreachable_links(path) + files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH) + + pwd = Pathname.pwd + + abspath = File.expand_path(path) + unreachable = [] + files.each do |f| + file = Pathname.new(f) + if file.symlink? && !file.realpath.to_s.start_with?(abspath) + unreachable << file.relative_path_from(pwd) + end + end + + unless unreachable.empty? + root = Pathname.new(path).relative_path_from(pwd) + err "Can't deploy application containing links '#{unreachable}' that reach outside its root '#{root}'" + end + end + + def find_sockets(path) + files = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH) + files && files.select { |f| File.socket? f } + end + def upload_app_bits(appname, path) display 'Uploading Application:' upload_file, file = "#{Dir.tmpdir}/#{appname}.zip", nil FileUtils.rm_f(upload_file) explode_dir = "#{Dir.tmpdir}/.vmc_#{appname}_files" FileUtils.rm_rf(explode_dir) # Make sure we didn't have anything left over.. - Dir.chdir(path) do - # Stage the app appropriately and do the appropriate fingerprinting, etc. - if war_file = Dir.glob('*.war').first - VMC::Cli::ZipUtil.unpack(war_file, explode_dir) - else - FileUtils.mkdir(explode_dir) - files = Dir.glob('{*,.[^\.]*}') - # Do not process .git files - files.delete('.git') if files - FileUtils.cp_r(files, explode_dir) - end + if path =~ /\.(war|zip)$/ + #single file that needs unpacking + VMC::Cli::ZipUtil.unpack(path, explode_dir) + elsif !File.directory? path + #single file that doesn't need unpacking + FileUtils.mkdir(explode_dir) + FileUtils.cp(path,explode_dir) + else + Dir.chdir(path) do + # Stage the app appropriately and do the appropriate fingerprinting, etc. + if war_file = Dir.glob('*.war').first + VMC::Cli::ZipUtil.unpack(war_file, explode_dir) + elsif zip_file = Dir.glob('*.zip').first + VMC::Cli::ZipUtil.unpack(zip_file, explode_dir) + else + check_unreachable_links(path) + FileUtils.mkdir(explode_dir) - # Send the resource list to the cloudcontroller, the response will tell us what it already has.. - unless @options[:noresources] - display ' Checking for available resources: ', false - fingerprints = [] - total_size = 0 - resource_files = Dir.glob("#{explode_dir}/**/*", File::FNM_DOTMATCH) - resource_files.each do |filename| - next if (File.directory?(filename) || !File.exists?(filename)) - fingerprints << { - :size => File.size(filename), - :sha1 => Digest::SHA1.file(filename).hexdigest, - :fn => filename - } - total_size += File.size(filename) - end + files = Dir.glob('{*,.[^\.]*}') - # Check to see if the resource check is worth the round trip - if (total_size > (64*1024)) # 64k for now - # Send resource fingerprints to the cloud controller - appcloud_resources = client.check_resources(fingerprints) - end - display 'OK'.green + # Do not process .git files + files.delete('.git') if files - if appcloud_resources - display ' Processing resources: ', false - # We can then delete what we do not need to send. - appcloud_resources.each do |resource| - FileUtils.rm_f resource[:fn] - # adjust filenames sans the explode_dir prefix - resource[:fn].sub!("#{explode_dir}/", '') + FileUtils.cp_r(files, explode_dir) + + find_sockets(explode_dir).each do |s| + File.delete s end - display 'OK'.green end + end + end + # Send the resource list to the cloudcontroller, the response will tell us what it already has.. + unless @options[:noresources] + display ' Checking for available resources: ', false + fingerprints = [] + total_size = 0 + resource_files = Dir.glob("#{explode_dir}/**/*", File::FNM_DOTMATCH) + resource_files.each do |filename| + next if (File.directory?(filename) || !File.exists?(filename)) + fingerprints << { + :size => File.size(filename), + :sha1 => Digest::SHA1.file(filename).hexdigest, + :fn => filename + } + total_size += File.size(filename) end - # If no resource needs to be sent, add an empty file to ensure we have - # a multi-part request that is expected by nginx fronting the CC. - if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty? - Dir.chdir(explode_dir) do - File.new(".__empty__", "w") - end + # Check to see if the resource check is worth the round trip + if (total_size > (64*1024)) # 64k for now + # Send resource fingerprints to the cloud controller + appcloud_resources = client.check_resources(fingerprints) end - # Perform Packing of the upload bits here. - display ' Packing application: ', false - VMC::Cli::ZipUtil.pack(explode_dir, upload_file) display 'OK'.green - upload_size = File.size(upload_file); - if upload_size > 1024*1024 - upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M' - elsif upload_size > 0 - upload_size = (upload_size/1024.0).round.to_s + 'K' - else - upload_size = '0K' + if appcloud_resources + display ' Processing resources: ', false + # We can then delete what we do not need to send. + appcloud_resources.each do |resource| + FileUtils.rm_f resource[:fn] + # adjust filenames sans the explode_dir prefix + resource[:fn].sub!("#{explode_dir}/", '') + end + display 'OK'.green end - upload_str = " Uploading (#{upload_size}): " - display upload_str, false + end - FileWithPercentOutput.display_str = upload_str - FileWithPercentOutput.upload_size = File.size(upload_file); - file = FileWithPercentOutput.open(upload_file, 'rb') + # If no resource needs to be sent, add an empty file to ensure we have + # a multi-part request that is expected by nginx fronting the CC. + if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty? + Dir.chdir(explode_dir) do + File.new(".__empty__", "w") + end + end + # Perform Packing of the upload bits here. + display ' Packing application: ', false + VMC::Cli::ZipUtil.pack(explode_dir, upload_file) + display 'OK'.green - client.upload_app(appname, file, appcloud_resources) - display 'OK'.green if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty? - - display 'Push Status: ', false - display 'OK'.green + upload_size = File.size(upload_file); + if upload_size > 1024*1024 + upload_size = (upload_size/(1024.0*1024.0)).round.to_s + 'M' + elsif upload_size > 0 + upload_size = (upload_size/1024.0).round.to_s + 'K' + else + upload_size = '0K' end - ensure - # Cleanup if we created an exploded directory. - FileUtils.rm_f(upload_file) if upload_file - FileUtils.rm_rf(explode_dir) if explode_dir - end + upload_str = " Uploading (#{upload_size}): " + display upload_str, false - def choose_existing_service(appname, user_services) - return unless prompt_ok + FileWithPercentOutput.display_str = upload_str + FileWithPercentOutput.upload_size = File.size(upload_file); + file = FileWithPercentOutput.open(upload_file, 'rb') - display "The following provisioned services are available" - name = ask( - "Please select one you which to prevision", - { :indexed => true, - :choices => user_services.collect { |s| s[:name] } - } - ) + client.upload_app(appname, file, appcloud_resources) + display 'OK'.green if VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty? - bind_service_banner(name, appname, false) + display 'Push Status: ', false + display 'OK'.green - true + ensure + # Cleanup if we created an exploded directory. + FileUtils.rm_f(upload_file) if upload_file + FileUtils.rm_rf(explode_dir) if explode_dir end - def choose_new_service(appname, services) - return unless prompt_ok - - display "The following system services are available" - - vendor = ask( - "Please select one you wish to provision", - { :indexed => true, - :choices => - services.values.collect { |type| - type.keys.collect(&:to_s) - }.flatten.sort! - } - ) - - default_name = random_service_name(vendor) - service_name = ask("Specify the name of the service", - :default => default_name) - - create_service_banner(vendor, service_name) - bind_service_banner(service_name, appname) - end - - def bind_services(appname, services) - user_services = client.services - - selected_existing = false - unless no_prompt || user_services.empty? - if ask("Would you like to use an existing provisioned service?", - :default => false) - selected_existing = choose_existing_service(appname, user_services) - end - end - - # Create a new service and bind it here - unless selected_existing - choose_new_service(appname, services) - end - end - - def provisioned_services_apps_hash - apps = client.apps - services_apps_hash = {} - apps.each {|app| - app[:services].each { |svc| - svc_apps = services_apps_hash[svc] - unless svc_apps - svc_apps = Set.new - services_apps_hash[svc] = svc_apps - end - svc_apps.add(app[:name]) - } unless app[:services] == nil - } - services_apps_hash - end - def check_app_limit usage = client_info[:usage] limits = client_info[:limits] return unless usage and limits and limits[:apps] if limits[:apps] == usage[:apps] @@ -894,11 +669,12 @@ end end def display_logfile(path, content, instance='0', banner=nil) banner ||= "====> #{path} <====\n\n" - if content && !content.empty? + + unless content.empty? display banner prefix = "[#{instance}: #{path}] -".bold if @options[:prefixlogs] unless prefix display content else @@ -907,46 +683,49 @@ end display '' end end - def log_file_paths - %w[logs/stderr.log logs/stdout.log logs/startup.log] - end - def grab_all_logs(appname) instances_info_envelope = client.app_instances(appname) return if instances_info_envelope.is_a?(Array) instances_info = instances_info_envelope[:instances] || [] instances_info.each do |entry| grab_logs(appname, entry[:index]) end end def grab_logs(appname, instance) - log_file_paths.each do |path| + files_under(appname, instance, "/logs").each do |path| begin content = client.app_files(appname, path, instance) - rescue + display_logfile(path, content, instance) + rescue VMC::Client::NotFound, VMC::Client::TargetError end - display_logfile(path, content, instance) end end + def files_under(appname, instance, path) + client.app_files(appname, path, instance).split("\n").collect do |l| + "#{path}/#{l.split[0]}" + end + rescue VMC::Client::NotFound, VMC::Client::TargetError + [] + end + def grab_crash_logs(appname, instance, was_staged=false) # stage crash info crashes(appname, false) unless was_staged instance ||= '0' map = VMC::Cli::Config.instances instance = map[instance] if map[instance] - ['/logs/err.log', '/logs/staging.log', 'logs/stderr.log', 'logs/stdout.log', 'logs/startup.log'].each do |path| - begin - content = client.app_files(appname, path, instance) - rescue - end + (files_under(appname, instance, "/logs") + + files_under(appname, instance, "/app/logs") + + files_under(appname, instance, "/app/log")).each do |path| + content = client.app_files(appname, path, instance) display_logfile(path, content, instance) end end def grab_startup_tail(appname, since = 0) @@ -960,11 +739,358 @@ tail = response_lines[since, lines] || [] new_lines = tail.size display tail.join("\n") if new_lines > 0 end since + new_lines + rescue VMC::Client::NotFound, VMC::Client::TargetError + 0 end - rescue + + def provisioned_services_apps_hash + apps = client.apps + services_apps_hash = {} + apps.each {|app| + app[:services].each { |svc| + svc_apps = services_apps_hash[svc] + unless svc_apps + svc_apps = Set.new + services_apps_hash[svc] = svc_apps + end + svc_apps.add(app[:name]) + } unless app[:services] == nil + } + services_apps_hash + end + + def delete_app(appname, force) + app = client.app_info(appname) + services_to_delete = [] + app_services = app[:services] + services_apps_hash = provisioned_services_apps_hash + app_services.each { |service| + del_service = force && no_prompt + unless no_prompt || force + del_service = ask( + "Provisioned service [#{service}] detected, would you like to delete it?", + :default => false + ) + + if del_service + apps_using_service = services_apps_hash[service].reject!{ |app| app == appname} + if apps_using_service.size > 0 + del_service = ask( + "Provisioned service [#{service}] is also used by #{apps_using_service.size == 1 ? "app" : "apps"} #{apps_using_service.entries}, are you sure you want to delete it?", + :default => false + ) + end + end + end + services_to_delete << service if del_service + } + + display "Deleting application [#{appname}]: ", false + client.delete_app(appname) + display 'OK'.green + + services_to_delete.each do |s| + delete_service_banner(s) + end + end + + def do_start(appname, push=false) + app = client.app_info(appname) + return display "Application '#{appname}' could not be found".red if app.nil? + return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED' + + + + if @options[:debug] + runtimes = client.runtimes_info + return display "Cannot get runtime information." unless runtimes + + runtime = runtimes[app[:staging][:stack].to_sym] + return display "Unknown runtime." unless runtime + + unless runtime[:debug_modes] and runtime[:debug_modes].include? @options[:debug] + modes = runtime[:debug_modes] || [] + + display "\nApplication '#{appname}' cannot start in '#{@options[:debug]}' mode" + + if push + display "Try 'vmc start' with one of the following modes: #{modes.inspect}" + else + display "Available modes: #{modes.inspect}" + end + + return + end + end + + banner = "Staging Application '#{appname}': " + display banner, false + + t = Thread.new do + count = 0 + while count < TAIL_TICKS do + display '.', false + sleep SLEEP_TIME + count += 1 + end + end + + app[:state] = 'STARTED' + app[:debug] = @options[:debug] + app[:console] = VMC::Cli::Framework.lookup_by_framework(app[:staging][:model]).console + client.update_app(appname, app) + + Thread.kill(t) + clear(LINE_LENGTH) + display "#{banner}#{'OK'.green}" + + banner = "Starting Application '#{appname}': " + display banner, false + + count = log_lines_displayed = 0 + failed = false + start_time = Time.now.to_i + + loop do + display '.', false unless count > TICKER_TICKS + sleep SLEEP_TIME + + break if app_started_properly(appname, count > HEALTH_TICKS) + + if !crashes(appname, false, start_time).empty? + # Check for the existance of crashes + display "\nError: Application [#{appname}] failed to start, logs information below.\n".red + grab_crash_logs(appname, '0', true) + if push and !no_prompt + display "\n" + delete_app(appname, false) if ask "Delete the application?", :default => true + end + failed = true + break + elsif count > TAIL_TICKS + log_lines_displayed = grab_startup_tail(appname, log_lines_displayed) + end + + count += 1 + if count > GIVEUP_TICKS # 2 minutes + display "\nApplication is taking too long to start, check your logs".yellow + break + end + end + exit(false) if failed + clear(LINE_LENGTH) + display "#{banner}#{'OK'.green}" + end + + def do_stop(appname) + app = client.app_info(appname) + return display "Application '#{appname}' already stopped".yellow if app[:state] == 'STOPPED' + display "Stopping Application '#{appname}': ", false + app[:state] = 'STOPPED' + client.update_app(appname, app) + display 'OK'.green + end + + def do_push(appname=nil) + unless @app_info || no_prompt + @manifest = { "applications" => { @path => { "name" => appname } } } + + interact + + if ask("Would you like to save this configuration?", :default => false) + save_manifest + end + + resolve_manifest(@manifest) + + @app_info = @manifest["applications"][@path] + end + + instances = info(:instances, 1) + exec = info(:exec, 'thin start') + + ignore_framework = @options[:noframework] + no_start = @options[:nostart] + + appname ||= info(:name) + url = info(:url) || info(:urls) + mem, memswitch = nil, info(:mem) + memswitch = normalize_mem(memswitch) if memswitch + command = info(:command) + runtime = info(:runtime) + + # Check app existing upfront if we have appname + app_checked = false + if appname + err "Application '#{appname}' already exists, use update" if app_exists?(appname) + app_checked = true + else + raise VMC::Client::AuthError unless client.logged_in? + end + + # check if we have hit our app limit + check_app_limit + # check memsize here for capacity + if memswitch && !no_start + check_has_capacity_for(mem_choice_to_quota(memswitch) * instances) + end + + appname ||= ask("Application Name") unless no_prompt + err "Application Name required." if appname.nil? || appname.empty? + + check_deploy_directory(@application) + + if !app_checked and app_exists?(appname) + err "Application '#{appname}' already exists, use update or delete." + end + + if ignore_framework + framework = VMC::Cli::Framework.new + elsif f = info(:framework) + info = Hash[f["info"].collect { |k, v| [k.to_sym, v] }] + + framework = VMC::Cli::Framework.create(f["name"], info) + exec = framework.exec if framework && framework.exec + else + framework = detect_framework(prompt_ok) + end + + err "Application Type undetermined for path '#{@application}'" unless framework + + if not runtime + default_runtime = framework.default_runtime @application + runtime = detect_runtime(default_runtime, !no_prompt) if framework.prompt_for_runtime? + end + command = ask("Start Command") if !command && framework.require_start_command? + + default_url = "None" + default_url = "#{appname}.#{VMC::Cli::Config.suggest_url}" if framework.require_url? + + + unless no_prompt || url || !framework.require_url? + url = ask( + "Application Deployed URL", + :default => default_url + ) + + # common error case is for prompted users to answer y or Y or yes or + # YES to this ask() resulting in an unintended URL of y. Special case + # this common error + url = nil if YES_SET.member? url + end + url = nil if url == "None" + default_url = nil if default_url == "None" + url ||= default_url + + if memswitch + mem = memswitch + elsif prompt_ok + mem = ask("Memory Reservation", + :default => framework.memory(runtime), + :choices => mem_choices) + else + mem = framework.memory runtime + end + + # Set to MB number + mem_quota = mem_choice_to_quota(mem) + + # check memsize here for capacity + check_has_capacity_for(mem_quota * instances) unless no_start + + display 'Creating Application: ', false + + manifest = { + :name => "#{appname}", + :staging => { + :framework => framework.name, + :runtime => runtime + }, + :uris => Array(url), + :instances => instances, + :resources => { + :memory => mem_quota + } + } + manifest[:staging][:command] = command if command + + # Send the manifest to the cloud controller + client.create_app(appname, manifest) + display 'OK'.green + + + existing = Set.new(client.services.collect { |s| s[:name] }) + + if @app_info && services = @app_info["services"] + services.each do |name, info| + unless existing.include? name + create_service_banner(info["type"], name, true) + end + + bind_service_banner(name, appname) + end + end + + # Stage and upload the app bits. + upload_app_bits(appname, @application) + + start(appname, true) unless no_start + end + + def do_stats(appname) + stats = client.app_stats(appname) + return display JSON.pretty_generate(stats) if @options[:json] + + stats_table = table do |t| + t.headings = 'Instance', 'CPU (Cores)', 'Memory (limit)', 'Disk (limit)', 'Uptime' + stats.each do |entry| + index = entry[:instance] + stat = entry[:stats] + hp = "#{stat[:host]}:#{stat[:port]}" + uptime = uptime_string(stat[:uptime]) + usage = stat[:usage] + if usage + cpu = usage[:cpu] + mem = (usage[:mem] * 1024) # mem comes in K's + disk = usage[:disk] + end + mem_quota = stat[:mem_quota] + disk_quota = stat[:disk_quota] + mem = "#{pretty_size(mem)} (#{pretty_size(mem_quota, 0)})" + disk = "#{pretty_size(disk)} (#{pretty_size(disk_quota, 0)})" + cpu = cpu ? cpu.to_s : 'NA' + cpu = "#{cpu}% (#{stat[:cores]})" + t << [index, cpu, mem, disk, uptime] + end + end + + if stats.empty? + display "No running instances for [#{appname}]".yellow + else + display stats_table + end + end + + def all_files(appname, path) + instances_info_envelope = client.app_instances(appname) + return if instances_info_envelope.is_a?(Array) + instances_info = instances_info_envelope[:instances] || [] + instances_info.each do |entry| + begin + content = client.app_files(appname, path, entry[:index]) + display_logfile( + path, + content, + entry[:index], + "====> [#{entry[:index]}: #{path}] <====\n".bold + ) + rescue VMC::Client::NotFound, VMC::Client::TargetError + end + end + end end class FileWithPercentOutput < ::File class << self attr_accessor :display_str, :upload_size