Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit dbe336aab837dc000dd68523212d8e307c63b8e3 0 parents
Cloud Foundry Engineer authored
Showing with 3,662 additions and 0 deletions.
  1. +3 −0  .gitignore
  2. +4 −0 Gemfile
  3. +24 −0 LICENSE
  4. +92 −0 README.md
  5. +17 −0 Rakefile
  6. +15 −0 TODO
  7. +6 −0 bin/vmc
  8. +30 −0 lib/cli.rb
  9. +57 −0 lib/cli/commands/admin.rb
  10. +937 −0 lib/cli/commands/apps.rb
  11. +79 −0 lib/cli/commands/base.rb
  12. +128 −0 lib/cli/commands/misc.rb
  13. +84 −0 lib/cli/commands/services.rb
  14. +60 −0 lib/cli/commands/user.rb
  15. +110 −0 lib/cli/config.rb
  16. +119 −0 lib/cli/core_ext.rb
  17. +19 −0 lib/cli/errors.rb
  18. +97 −0 lib/cli/frameworks.rb
  19. +485 −0 lib/cli/runner.rb
  20. +74 −0 lib/cli/services_helper.rb
  21. +103 −0 lib/cli/usage.rb
  22. +7 −0 lib/cli/version.rb
  23. +77 −0 lib/cli/zip_util.rb
  24. +3 −0  lib/vmc.rb
  25. +435 −0 lib/vmc/client.rb
  26. +21 −0 lib/vmc/const.rb
  27. +9 −0 spec/assets/app_info.txt
  28. +9 −0 spec/assets/app_listings.txt
  29. +9 −0 spec/assets/bad_create_app.txt
  30. +9 −0 spec/assets/delete_app.txt
  31. +9 −0 spec/assets/global_service_listings.txt
  32. +9 −0 spec/assets/good_create_app.txt
  33. +9 −0 spec/assets/good_create_service.txt
  34. +27 −0 spec/assets/info_authenticated.txt
  35. +15 −0 spec/assets/info_return.txt
  36. +16 −0 spec/assets/info_return_bad.txt
  37. +9 −0 spec/assets/login_fail.txt
  38. +9 −0 spec/assets/login_success.txt
  39. +1 −0  spec/assets/sample_token.txt
  40. +9 −0 spec/assets/service_already_exists.txt
  41. +9 −0 spec/assets/service_listings.txt
  42. +9 −0 spec/assets/service_not_found.txt
  43. +9 −0 spec/assets/user_info.txt
  44. +11 −0 spec/spec_helper.rb
  45. +73 −0 spec/unit/cli_opts_spec.rb
  46. +284 −0 spec/unit/client_spec.rb
  47. +32 −0 vmc.gemspec
3  .gitignore
@@ -0,0 +1,3 @@
+Gemfile.lock
+*.gem
+.DS_Store
4 Gemfile
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+gemspec
+
24 LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2010-2011 VMware Inc, All Rights Reserved
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+This software downloads additional open source software components upon install
+that are distributed under separate terms and conditions. Please see the license
+information provided in the individual software components for more information.
+
92 README.md
@@ -0,0 +1,92 @@
+# VMC
+
+The VMware Cloud CLI. This is the command line interface to VMware's Application Platform
+
+_Copyright 2010-2011, VMware, Inc. Licensed under the
+MIT license, please see the LICENSE file. All rights reserved._
+
+ Usage: vmc [options] command [<args>] [command_options]
+ Try 'vmc help [command]' or 'vmc help options' for more information.
+
+ Currently available vmc commands are:
+
+ Getting Started
+ target [url] Reports current target or sets a new target
+ login [email] [--email, --passwd] Login
+ info System and account information
+
+ Applications
+ apps List deployed applications
+
+ Application Creation
+ push [appname] Create, push, map, and start a new application
+ push [appname] --path Push application from specified path
+ push [appname] --url Set the url for the application
+ push [appname] --instances <N> Set the expected number <N> of instances
+ push [appname] --mem M Set the memory reservation for the application
+ push [appname] --no-start Do not auto-start the application
+
+ Application Operations
+ start <appname> Start the application
+ stop <appname> Stop the application
+ restart <appname> Restart the application
+ delete <appname> Delete the application
+ rename <appname> <newname> Rename the application
+
+ Application Updates
+ update <appname> [--path] Update the application bits
+ mem <appname> [memsize] Update the memory reservation for an application
+ map <appname> <url> Register the application to the url
+ unmap <appname> <url> Unregister the application from the url
+ instances <appname> <num|delta> Scale the application instances up or down
+
+ Application Information
+ crashes <appname> List recent application crashes
+ crashlogs <appname> Display log information for crashed applications
+ logs <appname> [--all] Display log information for the application
+ files <appname> [path] [--all] Display directory listing or file download for path
+ stats <appname> Display resource usage for the application
+ instances <appname> List application instances
+
+ Application Environment
+ env <appname> List application environment variables
+ env-add <appname> <variable[=]value> Add an environment variable to an application
+ env-del <appname> <variable> Delete an environment variable to an application
+
+ Services
+ services Lists of services available and provisioned
+ create-service <service> [--name,--bind] Create a provisioned service
+ create-service <service> <name> Create a provisioned service and assign it <name>
+ create-service <service> <name> <app> Create a provisioned service and assign it <name>, and bind to <app>
+ delete-service [servicename] Delete a provisioned service
+ bind-service <servicename> <appname> Bind a service to an application
+ unbind-service <servicename> <appname> Unbind service from the application
+ clone-services <src-app> <dest-app> Clone service bindings from <src-app> application to <dest-app>
+
+ Administration
+ user Display user account information
+ passwd Change the password for the current user
+ logout Logs current user out of the target system
+ add-user [--email, --passwd] Register a new user (requires admin privileges)
+ delete-user <user> Delete a user and all apps and services (requires admin privileges)
+
+ System
+ runtimes Display the supported runtimes of the target system
+ frameworks Display the recognized frameworks of the target system
+
+ Misc
+ aliases List aliases
+ alias <alias[=]command> Create an alias for a command
+ unalias <alias> Remove an alias
+ targets List known targets and associated authorization tokens
+
+ Help
+ help [command] Get general help or help on a specific command
+ help options Get help on available options
+
+## Simple Story (for Ruby apps)
+
+ vmc target api.vcloudlabs.com
+ vmc login
+ bundle package
+ vmc push
17 Rakefile
@@ -0,0 +1,17 @@
+require 'rake'
+require 'spec/rake/spectask'
+
+desc "Run specs"
+task :spec do
+ sh('bundle install')
+ Spec::Rake::SpecTask.new('spec') do |t|
+ t.spec_opts = %w(-fs -c)
+ t.spec_files = FileList['spec/**/*_spec.rb']
+ end
+end
+
+desc "Synonym for spec"
+task :test => :spec
+desc "Synonym for spec"
+task :tests => :spec
+task :default => :spec
15 TODO
@@ -0,0 +1,15 @@
+
+1. Use json load trick to load faster if available, fallback to json_pure. Change :symbols
+2. [DONE] Don't flush on :clear for percentage counter
+3. [DONE] Add --no-resource-check, should not do anything with war file but send it, e.g. no pack/unpack
+4. [DONE] Do auto-check on size of stuff to send? Don't bother with resources if small?
+5. [DONE] Do --log-prefix and also add --all to logs command[s]
+6. [DONE] fix aliases with no file
+7. [DONE] See if loading classes inside of requires save startup times.
+8. Add timeout for vmair?
+9. [DONE] register auto logs in if not logged in
+10. [DONE] Fix continous errors on push while checking start (start method)
+11. [DONE] zip filters [~ .idea, etc]
+12. [DONE] Make work with json on 1.9.2
+13. [DONE] Go back and match README
+14. Delete service remove from all apps? Causes 500 on actions to app afterwards.
6 bin/vmc
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+
+require File.expand_path('../../lib/cli', __FILE__)
+
+VMC::Cli::Runner.run(ARGV.dup)
+
30 lib/cli.rb
@@ -0,0 +1,30 @@
+
+ROOT = File.expand_path(File.dirname(__FILE__))
+
+module VMC
+
+ autoload :Client, "#{ROOT}/vmc/client"
+
+ module Cli
+
+ autoload :Config, "#{ROOT}/cli/config"
+ autoload :Framework, "#{ROOT}/cli/frameworks"
+ autoload :Runner, "#{ROOT}/cli/runner"
+ autoload :ZipUtil, "#{ROOT}/cli/zip_util"
+ autoload :ServicesHelper, "#{ROOT}/cli/services_helper"
+
+ module Command
+ autoload :Base, "#{ROOT}/cli/commands/base"
+ autoload :Admin, "#{ROOT}/cli/commands/admin"
+ autoload :Apps, "#{ROOT}/cli/commands/apps"
+ autoload :Misc, "#{ROOT}/cli/commands/misc"
+ autoload :Services, "#{ROOT}/cli/commands/services"
+ autoload :User, "#{ROOT}/cli/commands/user"
+ end
+
+ end
+end
+
+require "#{ROOT}/cli/version"
+require "#{ROOT}/cli/core_ext"
+require "#{ROOT}/cli/errors"
57 lib/cli/commands/admin.rb
@@ -0,0 +1,57 @@
+module VMC::Cli::Command
+
+ class Admin < Base
+
+ def add_user(email=nil)
+ email = @options[:email] unless email
+ password = @options[:password]
+ email = ask("Email: ") unless no_prompt || email
+ unless no_prompt || password
+ password = ask("Password: ") {|q| q.echo = '*'}
+ password2 = ask("Verify Password: ") {|q| q.echo = '*'}
+ err "Passwords did not match, try again" if password != password2
+ end
+ err "Need a valid email" unless email
+ err "Need a password" unless password
+ display 'Creating New User: ', false
+ client.add_user(email, password)
+ display 'OK'.green
+
+ # if we are not logged in for the current target, log in as the new user
+ return unless VMC::Cli::Config.auth_token.nil?
+ @options[:password] = password
+ cmd = User.new(@options)
+ cmd.login(email)
+ end
+
+ def delete_user(user_email)
+ # Check to make sure all apps and services are deleted before deleting the user
+ # implicit proxying
+
+ client.proxy_for(user_email)
+ @options[:proxy] = user_email
+ apps = client.apps
+
+ if (apps && !apps.empty?)
+ unless no_prompt
+ proceed = ask("\nDeployed applications and associated services will be DELETED, continue? [yN]: ")
+ err "Aborted" if proceed.upcase != 'Y'
+ end
+ cmd = Apps.new(@options)
+ apps.each { |app| cmd.delete_app(app[:name], true) }
+ end
+
+ services = client.services
+ if (services && !services.empty?)
+ cmd = Services.new(@options)
+ services.each { |s| cmd.delete_service(s[:name])}
+ end
+
+ display 'Deleting User: ', false
+ client.delete_user(user_email)
+ display 'OK'.green
+ end
+
+ end
+
+end
937 lib/cli/commands/apps.rb
@@ -0,0 +1,937 @@
+require 'digest/sha1'
+require 'fileutils'
+require 'tempfile'
+require 'tmpdir'
+require 'set'
+
+module VMC::Cli::Command
+
+ class Apps < Base
+ include VMC::Cli::ServicesHelper
+
+ def list
+ apps = client.apps
+ return display JSON.pretty_generate(apps || []) if @options[:json]
+
+ display "\n"
+ return display "No Applications" if apps.nil? || apps.empty?
+
+ apps_table = table do |t|
+ t.headings = 'Application', '# ', 'Health', 'URLS', 'Services'
+ apps.each do |app|
+ t << [app[:name], app[:instances], health(app), app[:uris].join(', '), app[:services].join(', ')]
+ end
+ end
+ display apps_table
+ end
+
+ alias :apps :list
+
+ SLEEP_TIME = 1
+ LINE_LENGTH = 80
+
+ # 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)
+
+ return display "Application '#{appname}' could not be found".red if app.nil?
+ return display "Application '#{appname}' already started".yellow if app[:state] == 'STARTED'
+
+ banner = 'Staging Application: '
+ 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'
+ client.update_app(appname, app)
+
+ 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
+ display "\n"
+ should_delete = ask 'Should I delete the application? (Y/n)? ' unless no_prompt
+ delete_app(appname, false) unless no_prompt || should_delete.upcase == 'N'
+ end
+ failed = true
+ break
+ elsif count > TAIL_TICKS
+ log_lines_displayed = grab_startup_tail(appname, log_lines_displayed)
+ end
+ rescue => e
+ err(e.message, '')
+ 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)
+ 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
+
+ unless memsize
+ choose do |menu|
+ menu.layout = :one_line
+ menu.prompt = "Update Memory Reservation? [Current:#{current_mem}] "
+ menu.default = current_mem
+ mem_choices.each { |choice| menu.choice(choice) { memsize = choice } }
+ end
+ end
+
+ mem = mem_choice_to_quota(mem)
+ memsize = mem_choice_to_quota(memsize)
+ current_mem = mem_choice_to_quota(current_mem)
+
+ display "Updating Memory Reservation to #{mem_quota_to_choice(memsize)}: ", false
+
+ # check memsize here for capacity
+ check_has_capacity_for((memsize - mem) * app[:instances])
+
+ mem = memsize
+
+ if (mem != current_mem)
+ app[:resources][:memory] = mem
+ client.update_app(appname, app)
+ display 'OK'.green
+ restart appname if app[:state] == 'STARTED'
+ else
+ display 'OK'.green
+ end
+ end
+
+ def map(appname, url)
+ app = client.app_info(appname)
+ uris = app[:uris] || []
+ uris << url
+ app[:uris] = uris
+ client.update_app(appname, app)
+ display "Succesfully 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
+
+ end
+
+ def delete(appname=nil)
+ force = @options[:force]
+ if @options[:all]
+ should_delete = force && no_prompt ? 'Y' : 'N'
+ unless no_prompt || force
+ should_delete = ask 'Delete ALL Applications and Services? (y/N)? '
+ end
+ if should_delete.upcase == 'Y'
+ 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]
+ app_services.each { |service|
+ del_service = force && no_prompt ? 'Y' : 'N'
+ unless no_prompt || force
+ del_service = ask("Provisioned service [#{service}] detected, would you like to delete it? [yN]: ")
+ end
+ services_to_delete << service if del_service.upcase == 'Y'
+ }
+ display "Deleting application [#{appname}]: ", false
+ client.delete_app(appname)
+ display 'OK'.green
+
+ services_to_delete.each do |s|
+ display "Deleting service [#{s}]: ", false
+ client.delete_service(s)
+ display 'OK'.green
+ 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
+ err 'No such file or directory'
+ end
+
+ def logs(appname)
+ return grab_all_logs(appname) if @options[:all] && !@options[:instance]
+ instance = @options[:instance] || '0'
+ grab_logs(appname, instance)
+ end
+
+ def crashes(appname, print_results=true, since=0)
+ crashed = client.app_crashes(appname)[:crashes]
+ crashed.delete_if { |c| c[:since] < since }
+ instance_map = {}
+
+# return display JSON.pretty_generate(apps) if @options[:json]
+
+
+ counter = 0
+ crashed = crashed.to_a.sort { |a,b| a[:since] - b[:since] }
+ crashed_table = table do |t|
+ t.headings = 'Name', 'Instance ID', 'Crashed Time'
+ crashed.each do |crash|
+ name = "#{appname}-#{counter += 1}"
+ instance_map[name] = crash[:instance]
+ t << [name, crash[:instance], Time.at(crash[:since]).strftime("%m/%d/%Y %I:%M%p")]
+ end
+ end
+
+ VMC::Cli::Config.store_instances(instance_map)
+
+ if @options[:json]
+ return display JSON.pretty_generate(crashed)
+ elsif print_results
+ display "\n"
+ if crashed.empty?
+ display "No crashed instances for [#{appname}]" if print_results
+ else
+ display crashed_table if print_results
+ end
+ end
+
+ crashed
+ end
+
+ def crashlogs(appname)
+ instance = @options[:instance] || '0'
+ grab_crash_logs(appname, instance)
+ end
+
+ def instances(appname, num=nil)
+ 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]
+ 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] unless appname
+ url = @options[:url]
+ mem, memswitch = nil, @options[:mem]
+ memswitch = normalize_mem(memswitch) if memswitch
+
+ # 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
+
+ unless no_prompt || @options[:path]
+ proceed = ask('Would you like to deploy from the current directory? [Yn]: ')
+ if proceed.upcase == 'N'
+ path = ask('Please enter in the deployment path: ')
+ end
+ end
+
+ path = File.expand_path(path)
+ check_deploy_directory(path)
+
+ appname = ask("Application Name: ") unless no_prompt || appname
+ err "Application Name required." if appname.nil? || appname.empty?
+
+ unless app_checked
+ err "Application '#{appname}' already exists, use update or delete." if app_exists?(appname)
+ end
+
+ unless no_prompt || url
+ url = ask("Application Deployed URL: '#{appname}.#{VMC::Cli::Config.suggest_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
+ if YES_SET.member?(url)
+ #silently revert to the stock url
+ url = "#{appname}.#{VMC::Cli::Config.suggest_url}"
+ end
+ end
+
+ url = "#{appname}.#{VMC::Cli::Config.suggest_url}" if url.nil? || url.empty?
+
+ # Detect the appropriate framework.
+ framework = nil
+ unless ignore_framework
+ framework = VMC::Cli::Framework.detect(path)
+ framework_correct = ask("Detected a #{framework}, is this correct? [Yn]: ") if prompt_ok && framework
+ framework_correct ||= 'y'
+ if prompt_ok && (framework.nil? || framework_correct.upcase == 'N')
+ display "#{"[WARNING]".yellow} Can't determine the Application Type." unless framework
+ framework = nil if framework_correct.upcase == 'N'
+ choose do |menu|
+ menu.layout = :one_line
+ menu.prompt = "Select Application Type: "
+ menu.default = framework
+ VMC::Cli::Framework.known_frameworks.each do |f|
+ menu.choice(f) { framework = VMC::Cli::Framework.lookup(f) }
+ end
+ end
+ 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
+ unless memswitch
+ mem = framework.memory
+ if prompt_ok
+ choose do |menu|
+ menu.layout = :one_line
+ menu.prompt = "Memory Reservation [Default:#{mem}] "
+ menu.default = mem
+ mem_choices.each { |choice| menu.choice(choice) { mem = choice } }
+ end
+ end
+ else
+ mem = memswitch
+ 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}'? [yN]: ")
+ bind_services(appname, services) if proceed.upcase == 'Y'
+ end
+ 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] || []
+ return display JSON.pretty_generate(env) if @options[:json]
+ return display "No Environment Variables" if env.empty?
+ etable = table do |t|
+ t.headings = 'Variable', 'Value'
+ env.each do |e|
+ k,v = e.split('=')
+ t << [k, v]
+ end
+ end
+ display "\n"
+ display etable
+ end
+
+ def environment_add(appname, k, v=nil)
+ app = client.app_info(appname)
+ env = app[:env] || []
+ k,v = k.split('=') unless v
+ env << "#{k}=#{v}"
+ display "Adding Environment Variable [#{k}=#{v}]: ", false
+ app[:env] = env
+ client.update_app(appname, app)
+ display 'OK'.green
+ restart appname if app[:state] == 'STARTED'
+ end
+
+ def environment_del(appname, variable)
+ app = client.app_info(appname)
+ env = app[:env] || []
+ deleted_env = nil
+ env.each do |e|
+ k,v = e.split('=')
+ if (k == variable)
+ deleted_env = e
+ break;
+ end
+ end
+ display "Deleting Environment Variable [#{variable}]: ", false
+ if deleted_env
+ env.delete(deleted_env)
+ app[:env] = env
+ client.update_app(appname, app)
+ display 'OK'.green
+ restart appname if app[:state] == 'STARTED'
+ else
+ display 'OK'.green
+ end
+ end
+
+ private
+
+ def app_exists?(appname)
+ app_info = client.app_info(appname)
+ app_info != nil
+ rescue VMC::Client::NotFound
+ 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 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('{*,.[^\.]*}')
+ FileUtils.cp_r(files, explode_dir)
+ 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
+
+ # 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
+
+ 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
+
+ end
+
+ # Perform Packing of the upload bits here.
+ unless VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
+ 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'
+ end
+ else
+ upload_size = '0K'
+ end
+
+ upload_str = " Uploading (#{upload_size}): "
+ display upload_str, false
+
+ unless VMC::Cli::ZipUtil.get_files_to_pack(explode_dir).empty?
+ FileWithPercentOutput.display_str = upload_str
+ FileWithPercentOutput.upload_size = File.size(upload_file);
+ file = FileWithPercentOutput.open(upload_file, 'rb')
+ end
+
+ 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
+ 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
+
+ def choose_existing_service(appname, user_services)
+ return unless prompt_ok
+ selected = false
+ choose do |menu|
+ menu.header = "The following provisioned services are available:"
+ menu.prompt = 'Please select one you wish to provision: '
+ menu.select_by = :index_or_name
+ user_services.each do |s|
+ menu.choice(s[:name]) do
+ display "Binding Service: ", false
+ client.bind_service(s[:name], appname)
+ display 'OK'.green
+ selected = true
+ end
+ end
+ end
+ selected
+ end
+
+ def choose_new_service(appname, services)
+ return unless prompt_ok
+ choose do |menu|
+ menu.header = "The following system services are available:"
+ menu.prompt = 'Please select one you wish to provision: '
+ menu.select_by = :index_or_name
+ service_choices = []
+ services.each do |service_type, value|
+ value.each do |vendor, version|
+ service_choices << vendor
+ end
+ end
+ service_choices.sort! {|a, b| a.to_s <=> b.to_s }
+ service_choices.each do |vendor|
+ menu.choice(vendor) do
+ default_name = random_service_name(vendor)
+ service_name = ask("Specify the name of the service [#{default_name}]: ")
+ service_name = default_name if service_name.empty?
+ create_service_banner(vendor, service_name)
+ bind_service_banner(service_name, appname)
+ end
+ end
+ end
+ end
+
+ def bind_services(appname, services)
+ user_services = client.services
+ selected_existing = false
+ unless no_prompt || user_services.empty?
+ use_existing = ask "Would you like to use an existing provisioned service [yN]? "
+ if use_existing.upcase == 'Y'
+ 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 check_app_limit
+ usage = client_info[:usage]
+ limits = client_info[:limits]
+ return unless usage and limits and limits[:apps]
+ if limits[:apps] == usage[:apps]
+ display "Not enough capacity for operation.".red
+ tapps = limits[:apps] || 0
+ apps = usage[:apps] || 0
+ err "Current Usage: (#{apps} of #{tapps} total apps already in use)"
+ end
+ end
+
+ def check_has_capacity_for(mem_wanted)
+ usage = client_info[:usage]
+ limits = client_info[:limits]
+ return unless usage and limits
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
+ if mem_wanted > available_for_use
+ tmem = pretty_size(limits[:memory]*1024*1024)
+ mem = pretty_size(usage[:memory]*1024*1024)
+ display "Not enough capacity for operation.".yellow
+ available = pretty_size(available_for_use * 1024 * 1024)
+ err "Current Usage: (#{mem} of #{tmem} total, #{available} available for use)"
+ end
+ end
+
+ def mem_choices
+ default = ['64M', '128M', '256M', '512M', '1G', '2G']
+
+ return default unless client_info
+ return default unless (usage = client_info[:usage] and limits = client_info[:limits])
+
+ available_for_use = limits[:memory].to_i - usage[:memory].to_i
+ check_has_capacity_for(64) if available_for_use < 64
+ return ['64M'] if available_for_use < 128
+ return ['64M', '128M'] if available_for_use < 256
+ return ['64M', '128M', '256M'] if available_for_use < 512
+ return ['64M', '128M', '256M', '512M'] if available_for_use < 1024
+ return ['64M', '128M', '256M', '512M', '1G'] if available_for_use < 2048
+ return ['64M', '128M', '256M', '512M', '1G', '2G']
+ end
+
+ def normalize_mem(mem)
+ return mem if /K|G|M/i =~ mem
+ "#{mem}M"
+ end
+
+ def mem_choice_to_quota(mem_choice)
+ (mem_choice =~ /(\d+)M/i) ? mem_quota = $1.to_i : mem_quota = mem_choice.to_i * 1024
+ mem_quota
+ end
+
+ def mem_quota_to_choice(mem)
+ if mem < 1024
+ mem_choice = "#{mem}M"
+ else
+ mem_choice = "#{(mem/1024).to_i}G"
+ end
+ mem_choice
+ end
+
+ def get_instances(appname)
+ instances_info_envelope = client.app_instances(appname)
+ # Empty array is returned if there are no instances running.
+ instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
+
+ instances_info = instances_info_envelope[:instances] || []
+ instances_info = instances_info.sort {|a,b| a[:index] - b[:index]}
+
+ return display JSON.pretty_generate(instances_info) if @options[:json]
+
+ return display "No running instances for [#{appname}]".yellow if instances_info.empty?
+
+ instances_table = table do |t|
+ t.headings = 'Index', 'State', 'Start Time'
+ instances_info.each do |entry|
+ t << [entry[:index], entry[:state], Time.at(entry[:since]).strftime("%m/%d/%Y %I:%M%p")]
+ end
+ end
+ display "\n"
+ display instances_table
+ end
+
+ def change_instances(appname, instances)
+ app = client.app_info(appname)
+
+ match = instances.match(/([+-])?\d+/)
+ err "Invalid number of instances '#{instances}'" unless match
+
+ instances = instances.to_i
+ current_instances = app[:instances]
+ new_instances = match.captures[0] ? current_instances + instances : instances
+ err "There must be at least 1 instance." if new_instances < 1
+
+ if current_instances == new_instances
+ display "Application [#{appname}] is already running #{new_instances} instance#{'s' if new_instances > 1}.".yellow
+ return
+ end
+
+ up_or_down = new_instances > current_instances ? 'up' : 'down'
+ display "Scaling Application instances #{up_or_down} to #{new_instances}: ", false
+ app[:instances] = new_instances
+ client.update_app(appname, app)
+ display 'OK'.green
+ end
+
+ def health(d)
+ return 'N/A' unless (d and d[:state])
+ return 'STOPPED' if d[:state] == 'STOPPED'
+
+ healthy_instances = d[:runningInstances]
+ expected_instance = d[:instances]
+ health = nil
+
+ if d[:state] == "STARTED" && expected_instance > 0 && healthy_instances
+ health = format("%.3f", healthy_instances.to_f / expected_instance).to_f
+ end
+
+ return 'RUNNING' if health && health == 1.0
+ return "#{(health * 100).round}%" if health
+ return 'N/A'
+ end
+
+ def app_started_properly(appname, error_on_health)
+ app = client.app_info(appname)
+ case health(app)
+ when 'N/A'
+ # Health manager not running.
+ err "\Application '#{appname}'s state is undetermined, not enough information available." if error_on_health
+ return false
+ when 'RUNNING'
+ return true
+ else
+ return false
+ end
+ end
+
+ def display_logfile(path, content, instance='0', banner=nil)
+ banner ||= "====> #{path} <====\n\n"
+ if content && !content.empty?
+ display banner
+ prefix = "[#{instance}: #{path}] -".bold if @options[:prefixlogs]
+ unless prefix
+ display content
+ else
+ lines = content.split("\n")
+ lines.each { |line| display "#{prefix} #{line}"}
+ 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|
+ begin
+ content = client.app_files(appname, path, instance)
+ rescue
+ end
+ display_logfile(path, content, instance)
+ end
+ 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
+ display_logfile(path, content, instance)
+ end
+ end
+
+ def grab_startup_tail(appname, since = 0)
+ new_lines = 0
+ path = "logs/startup.log"
+ content = client.app_files(appname, path)
+ if content && !content.empty?
+ display "\n==== displaying startup log ====\n\n" if since == 0
+ response_lines = content.split("\n")
+ lines = response_lines.size
+ tail = response_lines[since, lines] || []
+ new_lines = tail.size
+ display tail.join("\n") if new_lines > 0
+ end
+ since + new_lines
+ end
+ rescue
+ end
+
+ class FileWithPercentOutput < ::File
+ class << self
+ attr_accessor :display_str, :upload_size
+ end
+
+ def update_display(rsize)
+ @read ||= 0
+ @read += rsize
+ p = (@read * 100 / FileWithPercentOutput.upload_size).to_i
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
+ clear(FileWithPercentOutput.display_str.size + 5)
+ VMC::Cli::Config.output.print("#{FileWithPercentOutput.display_str} #{p}%")
+ VMC::Cli::Config.output.flush
+ end
+ end
+
+ def read(*args)
+ result = super(*args)
+ if result && result.size > 0
+ update_display(result.size)
+ else
+ unless VMC::Cli::Config.output.nil? || !STDOUT.tty?
+ clear(FileWithPercentOutput.display_str.size + 5)
+ VMC::Cli::Config.output.print(FileWithPercentOutput.display_str)
+ display('OK'.green)
+ end
+ end
+ result
+ end
+ end
+
+end
79 lib/cli/commands/base.rb
@@ -0,0 +1,79 @@
+
+require 'rubygems'
+require 'terminal-table/import'
+require 'highline/import'
+
+module VMC::Cli
+
+ module Command
+
+ class Base
+ attr_reader :no_prompt, :prompt_ok
+
+ def initialize(options={})
+ @options = options.dup
+ @no_prompt = @options[:noprompts]
+ @prompt_ok = !no_prompt
+
+ # Fix for system ruby and Highline (stdin) on MacOSX
+ if RUBY_PLATFORM =~ /darwin/ && RUBY_VERSION == '1.8.7' && RUBY_PATCHLEVEL <= 174
+ HighLine.track_eof = false
+ end
+
+ # Suppress colorize on Windows systems for now.
+ if !!RUBY_PLATFORM['mingw'] || !!RUBY_PLATFORM['mswin32'] || !!RUBY_PLATFORM['cygwin']
+ VMC::Cli::Config.colorize = false
+ end
+
+ end
+
+ def client
+ return @client if @client
+ @client = VMC::Client.new(target_url, auth_token)
+ @client.trace = VMC::Cli::Config.trace if VMC::Cli::Config.trace
+ @client.proxy_for @options[:proxy] if @options[:proxy]
+ @client
+ end
+
+ def client_info
+ return @client_info if @client_info
+ @client_info = client.info
+ end
+
+ def target_url
+ return @target_url if @target_url
+ @target_url = VMC::Cli::Config.target_url
+ end
+
+ def auth_token
+ return @auth_token if @auth_token
+ @auth_token = VMC::Cli::Config.auth_token
+ end
+
+ def runtimes_info
+ return @runtimes if @runtimes
+ info = client_info
+ @runtimes = {}
+ if info[:frameworks]
+ info[:frameworks].each_value do |f|
+ next unless f[:runtimes]
+ f[:runtimes].each { |r| @runtimes[r[:name]] = r}
+ end
+ end
+ @runtimes
+ end
+
+ def frameworks_info
+ return @frameworks if @frameworks
+ info = client_info
+ @frameworks = []
+ if info[:frameworks]
+ info[:frameworks].each_value { |f| @frameworks << [f[:name]] }
+ end
+ @frameworks
+ end
+
+ end
+ end
+end
+
128 lib/cli/commands/misc.rb
@@ -0,0 +1,128 @@
+module VMC::Cli::Command
+
+ class Misc < Base
+ def version
+ say "vmc #{VMC::Cli::VERSION}"
+ end
+
+ def target
+ return display JSON.pretty_generate({:target => target_url}) if @options[:json]
+ banner "[#{target_url}]"
+ end
+
+ def targets
+ targets = VMC::Cli::Config.targets
+ return display JSON.pretty_generate(targets) if @options[:json]
+ return display 'None specified' if targets.empty?
+ targets_table = table do |t|
+ t.headings = 'Target', 'Authorization'
+ targets.each { |target, token| t << [target, token] }
+ end
+ display "\n"
+ display targets_table
+ end
+
+ alias :tokens :targets
+
+ def set_target(target_url)
+ target_url = "http://#{target_url}" unless /^https?/ =~ target_url
+ target_url = target_url.gsub(/\/+$/, '')
+ client = VMC::Client.new(target_url)
+ unless client.target_valid?
+ if prompt_ok
+ display "Host is not valid: '#{target_url}'".red
+ show_response = ask "Would you like see the response [yN]? "
+ display "\n<<<\n#{client.raw_info}\n>>>\n" if show_response.upcase == 'Y'
+ end
+ exit(false)
+ else
+ VMC::Cli::Config.store_target(target_url)
+ say "Succesfully targeted to [#{target_url}]".green
+ end
+ end
+
+ def info
+ info = client_info
+ return display JSON.pretty_generate(info) if @options[:json]
+
+ display "\n#{info[:description]}"
+ display "For support visit #{info[:support]}"
+ display ""
+ display "Target: #{target_url} (v#{info[:version]})"
+ display "Client: v#{VMC::Cli::VERSION}"
+ if info[:user]
+ display ''
+ display "User: #{info[:user]}"
+ end
+ if usage = info[:usage] and limits = info[:limits]
+ tmem = pretty_size(limits[:memory]*1024*1024)
+ mem = pretty_size(usage[:memory]*1024*1024)
+ tser = limits[:services]
+ ser = usage[:services]
+ tapps = limits[:apps] || 0
+ apps = usage[:apps] || 0
+ display "Usage: Memory (#{mem} of #{tmem} total)"
+ display " Services (#{ser} of #{tser} total)"
+ display " Apps (#{apps} of #{tapps} total)" if limits[:apps]
+ end
+ end
+
+ def runtimes
+ raise VMC::Client::AuthError unless client.logged_in?
+ return display JSON.pretty_generate(runtimes_info) if @options[:json]
+ return display "No Runtimes" if runtimes_info.empty?
+ rtable = table do |t|
+ t.headings = 'Name', 'Description', 'Version'
+ runtimes_info.each_value { |rt| t << [rt[:name], rt[:description], rt[:version]] }
+ end
+ display "\n"
+ display rtable
+ end
+
+ def frameworks
+ raise VMC::Client::AuthError unless client.logged_in?
+ return display JSON.pretty_generate(frameworks_info) if @options[:json]
+ return display "No Frameworks" if frameworks_info.empty?
+ rtable = table do |t|
+ t.headings = ['Name']
+ frameworks_info.each { |f| t << f }
+ end
+ display "\n"
+ display rtable
+ end
+
+ def aliases
+ aliases = VMC::Cli::Config.aliases
+ return display JSON.pretty_generate(aliases) if @options[:json]
+ return display "No Aliases" if aliases.empty?
+ atable = table do |t|
+ t.headings = 'Alias', 'Command'
+ aliases.each { |k,v| t << [k, v] }
+ end
+ display "\n"
+ display atable
+ end
+
+ def alias(k, v=nil)
+ k,v = k.split('=') unless v
+ aliases = VMC::Cli::Config.aliases
+ aliases[k] = v
+ VMC::Cli::Config.store_aliases(aliases)
+ display "Successfully aliased '#{k}' to '#{v}'".green
+ end
+
+ def unalias(key)
+ aliases = VMC::Cli::Config.aliases
+ if aliases.has_key?(key)
+ aliases.delete(key)
+ VMC::Cli::Config.store_aliases(aliases)
+ display "Successfully unaliased '#{key}'".green
+ else
+ display "Unknown alias '#{key}'".red
+ end
+ end
+
+ end
+
+end
+
84 lib/cli/commands/services.rb
@@ -0,0 +1,84 @@
+module VMC::Cli::Command
+
+ class Services < Base
+ include VMC::Cli::ServicesHelper
+
+ def services
+ ss = client.services_info
+ ps = client.services
+ if @options[:json]
+ services = { :system => ss, :provisioned => ps }
+ return display JSON.pretty_generate(services)
+ end
+ display_system_services(ss)
+ display_provisioned_services(ps)
+ end
+
+ def create_service(service=nil, name=nil, appname=nil)
+ unless no_prompt || service
+ services = client.services_info
+ err 'No services available to provision' if services.empty?
+ choose do |menu|
+ menu.prompt = 'Please select one you wish to provision: '
+ menu.select_by = :index_or_name
+ services.each do |service_type, value|
+ value.each do |vendor, version|
+ menu.choice(vendor.to_s) { service = vendor.to_s }
+ end
+ end
+ end
+ end
+ name = @options[:name] unless name
+ unless name
+ name = random_service_name(service)
+ picked_name = true
+ end
+ create_service_banner(service, name, picked_name)
+ appname = @options[:bind] unless appname
+ bind_service_banner(name, appname) if appname
+ end
+
+ def delete_service(service=nil)
+ unless no_prompt || service
+ user_services = client.services
+ err 'No services available to delete' if user_services.empty?
+ choose do |menu|
+ menu.prompt = 'Please select one you wish to delete: '
+ menu.select_by = :index_or_name
+ user_services.each do |s|
+ menu.choice(s[:name]) { service = s[:name] }
+ end
+ end
+ end
+ err "Service name required." unless service
+ display "Deleting service [#{service}]: ", false
+ client.delete_service(service)
+ display 'OK'.green
+ end
+
+ def bind_service(service, appname)
+ bind_service_banner(service, appname)
+ end
+
+ def unbind_service(service, appname)
+ unbind_service_banner(service, appname)
+ end
+
+ def clone_services(src_app, dest_app)
+ begin
+ src = client.app_info(src_app)
+ dest = client.app_info(dest_app)
+ rescue
+ end
+
+ err "Application '#{src_app}' does not exist" unless src
+ err "Application '#{dest_app}' does not exist" unless dest
+
+ services = src[:services]
+ err 'No services to clone' unless services && !services.empty?
+ services.each { |service| bind_service_banner(service, dest_app, false) }
+ check_app_for_restart(dest_app)
+ end
+
+ end
+end
60 lib/cli/commands/user.rb
@@ -0,0 +1,60 @@
+module VMC::Cli::Command
+
+ class User < Base
+
+ def info
+ info = client_info
+ username = info[:user] || 'N/A'
+ return display JSON.pretty_generate([username]) if @options[:json]
+ display "\n[#{username}]"
+ end
+
+ def login(email=nil)
+ email = @options[:email] unless email
+ password = @options[:password]
+ tries = 0
+ email = ask("Email: ") unless no_prompt || email
+ password = ask("Password: ") {|q| q.echo = '*'} unless no_prompt || password
+ err "Need a valid email" unless email
+ err "Need a password" unless password
+ login_and_save_token(email, password)
+ say "Successfully logged into [#{target_url}]".green
+ rescue VMC::Client::TargetError
+ display "Problem with login, invalid account or password.".red
+ retry if (tries += 1) < 3 && prompt_ok && !@options[:password]
+ exit 1
+ rescue => e
+ display "Problem with login, #{e}, try again or register for an account.".red
+ exit 1
+ end
+
+ def logout
+ VMC::Cli::Config.remove_token_file
+ say "Successfully logged out of [#{target_url}]".green
+ end
+
+ def change_password(password=nil)
+ info = client_info
+ email = info[:user]
+ err "Need to be logged in to change password." unless email
+ say "Changing password for '#{email}'\n"
+ unless no_prompt
+ password = ask("New Password: ") {|q| q.echo = '*'}
+ password2 = ask("Verify Password: ") {|q| q.echo = '*'}
+ err "Passwords did not match, try again" if password != password2
+ end
+ err "Password required" unless password
+ client.change_password(password)
+ say "\nSuccessfully changed password".green
+ end
+
+ private
+
+ def login_and_save_token(email, password)
+ token = client.login(email, password)
+ VMC::Cli::Config.store_token(token)
+ end
+
+ end
+
+end
110 lib/cli/config.rb
@@ -0,0 +1,110 @@
+require "yaml"
+require 'fileutils'
+
+require 'rubygems'
+require 'json/pure'
+
+module VMC::Cli
+ class Config
+
+ DEFAULT_TARGET = 'api.vcap.me'
+ DEFAULT_SUGGEST = 'vcap.me'
+
+ TARGET_FILE = '~/.vmc_target'
+ TOKEN_FILE = '~/.vmc_token'
+ INSTANCES_FILE = '~/.vmc_instances'
+ ALIASES_FILE = '~/.vmc_aliases'
+
+ class << self
+ attr_accessor :colorize
+ attr_accessor :output
+ attr_accessor :trace
+ attr_accessor :nozip
+ attr_reader :suggest_url
+
+ def target_url
+ return @target_url if @target_url
+ target_file = File.expand_path(TARGET_FILE)
+ if File.exists? target_file
+ @target_url = File.read(target_file).strip!
+ ha = @target_url.split('.')
+ ha.shift
+ @suggest_url = ha.join('.')
+ @suggest_url = DEFAULT_SUGGEST if @suggest_url.empty?
+ else
+ @target_url = DEFAULT_TARGET
+ @suggest_url = DEFAULT_SUGGEST
+ end
+ @target_url = "http://#{@target_url}" unless /^https?/ =~ @target_url
+ @target_url = @target_url.gsub(/\/+$/, '')
+ @target_url
+ end
+
+ def store_target(target_host)
+ target_file = File.expand_path(TARGET_FILE)
+ File.open(target_file, 'w+') { |f| f.puts target_host }
+ FileUtils.chmod 0600, target_file
+ end
+
+ def all_tokens
+ token_file = File.expand_path(TOKEN_FILE)
+ return nil unless File.exists? token_file
+ contents = File.read(token_file).strip
+ JSON.parse(contents)
+ end
+
+ alias :targets :all_tokens
+
+ def auth_token
+ return @token if @token
+ tokens = all_tokens
+ @token = tokens[target_url] if tokens
+ end
+
+ def remove_token_file
+ FileUtils.rm_f(File.expand_path(TOKEN_FILE))
+ end
+
+ def store_token(token)
+ tokens = all_tokens || {}
+ tokens[target_url] = token
+ token_file = File.expand_path(TOKEN_FILE)
+ File.open(token_file, 'w+') { |f| f.write(tokens.to_json) }
+ FileUtils.chmod 0600, token_file
+ end
+
+ def instances
+ instances_file = File.expand_path(INSTANCES_FILE)
+ return nil unless File.exists? instances_file
+ contents = File.read(instances_file).strip
+ JSON.parse(contents)
+ end
+
+ def store_instances(instances)
+ instances_file = File.expand_path(INSTANCES_FILE)
+ File.open(instances_file, 'w') { |f| f.write(instances.to_json) }
+ end
+
+ def aliases
+ aliases_file = File.expand_path(ALIASES_FILE)
+ # bacward compatible
+ unless File.exists? aliases_file
+ old_aliases_file = File.expand_path('~/.vmc-aliases')
+ FileUtils.mv(old_aliases_file, aliases_file) if File.exists? old_aliases_file
+ end
+ aliases = YAML.load_file(aliases_file) rescue {}
+ end
+
+ def store_aliases(aliases)
+ aliases_file = File.expand_path(ALIASES_FILE)
+ File.open(aliases_file, 'wb') {|f| f.write(aliases.to_yaml)}
+ end
+
+ end
+
+ def initialize(work_dir = Dir.pwd)
+ @work_dir = work_dir
+ end
+
+ end
+end
119 lib/cli/core_ext.rb
@@ -0,0 +1,119 @@
+module VMCExtensions
+
+ def say(message)
+ VMC::Cli::Config.output.puts(message) if VMC::Cli::Config.output
+ end
+
+ def header(message, filler = '-')
+ say "\n"
+ say message
+ say filler.to_s * message.size
+ end
+
+ def banner(message)
+ say "\n"
+ say message
+ end
+
+ def display(message, nl=true)
+ if nl
+ say message
+ else
+ if VMC::Cli::Config.output
+ VMC::Cli::Config.output.print(message)
+ VMC::Cli::Config.output.flush
+ end
+ end
+ end
+
+ def clear(size=80)
+ return unless VMC::Cli::Config.output
+ VMC::Cli::Config.output.print("\r")
+ VMC::Cli::Config.output.print(" " * size)
+ VMC::Cli::Config.output.print("\r")
+ #VMC::Cli::Config.output.flush
+ end
+
+ def err(message, prefix='Error: ')
+ raise VMC::Cli::CliExit, "#{prefix}#{message}"
+ end
+
+ def quit(message = nil)
+ raise VMC::Cli::GracefulExit, message
+ end
+
+ def blank?
+ self.to_s.blank?
+ end
+
+ def uptime_string(delta)
+ num_seconds = delta.to_i
+ days = num_seconds / (60 * 60 * 24);
+ num_seconds -= days * (60 * 60 * 24);
+ hours = num_seconds / (60 * 60);
+ num_seconds -= hours * (60 * 60);
+ minutes = num_seconds / 60;
+ num_seconds -= minutes * 60;
+ "#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s"
+ end
+
+ def pretty_size(size, prec=1)
+ return 'NA' unless size
+ return "#{size}B" if size < 1024
+ return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024)
+ return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024)
+ return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0))
+ end
+
+end
+
+module VMCStringExtensions
+
+ def red
+ colorize("\e[0m\e[31m")
+ end
+
+ def green
+ colorize("\e[0m\e[32m")
+ end
+
+ def yellow
+ colorize("\e[0m\e[33m")
+ end
+
+ def bold
+ colorize("\e[0m\e[1m")
+ end
+
+ def colorize(color_code)
+ if VMC::Cli::Config.colorize
+ "#{color_code}#{self}\e[0m"
+ else
+ self
+ end
+ end
+
+ def blank?
+ self =~ /^\s*$/
+ end
+
+ def truncate(limit = 30)
+ return "" if self.blank?
+ etc = "..."
+ stripped = self.strip[0..limit]
+ if stripped.length > limit
+ stripped.gsub(/\s+?(\S+)?$/, "") + etc
+ else
+ stripped
+ end
+ end
+
+end
+
+class Object
+ include VMCExtensions
+end
+
+class String
+ include VMCStringExtensions
+end
19 lib/cli/errors.rb
@@ -0,0 +1,19 @@
+module VMC::Cli
+
+ class CliError < StandardError
+ def self.error_code(code = nil)
+ define_method(:error_code) { code }
+ end
+ end
+
+ class UnknownCommand < CliError; error_code(100); end
+ class TargetMissing < CliError; error_code(102); end
+ class TargetInaccessible < CliError; error_code(103); end
+
+ class TargetError < CliError; error_code(201); end
+ class AuthError < TargetError; error_code(202); end
+
+ class CliExit < CliError; error_code(400); end
+ class GracefulExit < CliExit; error_code(401); end
+
+end
97 lib/cli/frameworks.rb
@@ -0,0 +1,97 @@
+module VMC::Cli
+
+ class Framework
+
+ DEFAULT_FRAMEWORK = "http://b20nine.com/unknown"
+ DEFAULT_MEM = '256M'
+
+ FRAMEWORKS = {
+ 'Rails' => ['rails3', { :mem => '256M', :description => 'Rails Application'}],
+ 'Spring' => ['spring', { :mem => '512M', :description => 'Java SpringSource Spring Application'}],
+ 'Grails' => ['grails', { :mem => '512M', :description => 'Java SpringSource Grails Application'}],
+ 'Roo' => ['spring', { :mem => '512M', :description => 'Java SpringSource Roo Application'}],
+ 'JavaWeb' => ['spring', { :mem => '512M', :description => 'Java Web Application'}],
+ 'Sinatra' => ['sinatra', { :mem => '128M', :description => 'Sinatra Application'}],
+ 'Node' => ['node', { :mem => '64M', :description => 'Node.js Application'}],
+ }
+
+ class << self
+
+ def known_frameworks
+ FRAMEWORKS.keys
+ end
+
+ def lookup(name)
+ return Framework.new(*FRAMEWORKS[name])
+ end
+
+ def detect(path)
+ Dir.chdir(path) do
+
+ # Rails
+ if File.exist?('config/environment.rb')
+ return Framework.lookup('Rails')
+
+ # Java
+ elsif Dir.glob('*.war').first
+ war_file = Dir.glob('*.war').first
+ contents = ZipUtil.entry_lines(war_file)
+
+ # Spring Variations
+ if contents =~ /WEB-INF\/lib\/grails-web.*\.jar/
+ return Framework.lookup('Grails')
+ elsif contents =~ /WEB-INF\/classes\/org\/springframework/
+ return Framework.lookup('Spring')
+ elsif contents =~ /WEB-INF\/lib\/spring-core.*\.jar/
+ return Framework.lookup('Spring')
+ else
+ return Framework.lookup('JavaWeb')
+ end
+
+ # Simple Ruby Apps
+ elsif !Dir.glob('*.rb').empty?
+ matched_file = nil
+ Dir.glob('*.rb').each do |fname|
+ next if matched_file
+ File.open(fname, 'r') do |f|
+ str = f.read # This might want to be limited
+ matched_file = fname if (str && str.match(/^\s*require\s*['"]sinatra['"]/))
+ end
+ end
+ if matched_file
+ f = Framework.lookup('Sinatra')
+ f.exec = "ruby #{matched_file}"
+ return f
+ end
+
+ # Node.js
+ elsif !Dir.glob('*.js').empty?
+ # Fixme, make other files work too..
+ if File.exist?('app.js') || File.exist?('index.js') || File.exist?('main.js')
+ return Framework.lookup('Node')
+ end
+ end
+ end
+ nil
+ end
+
+ end
+
+ attr_reader :name, :description, :memory
+ attr_accessor :exec
+
+ alias :mem :memory
+
+ def initialize(framework=nil, opts={})
+ @name = framework || DEFAULT_FRAMEWORK
+ @memory = opts[:mem] || DEFAULT_MEM
+ @description = opts[:description] || 'Unknown Application Type'
+ @exec = opts[:exec]
+ end
+
+ def to_s
+ description
+ end
+ end
+
+end
485 lib/cli/runner.rb
@@ -0,0 +1,485 @@
+
+require 'optparse'
+
+require File.dirname(__FILE__) + '/usage'
+
+class VMC::Cli::Runner
+
+ attr_reader :namespace
+ attr_reader :action
+ attr_reader :args
+ attr_reader :options
+
+ def self.run(args)
+ new(args).run
+ end
+
+ def initialize(args=[])
+ @args = args
+ @options = { :colorize => true }
+ @exit_status = true
+ end
+
+ # Collect all the available options for all commands
+ # Some duplicates exists to capture all scenarios
+ def parse_options!
+ opts_parser = OptionParser.new do |opts|
+ opts.banner = "\nAvailable options:\n\n"
+
+ opts.on('--email EMAIL') { |email| @options[:email] = email }
+ opts.on('--user EMAIL') { |email| @options[:email] = email }
+ opts.on('--passwd PASS') { |pass| @options[:password] = pass }
+ opts.on('--pass PASS') { |pass| @options[:password] = pass }
+ opts.on('--password PASS') { |pass| @options[:password] = pass }
+ opts.on('--app NAME') { |name| @options[:name] = name }
+ opts.on('--name NAME') { |name| @options[:name] = name }
+ opts.on('--bind BIND') { |bind| @options[:bind] = bind }
+ opts.on('--instance INST') { |inst| @options[:instance] = inst }
+ opts.on('--instances INST') { |inst| @options[:instances] = inst }
+ opts.on('--url URL') { |url| @options[:url] = url }
+ opts.on('--mem MEM') { |mem| @options[:mem] = mem }
+ opts.on('--path PATH') { |path| @options[:path] = path }
+ opts.on('--no-start') { @options[:nostart] = true }
+ opts.on('--nostart') { @options[:nostart] = true }
+ opts.on('--force') { @options[:force] = true }
+ opts.on('--all') { @options[:all] = true }
+
+ # generic tracing and debugging
+ opts.on('-t [TKEY]') { |tkey| @options[:trace] = tkey || true }
+ opts.on('--trace [TKEY]') { |tkey| @options[:trace] = tkey || true }
+
+ opts.on('-q', '--quiet') { @options[:quiet] = true }
+
+ # Don't use builtin zip
+ opts.on('--no-zip') { @options[:nozip] = true }
+ opts.on('--nozip') { @options[:nozip] = true }
+
+ opts.on('--no-resources') { @options[:noresources] = true }
+ opts.on('--noresources') { @options[:noresources] = true }
+
+ opts.on('--no-color') { @options[:colorize] = false }
+ opts.on('--verbose') { @options[:verbose] = true }
+
+ opts.on('-n','--no-prompt') { @options[:noprompts] = true }
+ opts.on('--noprompt') { @options[:noprompts] = true }
+ opts.on('--non-interactive') { @options[:noprompts] = true }
+
+ opts.on('--prefix') { @options[:prefixlogs] = true }
+ opts.on('--prefix-logs') { @options[:prefixlogs] = true }
+ opts.on('--prefixlogs') { @options[:prefixlogs] = true }
+
+ opts.on('--json') { @options[:json] = true }
+
+ opts.on('-v', '--version') { set_cmd(:misc, :version) }
+ opts.on('-h', '--help') { puts "#{command_usage}\n"; exit }
+
+ opts.on('--runtime RUNTIME') { |rt| @options[:runtime] = rt }
+
+ # deprecated
+ opts.on('--exec EXEC') { |exec| @options[:exec] = exec }
+ opts.on('--noframework') { @options[:noframework] = true }
+ opts.on('--canary') { @options[:canary] = true }
+
+ # Proxying for another user, requires admin privileges
+ opts.on('-u PROXY') { |proxy| @options[:proxy] = proxy }
+
+ opts.on_tail('--options') { puts "#{opts}\n"; exit }
+ end
+ instances_delta_arg = check_instances_delta!
+ @args = opts_parser.parse!(@args)
+ @args.concat instances_delta_arg
+ convert_options!
+ self
+ end
+
+ def check_instances_delta!
+ return unless @args
+ instance_args = @args.select { |arg| /^[-]\d+$/ =~ arg } || []
+ @args.delete_if { |arg| instance_args.include? arg}
+ instance_args
+ end
+
+ def display_help
+ puts command_usage
+ exit
+ end
+
+ def convert_options!
+ # make sure certain options are valid and in correct form.
+ @options[:instances] = Integer(@options[:instances]) if @options[:instances]
+ @options[:instance] = Integer(@options[:instance]) if @options[:instance]
+ end
+
+ def set_cmd(namespace, action, args_range=0)
+ return if @help_only
+ unless args_range == "*" || args_range.is_a?(Range)
+ args_range = (args_range.to_i..args_range.to_i)
+ end
+
+ if args_range == "*" || args_range.include?(@args.size)
+ @namespace = namespace
+ @action = action
+ else
+ @exit_status = false
+ if @args.size > args_range.last
+ usage_error("Too many arguments for [#{action}]: %s" % [ @args[args_range.last..-1].map{|a| "'#{a}'"}.join(', ') ])
+ else
+ usage_error("Not enough arguments for [#{action}]")
+ end
+ end
+ end
+
+ def parse_command!
+ # just return if already set, happends with -v, -h
+ return if @namespace && @action
+
+ verb = @args.shift
+ case verb
+
+ when 'version'
+ usage('vmc version')
+ set_cmd(:misc, :version)
+
+ when 'target'
+ usage('vmc target [url] [--url]')
+ if @args.size == 1
+ set_cmd(:misc, :set_target, 1)
+ else
+ set_cmd(:misc, :target)
+ end
+
+ when 'targets'
+ usage('vmc targets')
+ set_cmd(:misc, :targets)
+
+ when 'tokens'
+ usage('vmc tokens')
+ set_cmd(:misc, :tokens)
+
+ when 'info'
+ usage('vmc info')
+ set_cmd(:misc, :info)
+
+ when 'runtimes'
+ usage('vmc runtimes')
+ set_cmd(:misc, :runtimes)
+
+ when 'frameworks'
+ usage('vmc frameworks')
+ set_cmd(:misc, :frameworks)
+
+ when 'user'
+ usage('vmc user')
+ set_cmd(:user, :info)
+
+ when 'login'
+ usage('vmc login [email] [--email EMAIL] [--passwd PASS]')
+ if @args.size == 1
+ set_cmd(:user, :login, 1)
+ else
+ set_cmd(:user, :login)
+ end
+
+ when 'logout'
+ usage('vmc logout')
+ set_cmd(:user, :logout)
+
+ when 'passwd'
+ usage('vmc passwd')
+ if @args.size == 1
+ set_cmd(:user, :change_password, 1)
+ else
+ set_cmd(:user, :change_password)
+ end
+
+ when 'add-user', 'add_user', 'create_user', 'create-user', 'register'
+ usage('vmc add-user [user] [--email EMAIL] [--passwd PASS]')
+ if @args.size == 1
+ set_cmd(:admin, :add_user, 1)
+ else
+ set_cmd(:admin, :add_user)
+ end
+
+ when 'delete-user', 'delete_user', 'unregister'
+ usage('vmc delete-user <user>')
+ set_cmd(:admin, :delete_user, 1)
+
+ when 'apps'
+ usage('vmc apps')
+ set_cmd(:apps, :apps)
+
+ when 'list'
+ usage('vmc list')
+ set_cmd(:apps, :list)
+
+ when 'start'
+ usage('vmc start <appname>')
+ set_cmd(:apps, :start, 1)
+
+ when 'stop'
+ usage('vmc stop <appname>')
+ set_cmd(:apps, :stop, 1)
+
+ when 'restart'
+ usage('vmc restart <appname>')
+ set_cmd(:apps, :restart, 1)
+
+ when 'rename'
+ usage('vmc rename <appname> <newname>')
+ set_cmd(:apps, :rename, 2)
+
+ when 'mem'
+ usage('vmc mem <appname> [memsize]')
+ if @args.size == 2
+ set_cmd(:apps, :mem, 2)
+ else
+ set_cmd(:apps, :mem, 1)
+ end
+
+ when 'stats'
+ usage('vmc stats <appname>')
+ set_cmd(:apps, :stats, 1)
+
+ when 'map'
+ usage('vmc map <appname> <url>')
+ set_cmd(:apps, :map, 2)
+
+ when 'unmap'
+ usage('vmc unmap <appname> <url>')
+ set_cmd(:apps, :unmap, 2)
+
+ when 'delete'
+ usage('vmc delete <appname>')
+ if @options[:all] && @args.size == 0
+ set_cmd(:apps, :delete)
+ else
+ set_cmd(:apps, :delete, 1)
+ end
+
+ when 'files'
+ usage('vmc files <appname> [path] [--instance N] [--all] [--prefix]')
+ if @args.size == 1
+ set_cmd(:apps, :files, 1)
+ else
+ set_cmd(:apps, :files, 2)
+ end
+
+ when 'logs'
+ usage('vmc logs <appname> [--instance N] [--all] [--prefix]')
+ set_cmd(:apps, :logs, 1)
+
+ when 'instances', 'scale'
+ if @args.size == 1
+ usage('vmc instances <appname>')
+ set_cmd(:apps, :instances, 1)
+ else
+ usage('vmc instances <appname> <num|delta>')
+ set_cmd(:apps, :instances, 2)
+ end
+
+ when 'crashes'
+ usage('vmc crashes <appname>')
+ set_cmd(:apps, :crashes, 1)
+
+ when 'crashlogs'
+ usage('vmc crashlogs <appname>')
+ set_cmd(:apps, :crashlogs, 1)
+
+ when 'push'
+ usage('vmc push [appname] [--path PATH] [--url URL] [--instances N] [--mem] [--no-start]')
+ if @args.size == 1
+ set_cmd(:apps, :push, 1)
+ else
+ set_cmd(:apps, :push, 0)
+ end
+
+ when 'update'
+ usage('vmc update <appname> [--path PATH]')
+ set_cmd(:apps, :update, 1)
+
+ when 'services'
+ usage('vmc services')
+ set_cmd(:services, :services)
+
+ when 'env'
+ usage('vmc env <appname>')
+ set_cmd(:apps, :environment, 1)
+
+ when 'env-add'
+ usage('vmc env-add <appname> <variable[=]value>')
+ if @args.size == 2
+ set_cmd(:apps, :environment_add, 2)
+ elsif @args.size == 3
+ set_cmd(:apps, :environment_add, 3)
+ end
+
+ when 'env-del'
+ usage('vmc env-del <appname> <variable>')
+ set_cmd(:apps, :environment_del, 2)
+
+ when 'create-service', 'create_service'
+ usage('vmc create-service [service] [servicename] [appname] [--name servicename] [--bind appname]')
+ set_cmd(:services, :create_service) if @args.size == 0
+ set_cmd(:services, :create_service, 1) if @args.size == 1
+ set_cmd(:services, :create_service, 2) if @args.size == 2
+ set_cmd(:services, :create_service, 3) if @args.size == 3
+
+ when 'delete-service', 'delete_service'
+ usage('vmc delete-service <service>')
+ if @args.size == 1
+ set_cmd(:services, :delete_service, 1)
+ else
+ set_cmd(:services, :delete_service)
+ end
+
+ when 'bind-service', 'bind_service'
+ usage('vmc bind-service <servicename> <appname>')
+ set_cmd(:services, :bind_service, 2)
+
+ when 'unbind-service', 'unbind_service'
+ usage('vmc unbind-service <servicename> <appname>')
+ set_cmd(:services, :unbind_service, 2)
+
+ when 'clone-services'
+ usage('vmc clone-services <src-app> <dest-app>')
+ set_cmd(:services, :clone_services, 2)
+
+ when 'aliases'
+ usage('vmc aliases')
+ set_cmd(:misc, :aliases)
+
+ when 'alias'
+ usage('vmc alias <alias[=]command>')
+ if @args.size == 1
+ set_cmd(:misc, :alias, 1)
+ elsif @args.size == 2
+ set_cmd(:misc, :alias, 2)
+ end
+
+ when 'unalias'
+ usage('vmc unalias <alias>')
+ set_cmd(:misc, :unalias, 1)
+
+ when 'help'
+ display_help if @args.size == 0
+ @help_only = true
+ parse_command!
+
+ when 'usage'
+ display basic_usage
+ exit(true)
+
+ when 'options'
+ # Simulate --options
+ @args = @args.unshift('--options')
+ parse_options!
+
+ else
+ if verb
+ display "vmc: Unknown command [#{verb}]"
+ display basic_usage
+ exit(false)
+ end
+ end
+ end
+
+ def process_aliases!
+ return if @args.empty?
+ aliases = VMC::Cli::Config.aliases
+ aliases.each_pair do |k,v|
+ if @args[0] == k
+ display "[#{@args[0]} aliased to #{aliases.invert[key]}]" if @options[:verbose]
+ @args[0] = v
+ break;
+ end
+ end
+ end
+
+ def usage(msg = nil)
+ @usage = msg if msg
+ @usage
+ end
+
+ def usage_error(msg = nil)
+ @usage_error = msg if msg
+ @usage_error
+ end
+
+ def run
+
+ trap('TERM') { print "\nInterrupted\n"; exit(false)}
+ trap('INT') { print "\nInterrupted\n"; exit(false)}
+
+ parse_options!
+
+ @options[:colorize] = false unless STDOUT.tty?
+
+ VMC::Cli::Config.colorize = @options.delete(:colorize)
+ VMC::Cli::Config.nozip = @options.delete(:nozip)
+ VMC::Cli::Config.trace = @options.delete(:trace)
+ VMC::Cli::Config.output ||= STDOUT unless @options[:quiet]
+
+ process_aliases!
+ parse_command!
+
+ if @namespace && @action
+ eval("VMC::Cli::Command::#{@namespace.to_s.capitalize}").new(@options).send(@action.to_sym, *@args)
+ elsif @help_only || @usage
+ display_usage
+ else
+ display basic_usage
+ exit(false)
+ end
+
+ rescue OptionParser::InvalidOption => e
+ rescue OptionParser::AmbiguousOption => e
+ puts(e.message.red)
+ puts("\n")
+ puts(basic_usage)
+ @exit_status = false
+ rescue VMC::Client::AuthError => e
+ if VMC::Cli::Config.auth_token.nil?
+ puts "Login Required".red
+ else
+ puts "Not Authorized".red
+ end
+ @exit_status = false
+ rescue VMC::Client::TargetError, VMC::Client::NotFound, VMC::Client::BadTarget => e
+ puts e.message.red
+ @exit_status = false
+ rescue VMC::Client::HTTPException => e
+ puts e.message.red
+ @exit_status = false
+ rescue VMC::Cli::GracefulExit => e
+ # Redirected commands end up generating this exception (kind of goto)
+ rescue VMC::Cli::CliExit => e
+ puts e.message.red
+ @exit_status = false
+ rescue VMC::Cli::CliError => e
+ say("Error #{e.error_code}: #{e.message}".red)
+ @exit_status = false
+ rescue SystemExit => e
+ @exit_status = e.success?
+ rescue SyntaxError => e
+ puts e.message.red
+ puts e.backtrace
+ @exit_status = false
+ rescue => e
+ puts e.message.red
+ puts e.backtrace
+ @exit_status = false
+ ensure
+ say("\n")
+ @exit_status == true if @exit_status.nil?
+ if @options[:verbose]
+ if @exit_status
+ puts "[#{@namespace}:#{@action}] SUCCEEDED".green
+ else
+ puts "[#{@namespace}:#{@action}] FAILED".red
+ end
+ say("\n")
+ end
+ exit(@exit_status)
+ end
+
+end
74 lib/cli/services_helper.rb
@@ -0,0 +1,74 @@
+
+module VMC::Cli
+ module ServicesHelper
+ def display_system_services(services=nil)
+ services ||= client.services_info
+
+ display "\n============== System Services ==============\n\n"
+