├── .gitignore ├── .gitmodules ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── TODO ├── af.gemspec ├── bin └── af ├── caldecott_helper ├── Gemfile ├── Gemfile.lock └── server.rb ├── config ├── clients.yml └── micro │ ├── offline.conf │ ├── paths.yml │ └── refresh_ip.rb ├── lib ├── cli.rb ├── cli │ ├── commands │ │ ├── admin.rb │ │ ├── apps.rb │ │ ├── base.rb │ │ ├── manifest.rb │ │ ├── micro.rb │ │ ├── misc.rb │ │ ├── services.rb │ │ └── user.rb │ ├── config.rb │ ├── console_helper.rb │ ├── core_ext.rb │ ├── errors.rb │ ├── file_helper.rb │ ├── frameworks.rb │ ├── manifest_helper.rb │ ├── runner.rb │ ├── services_helper.rb │ ├── tunnel_helper.rb │ ├── usage.rb │ ├── version.rb │ └── zip_util.rb ├── vmc.rb └── vmc │ ├── client.rb │ ├── const.rb │ ├── micro.rb │ └── micro │ ├── switcher │ ├── base.rb │ ├── darwin.rb │ ├── dummy.rb │ ├── linux.rb │ └── windows.rb │ └── vmrun.rb └── spec ├── assets ├── app_info.txt ├── app_listings.txt ├── app_not_found.txt ├── bad_create_app.txt ├── console_access.txt ├── delete_app.txt ├── global_service_listings.txt ├── good_create_app.txt ├── good_create_service.txt ├── info_authenticated.txt ├── info_nil_usage.txt ├── info_return.txt ├── info_return_bad.txt ├── invalid_console_access.txt ├── list_users.txt ├── login_fail.txt ├── login_success.txt ├── manifests │ ├── bad-manifest.yml │ ├── my-manifest.yml │ ├── someapp │ │ ├── manifest.yml │ │ └── somedir │ │ │ └── somesubdir │ │ │ └── .gitignore │ ├── somenomanifestapp │ │ └── .gitignore │ ├── sub-manifest.yml │ └── sym-manifest.yml ├── resources_return.txt ├── sample_token.txt ├── service_already_exists.txt ├── service_gateway_fail.txt ├── service_listings.txt ├── service_not_found.txt ├── standalone_app_info.txt └── user_info.txt ├── spec_helper.rb └── unit ├── cli_opts_spec.rb ├── client_spec.rb ├── command_admin_spec.rb ├── command_apps_spec.rb ├── command_info_spec.rb ├── command_services_spec.rb ├── command_user_spec.rb ├── console_helper_spec.rb ├── file_helper_spec.rb ├── frameworks_spec.rb ├── manifests_spec.rb ├── micro_cmd_spec.rb ├── services_helper_spec.rb ├── switcher_spec.rb └── vmrun_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | !caldecott_helper/Gemfile.lock 3 | .build 4 | *.gem 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/assets/tests"] 2 | path = spec/assets/tests 3 | url = https://github.com/cloudfoundry/vcap-test-assets.git 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011 VMware Inc, All Rights Reserved 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | This software downloads additional open source software components upon install 22 | that are distributed under separate terms and conditions. Please see the license 23 | information provided in the individual software components for more information. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AF 2 | 3 | The AppFog CLI. This is the command line interface to AppFog.com 4 | 5 | af is based on vmc but will have features specific to the AppFog service as well as having the default target set to AppFog's service 6 | 7 | ## Installation 8 | 9 | There are two ways to install af. Most users should install the RubyGem. 10 | 11 | $ sudo gem install af 12 | 13 | You can also check out the source for development. You will need [Bundler](http://gembundler.com/) to build af. 14 | 15 | $ git clone https://github.com/appfog/af.git 16 | $ cd af 17 | $ bundle install 18 | 19 | ## Usage 20 | 21 | _Copyright 2010-2012, VMware, Inc. Licensed under the 22 | MIT license, please see the LICENSE file. All rights reserved._ 23 | 24 | Usage: af [options] command [] [command_options] 25 | Try 'af help [command]' or 'af help options' for more information. 26 | 27 | Currently available af commands are: 28 | 29 | Getting Started 30 | target [url] Reports current target or sets a new target 31 | login [email] [--email, --passwd] Login 32 | info System and account information 33 | 34 | Applications 35 | apps List deployed applications 36 | 37 | Application Creation 38 | push [appname] Create, push, map, and start a new application 39 | push [appname] --infra Push application to specified infrastructure 40 | push [appname] --path Push application from specified path 41 | push [appname] --url Set the url for the application 42 | push [appname] --instances Set the expected number of instances 43 | push [appname] --mem M Set the memory reservation for the application 44 | push [appname] --no-start Do not auto-start the application 45 | push [appname] --label Add specified label to app revision record 46 | 47 | Application Download 48 | pull [path] Downloads last pushed source to or [path] 49 | 50 | Application Operations 51 | start Start the application 52 | stop Stop the application 53 | restart Restart the application 54 | delete Delete the application 55 | 56 | Application Updates 57 | update [--path] [--label] Update the application bits, with optional revision label 58 | mem [memsize] Update the memory reservation for an application 59 | map Register the application to the url 60 | unmap Unregister the application from the url 61 | instances Scale the application instances up or down 62 | rename Change the application's name 63 | 64 | Application Information 65 | crashes List recent application crashes 66 | crashlogs Display log information for crashed applications 67 | logs [--all] Display log information for the application 68 | files [path] [--all] Display directory listing or file download for path 69 | stats Display resource usage for the application 70 | instances List application instances 71 | history Show version history of the application 72 | diff Compare current directory with deployed application 73 | hash [path] [--full] Compute hash of directory, defaults to current 74 | 75 | Application Environment 76 | env List application environment variables 77 | env-add Add an environment variable to an application 78 | env-del Delete an environment variable to an application 79 | 80 | Services 81 | services Lists of services available and provisioned 82 | create-service [--name,--bind] Create a provisioned service 83 | create-service --infra Create a provisioned service on a specified infrastructure 84 | create-service Create a provisioned service and assign it 85 | create-service Create a provisioned service and assign it , and bind to 86 | delete-service [servicename] Delete a provisioned service 87 | bind-service Bind a service to an application 88 | unbind-service Unbind service from the application 89 | clone-services Clone service bindings from application to 90 | tunnel [--port] Create a local tunnel to a service 91 | tunnel Create a local tunnel to a service and start a local client 92 | 93 | Administration 94 | user Display user account information 95 | passwd Change the password for the current user 96 | logout Logs current user out of the target system 97 | add-user [--email, --passwd] Register a new user (requires admin privileges) 98 | delete-user Delete a user and all apps and services (requires admin privileges) 99 | 100 | System 101 | runtimes Display the supported runtimes of the target system 102 | frameworks Display the recognized frameworks of the target system 103 | infras Display the available infrastructures 104 | 105 | Micro Cloud Foundry 106 | micro status Display Micro Cloud Foundry VM status 107 | micro offline Configure Micro Cloud Foundry VM for offline mode 108 | micro online Configure Micro Cloud Foundry VM for online mode 109 | [--vmx file] Path to micro.vmx 110 | [--vmrun executable] Path to vmrun executable 111 | [--password cleartext] Cleartext password for guest VM vcap user 112 | [--save] Save cleartext password in ~/.vmc_micro 113 | 114 | Misc 115 | aliases List aliases 116 | alias Create an alias for a command 117 | unalias Remove an alias 118 | targets List known targets and associated authorization tokens 119 | 120 | Help 121 | help [command] Get general help or help on a specific command 122 | help options Get help on available options 123 | 124 | ## Sample Usage (for PHP apps) 125 | 126 | $ af login developer@example.com 127 | Attempting login to [https://api.appfog.com] 128 | Password: ********* 129 | Successfully logged into [https://api.appfog.com] 130 | 131 | $ af push 132 | Would you like to deploy from the current directory? [Yn]: Y 133 | Application Name: myapp 134 | Detected a PHP Application, is this correct? [Yn]: 135 | 1: AWS US East - Virginia 136 | 2: AWS EU West - Ireland 137 | 3: AWS Asia SE - Singapore 138 | 4: Rackspace AZ 1 - Dallas 139 | 5: HP AZ 2 - Las Vegas 140 | Select Infrastructure: 1 141 | Application Deployed URL [myapp.aws.af.cm]: 142 | Memory reservation (128M, 256M, 512M, 1G, 2G) [128M]: 143 | How many instances? [1]: 144 | Bind existing services to 'myapp'? [yN]: 145 | Create services to bind to 'myapp'? [yN]: 146 | Would you like to save this configuration? [yN]: 147 | Creating Application: OK 148 | Uploading Application: 149 | Checking for available resources: OK 150 | Processing resources: OK 151 | Packing application: OK 152 | Uploading (6K): OK 153 | Push Status: OK 154 | Staging Application 'myapp': OK 155 | Starting Application 'myapp': OK 156 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'spec/rake/spectask' 3 | 4 | desc "Run specs" 5 | task :spec => :build do 6 | Spec::Rake::SpecTask.new('spec') do |t| 7 | t.spec_opts = %w(-fs -c) 8 | t.spec_files = FileList['spec/**/*_spec.rb'] 9 | end 10 | end 11 | 12 | desc "Synonym for spec" 13 | task :test => :spec 14 | desc "Synonym for spec" 15 | task :tests => :spec 16 | task :default => :spec 17 | 18 | def tests_path 19 | if @tests_path == nil 20 | @tests_path = File.join(Dir.pwd, "spec/assets/tests") 21 | end 22 | @tests_path 23 | end 24 | TESTS_PATH = tests_path 25 | 26 | BUILD_ARTIFACT = File.join(Dir.pwd, "spec/assets/.build") 27 | 28 | TESTS_TO_BUILD = ["#{TESTS_PATH}/java_web/java_tiny_app", 29 | # "#{TESTS_PATH}/grails/guestbook", 30 | "#{TESTS_PATH}/lift/hello_lift", 31 | "#{TESTS_PATH}/spring/roo-guestbook", 32 | "#{TESTS_PATH}/spring/spring-osgi-hello", 33 | "#{TESTS_PATH}/standalone/java_app", 34 | "#{TESTS_PATH}/standalone/python_app" 35 | ] 36 | 37 | desc "Build the tests. If the git hash associated with the test assets has not changed, nothing is built. To force a build, invoke 'rake build[--force]'" 38 | task :build, [:force] do |t, args| 39 | sh('bundle install') 40 | sh('git submodule update --init') 41 | puts "\nBuilding tests" 42 | if build_required? args.force 43 | ENV['MAVEN_OPTS']="-XX:MaxPermSize=256M" 44 | TESTS_TO_BUILD.each do |test| 45 | puts "\tBuilding '#{test}'" 46 | Dir.chdir test do 47 | sh('mvn package -DskipTests') do |success, exit_code| 48 | unless success 49 | clear_build_artifact 50 | do_mvn_clean('-q') 51 | fail "\tFailed to build #{test} - aborting build" 52 | end 53 | end 54 | end 55 | puts "\tCompleted building '#{test}'" 56 | end 57 | save_git_hash 58 | else 59 | puts "Built artifacts in sync with test assets - no build required" 60 | end 61 | end 62 | 63 | desc "Clean the build artifacts" 64 | task :clean do 65 | puts "\nCleaning tests" 66 | clear_build_artifact 67 | TESTS_TO_BUILD.each do |test| 68 | puts "\tCleaning '#{test}'" 69 | Dir.chdir test do 70 | do_mvn_clean 71 | end 72 | puts "\tCompleted cleaning '#{test}'" 73 | end 74 | end 75 | 76 | def build_required? (force_build=nil) 77 | if File.exists?(BUILD_ARTIFACT) == false or (force_build and force_build == "--force") 78 | return true 79 | end 80 | Dir.chdir(tests_path) do 81 | saved_git_hash = IO.readlines(BUILD_ARTIFACT)[0].split[0] 82 | git_hash = `git rev-parse --short=8 --verify HEAD` 83 | saved_git_hash.to_s.strip != git_hash.to_s.strip 84 | end 85 | end 86 | 87 | def save_git_hash 88 | Dir.chdir(tests_path) do 89 | git_hash = `git rev-parse --short=8 --verify HEAD` 90 | File.open(BUILD_ARTIFACT, 'w') {|f| f.puts("#{git_hash}")} 91 | end 92 | end 93 | 94 | def clear_build_artifact 95 | puts "\tClearing build artifact #{BUILD_ARTIFACT}" 96 | File.unlink BUILD_ARTIFACT if File.exists? BUILD_ARTIFACT 97 | end 98 | 99 | def do_mvn_clean options=nil 100 | sh("mvn clean #{options}") 101 | end 102 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 2 | 1. Use json load trick to load faster if available, fallback to json_pure. Change :symbols 3 | 2. [DONE] Don't flush on :clear for percentage counter 4 | 3. [DONE] Add --no-resource-check, should not do anything with war file but send it, e.g. no pack/unpack 5 | 4. [DONE] Do auto-check on size of stuff to send? Don't bother with resources if small? 6 | 5. [DONE] Do --log-prefix and also add --all to logs command[s] 7 | 6. [DONE] fix aliases with no file 8 | 7. [DONE] See if loading classes inside of requires save startup times. 9 | 8. Add timeout for vmair? 10 | 9. [DONE] register auto logs in if not logged in 11 | 10. [DONE] Fix continous errors on push while checking start (start method) 12 | 11. [DONE] zip filters [~ .idea, etc] 13 | 12. [DONE] Make work with json on 1.9.2 14 | 13. [DONE] Go back and match README 15 | 14. Delete service remove from all apps? Causes 500 on actions to app afterwards. 16 | -------------------------------------------------------------------------------- /af.gemspec: -------------------------------------------------------------------------------- 1 | 2 | $:.unshift File.expand_path("../lib", __FILE__) 3 | 4 | require 'cli/version' 5 | 6 | spec = Gem::Specification.new do |s| 7 | s.name = "af" 8 | s.version = VMC::Cli::VERSION 9 | s.author = "AppFog" 10 | s.email = "support@appfog.com" 11 | s.homepage = "http://appfog.com" 12 | s.description = s.summary = "AppFog.com CLI" 13 | s.executables = %w(af) 14 | 15 | s.platform = Gem::Platform::RUBY 16 | s.extra_rdoc_files = ["README.md", "LICENSE"] 17 | 18 | s.add_dependency "caldecott", "= 0.0.5" 19 | s.add_dependency "json_pure", ">= 1.5.1", "< 1.7.0" 20 | s.add_dependency "rubyzip", "~> 0.9.4" 21 | s.add_dependency "rest-client", ">= 1.6.1", "< 1.7.0" 22 | s.add_dependency "terminal-table", "~> 1.4.2" 23 | s.add_dependency "interact", "~> 0.4.0" 24 | s.add_dependency "addressable", "~> 2.2.6" 25 | s.add_dependency "uuidtools", "~> 2.1.0" 26 | s.add_dependency "rb-readline", "~> 0.4.2" 27 | 28 | s.add_development_dependency "rake" 29 | s.add_development_dependency "rspec", "~> 1.3.0" 30 | s.add_development_dependency "webmock", "~> 1.5.0" 31 | 32 | s.bindir = "bin" 33 | s.require_path = 'lib' 34 | s.files = %w(LICENSE README.md Rakefile) + Dir.glob("{config,lib,caldecott_helper}/**/*") 35 | end 36 | -------------------------------------------------------------------------------- /bin/af: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path('../../lib/cli', __FILE__) 4 | 5 | VMC::Cli::Runner.run(ARGV.dup) 6 | 7 | -------------------------------------------------------------------------------- /caldecott_helper/Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'rack', '~> 1.2.0' 4 | gem 'caldecott', '= 0.0.3' 5 | gem 'bundler' 6 | gem 'em-websocket' 7 | gem 'async_sinatra' 8 | gem 'thin' 9 | gem 'json' 10 | gem 'uuidtools' 11 | -------------------------------------------------------------------------------- /caldecott_helper/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | addressable (2.2.6) 5 | async_sinatra (0.5.0) 6 | rack (>= 1.2.1) 7 | sinatra (>= 1.0) 8 | caldecott (0.0.3) 9 | addressable (= 2.2.6) 10 | async_sinatra (= 0.5.0) 11 | em-http-request (= 0.3.0) 12 | em-websocket (= 0.3.1) 13 | json (= 1.6.1) 14 | uuidtools (= 2.1.2) 15 | daemons (1.1.4) 16 | em-http-request (0.3.0) 17 | addressable (>= 2.0.0) 18 | escape_utils 19 | eventmachine (>= 0.12.9) 20 | em-websocket (0.3.1) 21 | addressable (>= 2.1.1) 22 | eventmachine (>= 0.12.9) 23 | escape_utils (0.2.4) 24 | eventmachine (0.12.10) 25 | json (1.6.1) 26 | rack (1.2.4) 27 | sinatra (1.2.7) 28 | rack (~> 1.1) 29 | tilt (>= 1.2.2, < 2.0) 30 | thin (1.2.11) 31 | daemons (>= 1.0.9) 32 | eventmachine (>= 0.12.6) 33 | rack (>= 1.0.0) 34 | tilt (1.3.3) 35 | uuidtools (2.1.2) 36 | 37 | PLATFORMS 38 | ruby 39 | 40 | DEPENDENCIES 41 | async_sinatra 42 | bundler 43 | caldecott (= 0.0.3) 44 | em-websocket 45 | json 46 | rack (~> 1.2.0) 47 | thin 48 | uuidtools 49 | -------------------------------------------------------------------------------- /caldecott_helper/server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) 2009-2011 VMware, Inc. 3 | $:.unshift(File.dirname(__FILE__) + '/lib') 4 | 5 | require 'rubygems' 6 | require 'bundler/setup' 7 | 8 | require 'caldecott' 9 | require 'sinatra' 10 | require 'json' 11 | require 'eventmachine' 12 | 13 | port = ENV['VMC_APP_PORT'] 14 | port ||= 8081 15 | 16 | # add vcap specific stuff to Caldecott 17 | class VcapHttpTunnel < Caldecott::Server::HttpTunnel 18 | get '/info' do 19 | { "version" => '0.0.4' }.to_json 20 | end 21 | 22 | def self.get_tunnels 23 | super 24 | end 25 | 26 | get '/services' do 27 | services_env = ENV['VMC_SERVICES'] 28 | return "no services env" if services_env.nil? or services_env.empty? 29 | services_env 30 | end 31 | 32 | get '/services/:service' do |service_name| 33 | services_env = ENV['VMC_SERVICES'] 34 | not_found if services_env.nil? 35 | 36 | services = JSON.parse(services_env) 37 | service = services.find { |s| s["name"] == service_name } 38 | not_found if service.nil? 39 | service["options"].to_json 40 | end 41 | end 42 | 43 | VcapHttpTunnel.run!(:port => port, :auth_token => ENV["CALDECOTT_AUTH"]) 44 | -------------------------------------------------------------------------------- /config/clients.yml: -------------------------------------------------------------------------------- 1 | redis: 2 | redis-cli: -h ${host} -p ${port} -a ${password} 3 | 4 | mysql: 5 | mysql: --protocol=TCP --host=${host} --port=${port} --user=${user} --password=${password} ${name} 6 | mysqldump: --protocol=TCP --host=${host} --port=${port} --user=${user} --password=${password} ${name} > ${Output file} 7 | 8 | mongodb: 9 | mongo: --host ${host} --port ${port} -u ${user} -p ${password} ${name} 10 | mongodump: --host ${host} --port ${port} -u ${user} -p ${password} --db ${name} 11 | mongorestore: --host ${host} --port ${port} -u ${user} -p ${password} --db ${name} ${Directory or filename to restore from} 12 | 13 | postgresql: 14 | psql: 15 | command: -h ${host} -p ${port} -d ${name} -U ${user} -w 16 | environment: 17 | - PGPASSWORD='${password}' 18 | -------------------------------------------------------------------------------- /config/micro/offline.conf: -------------------------------------------------------------------------------- 1 | no-resolv 2 | log-queries 3 | -------------------------------------------------------------------------------- /config/micro/paths.yml: -------------------------------------------------------------------------------- 1 | darwin: 2 | vmrun: 3 | - "/Applications/VMware Fusion.app/Contents/Library/" 4 | - "/Applications/Fusion.app/Contents/Library/" 5 | vmx: 6 | - "~/Documents/Virtual Machines.localized/" 7 | - "~/Documents/Virtual Machines/" 8 | - "~/Desktop/" 9 | 10 | linux: 11 | vmrun: 12 | - "/usr/bin/" 13 | vmx: 14 | - "~/" 15 | 16 | windows: 17 | vmrun: 18 | - "c:\\Program Files (x86)\\" 19 | - "c:\\Program Files\\" 20 | vmx: 21 | - "~\\Documents\\" 22 | - "~\\Desktop\\" 23 | -------------------------------------------------------------------------------- /config/micro/refresh_ip.rb: -------------------------------------------------------------------------------- 1 | #!/var/vcap/bosh/bin/ruby 2 | require 'socket' 3 | 4 | A_ROOT_SERVER = '198.41.0.4' 5 | 6 | begin 7 | retries ||= 0 8 | route ||= A_ROOT_SERVER 9 | orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true 10 | ip_address = UDPSocket.open {|s| s.connect(route, 1); s.addr.last } 11 | rescue Errno::ENETUNREACH 12 | # happens on boot when dhcp hasn't completed when we get here 13 | sleep 3 14 | retries += 1 15 | retry if retries < 10 16 | ensure 17 | Socket.do_not_reverse_lookup = orig 18 | end 19 | 20 | File.open("/tmp/ip.txt", 'w') { |file| file.write(ip_address) } 21 | -------------------------------------------------------------------------------- /lib/cli.rb: -------------------------------------------------------------------------------- 1 | require "rbconfig" 2 | 3 | ROOT = File.expand_path(File.dirname(__FILE__)) 4 | WINDOWS = !!(RbConfig::CONFIG['host_os'] =~ /mingw|mswin32|cygwin/) 5 | 6 | module VMC 7 | autoload :Client, "#{ROOT}/vmc/client" 8 | autoload :Micro, "#{ROOT}/vmc/micro" 9 | 10 | module Micro 11 | module Switcher 12 | autoload :Base, "#{ROOT}/vmc/micro/switcher/base" 13 | autoload :Darwin, "#{ROOT}/vmc/micro/switcher/darwin" 14 | autoload :Dummy, "#{ROOT}/vmc/micro/switcher/dummy" 15 | autoload :Linux, "#{ROOT}/vmc/micro/switcher/linux" 16 | autoload :Windows, "#{ROOT}/vmc/micro/switcher/windows" 17 | end 18 | autoload :VMrun, "#{ROOT}/vmc/micro/vmrun" 19 | end 20 | 21 | module Cli 22 | autoload :Config, "#{ROOT}/cli/config" 23 | autoload :Framework, "#{ROOT}/cli/frameworks" 24 | autoload :Runner, "#{ROOT}/cli/runner" 25 | autoload :ZipUtil, "#{ROOT}/cli/zip_util" 26 | autoload :ServicesHelper, "#{ROOT}/cli/services_helper" 27 | autoload :TunnelHelper, "#{ROOT}/cli/tunnel_helper" 28 | autoload :ManifestHelper, "#{ROOT}/cli/manifest_helper" 29 | autoload :ConsoleHelper, "#{ROOT}/cli/console_helper" 30 | autoload :FileHelper, "#{ROOT}/cli/file_helper" 31 | 32 | module Command 33 | autoload :Base, "#{ROOT}/cli/commands/base" 34 | autoload :Admin, "#{ROOT}/cli/commands/admin" 35 | autoload :Apps, "#{ROOT}/cli/commands/apps" 36 | autoload :Micro, "#{ROOT}/cli/commands/micro" 37 | autoload :Misc, "#{ROOT}/cli/commands/misc" 38 | autoload :Services, "#{ROOT}/cli/commands/services" 39 | autoload :User, "#{ROOT}/cli/commands/user" 40 | autoload :Manifest, "#{ROOT}/cli/commands/manifest" 41 | end 42 | 43 | end 44 | end 45 | 46 | require "#{ROOT}/cli/version" 47 | require "#{ROOT}/cli/core_ext" 48 | require "#{ROOT}/cli/errors" 49 | -------------------------------------------------------------------------------- /lib/cli/commands/admin.rb: -------------------------------------------------------------------------------- 1 | module VMC::Cli::Command 2 | 3 | class Admin < Base 4 | 5 | def list_users 6 | users = client.users 7 | users.sort! {|a, b| a[:email] <=> b[:email] } 8 | return display JSON.pretty_generate(users || []) if @options[:json] 9 | 10 | display "\n" 11 | return display "No Users" if users.nil? || users.empty? 12 | 13 | users_table = table do |t| 14 | t.headings = 'Email', 'Admin', 'Apps' 15 | users.each do |user| 16 | t << [user[:email], user[:admin], user[:apps].map {|x| x[:name]}.join(', ')] 17 | end 18 | end 19 | display users_table 20 | end 21 | 22 | alias :users :list_users 23 | 24 | def add_user(email=nil) 25 | email ||= @options[:email] 26 | email ||= ask("Email") unless no_prompt 27 | password = @options[:password] 28 | unless no_prompt || password 29 | password = ask("Password", :echo => "*") 30 | password2 = ask("Verify Password", :echo => "*") 31 | err "Passwords did not match, try again" if password != password2 32 | end 33 | err "Need a valid email" unless email 34 | err "Need a password" unless password 35 | err "Passwords may not contain braces" if password =~ /[{}]/ 36 | display 'Creating New User: ', false 37 | client.add_user(email, password) 38 | display 'OK'.green 39 | 40 | # if we are not logged in for the current target, log in as the new user 41 | return unless VMC::Cli::Config.auth_token.nil? 42 | @options[:password] = password 43 | cmd = User.new(@options) 44 | cmd.login(email) 45 | end 46 | 47 | def delete_user(user_email) 48 | # Check to make sure all apps and services are deleted before deleting the user 49 | # implicit proxying 50 | 51 | client.proxy_for(user_email) 52 | @options[:proxy] = user_email 53 | apps = client.apps 54 | 55 | if (apps && !apps.empty?) 56 | unless no_prompt 57 | proceed = ask( 58 | "\nDeployed applications and associated services will be DELETED, continue?", 59 | :default => false 60 | ) 61 | err "Aborted" unless proceed 62 | end 63 | cmd = Apps.new(@options.merge({ :force => true })) 64 | apps.each { |app| cmd.delete(app[:name]) } 65 | end 66 | 67 | services = client.services 68 | if (services && !services.empty?) 69 | cmd = Services.new(@options) 70 | services.each { |s| cmd.delete_service(s[:name])} 71 | end 72 | 73 | display 'Deleting User: ', false 74 | client.proxy = nil 75 | client.delete_user(user_email) 76 | display 'OK'.green 77 | end 78 | 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /lib/cli/commands/base.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'interact' 3 | require 'terminal-table/import' 4 | 5 | module VMC::Cli 6 | 7 | module Command 8 | 9 | class Base 10 | include Interactive 11 | 12 | attr_reader :no_prompt, :prompt_ok 13 | 14 | MANIFEST = "manifest.yml" 15 | 16 | def initialize(options={}) 17 | @options = options.dup 18 | @no_prompt = @options[:noprompts] 19 | @prompt_ok = !no_prompt 20 | 21 | # Suppress colorize on Windows systems for now. 22 | if WINDOWS 23 | VMC::Cli::Config.colorize = false 24 | end 25 | 26 | @path = @options[:path] || '.' 27 | 28 | load_manifest manifest_file if manifest_file 29 | end 30 | 31 | def manifest_file 32 | return @options[:manifest] if @options[:manifest] 33 | return @manifest_file if @manifest_file 34 | 35 | where = File.expand_path(@path) 36 | while true 37 | if File.exists?(File.join(where, MANIFEST)) 38 | @manifest_file = File.join(where, MANIFEST) 39 | break 40 | elsif File.basename(where) == "/" 41 | @manifest_file = nil 42 | break 43 | else 44 | where = File.expand_path("../", where) 45 | end 46 | end 47 | 48 | @manifest_file 49 | end 50 | 51 | def load_manifest_structure(file) 52 | manifest = YAML.load_file file 53 | 54 | Array(manifest["inherit"]).each do |p| 55 | manifest = merge_parent(manifest, p) 56 | end 57 | 58 | if apps = manifest["applications"] 59 | apps.each do |k, v| 60 | client.infra = v['infra'] if v['infra'] 61 | abs = File.expand_path(k, file) 62 | if Dir.pwd.start_with? abs 63 | manifest = merge_manifest(manifest, v) 64 | end 65 | end 66 | end 67 | 68 | manifest 69 | end 70 | 71 | def resolve_manifest(manifest) 72 | if apps = manifest["applications"] 73 | apps.each_value do |v| 74 | resolve_lexically(v, [manifest]) 75 | end 76 | end 77 | 78 | resolve_lexically(manifest, [manifest]) 79 | end 80 | 81 | def load_manifest(file) 82 | @manifest = load_manifest_structure(file) 83 | resolve_manifest(@manifest) 84 | end 85 | 86 | def merge_parent(child, path) 87 | file = File.expand_path("../" + path, manifest_file) 88 | merge_manifest(child, load_manifest_structure(file)) 89 | end 90 | 91 | def merge_manifest(child, parent) 92 | merge = proc do |_, old, new| 93 | if new.is_a?(Hash) and old.is_a?(Hash) 94 | old.merge(new, &merge) 95 | else 96 | new 97 | end 98 | end 99 | 100 | parent.merge(child, &merge) 101 | end 102 | 103 | def resolve_lexically(val, ctx = [@manifest]) 104 | case val 105 | when Hash 106 | val.each_value do |v| 107 | resolve_lexically(v, [val] + ctx) 108 | end 109 | when Array 110 | val.each do |v| 111 | resolve_lexically(v, ctx) 112 | end 113 | when String 114 | val.gsub!(/\$\{([[:alnum:]\-]+)\}/) do 115 | resolve_symbol($1, ctx) 116 | end 117 | end 118 | 119 | nil 120 | end 121 | 122 | def resolve_symbol(sym, ctx) 123 | case sym 124 | when "target-base" 125 | target_base(ctx) 126 | 127 | when "target-url" 128 | target_url(ctx) 129 | 130 | when "random-word" 131 | "%04x" % [rand(0x0100000)] 132 | 133 | else 134 | found = find_symbol(sym, ctx) 135 | 136 | if found 137 | resolve_lexically(found, ctx) 138 | found 139 | else 140 | err(sym, "Unknown symbol in manifest: ") 141 | end 142 | end 143 | end 144 | 145 | def find_symbol(sym, ctx) 146 | ctx.each do |h| 147 | if val = resolve_in(h, sym) 148 | return val 149 | end 150 | end 151 | 152 | nil 153 | end 154 | 155 | def resolve_in(hash, *where) 156 | find_in_hash(hash, ["properties"] + where) || 157 | find_in_hash(hash, ["applications", @application] + where) || 158 | find_in_hash(hash, where) 159 | end 160 | 161 | def manifest(*where) 162 | resolve_in(@manifest, *where) 163 | end 164 | 165 | def find_in_hash(hash, where) 166 | what = hash 167 | where.each do |x| 168 | return nil unless what.is_a?(Hash) 169 | what = what[x] 170 | end 171 | 172 | what 173 | end 174 | 175 | def target_url(ctx = []) 176 | find_symbol("target", ctx) || 177 | (@client && @client.target) || 178 | VMC::Cli::Config.target_url 179 | end 180 | 181 | def target_base(ctx = []) 182 | VMC::Cli::Config.base_of(find_symbol("target", ctx) || "api.#{client.suggest_url}") 183 | end 184 | 185 | # Inject a client to help in testing. 186 | def client(cli=nil) 187 | @client ||= cli 188 | return @client if @client 189 | @client = VMC::Client.new(target_url, auth_token) 190 | @client.trace = VMC::Cli::Config.trace if VMC::Cli::Config.trace 191 | @client.proxy_for @options[:proxy] if @options[:proxy] 192 | @client 193 | end 194 | 195 | def client_info 196 | @client_info ||= client.info 197 | end 198 | 199 | def auth_token 200 | @auth_token = VMC::Cli::Config.auth_token(@options[:token_file]) 201 | end 202 | 203 | def runtimes_info 204 | return @runtimes if @runtimes 205 | info = client_info 206 | @runtimes = {} 207 | if info[:frameworks] 208 | info[:frameworks].each_value do |f| 209 | next unless f[:runtimes] 210 | f[:runtimes].each { |r| @runtimes[r[:name]] = r} 211 | end 212 | end 213 | @runtimes 214 | end 215 | 216 | def frameworks_info 217 | return @frameworks if @frameworks 218 | info = client_info 219 | @frameworks = [] 220 | if info[:frameworks] 221 | info[:frameworks].each_value { |f| @frameworks << [f[:name]] } 222 | end 223 | @frameworks 224 | end 225 | 226 | def default_infra 227 | "aws" 228 | end 229 | 230 | end 231 | end 232 | end 233 | 234 | -------------------------------------------------------------------------------- /lib/cli/commands/manifest.rb: -------------------------------------------------------------------------------- 1 | module VMC::Cli::Command 2 | class Manifest < Base 3 | include VMC::Cli::ManifestHelper 4 | 5 | def initialize(options) 6 | super 7 | 8 | # don't resolve any of the manifest template stuff 9 | if manifest_file 10 | @manifest = load_manifest_structure manifest_file 11 | else 12 | @manifest = {} 13 | end 14 | end 15 | 16 | def edit 17 | build_manifest 18 | save_manifest 19 | end 20 | 21 | def extend(which) 22 | parent = load_manifest_structure which 23 | @manifest = load_manifest_structure which 24 | 25 | build_manifest 26 | 27 | simplify(@manifest, parent) 28 | 29 | @manifest["inherit"] ||= [] 30 | @manifest["inherit"] << which 31 | 32 | save_manifest(ask("Save where?")) 33 | end 34 | 35 | private 36 | 37 | def simplify(child, parent) 38 | return unless child.is_a?(Hash) and parent.is_a?(Hash) 39 | 40 | child.reject! do |k, v| 41 | if v == parent[k] 42 | puts "rejecting #{k}" 43 | true 44 | else 45 | simplify(v, parent[k]) 46 | false 47 | end 48 | end 49 | end 50 | 51 | def build_manifest 52 | @application = ask("Configure for which application?", :default => ".") 53 | interact true 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/cli/commands/micro.rb: -------------------------------------------------------------------------------- 1 | module VMC::Cli::Command 2 | class Micro < Base 3 | 4 | def initialize(args) 5 | super(args) 6 | end 7 | 8 | def offline(mode) 9 | command('offline') 10 | end 11 | 12 | def online(mode) 13 | command('online') 14 | end 15 | 16 | def status(mode) 17 | command('status') 18 | end 19 | 20 | def command(cmd) 21 | config = build_config 22 | switcher(config).send(cmd) 23 | store_config(config) 24 | end 25 | 26 | def switcher(config) 27 | case Micro.platform 28 | when :darwin 29 | switcher = VMC::Micro::Switcher::Darwin.new(config) 30 | when :linux 31 | switcher = VMC::Micro::Switcher::Linux.new(config) 32 | when :windows 33 | switcher = VMC::Micro::Switcher::Windows.new(config) 34 | when :dummy # for testing only 35 | switcher = VMC::Micro::Switcher::Dummy.new(config) 36 | else 37 | err "unsupported platform: #{Micro.platform}" 38 | end 39 | end 40 | 41 | # Returns the configuration needed to run the micro related subcommands. 42 | # First loads saved config from file (if there is any), then overrides 43 | # loaded values with command line arguments, and finally tries to guess 44 | # in case neither was used: 45 | # vmx location of micro.vmx file 46 | # vmrun location of vmrun command 47 | # password password for vcap user (in the guest vm) 48 | # platform current platform 49 | def build_config 50 | conf = VMC::Cli::Config.micro # returns {} if there isn't a saved config 51 | 52 | override(conf, 'vmx', true) do 53 | locate_vmx(Micro.platform) 54 | end 55 | 56 | override(conf, 'vmrun', true) do 57 | VMC::Micro::VMrun.locate(Micro.platform) 58 | end 59 | 60 | override(conf, 'password') do 61 | @password = ask("Please enter your Micro Cloud Foundry VM password (vcap user) password", :echo => "*") 62 | end 63 | 64 | conf['platform'] = Micro.platform 65 | 66 | conf 67 | end 68 | 69 | # Save the cleartext password if --save is supplied. 70 | # Note: it is due to vix we have to use a cleartext password :( 71 | # Only if --password is used and not --save is the password deleted from the 72 | # config file before it is stored to disk. 73 | def store_config(config) 74 | if @options[:save] 75 | warn("cleartext password saved in: #{VMC::Cli::Config::MICRO_FILE}") 76 | elsif @options[:password] || @password 77 | config.delete('password') 78 | end 79 | 80 | VMC::Cli::Config.store_micro(config) 81 | end 82 | 83 | # override with command line arguments and yield the block in case the option isn't set 84 | def override(config, option, escape=false, &blk) 85 | # override if given on the command line 86 | if opt = @options[option.to_sym] 87 | opt = VMC::Micro.escape_path(opt) if escape 88 | config[option] = opt 89 | end 90 | config[option] = yield unless config[option] 91 | end 92 | 93 | def locate_vmx(platform) 94 | paths = YAML.load_file(VMC::Micro.config_file('paths.yml')) 95 | vmx_paths = paths[platform.to_s]['vmx'] 96 | vmx = VMC::Micro.locate_file('micro.vmx', 'micro', vmx_paths) 97 | err "Unable to locate micro.vmx, please supply --vmx option" unless vmx 98 | vmx 99 | end 100 | 101 | def self.platform 102 | case RUBY_PLATFORM 103 | when /darwin/ # x86_64-darwin11.2.0 104 | :darwin 105 | when /linux/ # x86_64-linux 106 | :linux 107 | when /mingw|mswin32|cygwin/ # i386-mingw32 108 | :windows 109 | else 110 | RUBY_PLATFORM 111 | end 112 | end 113 | 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/cli/commands/misc.rb: -------------------------------------------------------------------------------- 1 | module VMC::Cli::Command 2 | 3 | class Misc < Base 4 | def version 5 | say "af #{VMC::Cli::VERSION}" 6 | end 7 | 8 | def target 9 | return display JSON.pretty_generate({:target => target_url}) if @options[:json] 10 | banner "[#{target_url}]" 11 | end 12 | 13 | def targets 14 | targets = VMC::Cli::Config.targets 15 | return display JSON.pretty_generate(targets) if @options[:json] 16 | return display 'None specified' if targets.empty? 17 | targets_table = table do |t| 18 | t.headings = 'Target', 'Authorization' 19 | targets.each { |target, token| t << [target, token] } 20 | end 21 | display "\n" 22 | display targets_table 23 | end 24 | 25 | alias :tokens :targets 26 | 27 | def set_target(target_url) 28 | target_url = "https://#{target_url}" unless /^https?/ =~ target_url 29 | target_url = target_url.gsub(/\/+$/, '') 30 | client = VMC::Client.new(target_url) 31 | unless client.target_valid? 32 | if prompt_ok 33 | display "Host is not available or is not valid: '#{target_url}'".red 34 | show_response = ask "Would you like see the response?", 35 | :default => false 36 | display "\n<<<\n#{client.raw_info}\n>>>\n" if show_response 37 | end 38 | exit(false) 39 | else 40 | VMC::Cli::Config.store_target(target_url) 41 | say "Successfully targeted to [#{target_url}]".green 42 | end 43 | end 44 | 45 | def info 46 | info = client_info 47 | return display JSON.pretty_generate(info) if @options[:json] 48 | 49 | display "\n#{info[:description]}" 50 | display "For support visit #{info[:support]}" 51 | display "" 52 | display "Target: #{target_url} (v#{info[:version]})" 53 | display "Client: v#{VMC::Cli::VERSION}" 54 | if info[:user] 55 | display '' 56 | display "User: #{info[:user]}" 57 | end 58 | if usage = info[:usage] and limits = info[:limits] 59 | tmem = pretty_size(limits[:memory]*1024*1024) 60 | mem = pretty_size((usage[:memory] || 0)*1024*1024) 61 | tser = limits[:services] 62 | ser = usage[:services] 63 | tapps = limits[:apps] || 0 64 | apps = usage[:apps] || 0 65 | display "Usage: Memory (#{mem} of #{tmem} total)" 66 | display " Services (#{ser} of #{tser} total)" 67 | display " Apps (#{apps} of #{tapps} total)" if limits[:apps] 68 | end 69 | end 70 | 71 | def runtimes 72 | raise VMC::Client::AuthError unless client.logged_in? 73 | return display JSON.pretty_generate(runtimes_info) if @options[:json] 74 | return display "No Runtimes" if runtimes_info.empty? 75 | rtable = table do |t| 76 | t.headings = 'Name', 'Description', 'Version' 77 | runtimes_info.each_value { |rt| t << [rt[:name], rt[:description], rt[:version]] } 78 | end 79 | display "\n" 80 | display rtable 81 | end 82 | 83 | def infras 84 | infras_info = client.infras 85 | return display "Multiple infras not supported" if infras_info.empty? 86 | if infras_info.detect {|i| i.has_key?(:available) && i[:available] == false } # If one or more infras is unavailable 87 | itable = table do |t| 88 | t.headings = [ 'Name','Description','Message' ] 89 | infras_info.each { |i| t << [i[:infra], i[:description], i[:available] ? '' : 'Unavailable - ' + i[:unavail_message]] } 90 | end 91 | else # All infras are available 92 | itable = table do |t| 93 | t.headings = [ 'Name','Description' ] 94 | infras_info.each { |i| t << [i[:infra], i[:description]] } 95 | end 96 | end 97 | display "\n" 98 | display itable 99 | end 100 | 101 | def frameworks 102 | raise VMC::Client::AuthError unless client.logged_in? 103 | return display JSON.pretty_generate(frameworks_info) if @options[:json] 104 | return display "No Frameworks" if frameworks_info.empty? 105 | rtable = table do |t| 106 | t.headings = ['Name'] 107 | frameworks_info.each { |f| t << f } 108 | end 109 | display "\n" 110 | display rtable 111 | end 112 | 113 | def aliases 114 | aliases = VMC::Cli::Config.aliases 115 | return display JSON.pretty_generate(aliases) if @options[:json] 116 | return display "No Aliases" if aliases.empty? 117 | atable = table do |t| 118 | t.headings = 'Alias', 'Command' 119 | aliases.each { |k,v| t << [k, v] } 120 | end 121 | display "\n" 122 | display atable 123 | end 124 | 125 | def alias(k, v=nil) 126 | k,v = k.split('=') unless v 127 | aliases = VMC::Cli::Config.aliases 128 | aliases[k] = v 129 | VMC::Cli::Config.store_aliases(aliases) 130 | display "Successfully aliased '#{k}' to '#{v}'".green 131 | end 132 | 133 | def unalias(key) 134 | aliases = VMC::Cli::Config.aliases 135 | if aliases.has_key?(key) 136 | aliases.delete(key) 137 | VMC::Cli::Config.store_aliases(aliases) 138 | display "Successfully unaliased '#{key}'".green 139 | else 140 | display "Unknown alias '#{key}'".red 141 | end 142 | end 143 | 144 | end 145 | 146 | end 147 | 148 | -------------------------------------------------------------------------------- /lib/cli/commands/services.rb: -------------------------------------------------------------------------------- 1 | require "uuidtools" 2 | 3 | module VMC::Cli::Command 4 | 5 | class Services < Base 6 | include VMC::Cli::ServicesHelper 7 | include VMC::Cli::TunnelHelper 8 | 9 | def services 10 | ss = client.services_info 11 | ps = client.services 12 | ps.sort! {|a, b| a[:name] <=> b[:name] } 13 | 14 | if @options[:json] 15 | services = { :system => ss, :provisioned => ps } 16 | return display JSON.pretty_generate(services) 17 | end 18 | display_system_services(ss) 19 | display_provisioned_services(ps) 20 | end 21 | 22 | def create_service(service=nil, name=nil, appname=nil) 23 | 24 | unless no_prompt || service 25 | services = client.services_info 26 | err 'No services available to provision' if services.empty? 27 | service = ask( 28 | "Which service would you like to provision?", 29 | { :indexed => true, 30 | :choices => 31 | services.values.collect { |type| 32 | type.keys.collect(&:to_s) 33 | }.flatten 34 | } 35 | ) 36 | end 37 | name = @options[:name] unless name 38 | unless name 39 | name = random_service_name(service) 40 | picked_name = true 41 | end 42 | 43 | if client.infra_supported? 44 | unless no_prompt || @options[:infra] 45 | @options[:infra] = client.infra_name_for_description( 46 | ask("Select Infrastructure", 47 | :indexed => true, :choices => client.infra_descriptions)) 48 | end 49 | end 50 | 51 | create_service_banner(service, name, picked_name, @options[:infra]) 52 | appname = @options[:bind] unless appname 53 | bind_service_banner(name, appname) if appname 54 | end 55 | 56 | def delete_service(service=nil) 57 | unless no_prompt || service 58 | user_services = client.services 59 | err 'No services available to delete' if user_services.empty? 60 | service = ask( 61 | "Which service would you like to delete?", 62 | { :indexed => true, 63 | :choices => user_services.collect { |s| s[:name] } 64 | } 65 | ) 66 | end 67 | err "Service name required." unless service 68 | display "Deleting service [#{service}]: ", false 69 | client.delete_service(service) 70 | display 'OK'.green 71 | end 72 | 73 | def bind_service(service, appname) 74 | bind_service_banner(service, appname) 75 | end 76 | 77 | def unbind_service(service, appname) 78 | unbind_service_banner(service, appname) 79 | end 80 | 81 | def clone_services(src_app, dest_app) 82 | begin 83 | src = client.app_info(src_app) 84 | dest = client.app_info(dest_app) 85 | rescue 86 | end 87 | 88 | err "Application '#{src_app}' does not exist" unless src 89 | err "Application '#{dest_app}' does not exist" unless dest 90 | 91 | services = src[:services] 92 | err 'No services to clone' unless services && !services.empty? 93 | services.each { |service| bind_service_banner(service, dest_app, false) } 94 | check_app_for_restart(dest_app) 95 | end 96 | 97 | def export_service(service) 98 | display "Exporting data from '#{service}': ", false 99 | export_info = client.export_service(service) 100 | if export_info 101 | display 'OK'.green 102 | puts export_info[:uri] 103 | else 104 | err "Export data from '#{service}': failed" 105 | end 106 | end 107 | 108 | def import_service(service,url) 109 | display "Importing data into '#{service}': ", false 110 | import_info = client.import_service(service,url) 111 | if import_info 112 | display 'OK'.green 113 | else 114 | err "Import data into '#{service}' failed" 115 | end 116 | end 117 | 118 | def tunnel(service=nil, client_name=nil) 119 | unless defined? Caldecott 120 | display "To use `af tunnel', you must first install Caldecott:" 121 | display "" 122 | display "\tgem install caldecott" 123 | display "" 124 | display "Note that you'll need a C compiler. If you're on OS X, Xcode" 125 | display "will provide one. If you're on Windows, try DevKit." 126 | display "" 127 | display "This manual step will be removed in the future." 128 | display "" 129 | err "Caldecott is not installed." 130 | end 131 | 132 | ps = client.services 133 | err "No services available to tunnel to" if ps.empty? 134 | 135 | unless service 136 | choices = ps.collect { |s| s[:name] }.sort 137 | service = ask( 138 | "Which service to tunnel to?", 139 | :choices => choices, 140 | :indexed => true 141 | ) 142 | end 143 | 144 | info = ps.select { |s| s[:name] == service }.first 145 | 146 | err "Unknown service '#{service}'" unless info 147 | 148 | port = pick_tunnel_port(@options[:port] || 10000) 149 | 150 | raise VMC::Client::AuthError unless client.logged_in? 151 | 152 | infra_name = nil 153 | if client.infra_supported? 154 | infra_name = info[:infra] ? info[:infra][:name] : default_infra 155 | err "Infra '#{infra_name}' is not valid" unless client.infra_valid?(infra_name) 156 | end 157 | 158 | if not tunnel_pushed?(infra_name) 159 | display "Deploying tunnel application '#{tunnel_appname(infra_name)}'." 160 | auth = UUIDTools::UUID.random_create.to_s 161 | push_caldecott(auth,infra_name) 162 | bind_service_banner(service, tunnel_appname(infra_name), false) 163 | start_caldecott(infra_name) 164 | else 165 | auth = tunnel_auth(infra_name) 166 | end 167 | 168 | if not tunnel_healthy?(auth,infra_name) 169 | display "Redeploying tunnel application '#{tunnel_appname(infra_name)}'." 170 | 171 | # We don't expect caldecott not to be running, so take the 172 | # most aggressive restart method.. delete/re-push 173 | client.delete_app(tunnel_appname(infra_name)) 174 | invalidate_tunnel_app_info(infra_name) 175 | 176 | push_caldecott(auth,infra_name) 177 | bind_service_banner(service, tunnel_appname(infra_name), false) 178 | start_caldecott(infra_name) 179 | end 180 | 181 | if not tunnel_bound?(service,infra_name) 182 | bind_service_banner(service, tunnel_appname(infra_name)) 183 | end 184 | 185 | conn_info = tunnel_connection_info info[:vendor], service, auth, infra_name 186 | display_tunnel_connection_info(conn_info) 187 | display "Starting tunnel to #{service.bold} on port #{port.to_s.bold}." 188 | start_tunnel(port, conn_info, auth, infra_name) 189 | 190 | clients = get_clients_for(info[:vendor]) 191 | 192 | if clients.empty? 193 | client_name ||= "none" 194 | else 195 | client_name ||= ask( 196 | "Which client would you like to start?", 197 | :choices => ["none"] + clients.keys, 198 | :indexed => true 199 | ) 200 | end 201 | 202 | if client_name == "none" 203 | wait_for_tunnel_end 204 | else 205 | wait_for_tunnel_start(port) 206 | unless start_local_prog(clients, client_name, conn_info, port) 207 | err "'#{client_name}' execution failed; is it in your $PATH?" 208 | end 209 | end 210 | end 211 | 212 | def get_clients_for(type) 213 | conf = VMC::Cli::Config.clients 214 | conf[type] || {} 215 | end 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /lib/cli/commands/user.rb: -------------------------------------------------------------------------------- 1 | module VMC::Cli::Command 2 | 3 | class User < Base 4 | 5 | # Errors 6 | class InvalidLogin < VMC::Client::TargetError; end 7 | 8 | def info 9 | info = client_info 10 | username = info[:user] || 'N/A' 11 | return display JSON.pretty_generate([username]) if @options[:json] 12 | display "\n[#{username}]" 13 | end 14 | 15 | def login(email=nil) 16 | display "Attempting login to [#{target_url}]" if target_url 17 | begin 18 | email = @options[:email] unless email 19 | password = @options[:password] 20 | tries ||= 0 21 | 22 | unless no_prompt 23 | email ||= ask("Email") 24 | password ||= ask("Password", :echo => "*") 25 | end 26 | 27 | err "Need a valid email" unless email 28 | err "Need a password" unless password 29 | login_and_save_token(email, password) 30 | say "Successfully logged into [#{target_url}]".green 31 | rescue VMC::Client::TargetError 32 | if (tries += 1) < 3 && prompt_ok && !@options[:password] 33 | display "Problem with login, invalid account or password when attempting to login to '#{target_url}'".red 34 | retry 35 | end 36 | raise InvalidLogin, "Problem with login, invalid account or password when attempting to login to '#{target_url}'" 37 | end 38 | end 39 | 40 | def logout 41 | VMC::Cli::Config.remove_token_file 42 | say "Successfully logged out of [#{target_url}]".green 43 | end 44 | 45 | def change_password(password=nil) 46 | info = client_info 47 | email = info[:user] 48 | err "Need to be logged in to change password." unless email 49 | say "Changing password for '#{email}'\n" 50 | unless no_prompt 51 | password = ask "New Password", :echo => "*" 52 | password2 = ask "Verify Password", :echo => "*" 53 | err "Passwords did not match, try again" if password != password2 54 | end 55 | err "Password required" unless password 56 | err "Passwords may not contain braces" if password =~ /[{}]/ 57 | client.change_password(password) 58 | say "\nSuccessfully changed password".green 59 | end 60 | 61 | private 62 | 63 | def login_and_save_token(email, password) 64 | token = client.login(email, password) 65 | VMC::Cli::Config.store_token(token, @options[:token_file]) 66 | end 67 | 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /lib/cli/config.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require 'fileutils' 3 | 4 | require 'rubygems' 5 | require 'json/pure' 6 | 7 | module VMC::Cli 8 | class Config 9 | 10 | DEFAULT_TARGET = 'api.appfog.com' 11 | 12 | TARGET_FILE = '~/.af_target' 13 | TOKEN_FILE = '~/.af_token' 14 | INSTANCES_FILE = '~/.af_instances' 15 | ALIASES_FILE = '~/.af_aliases' 16 | CLIENTS_FILE = '~/.af_clients' 17 | MICRO_FILE = '~/.af_micro' 18 | CRASH_FILE = '~/.af_crash' 19 | 20 | STOCK_CLIENTS = File.expand_path("../../../config/clients.yml", __FILE__) 21 | 22 | class << self 23 | attr_accessor :colorize 24 | attr_accessor :output 25 | attr_accessor :trace 26 | attr_accessor :nozip 27 | attr_accessor :infra 28 | 29 | def target_url 30 | return @target_url if @target_url 31 | target_file = File.expand_path(TARGET_FILE) 32 | if File.exists? target_file 33 | @target_url = lock_and_read(target_file).strip 34 | else 35 | @target_url = DEFAULT_TARGET 36 | end 37 | @target_url = "https://#{@target_url}" unless /^https?/ =~ @target_url 38 | @target_url = @target_url.gsub(/\/+$/, '') 39 | @target_url 40 | end 41 | 42 | def base_of(url) 43 | url.sub(/^[^\.]+\./, "") 44 | end 45 | 46 | def store_target(target_host) 47 | target_file = File.expand_path(TARGET_FILE) 48 | lock_and_write(target_file, target_host) 49 | end 50 | 51 | def store_crash(log) 52 | crash_file = File.expand_path(CRASH_FILE) 53 | lock_and_write(crash_file, log) 54 | end 55 | 56 | def all_tokens(token_file_path=nil) 57 | token_file = File.expand_path(token_file_path || TOKEN_FILE) 58 | return nil unless File.exists? token_file 59 | contents = lock_and_read(token_file).strip 60 | JSON.parse(contents) 61 | end 62 | 63 | alias :targets :all_tokens 64 | 65 | def auth_token(token_file_path=nil) 66 | return @token if @token 67 | tokens = all_tokens(token_file_path) 68 | @token = tokens[target_url] if tokens 69 | end 70 | 71 | def remove_token_file 72 | FileUtils.rm_f(File.expand_path(TOKEN_FILE)) 73 | end 74 | 75 | def store_token(token, token_file_path=nil) 76 | tokens = all_tokens(token_file_path) || {} 77 | tokens[target_url] = token 78 | token_file = File.expand_path(token_file_path || TOKEN_FILE) 79 | lock_and_write(token_file, tokens.to_json) 80 | end 81 | 82 | def instances 83 | instances_file = File.expand_path(INSTANCES_FILE) 84 | return nil unless File.exists? instances_file 85 | contents = lock_and_read(instances_file).strip 86 | JSON.parse(contents) 87 | end 88 | 89 | def store_instances(instances) 90 | instances_file = File.expand_path(INSTANCES_FILE) 91 | lock_and_write(instances_file, instances.to_json) 92 | end 93 | 94 | def aliases 95 | aliases_file = File.expand_path(ALIASES_FILE) 96 | # bacward compatible 97 | unless File.exists? aliases_file 98 | old_aliases_file = File.expand_path('~/.vmc-aliases') 99 | FileUtils.mv(old_aliases_file, aliases_file) if File.exists? old_aliases_file 100 | end 101 | aliases = YAML.load_file(aliases_file) rescue {} 102 | end 103 | 104 | def store_aliases(aliases) 105 | aliases_file = File.expand_path(ALIASES_FILE) 106 | File.open(aliases_file, 'wb') {|f| f.write(aliases.to_yaml)} 107 | end 108 | 109 | def micro 110 | micro_file = File.expand_path(MICRO_FILE) 111 | return {} unless File.exists? micro_file 112 | contents = lock_and_read(micro_file).strip 113 | JSON.parse(contents) 114 | end 115 | 116 | def store_micro(micro) 117 | micro_file = File.expand_path(MICRO_FILE) 118 | lock_and_write(micro_file, micro.to_json) 119 | end 120 | 121 | def deep_merge(a, b) 122 | merge = proc do |_, old, new| 123 | if new.is_a?(Hash) and old.is_a?(Hash) 124 | old.merge(new, &merge) 125 | else 126 | new 127 | end 128 | end 129 | 130 | a.merge(b, &merge) 131 | end 132 | 133 | def clients 134 | return @clients if @clients 135 | 136 | stock = YAML.load_file(STOCK_CLIENTS) 137 | clients = File.expand_path CLIENTS_FILE 138 | if File.exists? clients 139 | user = YAML.load_file(clients) 140 | @clients = deep_merge(stock, user) 141 | else 142 | @clients = stock 143 | end 144 | end 145 | 146 | def lock_and_read(file) 147 | File.open(file, File::RDONLY) {|f| 148 | if defined? JRUBY_VERSION 149 | f.flock(File::LOCK_SH) 150 | else 151 | f.flock(File::LOCK_EX) 152 | end 153 | contents = f.read 154 | f.flock(File::LOCK_UN) 155 | contents 156 | } 157 | end 158 | 159 | def lock_and_write(file, contents) 160 | File.open(file, File::RDWR | File::CREAT, 0600) {|f| 161 | f.flock(File::LOCK_EX) 162 | f.rewind 163 | f.puts contents 164 | f.flush 165 | f.truncate(f.pos) 166 | f.flock(File::LOCK_UN) 167 | } 168 | end 169 | end 170 | 171 | def initialize(work_dir = Dir.pwd) 172 | @work_dir = work_dir 173 | end 174 | 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/cli/console_helper.rb: -------------------------------------------------------------------------------- 1 | require 'net/telnet' 2 | require 'readline' 3 | 4 | module VMC::Cli 5 | module ConsoleHelper 6 | 7 | def console_connection_info(appname) 8 | app = client.app_info(appname) 9 | fw = VMC::Cli::Framework.lookup_by_framework(app[:staging][:model]) 10 | if !fw.console 11 | err "'#{appname}' is a #{fw.name} application. " + 12 | "Console access is not supported for #{fw.name} applications." 13 | end 14 | instances_info_envelope = client.app_instances(appname) 15 | instances_info_envelope = {} if instances_info_envelope.is_a?(Array) 16 | 17 | instances_info = instances_info_envelope[:instances] || [] 18 | err "No running instances for [#{appname}]" if instances_info.empty? 19 | 20 | entry = instances_info[0] 21 | if !entry[:console_port] 22 | begin 23 | client.app_files(appname, '/app/cf-rails-console') 24 | err "Console port not provided for [#{appname}]. Try restarting the app." 25 | rescue VMC::Client::TargetError, VMC::Client::NotFound 26 | err "Console access not supported for [#{appname}]. " + 27 | "Please redeploy your app to enable support." 28 | end 29 | end 30 | conn_info = { 31 | 'hostname' => entry[:console_ip], 32 | 'port' => entry[:console_port] 33 | } 34 | end 35 | 36 | def start_local_console(port, appname) 37 | auth_info = console_credentials(appname) 38 | display "Connecting to '#{appname}' console: ", false 39 | prompt = console_login(auth_info, port) 40 | display "OK".green 41 | display "\n" 42 | initialize_readline 43 | run_console prompt 44 | end 45 | 46 | def console_login(auth_info, port) 47 | if !auth_info["username"] || !auth_info["password"] 48 | err "Unable to verify console credentials." 49 | end 50 | @telnet_client = telnet_client(port) 51 | prompt = nil 52 | err_msg = "Login attempt timed out." 53 | 5.times do 54 | begin 55 | results = @telnet_client.login("Name"=>auth_info["username"], 56 | "Password"=>auth_info["password"]) 57 | lines = results.sub("Login: Password: ", "").split("\n") 58 | last_line = lines.pop 59 | if last_line =~ /[$%#>] \z/n 60 | prompt = last_line 61 | elsif last_line =~ /Login failed/ 62 | err_msg = last_line 63 | end 64 | break 65 | rescue TimeoutError 66 | sleep 1 67 | rescue EOFError 68 | #This may happen if we login right after app starts 69 | close_console 70 | sleep 5 71 | @telnet_client = telnet_client(port) 72 | end 73 | display ".", false 74 | end 75 | unless prompt 76 | close_console 77 | err err_msg 78 | end 79 | prompt 80 | end 81 | 82 | def send_console_command(cmd) 83 | results = @telnet_client.cmd(cmd) 84 | results.split("\n") 85 | end 86 | 87 | def console_credentials(appname) 88 | content = client.app_files(appname, '/app/cf-rails-console/.consoleaccess', '0') 89 | YAML.load(content) 90 | end 91 | 92 | def close_console 93 | @telnet_client.close 94 | end 95 | 96 | def console_tab_completion_data(cmd) 97 | begin 98 | results = @telnet_client.cmd("String"=> cmd + "\t", "Match"=>/\S*\n$/, "Timeout"=>10) 99 | results.chomp.split(",") 100 | rescue TimeoutError 101 | [] #Just return empty results if timeout occurred on tab completion 102 | end 103 | end 104 | 105 | private 106 | def telnet_client(port) 107 | Net::Telnet.new({"Port"=>port, "Prompt"=>/[$%#>] \z|Login failed/n, "Timeout"=>30, "FailEOF"=>true}) 108 | end 109 | 110 | def readline_with_history(prompt) 111 | line = Readline::readline(prompt) 112 | return nil if line == nil || line == 'quit' || line == 'exit' 113 | Readline::HISTORY.push(line) if not line =~ /^\s*$/ and Readline::HISTORY.to_a[-1] != line 114 | line 115 | end 116 | 117 | def run_console(prompt) 118 | prev = trap("INT") { |x| exit_console; prev.call(x); exit } 119 | prev = trap("TERM") { |x| exit_console; prev.call(x); exit } 120 | loop do 121 | cmd = readline_with_history(prompt) 122 | if(cmd == nil) 123 | exit_console 124 | break 125 | end 126 | prompt = send_console_command_display_results(cmd, prompt) 127 | end 128 | end 129 | 130 | def exit_console 131 | #TimeoutError expected, as exit doesn't return anything 132 | @telnet_client.cmd("String"=>"exit","Timeout"=>1) rescue TimeoutError 133 | close_console 134 | end 135 | 136 | def send_console_command_display_results(cmd, prompt) 137 | begin 138 | lines = send_console_command cmd 139 | #Assumes the last line is a prompt 140 | prompt = lines.pop 141 | lines.each {|line| display line if line != cmd} 142 | rescue TimeoutError 143 | display "Timed out sending command to server.".red 144 | rescue EOFError 145 | err "The console connection has been terminated. Perhaps the app was stopped or deleted?" 146 | end 147 | prompt 148 | end 149 | 150 | def initialize_readline 151 | if Readline.respond_to?("basic_word_break_characters=") 152 | Readline.basic_word_break_characters= " \t\n`><=;|&{(" 153 | end 154 | Readline.completion_append_character = nil 155 | #Assumes that sending a String ending with tab will return a non-empty 156 | #String of comma-separated completion options, terminated by a new line 157 | #For example, "app.\t" might result in "to_s,nil?,etc\n" 158 | Readline.completion_proc = proc {|s| 159 | console_tab_completion_data s 160 | } 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/cli/core_ext.rb: -------------------------------------------------------------------------------- 1 | module VMCExtensions 2 | 3 | def say(message) 4 | VMC::Cli::Config.output.puts(message) if VMC::Cli::Config.output 5 | end 6 | 7 | def header(message, filler = '-') 8 | say "\n" 9 | say message 10 | say filler.to_s * message.size 11 | end 12 | 13 | def banner(message) 14 | say "\n" 15 | say message 16 | end 17 | 18 | def display(message, nl=true) 19 | if nl 20 | say message 21 | else 22 | if VMC::Cli::Config.output 23 | VMC::Cli::Config.output.print(message) 24 | VMC::Cli::Config.output.flush 25 | end 26 | end 27 | end 28 | 29 | def clear(size=80) 30 | return unless VMC::Cli::Config.output 31 | VMC::Cli::Config.output.print("\r") 32 | VMC::Cli::Config.output.print(" " * size) 33 | VMC::Cli::Config.output.print("\r") 34 | #VMC::Cli::Config.output.flush 35 | end 36 | 37 | def err(message, prefix='Error: ') 38 | raise VMC::Cli::CliExit, "#{prefix}#{message}" 39 | end 40 | 41 | def warn(msg) 42 | say "#{"[WARNING]".yellow} #{msg}" 43 | end 44 | 45 | def quit(message = nil) 46 | raise VMC::Cli::GracefulExit, message 47 | end 48 | 49 | def blank? 50 | self.to_s.blank? 51 | end 52 | 53 | def uptime_string(delta) 54 | num_seconds = delta.to_i 55 | days = num_seconds / (60 * 60 * 24); 56 | num_seconds -= days * (60 * 60 * 24); 57 | hours = num_seconds / (60 * 60); 58 | num_seconds -= hours * (60 * 60); 59 | minutes = num_seconds / 60; 60 | num_seconds -= minutes * 60; 61 | "#{days}d:#{hours}h:#{minutes}m:#{num_seconds}s" 62 | end 63 | 64 | def pretty_size(size, prec=1) 65 | return 'NA' unless size 66 | return "#{size}B" if size < 1024 67 | return sprintf("%.#{prec}fK", size/1024.0) if size < (1024*1024) 68 | return sprintf("%.#{prec}fM", size/(1024.0*1024.0)) if size < (1024*1024*1024) 69 | return sprintf("%.#{prec}fG", size/(1024.0*1024.0*1024.0)) 70 | end 71 | end 72 | 73 | module VMCStringExtensions 74 | 75 | def red 76 | colorize("\e[0m\e[31m") 77 | end 78 | 79 | def green 80 | colorize("\e[0m\e[32m") 81 | end 82 | 83 | def yellow 84 | colorize("\e[0m\e[33m") 85 | end 86 | 87 | def bold 88 | colorize("\e[0m\e[1m") 89 | end 90 | 91 | def colorize(color_code) 92 | if VMC::Cli::Config.colorize 93 | "#{color_code}#{self}\e[0m" 94 | else 95 | self 96 | end 97 | end 98 | 99 | def blank? 100 | self =~ /^\s*$/ 101 | end 102 | 103 | def truncate(limit = 30) 104 | return "" if self.blank? 105 | etc = "..." 106 | stripped = self.strip[0..limit] 107 | if stripped.length > limit 108 | stripped.gsub(/\s+?(\S+)?$/, "") + etc 109 | else 110 | stripped 111 | end 112 | end 113 | 114 | end 115 | 116 | class Object 117 | include VMCExtensions 118 | end 119 | 120 | class String 121 | include VMCStringExtensions 122 | end 123 | -------------------------------------------------------------------------------- /lib/cli/errors.rb: -------------------------------------------------------------------------------- 1 | module VMC::Cli 2 | 3 | class CliError < StandardError 4 | def self.error_code(code = nil) 5 | define_method(:error_code) { code } 6 | end 7 | end 8 | 9 | class UnknownCommand < CliError; error_code(100); end 10 | class TargetMissing < CliError; error_code(102); end 11 | class TargetInaccessible < CliError; error_code(103); end 12 | 13 | class TargetError < CliError; error_code(201); end 14 | class AuthError < TargetError; error_code(202); end 15 | 16 | class CliExit < CliError; error_code(400); end 17 | class GracefulExit < CliExit; error_code(401); end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /lib/cli/file_helper.rb: -------------------------------------------------------------------------------- 1 | module VMC::Cli 2 | module FileHelper 3 | 4 | class AppFogIgnore 5 | 6 | def initialize(patterns,project_root = "") 7 | @patterns = patterns + [ ".git/" ] 8 | @project_root = project_root 9 | end 10 | 11 | def included_files(filenames) 12 | exclude_dots_only(filenames).reject do |filename| 13 | exclude = false 14 | @patterns.each do |pattern| 15 | if is_negative_pattern?(pattern) 16 | exclude = false if negative_match(pattern,filename) 17 | else 18 | exclude ||= match(pattern,filename) 19 | end 20 | end 21 | exclude 22 | end 23 | end 24 | 25 | def exclude_dots_only(filenames) 26 | filenames.reject do |filename| 27 | base = File.basename(filename) 28 | base == "." || base == ".." 29 | end 30 | end 31 | 32 | 33 | 34 | def excluded_files(filenames) 35 | filenames - included_files(filenames) 36 | end 37 | 38 | def self.from_file(project_root) 39 | f = "#{project_root}/.afignore" 40 | if File.exists?(f) 41 | contents = File.read(f).split("\n") 42 | AppFogIgnore.new(contents,project_root) 43 | else 44 | AppFogIgnore.new([],project_root) 45 | end 46 | end 47 | 48 | def match(pattern,filename) 49 | 50 | filename = filename.sub(/^#{@project_root}\//,'') # remove any project directory prefix 51 | 52 | return false if pattern =~ /^\s*$/ # ignore blank lines 53 | 54 | return false if pattern =~ /^#/ # lines starting with # are comments 55 | 56 | return false if pattern =~ /^!/ # lines starting with ! are negated 57 | 58 | if pattern =~ /\/$/ 59 | # pattern ending in a slash should ignore directory and all its children 60 | dirname = pattern.sub(/\/$/,'') 61 | return filename == dirname || filename =~ /^#{dirname}\/.*$/ 62 | end 63 | 64 | if pattern =~ /^\// 65 | parts = filename.split('/') 66 | return File.fnmatch(pattern.sub(/^\//,''),parts[0]) 67 | end 68 | 69 | if pattern.include? '/' 70 | return File.fnmatch(pattern,filename) 71 | end 72 | 73 | File.fnmatch(pattern,filename,File::FNM_PATHNAME) 74 | end 75 | 76 | def is_negative_pattern?(pattern) 77 | pattern =~ /^!/ 78 | end 79 | 80 | def negative_match(pattern,filename) 81 | return false unless pattern =~ /^!/ 82 | match(pattern.sub(/^!/,''),filename) 83 | end 84 | 85 | end 86 | 87 | def ignore_sockets(files) 88 | files.reject { |f| File.socket? f } 89 | end 90 | 91 | def check_unreachable_links(path,files) 92 | pwd = Pathname.new(path) 93 | abspath = pwd.realpath.to_s 94 | unreachable = [] 95 | files.each do |f| 96 | file = Pathname.new(f) 97 | if file.symlink? && !file.realpath.to_s.start_with?(abspath) 98 | unreachable << file.relative_path_from(pwd).to_s 99 | end 100 | end 101 | 102 | unless unreachable.empty? 103 | root = pwd.relative_path_from(pwd).to_s 104 | err "Can't deploy application containing links '#{unreachable.join(",")}' that reach outside its root '#{root}'" 105 | end 106 | end 107 | 108 | def copy_files(project_root,files,dest_dir) 109 | project_root = Pathname.new(project_root) 110 | files.each do |f| 111 | dest = Pathname.new(f).relative_path_from(project_root) 112 | if File.symlink?(f) 113 | FileUtils.copy_entry(f,"#{dest_dir}/#{dest}") 114 | elsif File.directory?(f) 115 | FileUtils.mkdir_p("#{dest_dir}/#{dest}") 116 | else 117 | FileUtils.cp(f,"#{dest_dir}/#{dest}") 118 | end 119 | end 120 | end 121 | 122 | end 123 | end -------------------------------------------------------------------------------- /lib/cli/frameworks.rb: -------------------------------------------------------------------------------- 1 | module VMC::Cli 2 | 3 | class Framework 4 | 5 | DEFAULT_FRAMEWORK = "http://b20nine.com/unknown" 6 | DEFAULT_MEM = '256M' 7 | 8 | FRAMEWORKS = { 9 | 'Rails' => ['rails3', { :mem => '256M', :description => 'Rails Application', :console=>true}], 10 | 'Spring' => ['spring', { :mem => '512M', :description => 'Java SpringSource Spring Application'}], 11 | 'Grails' => ['grails', { :mem => '512M', :description => 'Java SpringSource Grails Application'}], 12 | 'Lift' => ['lift', { :mem => '512M', :description => 'Scala Lift Application'}], 13 | 'JavaWeb' => ['java_web',{ :mem => '512M', :description => 'Java Web Application'}], 14 | 'Standalone' => ['standalone', { :mem => '64M', :description => 'Standalone Application'}], 15 | 'Sinatra' => ['sinatra', { :mem => '128M', :description => 'Sinatra Application'}], 16 | 'Node' => ['node', { :mem => '64M', :description => 'Node.js Application'}], 17 | 'PHP' => ['php', { :mem => '128M', :description => 'PHP Application'}], 18 | 'Erlang/OTP Rebar' => ['otp_rebar', { :mem => '64M', :description => 'Erlang/OTP Rebar Application'}], 19 | 'WSGI' => ['wsgi', { :mem => '64M', :description => 'Python WSGI Application'}], 20 | 'Django' => ['django', { :mem => '128M', :description => 'Python Django Application'}], 21 | 'aspdotnet' => ['aspdotnet', { :mem => '128M', :description => 'ASP.NET Application'}], 22 | 'Rack' => ['rack', { :mem => '128M', :description => 'Rack Application'}], 23 | 'Play' => ['play', { :mem => '256M', :description => 'Play Framework Application'}] 24 | } 25 | 26 | class << self 27 | 28 | def known_frameworks(available_frameworks) 29 | frameworks = [] 30 | FRAMEWORKS.each do |key,fw| 31 | frameworks << key if available_frameworks.include? [fw[0]] 32 | end 33 | frameworks 34 | end 35 | 36 | def lookup(name) 37 | return create(*FRAMEWORKS[name]) 38 | end 39 | 40 | def lookup_by_framework(name) 41 | FRAMEWORKS.each do |key,fw| 42 | return create(fw[0],fw[1]) if fw[0] == name 43 | end 44 | end 45 | 46 | def create(name,opts) 47 | if name == "standalone" 48 | return StandaloneFramework.new(name, opts) 49 | else 50 | return Framework.new(name,opts) 51 | end 52 | end 53 | 54 | def detect(path, available_frameworks) 55 | if !File.directory? path 56 | if path.end_with?('.war') 57 | return detect_framework_from_war path 58 | elsif path.end_with?('.zip') 59 | return detect_framework_from_zip path, available_frameworks 60 | elsif available_frameworks.include?(["standalone"]) 61 | return Framework.lookup('Standalone') 62 | else 63 | return nil 64 | end 65 | end 66 | Dir.chdir(path) do 67 | # Rails 68 | if File.exist?('config/environment.rb') 69 | return Framework.lookup('Rails') 70 | 71 | # Rack 72 | elsif File.exist?('config.ru') && available_frameworks.include?(["rack"]) 73 | return Framework.lookup('Rack') 74 | 75 | # Java Web Apps 76 | elsif Dir.glob('*.war').first 77 | return detect_framework_from_war(Dir.glob('*.war').first) 78 | 79 | elsif File.exist?('WEB-INF/web.xml') 80 | return detect_framework_from_war 81 | 82 | # Simple Ruby Apps 83 | elsif !Dir.glob('*.rb').empty? 84 | matched_file = nil 85 | Dir.glob('*.rb').each do |fname| 86 | next if matched_file 87 | File.open(fname, 'r') do |f| 88 | str = f.read # This might want to be limited 89 | matched_file = fname if (str && str.match(/^\s*\#?\s*require\s*\(?\s*['"]sinatra['"]/)) 90 | end 91 | end 92 | if matched_file 93 | # Sinatra apps 94 | f = Framework.lookup('Sinatra') 95 | f.exec = "ruby #{matched_file}" 96 | return f 97 | end 98 | 99 | # PHP 100 | elsif !Dir.glob('*.php').empty? 101 | return Framework.lookup('PHP') 102 | 103 | # Erlang/OTP using Rebar 104 | elsif !Dir.glob('releases/*/*.rel').empty? && !Dir.glob('releases/*/*.boot').empty? 105 | return Framework.lookup('Erlang/OTP Rebar') 106 | 107 | # Python Django 108 | # XXX: not all django projects keep settings.py in top-level directory 109 | elsif File.exist?('manage.py') && File.exist?('settings.py') 110 | return Framework.lookup('Django') 111 | 112 | # Python 113 | elsif !Dir.glob('wsgi.py').empty? 114 | return Framework.lookup('WSGI') 115 | 116 | # .Net 117 | elsif !Dir.glob('web.config', File::FNM_CASEFOLD).empty? 118 | return Framework.lookup('aspdotnet') 119 | 120 | # Node.js 121 | elsif !Dir.glob('*.js').empty? 122 | if File.exist?('server.js') || File.exist?('app.js') || File.exist?('index.js') || File.exist?('main.js') 123 | return Framework.lookup('Node') 124 | end 125 | 126 | # Play or Standalone Apps 127 | elsif Dir.glob('*.zip').first 128 | zip_file = Dir.glob('*.zip').first 129 | return detect_framework_from_zip zip_file, available_frameworks 130 | end 131 | 132 | # Default to Standalone if no other match was made 133 | return Framework.lookup('Standalone') if available_frameworks.include?(["standalone"]) 134 | end 135 | end 136 | 137 | def detect_framework_from_war(war_file=nil) 138 | if war_file 139 | contents = ZipUtil.entry_lines(war_file) 140 | else 141 | #assume we are working with current dir 142 | contents = Dir['**/*'].join("\n") 143 | end 144 | 145 | # Spring/Lift Variations 146 | if contents =~ /WEB-INF\/lib\/grails-web.*\.jar/ 147 | return Framework.lookup('Grails') 148 | elsif contents =~ /WEB-INF\/lib\/lift-webkit.*\.jar/ 149 | return Framework.lookup('Lift') 150 | elsif contents =~ /WEB-INF\/classes\/org\/springframework/ 151 | return Framework.lookup('Spring') 152 | elsif contents =~ /WEB-INF\/lib\/spring-core.*\.jar/ 153 | return Framework.lookup('Spring') 154 | elsif contents =~ /WEB-INF\/lib\/org\.springframework\.core.*\.jar/ 155 | return Framework.lookup('Spring') 156 | else 157 | return Framework.lookup('JavaWeb') 158 | end 159 | end 160 | 161 | def detect_framework_from_zip(zip_file, available_frameworks) 162 | contents = ZipUtil.entry_lines(zip_file) 163 | detect_framework_from_zip_contents(contents, available_frameworks) 164 | end 165 | 166 | def detect_framework_from_zip_contents(contents, available_frameworks) 167 | if available_frameworks.include?(["play"]) && contents =~ /lib\/play\..*\.jar/ 168 | return Framework.lookup('Play') 169 | elsif available_frameworks.include?(["standalone"]) 170 | return Framework.lookup('Standalone') 171 | end 172 | end 173 | end 174 | 175 | attr_reader :name, :description, :console 176 | attr_accessor :exec 177 | 178 | def initialize(framework=nil, opts={}) 179 | @name = framework || DEFAULT_FRAMEWORK 180 | @memory = opts[:mem] || DEFAULT_MEM 181 | @description = opts[:description] || 'Unknown Application Type' 182 | @exec = opts[:exec] 183 | @console = opts[:console] || false 184 | end 185 | 186 | def to_s 187 | description 188 | end 189 | 190 | def require_url? 191 | true 192 | end 193 | 194 | def require_start_command? 195 | false 196 | end 197 | 198 | def prompt_for_runtime? 199 | false 200 | end 201 | 202 | def default_runtime(path) 203 | nil 204 | end 205 | 206 | def memory(runtime=nil) 207 | @memory 208 | end 209 | 210 | alias :mem :memory 211 | end 212 | 213 | class StandaloneFramework < Framework 214 | def require_url? 215 | false 216 | end 217 | 218 | def require_start_command? 219 | true 220 | end 221 | 222 | def prompt_for_runtime? 223 | true 224 | end 225 | 226 | def default_runtime(path) 227 | if !File.directory? path 228 | if path =~ /\.(jar|class)$/ 229 | return "java" 230 | elsif path =~ /\.(rb)$/ 231 | return "ruby18" 232 | elsif path =~ /\.(zip)$/ 233 | return detect_runtime_from_zip path 234 | end 235 | else 236 | Dir.chdir(path) do 237 | return "ruby18" if not Dir.glob('**/*.rb').empty? 238 | if !Dir.glob('**/*.class').empty? || !Dir.glob('**/*.jar').empty? 239 | return "java" 240 | elsif Dir.glob('*.zip').first 241 | zip_file = Dir.glob('*.zip').first 242 | return detect_runtime_from_zip zip_file 243 | end 244 | end 245 | end 246 | return nil 247 | end 248 | 249 | def memory(runtime=nil) 250 | default_mem = @memory 251 | default_mem = '128M' if runtime =~ /\Aruby/ || runtime == "php" 252 | default_mem = '512M' if runtime == "java" || runtime == "java7" 253 | default_mem 254 | end 255 | 256 | private 257 | def detect_runtime_from_zip(zip_file) 258 | contents = ZipUtil.entry_lines(zip_file) 259 | if contents =~ /\.(jar)$/ 260 | return "java" 261 | end 262 | end 263 | end 264 | 265 | end 266 | -------------------------------------------------------------------------------- /lib/cli/manifest_helper.rb: -------------------------------------------------------------------------------- 1 | require "set" 2 | 3 | module VMC::Cli::ManifestHelper 4 | include VMC::Cli::ServicesHelper 5 | 6 | DEFAULTS = { 7 | "url" => "${name}.${target-base}", 8 | "mem" => "128M", 9 | "instances" => 1 10 | } 11 | 12 | MANIFEST = "manifest.yml" 13 | 14 | YES_SET = Set.new(["y", "Y", "yes", "YES"]) 15 | 16 | # take a block and call it once for each app to push/update. 17 | # with @application and @app_info set appropriately 18 | def each_app(panic=true) 19 | if @manifest and all_apps = @manifest["applications"] 20 | where = File.expand_path(@path) 21 | single = false 22 | 23 | all_apps.each do |path, info| 24 | app = File.expand_path("../" + path, manifest_file) 25 | if where.start_with?(app) 26 | @application = app 27 | @app_info = info 28 | yield info["name"] 29 | single = true 30 | break 31 | end 32 | end 33 | 34 | unless single 35 | if where == File.expand_path("../", manifest_file) 36 | ordered_by_deps(all_apps).each do |path, info| 37 | app = File.expand_path("../" + path, manifest_file) 38 | @application = app 39 | @app_info = info 40 | yield info["name"] 41 | end 42 | else 43 | err "Path '#{@path}' is not known to manifest '#{manifest_file}'." 44 | end 45 | end 46 | else 47 | @application = @path 48 | @app_info = @manifest 49 | if @app_info 50 | yield @app_info["name"] 51 | elsif panic 52 | err "No applications." 53 | end 54 | end 55 | 56 | nil 57 | ensure 58 | @application = nil 59 | @app_info = nil 60 | end 61 | 62 | def interact(many=false) 63 | @manifest ||= {} 64 | configure_app(many) 65 | end 66 | 67 | def target_manifest 68 | @options[:manifest] || MANIFEST 69 | end 70 | 71 | def save_manifest(save_to = nil) 72 | save_to ||= target_manifest 73 | 74 | File.open(save_to, "w") do |f| 75 | f.write @manifest.to_yaml 76 | end 77 | 78 | say "Manifest written to #{save_to}." 79 | end 80 | 81 | def configure_app(many=false) 82 | name = manifest("name") || 83 | set(ask("Application Name", :default => manifest("name")), "name") 84 | 85 | if manifest "framework" 86 | framework = VMC::Cli::Framework.lookup_by_framework manifest("framework","name") 87 | else 88 | framework = detect_framework 89 | set framework.name, "framework", "name" 90 | set( 91 | { "mem" => framework.mem, 92 | "description" => framework.description, 93 | "exec" => framework.exec 94 | }, 95 | "framework", 96 | "info" 97 | ) 98 | end 99 | 100 | default_runtime = manifest "runtime" 101 | if not default_runtime 102 | default_runtime = framework.default_runtime(@application) 103 | set(detect_runtime(default_runtime), "runtime") if framework.prompt_for_runtime? 104 | end 105 | default_command = manifest "command" 106 | set ask("Start Command", :default => default_command), "command" if framework.require_start_command? 107 | 108 | if client.infra_supported? 109 | infra = @options[:infra] || manifest("infra") || 110 | client.infra_name_for_description( 111 | ask("Select Infrastructure",:indexed => true, :choices => client.infra_descriptions)) 112 | set infra.dup, "infra" 113 | client.infra = infra 114 | end 115 | 116 | url_template = manifest("url") || DEFAULTS["url"] 117 | url_resolved = url_template.dup 118 | resolve_lexically(url_resolved) 119 | 120 | if !framework.require_url? 121 | url_resolved = "None" 122 | end 123 | url = ask("Application Deployed URL", :default => url_resolved) 124 | 125 | if url == url_resolved && url != "None" 126 | url = url_template 127 | end 128 | 129 | # common error case is for prompted users to answer y or Y or yes or 130 | # YES to this ask() resulting in an unintended URL of y. Special 131 | # case this common error 132 | url = url_resolved if YES_SET.member? url 133 | 134 | if(url == "None") 135 | url = nil 136 | end 137 | 138 | set url, "url" 139 | 140 | default_mem = manifest("mem") 141 | default_mem = framework.memory(manifest("runtime")) if not default_mem 142 | set ask( 143 | "Memory reservation", 144 | :default => 145 | default_mem || 146 | DEFAULTS["mem"], 147 | :choices => ["128M", "256M", "512M", "1G", "2G"] 148 | ), "mem" 149 | 150 | set ask( 151 | "How many instances?", 152 | :default => manifest("instances") || DEFAULTS["instances"] 153 | ), "instances" 154 | 155 | unless manifest "services" 156 | user_services = services_for_infra(manifest("infra")) 157 | user_services.sort! {|a, b| a[:name] <=> b[:name] } 158 | 159 | unless user_services.empty? 160 | if ask "Bind existing services to '#{name}'?", :default => false 161 | bind_services(user_services) 162 | end 163 | end 164 | 165 | services = client.services_info 166 | unless services.empty? 167 | if ask "Create services to bind to '#{name}'?", :default => false 168 | create_services(services.values.collect(&:keys).flatten) 169 | end 170 | end 171 | end 172 | 173 | if many and ask("Configure for another application?", :default => false) 174 | @application = ask "Application path?" 175 | configure_app 176 | end 177 | end 178 | 179 | def set(what, *where) 180 | where.unshift "applications", @application 181 | 182 | which = @manifest 183 | where.each_with_index do |k, i| 184 | if i + 1 == where.size 185 | which[k] = what 186 | else 187 | which = (which[k] ||= {}) 188 | end 189 | end 190 | 191 | what 192 | end 193 | 194 | # Detect the appropriate framework. 195 | def detect_framework(prompt_ok = true) 196 | framework = VMC::Cli::Framework.detect(@application, frameworks_info) 197 | framework_correct = ask("Detected a #{framework}, is this correct?", :default => true) if prompt_ok && framework 198 | if prompt_ok && (framework.nil? || !framework_correct) 199 | display "#{"[WARNING]".yellow} Can't determine the Application Type." unless framework 200 | framework = nil if !framework_correct 201 | framework = VMC::Cli::Framework.lookup( 202 | ask( 203 | "Select Application Type", 204 | :indexed => true, 205 | :default => framework, 206 | :choices => VMC::Cli::Framework.known_frameworks(frameworks_info) 207 | ) 208 | ) 209 | display "Selected #{framework}" 210 | end 211 | 212 | framework 213 | end 214 | 215 | # Detect the appropriate runtime. 216 | def detect_runtime(default, prompt_ok=true) 217 | runtime = nil 218 | runtime_keys=[] 219 | runtimes_info.keys.each {|runtime_key| runtime_keys << runtime_key.dup } 220 | runtime_keys.sort! 221 | if prompt_ok 222 | runtime = ask( 223 | "Select Runtime", 224 | :indexed => true, 225 | :default => default, 226 | :choices => runtime_keys 227 | ) 228 | display "Selected #{runtime}" 229 | end 230 | runtime 231 | end 232 | 233 | def bind_services(user_services, chosen = 0) 234 | svcname = ask( 235 | "Which one?", 236 | :indexed => true, 237 | :choices => user_services.collect { |p| p[:name] }) 238 | 239 | svc = user_services.find { |p| p[:name] == svcname } 240 | 241 | set svc[:vendor], "services", svcname, "type" 242 | 243 | if chosen + 1 < user_services.size && ask("Bind another?", :default => false) 244 | bind_services(user_services, chosen + 1) 245 | end 246 | end 247 | 248 | def create_services(services) 249 | svcs = services.collect(&:to_s).sort! 250 | 251 | configure_service( 252 | ask( 253 | "What kind of service?", 254 | :indexed => true, 255 | :choices => svcs 256 | ) 257 | ) 258 | 259 | if ask "Create another?", :default => false 260 | create_services(services) 261 | end 262 | end 263 | 264 | def configure_service(vendor) 265 | default_name = random_service_name(vendor) 266 | name = ask "Specify the name of the service", :default => default_name 267 | 268 | set vendor, "services", name, "type" 269 | end 270 | 271 | private 272 | def services_for_infra(infra) 273 | if client.infra_supported? 274 | client.services.select { |s| s[:infra] && s[:infra][:provider] == manifest("infra") } 275 | else 276 | client.services 277 | end 278 | end 279 | 280 | def ordered_by_deps(apps, abspaths = nil, processed = Set[]) 281 | unless abspaths 282 | abspaths = {} 283 | apps.each do |p, i| 284 | ep = File.expand_path("../" + p, manifest_file) 285 | abspaths[ep] = i 286 | end 287 | end 288 | 289 | ordered = [] 290 | apps.each do |path, info| 291 | epath = File.expand_path("../" + path, manifest_file) 292 | 293 | if deps = info["depends-on"] 294 | dep_apps = {} 295 | deps.each do |dep| 296 | edep = File.expand_path("../" + dep, manifest_file) 297 | 298 | err "Circular dependency detected." if processed.include? edep 299 | 300 | dep_apps[dep] = abspaths[edep] 301 | end 302 | 303 | processed.add(epath) 304 | 305 | ordered += ordered_by_deps(dep_apps, abspaths, processed) 306 | ordered << [path, info] 307 | elsif not processed.include? epath 308 | ordered << [path, info] 309 | processed.add(epath) 310 | end 311 | end 312 | 313 | ordered 314 | end 315 | 316 | end 317 | -------------------------------------------------------------------------------- /lib/cli/services_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | module VMC::Cli 3 | module ServicesHelper 4 | def display_system_services(services=nil) 5 | services ||= client.services_info 6 | 7 | display "\n============== System Services ==============\n\n" 8 | 9 | return display "No system services available" if services.empty? 10 | 11 | displayed_services = [] 12 | services.each do |service_type, value| 13 | value.each do |vendor, version| 14 | version.each do |version_str, service| 15 | displayed_services << [ vendor, version_str, service[:description] ] 16 | end 17 | end 18 | end 19 | displayed_services.sort! { |a, b| a.first.to_s <=> b.first.to_s} 20 | 21 | services_table = table do |t| 22 | t.headings = 'Service', 'Version', 'Description' 23 | displayed_services.each { |s| t << s } 24 | end 25 | display services_table 26 | end 27 | 28 | def display_provisioned_services(services=nil) 29 | services ||= client.services 30 | display "\n=========== Provisioned Services ============\n\n" 31 | display_provisioned_services_table(services) 32 | end 33 | 34 | def display_provisioned_services_table(services) 35 | return unless services && !services.empty? 36 | 37 | infra_supported = !services.detect { |a| a[:infra] }.nil? 38 | services_table = table do |t| 39 | t.headings = 'Name', 'Service' 40 | t.headings << 'In' if infra_supported 41 | services.each do |service| 42 | s = [ service[:name], service[:vendor] ] 43 | if infra_supported 44 | s << ( service[:infra] ? service[:infra][:provider] : " " ) 45 | end 46 | t << s 47 | end 48 | end 49 | display services_table 50 | end 51 | 52 | def create_service_banner(service, name, display_name=false, infra=nil) 53 | sn = " [#{name}]" if display_name 54 | display "Creating Service#{sn}: ", false 55 | client.create_service(infra,service, name) 56 | display 'OK'.green 57 | end 58 | 59 | def bind_service_banner(service, appname, check_restart=true) 60 | display "Binding Service [#{service}]: ", false 61 | client.bind_service(service, appname) 62 | display 'OK'.green 63 | check_app_for_restart(appname) if check_restart 64 | end 65 | 66 | def unbind_service_banner(service, appname, check_restart=true) 67 | display "Unbinding Service [#{service}]: ", false 68 | client.unbind_service(service, appname) 69 | display 'OK'.green 70 | check_app_for_restart(appname) if check_restart 71 | end 72 | 73 | def delete_service_banner(service) 74 | display "Deleting service [#{service}]: ", false 75 | client.delete_service(service) 76 | display 'OK'.green 77 | end 78 | 79 | def random_service_name(service) 80 | r = "%04x" % [rand(0x0100000)] 81 | "#{service.to_s}-#{r}" 82 | end 83 | 84 | def generate_cloned_service_name(src_appname,dest_appname,src_servicename,dest_infra) 85 | r = "%04x" % [rand(0x0100000)] 86 | dest_servicename = src_servicename.sub(src_appname,dest_appname).sub(/-[0-9A-Fa-f]{4,5}/,"-#{r}") 87 | if src_servicename == dest_servicename 88 | if dest_infra 89 | dest_servicename = "#{dest_servicename}-#{dest_infra}" 90 | else 91 | dest_servicename = "#{dest_servicename}-#{r}" 92 | end 93 | end 94 | dest_servicename 95 | end 96 | 97 | def check_app_for_restart(appname) 98 | app = client.app_info(appname) 99 | cmd = VMC::Cli::Command::Apps.new(@options) 100 | cmd.restart(appname) if app[:state] == 'STARTED' 101 | end 102 | 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/cli/tunnel_helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009-2011 VMware, Inc. 2 | 3 | require 'addressable/uri' 4 | 5 | begin 6 | require 'caldecott' 7 | rescue LoadError 8 | end 9 | 10 | module VMC::Cli 11 | module TunnelHelper 12 | PORT_RANGE = 10 13 | 14 | HELPER_APP = File.expand_path("../../../caldecott_helper", __FILE__) 15 | 16 | # bump this AND the version info reported by HELPER_APP/server.rb 17 | # this is to keep the helper in sync with any updates here 18 | HELPER_VERSION = '0.0.4' 19 | 20 | def tunnel_uniquename(infra) 21 | random_service_name(tunnel_appname(infra)) 22 | end 23 | 24 | def tunnel_appname(infra) 25 | infra ? "caldecott-#{infra}" : "caldecott" 26 | end 27 | 28 | def tunnel_app_info(infra) 29 | begin 30 | client.app_info(tunnel_appname(infra)) 31 | rescue => e 32 | nil 33 | end 34 | end 35 | 36 | def tunnel_auth(infra) 37 | tunnel_app_info(infra)[:env].each do |e| 38 | name, val = e.split("=", 2) 39 | return val if name == "CALDECOTT_AUTH" 40 | end 41 | nil 42 | end 43 | 44 | def tunnel_url(infra) 45 | 46 | tun_url = tunnel_app_info(infra)[:uris][0] 47 | 48 | ["https", "http"].each do |scheme| 49 | url = "#{scheme}://#{tun_url}" 50 | begin 51 | RestClient.get(url) 52 | 53 | # https failed 54 | rescue Errno::ECONNREFUSED 55 | 56 | # we expect a 404 since this request isn't auth'd 57 | rescue RestClient::ResourceNotFound 58 | return url 59 | end 60 | end 61 | 62 | err "Cannot determine URL for #{tun_url}" 63 | end 64 | 65 | def invalidate_tunnel_app_info(infra) 66 | end 67 | 68 | def tunnel_pushed?(infra) 69 | not tunnel_app_info(infra).nil? 70 | end 71 | 72 | def tunnel_healthy?(token,infra) 73 | return false unless tunnel_app_info(infra)[:state] == 'STARTED' 74 | 75 | begin 76 | response = RestClient.get( 77 | "#{tunnel_url(infra)}/info", 78 | "Auth-Token" => token 79 | ) 80 | info = JSON.parse(response) 81 | if info["version"] == HELPER_VERSION 82 | true 83 | else 84 | stop_caldecott(infra) 85 | false 86 | end 87 | rescue RestClient::Exception 88 | stop_caldecott(infra) 89 | false 90 | end 91 | end 92 | 93 | def tunnel_bound?(service,infra) 94 | tunnel_app_info(infra)[:services].include?(service) 95 | end 96 | 97 | def tunnel_connection_info(type, service, token, infra) 98 | display "Getting tunnel connection info: ", false 99 | response = nil 100 | 10.times do 101 | begin 102 | response = RestClient.get(tunnel_url(infra) + "/" + VMC::Client.path("services", service), "Auth-Token" => token) 103 | break 104 | rescue RestClient::Exception => e 105 | puts "Error infra: #{infra}, url: #{tunnel_url(infra)}" 106 | display tunnel_url(infra) 107 | puts e.message.red 108 | sleep 1 109 | end 110 | 111 | display ".", false 112 | end 113 | 114 | unless response 115 | err "Expected remote tunnel to know about #{service}, but it doesn't" 116 | end 117 | 118 | display "OK".green 119 | 120 | info = JSON.parse(response) 121 | info["infra"] = infra 122 | case type 123 | when "rabbitmq" 124 | uri = Addressable::URI.parse info["url"] 125 | info["hostname"] = uri.host 126 | info["port"] = uri.port 127 | info["vhost"] = uri.path[1..-1] 128 | info["user"] = uri.user 129 | info["password"] = uri.password 130 | info.delete "url" 131 | 132 | # we use "db" as the "name" for mongo 133 | # existing "name" is junk 134 | when "mongodb" 135 | info["name"] = info["db"] 136 | info.delete "db" 137 | 138 | # our "name" is irrelevant for redis 139 | when "redis" 140 | info.delete "name" 141 | end 142 | 143 | ['hostname', 'port', 'password'].each do |k| 144 | err "Could not determine #{k} for #{service}" if info[k].nil? 145 | end 146 | 147 | info 148 | end 149 | 150 | def display_tunnel_connection_info(info) 151 | display '' 152 | display "Service connection info: " 153 | 154 | to_show = [nil, nil, nil] # reserved for user, pass, db name 155 | info.keys.each do |k| 156 | case k 157 | when "host", "hostname", "port", "node_id" 158 | # skip 159 | when "user", "username" 160 | # prefer "username" over "user" 161 | to_show[0] = k unless to_show[0] == "username" 162 | when "password" 163 | to_show[1] = k 164 | when "name" 165 | to_show[2] = k 166 | else 167 | to_show << k 168 | end 169 | end 170 | to_show.compact! 171 | 172 | align_len = to_show.collect(&:size).max + 1 173 | 174 | to_show.each do |k| 175 | # TODO: modify the server services rest call to have explicit knowledge 176 | # about the items to return. It should return all of them if 177 | # the service is unknown so that we don't have to do this weird 178 | # filtering. 179 | display " #{k.ljust align_len}: ", false 180 | display "#{info[k]}".yellow 181 | end 182 | display '' 183 | end 184 | 185 | def start_tunnel(local_port, conn_info, auth, infra) 186 | @local_tunnel_thread = Thread.new do 187 | Caldecott::Client.start({ 188 | :local_port => local_port, 189 | :tun_url => tunnel_url(infra), 190 | :dst_host => conn_info['hostname'], 191 | :dst_port => conn_info['port'], 192 | :log_file => STDOUT, 193 | :log_level => ENV["VMC_TUNNEL_DEBUG"] || "ERROR", 194 | :auth_token => auth, 195 | :quiet => true 196 | }) 197 | end 198 | 199 | at_exit { @local_tunnel_thread.kill } 200 | end 201 | 202 | 203 | 204 | def pick_tunnel_port(port) 205 | original = port 206 | 207 | PORT_RANGE.times do |n| 208 | begin 209 | TCPSocket.open('localhost', port) 210 | port += 1 211 | rescue 212 | return port 213 | end 214 | end 215 | 216 | grab_ephemeral_port 217 | end 218 | 219 | def grab_ephemeral_port 220 | socket = TCPServer.new('0.0.0.0', 0) 221 | socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) 222 | Socket.do_not_reverse_lookup = true 223 | port = socket.addr[1] 224 | socket.close 225 | return port 226 | end 227 | 228 | def wait_for_tunnel_start(port) 229 | 10.times do |n| 230 | begin 231 | client = TCPSocket.open('localhost', port) 232 | display '' if n > 0 233 | client.close 234 | return true 235 | rescue => e 236 | display "Waiting for local tunnel to become available", false if n == 0 237 | display '.', false 238 | sleep 1 239 | end 240 | end 241 | err "Could not connect to local tunnel." 242 | end 243 | 244 | def wait_for_tunnel_end 245 | display "Open another shell to run command-line clients or" 246 | display "use a UI tool to connect using the displayed information." 247 | display "Press Ctrl-C to exit..." 248 | @local_tunnel_thread.join 249 | end 250 | 251 | def resolve_symbols(str, info, local_port) 252 | str.gsub(/\$\{\s*([^\}]+)\s*\}/) do 253 | case $1 254 | when "host" 255 | # TODO: determine proper host 256 | "localhost" 257 | when "port" 258 | local_port 259 | when "user", "username" 260 | info["username"] 261 | else 262 | info[$1] || ask($1) 263 | end 264 | end 265 | end 266 | 267 | def start_local_prog(clients, command, info, port) 268 | client = clients[File.basename(command)] 269 | 270 | cmdline = "#{command} " 271 | 272 | case client 273 | when Hash 274 | cmdline << resolve_symbols(client["command"], info, port) 275 | client["environment"].each do |e| 276 | if e =~ /([^=]+)=(["']?)([^"']*)\2/ 277 | ENV[$1] = resolve_symbols($3, info, port) 278 | else 279 | err "Invalid environment variable: #{e}" 280 | end 281 | end 282 | when String 283 | cmdline << resolve_symbols(client, info, port) 284 | else 285 | err "Unknown client info: #{client.inspect}." 286 | end 287 | 288 | display "Launching '#{cmdline}'" 289 | display '' 290 | 291 | system(cmdline) 292 | end 293 | 294 | def push_caldecott(token,infra) 295 | manifest = { 296 | :name => tunnel_appname(infra), 297 | :staging => {:framework => "sinatra", :runtime => "ruby18" }, 298 | :uris => ["#{tunnel_uniquename(infra)}.#{client.base_for_infra(infra)}"], 299 | :instances => 1, 300 | :resources => {:memory => 64}, 301 | :env => ["CALDECOTT_AUTH=#{token}"] 302 | } 303 | manifest[:infra] = { :provider => infra } if infra 304 | 305 | client.create_app( 306 | tunnel_appname(infra), 307 | manifest 308 | ) 309 | 310 | apps_cmd.send(:upload_app_bits, tunnel_appname(infra), HELPER_APP, infra) 311 | 312 | invalidate_tunnel_app_info(infra) 313 | end 314 | 315 | def stop_caldecott(infra) 316 | apps_cmd.stop(tunnel_appname(infra)) 317 | 318 | invalidate_tunnel_app_info(infra) 319 | end 320 | 321 | def start_caldecott(infra) 322 | apps_cmd.start(tunnel_appname(infra)) 323 | 324 | invalidate_tunnel_app_info(infra) 325 | end 326 | 327 | private 328 | 329 | def apps_cmd 330 | a = Command::Apps.new(@options) 331 | a.client client 332 | a 333 | end 334 | 335 | end 336 | end 337 | -------------------------------------------------------------------------------- /lib/cli/usage.rb: -------------------------------------------------------------------------------- 1 | class VMC::Cli::Runner 2 | 3 | def basic_usage 4 | "Usage: af [options] command [] [command_options]\n" + 5 | "Try 'af help [command]' or 'af help options' for more information." 6 | end 7 | 8 | def display_usage 9 | if @usage 10 | say @usage_error if @usage_error 11 | say "Usage: #{@usage}" 12 | return 13 | elsif @verb_usage 14 | say @verb_usage 15 | return 16 | end 17 | say command_usage 18 | end 19 | 20 | def command_usage 21 | <<-USAGE 22 | 23 | #{basic_usage} 24 | 25 | Currently available af commands are: 26 | 27 | Getting Started 28 | target [url] Reports current target or sets a new target 29 | login [email] [--email, --passwd] Login 30 | info System and account information 31 | 32 | Applications 33 | apps List deployed applications 34 | 35 | Application Creation 36 | push [appname] Create, push, map, and start a new application 37 | push [appname] --infra Push application to specified infrastructure 38 | push [appname] --path Push application from specified path 39 | push [appname] --url Set the url for the application 40 | push [appname] --instances Set the expected number of instances 41 | push [appname] --mem M Set the memory reservation for the application 42 | push [appname] --runtime RUNTIME Set the runtime to use for the application 43 | push [appname] --debug [MODE] Push application and start in a debug mode 44 | push [appname] --no-start Do not auto-start the application 45 | push [appname] --label Add specified label to app revision record 46 | 47 | Application Operations 48 | start [--debug [MODE]] Start the application 49 | stop Stop the application 50 | restart [--debug [MODE]] Restart the application 51 | delete Delete the application 52 | clone [infra] --label LABEL Clone the application and services 53 | 54 | Application Updates 55 | update [--path,--debug [MODE],--label] Update the application bits 56 | mem [memsize] Update the memory reservation for an application 57 | map Register the application to the url 58 | unmap Unregister the application from the url 59 | instances Scale the application instances up or down 60 | 61 | Application Information 62 | crashes List recent application crashes 63 | crashlogs Display log information for crashed applications 64 | logs [--all] Display log information for the application 65 | files [path] [--all] Display directory listing or file download for [path] 66 | stats Display resource usage for the application 67 | instances List application instances 68 | history Show version history of the application 69 | diff Compare current directory with deployed application 70 | hash [path] [--full] Compute hash of directory, defaults to current 71 | 72 | Application Download 73 | pull [path] Downloads last pushed source to or [path] 74 | download [path] Downloads last pushed source to zipfile 75 | 76 | Application Environment 77 | env List application environment variables 78 | env-add Add an environment variable to an application 79 | env-del Delete an environment variable to an application 80 | 81 | Services 82 | services Lists of services available and provisioned 83 | create-service [--name,--bind] Create a provisioned service 84 | create-service --infra Create a provisioned service on a specified infrastructure 85 | create-service Create a provisioned service and assign it 86 | create-service Create a provisioned service and assign it , and bind to 87 | delete-service [servicename] Delete a provisioned service 88 | bind-service Bind a service to an application 89 | unbind-service Unbind service from the application 90 | clone-services Clone service bindings from application to 91 | export-service Export the data from a service 92 | import-service Import data into a service 93 | tunnel [--port] Create a local tunnel to a service 94 | tunnel Create a local tunnel to a service and start a local client 95 | 96 | Administration 97 | user Display user account information 98 | passwd Change the password for the current user 99 | logout Logs current user out of the target system 100 | add-user [--email, --passwd] Register a new user (requires admin privileges) 101 | delete-user Delete a user and all apps and services (requires admin privileges) 102 | 103 | System 104 | runtimes Display the supported runtimes of the target system 105 | frameworks Display the recognized frameworks of the target system 106 | infras Display the available infrastructures 107 | 108 | Micro Cloud Foundry 109 | micro status Display Micro Cloud Foundry VM status 110 | micro offline Configure Micro Cloud Foundry VM for offline mode 111 | micro online Configure Micro Cloud Foundry VM for online mode 112 | [--vmx file] Path to micro.vmx 113 | [--vmrun executable] Path to vmrun executable 114 | [--password cleartext] Cleartext password for guest VM vcap user 115 | [--save] Save cleartext password in ~/.af_micro 116 | 117 | Misc 118 | aliases List aliases 119 | alias Create an alias for a command 120 | unalias Remove an alias 121 | targets List known targets and associated authorization tokens 122 | 123 | Help 124 | help [command] Get general help or help on a specific command 125 | help options Get help on available options 126 | USAGE 127 | 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/cli/version.rb: -------------------------------------------------------------------------------- 1 | module VMC 2 | module Cli 3 | # This version number is used as the RubyGem release version. 4 | # The internal VMC version number is VMC::VERSION. 5 | VERSION = '0.3.22' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/cli/zip_util.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'zip/zipfilesystem' 3 | require 'pathname' 4 | 5 | module VMC::Cli 6 | 7 | class ZipUtil 8 | 9 | PACK_EXCLUSION_GLOBS = ['..', '.', '*~', '#*#', '*.log'] 10 | 11 | class << self 12 | 13 | def to_dev_null 14 | if WINDOWS 15 | 'nul' 16 | else 17 | '/dev/null' 18 | end 19 | end 20 | 21 | def entry_lines(file) 22 | contents = nil 23 | unless VMC::Cli::Config.nozip 24 | contents = `unzip -l #{file} 2> #{to_dev_null}` 25 | contents = nil if $? != 0 26 | end 27 | # Do Ruby version if told to or native version failed 28 | unless contents 29 | entries = [] 30 | Zip::ZipFile.foreach(file) { |zentry| entries << zentry } 31 | contents = entries.join("\n") 32 | end 33 | contents 34 | end 35 | 36 | def unpack(file, dest) 37 | unless VMC::Cli::Config.nozip 38 | FileUtils.mkdir(dest) 39 | `unzip -q #{file} -d #{dest} 2> #{to_dev_null}` 40 | return unless $? != 0 41 | end 42 | # Do Ruby version if told to or native version failed 43 | Zip::ZipFile.foreach(file) do |zentry| 44 | epath = "#{dest}/#{zentry}" 45 | dirname = File.dirname(epath) 46 | FileUtils.mkdir_p(dirname) unless File.exists?(dirname) 47 | zentry.extract(epath) unless File.exists?(epath) 48 | end 49 | end 50 | 51 | def get_files_to_pack(dir) 52 | Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).select do |f| 53 | process = true 54 | PACK_EXCLUSION_GLOBS.each { |e| process = false if File.fnmatch(e, File.basename(f)) } 55 | process && File.exists?(f) 56 | end 57 | end 58 | 59 | def pack(dir, zipfile) 60 | unless VMC::Cli::Config.nozip 61 | excludes = PACK_EXCLUSION_GLOBS.map { |e| "\\#{e}" } 62 | excludes = excludes.join(' ') 63 | Dir.chdir(dir) do 64 | `zip -y -q -r #{zipfile} . -x #{excludes} 2> #{to_dev_null}` 65 | return unless $? != 0 66 | end 67 | end 68 | # Do Ruby version if told to or native version failed 69 | Zip::ZipFile::open(zipfile, true) do |zf| 70 | get_files_to_pack(dir).each do |f| 71 | zf.add(f.sub("#{dir}/",''), f) 72 | end 73 | end 74 | end 75 | 76 | BLOCKSIZE_TO_READ = 1024 * 1000 77 | 78 | # not a valid tar file, since tar files include last modified date which breaks it for 79 | # use in hashing. this method just wraps up everything for use in hashing. 80 | def tar(path) 81 | tar_filename = Pathname.new(path).realpath.to_path + '.tar' 82 | File.open(tar_filename, 'wb') do |tarfile| 83 | get_files_to_pack(path).each do |file| 84 | if File.file?(file) 85 | File.open(file, 'rb') do |f| 86 | while buffer = f.read(BLOCKSIZE_TO_READ) 87 | tarfile.write buffer 88 | end 89 | end 90 | else 91 | tarfile.write file.gsub(path, '') 92 | end 93 | end 94 | end 95 | 96 | tar_filename 97 | 98 | end 99 | 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/vmc.rb: -------------------------------------------------------------------------------- 1 | module VMC; end 2 | 3 | require 'vmc/client' 4 | -------------------------------------------------------------------------------- /lib/vmc/const.rb: -------------------------------------------------------------------------------- 1 | module VMC 2 | 3 | # This is the internal VMC version number, and is not necessarily 4 | # the same as the RubyGem version (VMC::Cli::VERSION). 5 | VERSION = '0.3.18.1' 6 | 7 | # Targets 8 | DEFAULT_TARGET = 'https://api.appfog.com' 9 | DEFAULT_LOCAL_TARGET = 'http://api.vcap.me' 10 | 11 | # General Paths 12 | INFO_PATH = 'info' 13 | GLOBAL_SERVICES_PATH = ['info', 'services'] 14 | GLOBAL_RUNTIMES_PATH = ['info', 'runtimes'] 15 | GLOBAL_INFRAS_PATH = ['info', 'infras'] 16 | RESOURCES_PATH = 'resources' 17 | 18 | # User specific paths 19 | APPS_PATH = 'apps' 20 | SERVICES_PATH = 'services' 21 | USERS_PATH = 'users' 22 | 23 | # Service paths 24 | SERVICE_EXPORT_PATH = ['services','export'] 25 | SERVICE_IMPORT_PATH = ['services','import'] 26 | 27 | end 28 | -------------------------------------------------------------------------------- /lib/vmc/micro.rb: -------------------------------------------------------------------------------- 1 | require 'find' 2 | 3 | module VMC::Micro 4 | def config_file(file) 5 | File.join(File.dirname(__FILE__), '..', '..', 'config', 'micro', file) 6 | end 7 | 8 | def escape_path(path) 9 | path = File.expand_path(path) 10 | if RUBY_PLATFORM =~ /mingw|mswin32|cygwin/ 11 | if path.include?(' ') 12 | return '"' + path + '"' 13 | else 14 | return path 15 | end 16 | else 17 | return path.gsub(' ', '\ ') 18 | end 19 | end 20 | 21 | def locate_file(file, directory, search_paths) 22 | search_paths.each do |path| 23 | expanded_path = File.expand_path(path) 24 | if File.exists?(expanded_path) 25 | Find.find(expanded_path) do |current| 26 | if File.directory?(current) && current.include?(directory) 27 | full_path = File.join(current, file) 28 | return self.escape_path(full_path) if File.exists?(full_path) 29 | end 30 | end 31 | end 32 | end 33 | 34 | false 35 | end 36 | 37 | def run_command(command, args=nil) 38 | # TODO switch to using posix-spawn instead 39 | result = %x{#{command} #{args} 2>&1} 40 | unless $?.exitstatus == 0 41 | if block_given? 42 | yield 43 | else 44 | raise "failed to execute #{command} #{args}:\n#{result}" 45 | end 46 | else 47 | result.split(/\n/) 48 | end 49 | end 50 | 51 | module_function :config_file 52 | module_function :escape_path 53 | module_function :locate_file 54 | module_function :run_command 55 | 56 | end 57 | -------------------------------------------------------------------------------- /lib/vmc/micro/switcher/base.rb: -------------------------------------------------------------------------------- 1 | require 'interact' 2 | 3 | module VMC::Micro::Switcher 4 | class Base 5 | include Interactive 6 | 7 | def initialize(config) 8 | @config = config 9 | 10 | @vmrun = VMC::Micro::VMrun.new(config) 11 | unless @vmrun.running? 12 | if ask("Micro Cloud Foundry VM is not running. Do you want to start it?", :choices => ['y', 'n']) == 'y' 13 | display "Starting Micro Cloud Foundry VM: ", false 14 | @vmrun.start 15 | say "done".green 16 | else 17 | err "Micro Cloud Foundry VM needs to be running." 18 | end 19 | end 20 | 21 | err "Micro Cloud Foundry VM initial setup needs to be completed before using 'vmc micro'" unless @vmrun.ready? 22 | end 23 | 24 | def offline 25 | unless @vmrun.offline? 26 | # save online connection type so we can restore it later 27 | @config['online_connection_type'] = @vmrun.connection_type 28 | 29 | if (@config['online_connection_type'] != 'nat') 30 | if ask("Reconfigure Micro Cloud Foundry VM network to nat mode and reboot?", :choices => ['y', 'n']) == 'y' 31 | display "Rebooting Micro Cloud Foundry VM: ", false 32 | @vmrun.connection_type = 'nat' 33 | @vmrun.reset 34 | say "done".green 35 | else 36 | err "Aborted" 37 | end 38 | end 39 | 40 | display "Setting Micro Cloud Foundry VM to offline mode: ", false 41 | @vmrun.offline! 42 | say "done".green 43 | display "Setting host DNS server: ", false 44 | 45 | @config['domain'] = @vmrun.domain 46 | @config['ip'] = @vmrun.ip 47 | set_nameserver(@config['domain'], @config['ip']) 48 | say "done".green 49 | else 50 | say "Micro Cloud Foundry VM already in offline mode".yellow 51 | end 52 | end 53 | 54 | def online 55 | if @vmrun.offline? 56 | current_connection_type = @vmrun.connection_type 57 | @config['online_connection_type'] ||= current_connection_type 58 | 59 | if (@config['online_connection_type'] != current_connection_type) 60 | # TODO handle missing connection type in saved config 61 | question = "Reconfigure Micro Cloud Foundry VM network to #{@config['online_connection_type']} mode and reboot?" 62 | if ask(question, :choices => ['y', 'n']) == 'y' 63 | display "Rebooting Micro Cloud Foundry VM: ", false 64 | @vmrun.connection_type = @config['online_connection_type'] 65 | @vmrun.reset 66 | say "done".green 67 | else 68 | err "Aborted" 69 | end 70 | end 71 | 72 | display "Unsetting host DNS server: ", false 73 | # TODO handle missing domain and ip in saved config (look at the VM) 74 | @config['domain'] ||= @vmrun.domain 75 | @config['ip'] ||= @vmrun.ip 76 | unset_nameserver(@config['domain'], @config['ip']) 77 | say "done".green 78 | 79 | display "Setting Micro Cloud Foundry VM to online mode: ", false 80 | @vmrun.online! 81 | say "done".green 82 | else 83 | say "Micro Cloud Foundry already in online mode".yellow 84 | end 85 | end 86 | 87 | def status 88 | mode = @vmrun.offline? ? 'offline' : 'online' 89 | say "Micro Cloud Foundry VM currently in #{mode.green} mode" 90 | # should the VMX path be unescaped? 91 | say "VMX Path: #{@vmrun.vmx}" 92 | say "Domain: #{@vmrun.domain.green}" 93 | say "IP Address: #{@vmrun.ip.green}" 94 | end 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /lib/vmc/micro/switcher/darwin.rb: -------------------------------------------------------------------------------- 1 | module VMC::Micro::Switcher 2 | 3 | class Darwin < Base 4 | def adminrun(command) 5 | VMC::Micro.run_command("osascript", "-e 'do shell script \"#{command}\" with administrator privileges'") 6 | end 7 | 8 | def set_nameserver(domain, ip) 9 | File.open("/tmp/#{domain}", 'w') { |file| file.write("nameserver #{ip}") } 10 | adminrun("mkdir -p /etc/resolver;mv /tmp/#{domain} /etc/resolver/") 11 | end 12 | 13 | def unset_nameserver(domain, ip) 14 | err "domain missing" unless domain 15 | adminrun("rm -f /etc/resolver/#{domain}") 16 | end 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /lib/vmc/micro/switcher/dummy.rb: -------------------------------------------------------------------------------- 1 | # only used for testing 2 | module VMC::Micro::Switcher 3 | 4 | class Dummy < Base 5 | def adminrun(command) 6 | end 7 | 8 | def set_nameserver(domain, ip) 9 | end 10 | 11 | def unset_nameserver(domain, ip) 12 | end 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/vmc/micro/switcher/linux.rb: -------------------------------------------------------------------------------- 1 | module VMC::Micro::Switcher 2 | 3 | class Linux < Base 4 | def set_nameserver(domain, ip) 5 | VMC::Micro.run_command("sudo", "sed -i'.backup' '1 i nameserver #{ip}' /etc/resolv.conf") 6 | # lock resolv.conf so Network Manager doesn't clear out the file when offline 7 | VMC::Micro.run_command("sudo", "chattr +i /etc/resolv.conf") 8 | end 9 | 10 | def unset_nameserver(domain, ip) 11 | VMC::Micro.run_command("sudo", "chattr -i /etc/resolv.conf") 12 | VMC::Micro.run_command("sudo", "sed -i'.backup' '/#{ip}/d' /etc/resolv.conf") 13 | end 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/vmc/micro/switcher/windows.rb: -------------------------------------------------------------------------------- 1 | module VMC::Micro::Switcher 2 | 3 | class Windows < Base 4 | def version? 5 | VMC::Micro.run_command("cmd", "/c ver").to_s.scan(/\d+\.\d+/).first.to_f 6 | end 7 | 8 | def adminrun(command, args=nil) 9 | if version? > 5.2 10 | require 'win32ole' 11 | shell = WIN32OLE.new("Shell.Application") 12 | shell.ShellExecute(command, args, nil, "runas", 0) 13 | else 14 | # on older version this will try to run the command, and if you don't have 15 | # admin privilges it will tell you so and exit 16 | VMC::Micro.run_command(command, args) 17 | end 18 | end 19 | 20 | # TODO better method to figure out the interface name is to get the NAT ip and find the 21 | # interface with the correct subnet 22 | def set_nameserver(domain, ip) 23 | adminrun("netsh", "interface ip set dns \"VMware Network Adapter VMnet8\" static #{ip}") 24 | end 25 | 26 | def unset_nameserver(domain, ip) 27 | adminrun("netsh", "interface ip set dns \"VMware Network Adapter VMnet8\" static none") 28 | end 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/vmc/micro/vmrun.rb: -------------------------------------------------------------------------------- 1 | module VMC::Micro 2 | class VMrun 3 | attr_reader :vmx, :vmrun 4 | 5 | def initialize(config) 6 | @platform = config['platform'] 7 | @user = 'root' # must use root as we muck around with system settings 8 | @password = config['password'] 9 | @vmrun = config['vmrun'] 10 | @vmx = config['vmx'] 11 | 12 | # TODO honor TMPDIR 13 | if @platform == :windows 14 | @temp_dir = ENV['temp'] 15 | else 16 | @temp_dir = '/tmp' 17 | end 18 | end 19 | 20 | def connection_type 21 | read_variable('ethernet0.connectionType') 22 | end 23 | 24 | def connection_type=(type) 25 | write_variable("ethernet0.connectionType", type) 26 | end 27 | 28 | def nat? 29 | connection_type == "nat" 30 | end 31 | 32 | def bridged? 33 | connection_type == "bridged" 34 | end 35 | 36 | def domain 37 | # switch to Dir.mktmpdir 38 | state_config = VMC::Micro.escape_path(File.join(@temp_dir, 'state.yml')) 39 | run('CopyFileFromGuestToHost', "/var/vcap/bosh/state.yml #{state_config}") 40 | bosh_config = YAML.load_file(state_config) 41 | bosh_config['properties']['domain'] 42 | end 43 | 44 | def ip 45 | # switch to Dir.mktmpdir 46 | path = VMC::Micro.escape_path(VMC::Micro.config_file('refresh_ip.rb')) 47 | ip_file = VMC::Micro.escape_path(File.join(@temp_dir, 'ip.txt')) 48 | run('CopyFileFromHostToGuest', "#{path} /tmp/refresh_ip.rb") 49 | run('runProgramInGuest', '/tmp/refresh_ip.rb') 50 | run('CopyFileFromGuestToHost', "/tmp/ip.txt #{ip_file}") 51 | File.open(ip_file, 'r') { |file| file.read } 52 | end 53 | 54 | def list 55 | vms = run("list") 56 | vms.delete_if { |line| line =~ /^Total/ } 57 | vms.map { |line| VMC::Micro.escape_path(File.expand_path(line)) } 58 | end 59 | 60 | def offline? 61 | command = "-gu #{@user} -gp #{@password} runProgramInGuest" 62 | args = '/usr/bin/test -e /var/vcap/micro/offline' 63 | # why not use run_command? 64 | result = %x{#{@vmrun} #{command} #{@vmx} #{args}} 65 | 66 | if result.include?('Guest program exited with non-zero exit code: 1') 67 | return false 68 | elsif $?.exitstatus == 0 69 | return true 70 | else 71 | raise "failed to execute vmrun:\n#{result}" 72 | end 73 | end 74 | 75 | def offline! 76 | path = VMC::Micro.escape_path(VMC::Micro.config_file('offline.conf')) 77 | run('CopyFileFromHostToGuest', "#{path} /etc/dnsmasq.d/offline.conf") 78 | run('runProgramInGuest', '/usr/bin/touch /var/vcap/micro/offline') 79 | restart_dnsmasq 80 | end 81 | 82 | def online! 83 | run('runProgramInGuest', '/bin/rm -f /etc/dnsmasq.d/offline.conf') 84 | run('runProgramInGuest', '/bin/rm -f /var/vcap/micro/offline') 85 | restart_dnsmasq 86 | end 87 | 88 | # check to see if the micro cloud has been configured 89 | # uses default password to check 90 | def ready? 91 | command = "-gu root -gp 'ca$hc0w' runProgramInGuest" 92 | args = '/usr/bin/test -e /var/vcap/micro/micro.json' 93 | result = %x{#{@vmrun} #{command} #{@vmx} #{args}} 94 | 95 | if result.include?('Invalid user name or password for the guest OS') || $?.exitstatus == 0 96 | return true 97 | elsif $?.exitstatus == 1 98 | return false 99 | else 100 | raise "failed to execute vmrun:\n#{result}" 101 | end 102 | end 103 | 104 | def read_variable(var) 105 | # TODO deal with non-ok return 106 | run("readVariable", "runtimeConfig #{var}").first 107 | end 108 | 109 | def write_variable(var, value) 110 | run('writeVariable', "runtimeConfig #{var} #{value}") 111 | end 112 | 113 | def reset 114 | run('reset', 'soft') 115 | end 116 | 117 | def restart_dnsmasq 118 | # restart command doesn't always work, start and stop seems to be more reliable 119 | run('runProgramInGuest', '/etc/init.d/dnsmasq stop') 120 | run('runProgramInGuest', '/etc/init.d/dnsmasq start') 121 | end 122 | 123 | def run(command, args=nil) 124 | if command.include?('Guest') 125 | command = "-gu #{@user} -gp #{@password} #{command}" 126 | end 127 | VMC::Micro.run_command(@vmrun, "#{command} #{@vmx} #{args}") 128 | end 129 | 130 | def running? 131 | vms = list 132 | if @platform == :windows 133 | vms.map! { |x| x.downcase } 134 | vms.include?(@vmx.downcase) 135 | else 136 | vms.include?(@vmx) 137 | end 138 | end 139 | 140 | def start 141 | run('start') unless running? 142 | end 143 | 144 | def stop 145 | run('stop') if running? 146 | end 147 | 148 | def self.locate(platform) 149 | paths = YAML.load_file(VMC::Micro.config_file('paths.yml')) 150 | vmrun_paths = paths[platform.to_s]['vmrun'] 151 | vmrun_exe = @platform == :windows ? 'vmrun.exe' : 'vmrun' 152 | vmrun = VMC::Micro.locate_file(vmrun_exe, "VMware", vmrun_paths) 153 | err "Unable to locate vmrun, please supply --vmrun option" unless vmrun 154 | vmrun 155 | end 156 | end 157 | 158 | end 159 | -------------------------------------------------------------------------------- /spec/assets/app_info.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Fri, 04 Mar 2011 02:56:21 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 243 8 | 9 | {"resources":{"memory":64},"uris":["foo.vcap.me"],"staging":{"stack":"ruby foo.rb","model":"http://b20nine.com/unknown"},"state":"STARTED","instances":1,"name":"foo","meta":{"version":1,"created":1299207348},"services":[],"runningInstances":1} -------------------------------------------------------------------------------- /spec/assets/app_listings.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 19:46:19 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 251 8 | 9 | [{"resources":{"memory":128},"uris":["r.vcap.me"],"staging":{"stack":"ruby redis_sample.rb","model":"http://b20nine.com/unknown"},"state":"STARTED","instances":1,"name":"r","meta":{"version":1,"created":1298681379},"services":[],"runningInstances":1}] -------------------------------------------------------------------------------- /spec/assets/app_not_found.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 404 Not Found 2 | Server: nginx/0.7.65 3 | Date: Fri, 04 Mar 2011 02:56:21 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 9 8 | 9 | Not Found -------------------------------------------------------------------------------- /spec/assets/bad_create_app.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 400 Bad Request 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 23:37:51 GMT 4 | Content-Type: text/html;charset=utf-8 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 157 8 | 9 | {"code":10050,"description":"Invalid number of instances: \"'', App instances not presentApp instances is not a numberInstances must be between 1 and 100\""} -------------------------------------------------------------------------------- /spec/assets/console_access.txt: -------------------------------------------------------------------------------- 1 | username: cfuser 2 | password: testpw -------------------------------------------------------------------------------- /spec/assets/delete_app.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 204 2 | Server: nginx/0.7.65 3 | Date: Fri, 04 Mar 2011 00:07:10 GMT 4 | Content-Type: text/html;charset=utf-8 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Location: http://api.vcap.me/apps/foo 8 | Content-Length: 0 9 | 10 | -------------------------------------------------------------------------------- /spec/assets/global_service_listings.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 20:20:15 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 381 8 | 9 | {"key-value":{"redis":{"2":{"type":"key-value","tiers":{"free":{"order":1,"description":"Free offering (64MiB)"}},"version":"2","vendor":"redis","description":"Redis key-value store service"}}},"database":{"mysql":{"5.1":{"type":"database","tiers":{"free":{"order":1,"description":"Free offering (1GiB)"}},"version":"5.1","description":"MySQL database service","vendor":"mysql"}}}} -------------------------------------------------------------------------------- /spec/assets/good_create_app.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 302 Moved Temporarily 2 | Server: nginx/0.7.65 3 | Date: Fri, 04 Mar 2011 00:07:10 GMT 4 | Content-Type: text/html;charset=utf-8 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Location: http://api.vcap.me/apps/foo 8 | Content-Length: 0 9 | 10 | -------------------------------------------------------------------------------- /spec/assets/good_create_service.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 302 Moved Temporarily 2 | Server: nginx/0.7.65 3 | Date: Fri, 04 Mar 2011 02:15:55 GMT 4 | Content-Type: text/html;charset=utf-8 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Location: http://api.vcap.me/services/redis-86b7a8655555 8 | Content-Length: 0 9 | 10 | -------------------------------------------------------------------------------- /spec/assets/info_authenticated.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 19:25:34 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 380 8 | 9 | { 10 | "name": "vcap", 11 | "build": "3465a13ab528443f1afcd3c9c2861a078549b8e5", 12 | "support": "ac-support@vmware.com", 13 | "version": 0.999, 14 | "limits": { 15 | "apps": 50, 16 | "memory": 8192, 17 | "app_uris": 4, 18 | "services": 4 19 | }, 20 | "user": "derek@gmail.com", 21 | "description": "VMware's Cloud Application Platform", 22 | "usage": { 23 | "apps": 1, 24 | "memory": 128, 25 | "services": 0 26 | } 27 | } -------------------------------------------------------------------------------- /spec/assets/info_nil_usage.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 19:25:34 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 380 8 | 9 | { 10 | "name": "vcap", 11 | "build": "346578549b8e5", 12 | "support": "ac-support@vmware.com", 13 | "version": 0.999, 14 | "limits": { 15 | "apps": 50, 16 | "memory": 8192, 17 | "app_uris": 4, 18 | "services": 4 19 | }, 20 | "user": "nerf@gmail.com", 21 | "description": "VMware's Cloud Application Platform", 22 | "usage": { 23 | "apps": null, 24 | "memory": null, 25 | "services": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/assets/info_return.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 19:04:04 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 189 8 | 9 | { 10 | "name": "vcap", 11 | "build": "3465a13ab528443f1afcd3c9c2861a078549b8e5", 12 | "support": "ac-support@vmware.com", 13 | "version": 0.999, 14 | "description": "VMware's Cloud Application Platform" 15 | } -------------------------------------------------------------------------------- /spec/assets/info_return_bad.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 301 Moved Permanently 2 | Location: http://www.google.com/ 3 | Content-Type: text/html; charset=UTF-8 4 | Date: Thu, 03 Mar 2011 19:06:04 GMT 5 | Expires: Sat, 02 Apr 2011 19:06:04 GMT 6 | Cache-Control: public, max-age=2592000 7 | Server: gws 8 | Content-Length: 219 9 | X-XSS-Protection: 1; mode=block 10 | 11 | 12 | 301 Moved 13 |

301 Moved

14 | The document has moved 15 | here. 16 | 17 | -------------------------------------------------------------------------------- /spec/assets/invalid_console_access.txt: -------------------------------------------------------------------------------- 1 | username: cfuser -------------------------------------------------------------------------------- /spec/assets/list_users.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Sun, 22 May 2011 18:58:41 GMT 4 | Content-Type: application/json; charset=utf-8 5 | Transfer-Encoding: chunked 6 | Connection: keep-alive 7 | Keep-Alive: timeout=20 8 | ETag: "eb21ee2635a8b6b378f896e26b61f006" 9 | Cache-Control: max-age=0, private, must-revalidate 10 | X-UA-Compatible: IE=Edge 11 | Content-Length: 365 12 | 13 | [{"email":"test@example.com","admin":true,"apps":[{"name":"chat","state":"STARTED"},{"name":"smith","state":"STARTED"},{"name":"env","state":"STARTED"},{"name":"redirect","state":"STARTED"}]},{"email":"user2@example.com","admin":false,"apps":[]},{"email":"autotest@adamgreenfield.com","admin":false,"apps":[]},{"email":"user3@example.com","admin":false,"apps":[]}] 14 | -------------------------------------------------------------------------------- /spec/assets/login_fail.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 403 Forbidden 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 18:29:40 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 43 8 | 9 | {"code":2002,"description":"Invalid token"} -------------------------------------------------------------------------------- /spec/assets/login_success.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 18:26:45 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 112 8 | 9 | {"token":"04085b082214646572656b40676d61696c2e636f6d6c2b07e517794d22198e59d96b40ccbc3625964dfb8dcca21174835d7e"} -------------------------------------------------------------------------------- /spec/assets/manifests/bad-manifest.yml: -------------------------------------------------------------------------------- 1 | foo: ${bad-symbol} 2 | -------------------------------------------------------------------------------- /spec/assets/manifests/my-manifest.yml: -------------------------------------------------------------------------------- 1 | foo: 1 2 | bar: 2 3 | -------------------------------------------------------------------------------- /spec/assets/manifests/someapp/manifest.yml: -------------------------------------------------------------------------------- 1 | fizz: 1 2 | buzz: 2 3 | -------------------------------------------------------------------------------- /spec/assets/manifests/someapp/somedir/somesubdir/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appfog/af/b580808a25bb725477fb9fdbe107ba011230e1da/spec/assets/manifests/someapp/somedir/somesubdir/.gitignore -------------------------------------------------------------------------------- /spec/assets/manifests/somenomanifestapp/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appfog/af/b580808a25bb725477fb9fdbe107ba011230e1da/spec/assets/manifests/somenomanifestapp/.gitignore -------------------------------------------------------------------------------- /spec/assets/manifests/sub-manifest.yml: -------------------------------------------------------------------------------- 1 | inherit: "sym-manifest.yml" 2 | 3 | second: subbed ${third} 4 | d: subbed bar 5 | 6 | some-hash: 7 | hello: 8 | foo: one 9 | -------------------------------------------------------------------------------- /spec/assets/manifests/sym-manifest.yml: -------------------------------------------------------------------------------- 1 | c: 42 2 | d: bar 3 | 4 | foo: foo ${b} baz 5 | bar: fizz ${a} 6 | 7 | fizz: foo ${d} baz 8 | buzz: fizz ${c} 9 | 10 | properties: 11 | a: 43 12 | b: baz 13 | 14 | first: foo ${second} 15 | second: bar ${third} 16 | third: baz 17 | 18 | some-hash: 19 | hello: 20 | foo: 1 21 | bar: ${foo}-2 22 | 23 | goodbye: 24 | fizz: 3 25 | buzz: 4 26 | 27 | target: api.somecloud.com 28 | base: ${target-base} 29 | url: ${target-url} 30 | random: ${random-word} 31 | 32 | parent: 33 | foo: 0 34 | fizz: -1 35 | 36 | sub: 37 | foo: 1 38 | bar: ${foo} 39 | baz: ${fizz} 40 | 41 | sub2: 42 | foo: 2 43 | fizz: -2 44 | bar: ${foo} 45 | baz: ${fizz} 46 | 47 | bar: ${foo} 48 | -------------------------------------------------------------------------------- /spec/assets/resources_return.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 18:26:45 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 123 8 | 9 | [{"size":684,"sha1":"a956df4455d5c9c35ed7f4e2fd46ffd02089239f","fn":"/tmp/.vmc_foo_files/node_modules/.bin/express@2.5.1"}] 10 | -------------------------------------------------------------------------------- /spec/assets/sample_token.txt: -------------------------------------------------------------------------------- 1 | 04085b082214646572656b40676d61696c2e636f6d6c2b07f014794d2219b3dd205faeaefc0a8c70bb3ba786628c8cb8d667 2 | -------------------------------------------------------------------------------- /spec/assets/service_already_exists.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 400 Bad Request 2 | Server: nginx/0.7.65 3 | Date: Fri, 04 Mar 2011 02:19:28 GMT 4 | Content-Type: text/html;charset=utf-8 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 93 8 | 9 | {"code":20001,"description":"A service with the name: \"redis-86b7a8655555\" already exists"} -------------------------------------------------------------------------------- /spec/assets/service_gateway_fail.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 502 Bad Gateway 2 | Server : nginx/0.8.54 3 | Date : Tue, 16 Aug 2011 20:54:51 GMT 4 | Content_type : application/json; charset=utf-8 5 | Connection : keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 68 8 | 9 | {"code":503,"description":"Unexpected response from service gateway"} 10 | -------------------------------------------------------------------------------- /spec/assets/service_listings.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 21:46:29 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 313 8 | 9 | [{"type":"key-value","vendor":"redis","version":"2","options":{"name":"redis-83ddf593-0690-4856-baba-24cc7ad9b1b0","port":6148,"node_id":"redis_node_1","hostname":"127.0.0.1","password":"2167d1f8-c251-46a8-8eed-7d390ab74757"},"tier":"free","name":"redis-7ed7da9","meta":{"version":1,"created":1299188448},"id":3}] -------------------------------------------------------------------------------- /spec/assets/service_not_found.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 404 Not Found 2 | Server: nginx/0.7.65 3 | Date: Fri, 04 Mar 2011 02:26:39 GMT 4 | Content-Type: text/html;charset=utf-8 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 75 8 | 9 | {"code":21100,"description":"Service provision call failed due to timeout"} -------------------------------------------------------------------------------- /spec/assets/standalone_app_info.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Fri, 04 Mar 2011 02:56:21 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 243 8 | 9 | {"resources":{"memory":64},"uris":["foo.vcap.me"],"staging":{"runtime":"ruby18" ,"framework":"standalone"},"state":"STARTED","instances":1,"name":"foo","meta":{"version":1,"created":1299207348},"services":[],"runningInstances":1} -------------------------------------------------------------------------------- /spec/assets/user_info.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 200 OK 2 | Server: nginx/0.7.65 3 | Date: Thu, 03 Mar 2011 19:38:32 GMT 4 | Content-Type: application/json 5 | Connection: keep-alive 6 | Keep-Alive: timeout=20 7 | Content-Length: 69 8 | 9 | {"meta":{"version":1,"created":1298681367},"email":"derek@gmail.com"} -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | $:.unshift('./lib') 3 | require 'bundler' 4 | require 'bundler/setup' 5 | require 'vmc' 6 | require 'cli' 7 | 8 | require 'spec' 9 | require 'webmock/rspec' 10 | 11 | def spec_asset(filename) 12 | File.expand_path(File.join(File.dirname(__FILE__), "assets", filename)) 13 | end 14 | -------------------------------------------------------------------------------- /spec/unit/cli_opts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'VMC::Cli::Runner' do 4 | 5 | it 'should parse email and password correctly' do 6 | args = "--email derek@gmail.com --password foo" 7 | cli = VMC::Cli::Runner.new(args.split).parse_options! 8 | cli.options.should have(3).items 9 | cli.options.should have_key :email 10 | cli.options[:email].should == 'derek@gmail.com' 11 | cli.options[:password].should == 'foo' 12 | end 13 | 14 | it 'should parse multiple variations of password' do 15 | args = "--password foo" 16 | cli = VMC::Cli::Runner.new(args.split).parse_options! 17 | cli.options[:password].should == 'foo' 18 | 19 | args = "--pass foo" 20 | cli = VMC::Cli::Runner.new(args.split).parse_options! 21 | cli.options[:password].should == 'foo' 22 | 23 | args = "--passwd foo" 24 | cli = VMC::Cli::Runner.new(args.split).parse_options! 25 | cli.options[:password].should == 'foo' 26 | end 27 | 28 | it 'should parse name and bind args correctly' do 29 | args = "--name foo --bind bar" 30 | cli = VMC::Cli::Runner.new(args.split).parse_options! 31 | cli.options[:name].should == 'foo' 32 | cli.options[:bind].should == 'bar' 33 | end 34 | 35 | it 'should parse instances and instance into a number and string' do 36 | args = "--instances 1 --instance 2" 37 | cli = VMC::Cli::Runner.new(args.split).parse_options! 38 | cli.options[:instances].should == 1 39 | cli.options[:instance].should == "2" 40 | end 41 | 42 | it 'should parse url, mem, path correctly' do 43 | args = "--mem 64 --url http://foo.vcap.me --path ~derek" 44 | cli = VMC::Cli::Runner.new(args.split).parse_options! 45 | cli.options[:mem].should == '64' 46 | cli.options[:url].should == 'http://foo.vcap.me' 47 | cli.options[:path].should == '~derek' 48 | end 49 | 50 | it 'should parse multiple forms of nostart correctly' do 51 | cli = VMC::Cli::Runner.new().parse_options! 52 | cli.options[:nostart].should_not be 53 | args = "--nostart" 54 | cli = VMC::Cli::Runner.new(args.split).parse_options! 55 | cli.options[:nostart].should be_true 56 | args = "--no-start" 57 | cli = VMC::Cli::Runner.new(args.split).parse_options! 58 | cli.options[:nostart].should be_true 59 | end 60 | 61 | it 'should parse force and all correctly' do 62 | args = "--force --all" 63 | cli = VMC::Cli::Runner.new(args.split).parse_options! 64 | cli.options[:force].should be_true 65 | cli.options[:all].should be_true 66 | end 67 | 68 | it 'should parse debug correctly' do 69 | cli = VMC::Cli::Runner.new().parse_options! 70 | cli.options[:debug].should_not be 71 | args = "--debug" 72 | cli = VMC::Cli::Runner.new(args.split).parse_options! 73 | cli.options[:debug].should == 'run' 74 | args = "--debug suspend" 75 | cli = VMC::Cli::Runner.new(args.split).parse_options! 76 | cli.options[:debug].should == 'suspend' 77 | args = "-d" 78 | cli = VMC::Cli::Runner.new(args.split).parse_options! 79 | cli.options[:debug].should == 'run' 80 | args = "-d suspend" 81 | cli = VMC::Cli::Runner.new(args.split).parse_options! 82 | cli.options[:debug].should == 'suspend' 83 | end 84 | 85 | it 'should parse manifest override correctly' do 86 | cli = VMC::Cli::Runner.new().parse_options! 87 | cli.options[:manifest].should_not be 88 | args = "--manifest foo" 89 | cli = VMC::Cli::Runner.new(args.split).parse_options! 90 | cli.options[:manifest].should == 'foo' 91 | args = "-m foo" 92 | cli = VMC::Cli::Runner.new(args.split).parse_options! 93 | cli.options[:manifest].should == 'foo' 94 | end 95 | 96 | it 'should parse token override correctly' do 97 | cli = VMC::Cli::Runner.new().parse_options! 98 | cli.options[:token_file].should_not be 99 | args = "--token-file /tmp/foobar" 100 | cli = VMC::Cli::Runner.new(args.split).parse_options! 101 | cli.options[:token_file].should == '/tmp/foobar' 102 | end 103 | 104 | it "should parse infra correctly" do 105 | cli = VMC::Cli::Runner.new().parse_options! 106 | cli.options[:infra].should_not be 107 | args = "--infra aws" 108 | cli = VMC::Cli::Runner.new(args.split).parse_options! 109 | cli.options[:infra].should == "aws" 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /spec/unit/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'VMC::Client' do 4 | include WebMock::API 5 | 6 | before(:all) do 7 | @target = VMC::DEFAULT_TARGET 8 | @local_target = VMC::DEFAULT_LOCAL_TARGET 9 | @user = 'derek@gmail.com' 10 | @password = 'foo' 11 | @auth_token = spec_asset('sample_token.txt') 12 | end 13 | 14 | before(:each) do 15 | # make sure these get cleared so we don't have tests pass that shouldn't 16 | RestClient.proxy = nil 17 | ENV['http_proxy'] = nil 18 | ENV['https_proxy'] = nil 19 | end 20 | 21 | it 'should report its version' do 22 | VMC::Client.version.should =~ /\d.\d.\d/ 23 | end 24 | 25 | it 'should default to local target' do 26 | client = VMC::Client.new 27 | client.target.should == VMC::DEFAULT_TARGET 28 | end 29 | 30 | it 'should default to use secure protocol' do 31 | client = VMC::Client.new 32 | client.target.match(/^https/) 33 | end 34 | 35 | it 'should normalize target with no scheme' do 36 | client = VMC::Client.new('api.cloudfoundry.com') 37 | client.target.should == 'https://api.cloudfoundry.com' 38 | end 39 | 40 | it 'should properly initialize with auth_token' do 41 | client = VMC::Client.new(@target, @auth_token) 42 | client.target.should == @target 43 | client.auth_token.should == @auth_token 44 | end 45 | 46 | it 'should allow login correctly and return an auth_token' do 47 | login_path = "#{@local_target}/users/#{@user}/tokens" 48 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 49 | client = VMC::Client.new(@local_target) 50 | auth_token = client.login(@user, @password) 51 | client.target.should == @local_target 52 | client.user.should == @user 53 | client.auth_token.should be 54 | auth_token.should be 55 | auth_token.should == client.auth_token 56 | end 57 | 58 | it 'should raise exception if login fails' do 59 | login_path = "#{@local_target}/users/#{@user}/tokens" 60 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_fail.txt'))) 61 | client = VMC::Client.new(@local_target) 62 | expect { client.login(@user, @password) }.to raise_error(VMC::Client::TargetError) 63 | end 64 | 65 | it 'should allow admin users to proxy for others' do 66 | proxy = 'vadim@gmail.com' 67 | client = VMC::Client.new(@target) 68 | client.proxy_for(proxy) 69 | client.proxy.should == proxy 70 | end 71 | 72 | it 'should properly get info for valid target cloud' do 73 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 74 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_return.txt'))) 75 | client = VMC::Client.new(@local_target) 76 | info = client.info 77 | a_request(:get, info_path).should have_been_made.once 78 | info.should have_key :support 79 | info.should have_key :description 80 | info.should have_key :name 81 | info.should have_key :version 82 | info.should have_key :build 83 | end 84 | 85 | it 'should raise and exception for a bad target' do 86 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 87 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_return_bad.txt'))) 88 | client = VMC::Client.new(@local_target) 89 | expect {info = client.info}.to raise_error(VMC::Client::BadResponse) 90 | a_request(:get, info_path).should have_been_made.once 91 | end 92 | 93 | it 'should have target_valid? return true for a good target' do 94 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 95 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_return.txt'))) 96 | client = VMC::Client.new(@local_target) 97 | client.target_valid?.should be_true 98 | end 99 | 100 | it 'should have target_valid? return false for a bad target' do 101 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 102 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_return_bad.txt'))) 103 | client = VMC::Client.new(@local_target) 104 | client.target_valid?.should be_false 105 | end 106 | 107 | it 'should respond ok if properly logged in' do 108 | login_path = "#{@local_target}/users/#{@user}/tokens" 109 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 110 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 111 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 112 | client = VMC::Client.new(@local_target) 113 | client.login(@user, @password) 114 | client.logged_in?.should be_true 115 | end 116 | 117 | it 'should fail when trying to change password unless logged in' do 118 | login_path = "#{@local_target}/users/#{@user}/tokens" 119 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 120 | user_info_path = "#{@local_target}/users/#{@user}" 121 | stub_request(:get, user_info_path).to_return(File.new(spec_asset('user_info.txt'))) 122 | stub_request(:put, user_info_path) 123 | client = VMC::Client.new(@local_target) 124 | client.login(@user, @password) 125 | client.change_password('bar') 126 | end 127 | 128 | it 'should get a proper list of apps' do 129 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 130 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 131 | apps_path = "#{@local_target}/#{VMC::APPS_PATH}" 132 | stub_request(:get, apps_path).to_return(File.new(spec_asset('app_listings.txt'))) 133 | client = VMC::Client.new(@local_target, @auth_token) 134 | apps = client.apps 135 | apps.should have(1).items 136 | app = apps.first 137 | app.should have_key :state 138 | app.should have_key :uris 139 | app.should have_key :name 140 | app.should have_key :services 141 | app.should have_key :instances 142 | end 143 | 144 | it 'should get a proper list of users' do 145 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 146 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 147 | users_path = "#{@local_target}/#{VMC::USERS_PATH}" 148 | stub_request(:get, users_path).to_return(File.new(spec_asset('list_users.txt'))) 149 | client = VMC::Client.new(@local_target, @auth_token) 150 | users = client.users 151 | users.should have(4).items 152 | user = users.first 153 | user.should have_key :email 154 | user.should have_key :admin 155 | user.should have_key :apps 156 | end 157 | 158 | it 'should get a proper list of services' do 159 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 160 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 161 | services_path = "#{@local_target}/#{VMC::Client.path(VMC::GLOBAL_SERVICES_PATH)}" 162 | stub_request(:get, services_path).to_return(File.new(spec_asset('global_service_listings.txt'))) 163 | client = VMC::Client.new(@local_target, @auth_token) 164 | services = client.services_info 165 | services.should have(2).items 166 | # FIXME, add in more details. 167 | end 168 | 169 | it 'should get a proper list of provisioned services' do 170 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 171 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 172 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}" 173 | stub_request(:get, services_path).to_return(File.new(spec_asset('service_listings.txt'))) 174 | client = VMC::Client.new(@local_target, @auth_token) 175 | app_services = client.services 176 | app_services.should have(1).items 177 | redis = app_services.first 178 | redis.should have_key :type 179 | redis.should have_key :vendor 180 | end 181 | 182 | it 'should raise when trying to create an app with no manifest' do 183 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 184 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 185 | app_path = "#{@local_target}/#{VMC::APPS_PATH}" 186 | stub_request(:post, app_path).to_return(File.new(spec_asset('bad_create_app.txt'))) 187 | client = VMC::Client.new(@local_target, @auth_token) 188 | expect { client.create_app('foo') }.to raise_error(VMC::Client::NotFound) 189 | end 190 | 191 | it 'should create an app with a simple manifest' do 192 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 193 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 194 | app_path = "#{@local_target}/#{VMC::APPS_PATH}" 195 | stub_request(:post, app_path).to_return(File.new(spec_asset('good_create_app.txt'))) 196 | client = VMC::Client.new(@local_target, @auth_token) 197 | manifest = { 198 | :name => 'foo', 199 | :uris => ['foo.vcap.me'], 200 | :instances => 1, 201 | :staging => { :model => 'nodejs/1.0' }, 202 | :resources => { :memory => 64 } 203 | } 204 | client.create_app('foo', manifest) 205 | end 206 | 207 | it 'should allow us to delete an app we created' do 208 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 209 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 210 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 211 | stub_request(:delete, app_path).to_return(File.new(spec_asset('delete_app.txt'))) 212 | client = VMC::Client.new(@local_target, @auth_token) 213 | client.delete_app('foo') 214 | end 215 | 216 | it 'should provision a service' do 217 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 218 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 219 | global_services_path = "#{@local_target}/#{VMC::Client.path(VMC::GLOBAL_SERVICES_PATH)}" 220 | stub_request(:get, global_services_path).to_return(File.new(spec_asset('global_service_listings.txt'))) 221 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}" 222 | stub_request(:post, services_path).to_return(File.new(spec_asset('good_create_service.txt'))) 223 | client = VMC::Client.new(@local_target, @auth_token) 224 | client.create_service('aws', 'redis', 'foo') 225 | end 226 | 227 | it 'should complain if we try to provision a service that already exists with same name' do 228 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 229 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 230 | global_services_path = "#{@local_target}/#{VMC::Client.path(VMC::GLOBAL_SERVICES_PATH)}" 231 | stub_request(:get, global_services_path).to_return(File.new(spec_asset('global_service_listings.txt'))) 232 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}" 233 | stub_request(:post, services_path).to_return(File.new(spec_asset('service_already_exists.txt'))) 234 | client = VMC::Client.new(@local_target, @auth_token) 235 | expect { client.create_service('aws','redis', 'foo') }.to raise_error(VMC::Client::NotFound) 236 | end 237 | 238 | it 'should complain if we try to provision a service that does not exist' do 239 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 240 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 241 | global_services_path = "#{@local_target}/#{VMC::Client.path(VMC::GLOBAL_SERVICES_PATH)}" 242 | stub_request(:get, global_services_path).to_return(File.new(spec_asset('global_service_listings.txt'))) 243 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}" 244 | stub_request(:post, services_path).to_return(File.new(spec_asset('service_not_found.txt'))) 245 | client = VMC::Client.new(@local_target, @auth_token) 246 | expect { client.create_service('aws', 'redis', 'foo') }.to raise_error(VMC::Client::NotFound) 247 | end 248 | 249 | it 'should allow us to delete a provisioned service' do 250 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 251 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 252 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}" 253 | stub_request(:get, services_path).to_return(File.new(spec_asset('service_listings.txt'))) 254 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}/redis-7ed7da9" 255 | stub_request(:delete, services_path) 256 | client = VMC::Client.new(@local_target, @auth_token) 257 | client.delete_service('redis-7ed7da9') 258 | end 259 | 260 | it 'should bind a service to an app' do 261 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 262 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 263 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 264 | stub_request(:get, app_path).to_return(File.new(spec_asset('app_info.txt'))) 265 | stub_request(:put, app_path) 266 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}" 267 | # XXX - client only tests that we are not referencing a known service on another infrastructure. 268 | stub_request(:get, services_path).to_return(File.new(spec_asset('service_listings.txt'))) 269 | client = VMC::Client.new(@local_target, @auth_token) 270 | client.bind_service('my-redis', 'foo') 271 | a_request(:get, app_path).should have_been_made.once 272 | a_request(:put, app_path).should have_been_made.once 273 | end 274 | 275 | it 'should unbind an existing service from an app' do 276 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 277 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 278 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 279 | stub_request(:get, app_path).to_return(File.new(spec_asset('app_info.txt'))) 280 | stub_request(:put, app_path) 281 | client = VMC::Client.new(@local_target, @auth_token) 282 | client.unbind_service('my-redis', 'foo') 283 | a_request(:get, app_path).should have_been_made.once 284 | a_request(:put, app_path).should have_been_made.once 285 | end 286 | 287 | it 'should set a proxy if one is set' do 288 | target = "http://nonlocal.domain.com" 289 | info_path = "#{target}/#{VMC::INFO_PATH}" 290 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_return.txt'))) 291 | proxy = 'http://proxy.vmware.com:3128' 292 | ENV['http_proxy'] = proxy 293 | client = VMC::Client.new(target) 294 | info = client.info 295 | RestClient.proxy.should == proxy 296 | end 297 | 298 | it 'should not set a proxy when accessing localhost' do 299 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 300 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_return.txt'))) 301 | proxy = 'http://proxy.vmware.com:3128' 302 | ENV['http_proxy'] = proxy 303 | client = VMC::Client.new(@local_target) 304 | info = client.info 305 | RestClient.proxy.should == nil 306 | end 307 | 308 | it 'should use a secure proxy over a normal proxy if one is set' do 309 | info_path = "#{@target}/#{VMC::INFO_PATH}" 310 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_return.txt'))) 311 | proxy = 'http://proxy.vmware.com:3128' 312 | secure_proxy = 'http://secure-proxy.vmware.com:3128' 313 | ENV['http_proxy'] = proxy 314 | ENV['https_proxy'] = secure_proxy 315 | client = VMC::Client.new(@target) 316 | info = client.info 317 | RestClient.proxy.should == secure_proxy 318 | end 319 | 320 | it 'should not use a secure proxy for non-secure site' do 321 | target = "http://nonlocal.domain.com" 322 | info_path = "#{target}/#{VMC::INFO_PATH}" 323 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_return.txt'))) 324 | proxy = 'http://proxy.vmware.com:3128' 325 | secure_proxy = 'http://secure-proxy.vmware.com:3128' 326 | ENV['http_proxy'] = proxy 327 | ENV['https_proxy'] = secure_proxy 328 | client = VMC::Client.new(target) 329 | info = client.info 330 | RestClient.proxy.should == proxy 331 | end 332 | 333 | it 'should fail when there is a service gateway failure' do 334 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 335 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 336 | global_services_path = "#{@local_target}/#{VMC::Client.path(VMC::GLOBAL_SERVICES_PATH)}" 337 | stub_request(:get, global_services_path).to_return(File.new(spec_asset('global_service_listings.txt'))) 338 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}" 339 | # A service gateway failure will typically happen when provisioning a new service instance - 340 | # e.g. provisioning too many instances of mysql service. 341 | stub_request(:post, services_path).to_return(File.new(spec_asset('service_gateway_fail.txt'))) 342 | client = VMC::Client.new(@local_target, @auth_token) 343 | expect { client.create_service('aws', 'mysql', 'foo') }.to raise_error(VMC::Client::TargetError) 344 | end 345 | 346 | # WebMock.allow_net_connect! 347 | 348 | end 349 | -------------------------------------------------------------------------------- /spec/unit/command_admin_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'stringio' 3 | 4 | describe 'VMC::Cli::Command::Admin' do 5 | 6 | include WebMock::API 7 | 8 | before(:all) do 9 | @target = VMC::DEFAULT_TARGET 10 | @local_target = VMC::DEFAULT_LOCAL_TARGET 11 | @user = 'derek@gmail.com' 12 | @password = 'foo' 13 | @auth_token = spec_asset('sample_token.txt') 14 | end 15 | 16 | before(:each) do 17 | # make sure these get cleared so we don't have tests pass that shouldn't 18 | RestClient.proxy = nil 19 | ENV['http_proxy'] = nil 20 | ENV['https_proxy'] = nil 21 | end 22 | 23 | it 'should throw an error when a new user password contains a right curly brace' do 24 | @client = VMC::Client.new(@local_target, @auth_token) 25 | 26 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 27 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_nil_usage.txt'))) 28 | 29 | command = VMC::Cli::Command::Admin.new(:password => 'right}brace') 30 | command.client(@client) 31 | 32 | expect {command.add_user(@user)}.to raise_error(/Passwords may not contain braces/) 33 | end 34 | 35 | it 'should throw an error when a new user password contains a left curly brace' do 36 | @client = VMC::Client.new(@local_target, @auth_token) 37 | 38 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 39 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_nil_usage.txt'))) 40 | 41 | command = VMC::Cli::Command::Admin.new(:password => 'left{brace') 42 | command.client(@client) 43 | 44 | expect {command.add_user(@user)}.to raise_error(/Passwords may not contain braces/) 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /spec/unit/command_apps_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'VMC::Cli::Command::Apps' do 4 | 5 | include WebMock::API 6 | 7 | before(:all) do 8 | @target = VMC::DEFAULT_TARGET 9 | @local_target = VMC::DEFAULT_LOCAL_TARGET 10 | @user = 'derek@gmail.com' 11 | @password = 'foo' 12 | @auth_token = spec_asset('sample_token.txt') 13 | end 14 | 15 | before(:each) do 16 | # make sure these get cleared so we don't have tests pass that shouldn't 17 | RestClient.proxy = nil 18 | ENV['http_proxy'] = nil 19 | ENV['https_proxy'] = nil 20 | end 21 | 22 | it 'should not fail when there is an attempt to upload an app with links internal to the root' do 23 | @client = VMC::Client.new(@local_target, @auth_token) 24 | 25 | login_path = "#{@local_target}/users/#{@user}/tokens" 26 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 27 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 28 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 29 | 30 | app = spec_asset('tests/node/node_npm') 31 | options = { 32 | :name => 'foo', 33 | :uris => ['foo.vcap.me'], 34 | :instances => 1, 35 | :staging => { :model => 'nodejs/1.0' }, 36 | :path => app, 37 | :resources => { :memory => 64 } 38 | } 39 | command = VMC::Cli::Command::Apps.new(options) 40 | command.client(@client) 41 | 42 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 43 | stub_request(:get, app_path).to_return(File.new(spec_asset('app_info.txt'))) 44 | 45 | resource_path = "#{@local_target}/#{VMC::RESOURCES_PATH}" 46 | stub_request(:post, resource_path).to_return(File.new(spec_asset('resources_return.txt'))) 47 | 48 | app_upload_path = "#{@local_target}/#{VMC::APPS_PATH}/foo/application" 49 | stub_request(:post, app_upload_path) 50 | 51 | stub_request(:put, app_path) 52 | 53 | # Both 'vmc push ..' and 'vmc update ..' ultimately end up calling 54 | # the client 'update' command. The 'update' command determines the list 55 | # of files to upload (via the 'resources' end-point), uploads the needed 56 | # files and then starts up the app. The check for unreachable links 57 | # is made prior to the resource check. 58 | command.update('foo') 59 | 60 | a_request(:post, app_upload_path).should have_been_made.once 61 | a_request(:put, app_path).should have_been_made.once 62 | 63 | end 64 | 65 | it 'should fail when there is an attempt to upload an app with links reaching outside the app root' do 66 | @client = VMC::Client.new(@local_target, @auth_token) 67 | 68 | login_path = "#{@local_target}/users/#{@user}/tokens" 69 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 70 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 71 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 72 | 73 | app = spec_asset('tests/node/app_with_external_link') 74 | options = { 75 | :name => 'foo', 76 | :uris => ['foo.vcap.me'], 77 | :instances => 1, 78 | :staging => { :model => 'nodejs/1.0' }, 79 | :path => app, 80 | :resources => { :memory => 64 } 81 | } 82 | command = VMC::Cli::Command::Apps.new(options) 83 | command.client(@client) 84 | 85 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 86 | stub_request(:get, app_path).to_return(File.new(spec_asset('app_info.txt'))) 87 | 88 | expect { command.update('foo')}.to raise_error(/Can't deploy application containing links/) 89 | end 90 | 91 | it 'should not fail when there is an attempt to update an app using a single file' do 92 | @client = VMC::Client.new(@local_target, @auth_token) 93 | 94 | login_path = "#{@local_target}/users/#{@user}/tokens" 95 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 96 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 97 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 98 | 99 | app = spec_asset('tests/standalone/simple_ruby_app/simple.rb') 100 | options = { 101 | :name => 'foo', 102 | :uris => ['foo.vcap.me'], 103 | :instances => 1, 104 | :staging => { :framework => 'standalone', :runtime => 'ruby18', :command=>"ruby simple.rb" }, 105 | :path => app, 106 | :resources => { :memory => 128 } 107 | } 108 | command = VMC::Cli::Command::Apps.new(options) 109 | command.client(@client) 110 | 111 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 112 | stub_request(:get, app_path).to_return(File.new(spec_asset('standalone_app_info.txt'))) 113 | 114 | resource_path = "#{@local_target}/#{VMC::RESOURCES_PATH}" 115 | stub_request(:post, resource_path).to_return(File.new(spec_asset('resources_return.txt'))) 116 | 117 | app_upload_path = "#{@local_target}/#{VMC::APPS_PATH}/foo/application" 118 | stub_request(:post, app_upload_path) 119 | 120 | stub_request(:put, app_path) 121 | 122 | # Both 'vmc push ..' and 'vmc update ..' ultimately end up calling upload_app_bits 123 | command.update('foo') 124 | 125 | a_request(:post, app_upload_path).should have_been_made.once 126 | a_request(:put, app_path).should have_been_made.once 127 | 128 | end 129 | 130 | it 'should not fail when there is an attempt to update an app using a single WAR file' do 131 | @client = VMC::Client.new(@local_target, @auth_token) 132 | 133 | login_path = "#{@local_target}/users/#{@user}/tokens" 134 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 135 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 136 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 137 | 138 | app = spec_asset('tests/spring/spring-osgi-hello/target/hello.war') 139 | options = { 140 | :name => 'foo', 141 | :uris => ['foo.vcap.me'], 142 | :instances => 1, 143 | :staging => { :framework => 'spring'}, 144 | :path => app, 145 | :resources => { :memory => 512 } 146 | } 147 | command = VMC::Cli::Command::Apps.new(options) 148 | command.client(@client) 149 | 150 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 151 | stub_request(:get, app_path).to_return(File.new(spec_asset('standalone_app_info.txt'))) 152 | 153 | resource_path = "#{@local_target}/#{VMC::RESOURCES_PATH}" 154 | stub_request(:post, resource_path).to_return(File.new(spec_asset('resources_return.txt'))) 155 | 156 | app_upload_path = "#{@local_target}/#{VMC::APPS_PATH}/foo/application" 157 | stub_request(:post, app_upload_path) 158 | 159 | stub_request(:put, app_path) 160 | 161 | # Both 'vmc push ..' and 'vmc update ..' ultimately end up calling upload_app_bits 162 | command.update('foo') 163 | 164 | a_request(:post, app_upload_path).should have_been_made.once 165 | a_request(:put, app_path).should have_been_made.once 166 | 167 | end 168 | 169 | it 'should not fail when there is an attempt to update an app using a single zip file' do 170 | @client = VMC::Client.new(@local_target, @auth_token) 171 | 172 | login_path = "#{@local_target}/users/#{@user}/tokens" 173 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 174 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 175 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 176 | 177 | app = spec_asset('tests/standalone/java_app/target/zip/standalone-java-app-1.0.0.BUILD-SNAPSHOT-jar.zip') 178 | options = { 179 | :name => 'foo', 180 | :uris => ['foo.vcap.me'], 181 | :instances => 1, 182 | :staging => { :framework => 'standalone', :runtime => 'java', :command=>"java HelloCloud" }, 183 | :path => app, 184 | :resources => { :memory => 128 } 185 | } 186 | command = VMC::Cli::Command::Apps.new(options) 187 | command.client(@client) 188 | 189 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 190 | stub_request(:get, app_path).to_return(File.new(spec_asset('standalone_app_info.txt'))) 191 | 192 | resource_path = "#{@local_target}/#{VMC::RESOURCES_PATH}" 193 | stub_request(:post, resource_path).to_return(File.new(spec_asset('resources_return.txt'))) 194 | 195 | app_upload_path = "#{@local_target}/#{VMC::APPS_PATH}/foo/application" 196 | stub_request(:post, app_upload_path) 197 | 198 | stub_request(:put, app_path) 199 | 200 | # Both 'vmc push ..' and 'vmc update ..' ultimately end up calling upload_app_bits 201 | command.update('foo') 202 | 203 | a_request(:post, app_upload_path).should have_been_made.once 204 | a_request(:put, app_path).should have_been_made.once 205 | 206 | end 207 | 208 | it 'should not fail when there is an attempt to update an app using a dir containing a zip file' do 209 | @client = VMC::Client.new(@local_target, @auth_token) 210 | 211 | login_path = "#{@local_target}/users/#{@user}/tokens" 212 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 213 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 214 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 215 | 216 | app = spec_asset('tests/standalone/java_app/target/zip') 217 | options = { 218 | :name => 'foo', 219 | :uris => ['foo.vcap.me'], 220 | :instances => 1, 221 | :staging => { :framework => 'standalone', :runtime => 'java', :command=>"java HelloCloud" }, 222 | :path => app, 223 | :resources => { :memory => 128 } 224 | } 225 | command = VMC::Cli::Command::Apps.new(options) 226 | command.client(@client) 227 | 228 | app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 229 | stub_request(:get, app_path).to_return(File.new(spec_asset('standalone_app_info.txt'))) 230 | 231 | resource_path = "#{@local_target}/#{VMC::RESOURCES_PATH}" 232 | stub_request(:post, resource_path).to_return(File.new(spec_asset('resources_return.txt'))) 233 | 234 | app_upload_path = "#{@local_target}/#{VMC::APPS_PATH}/foo/application" 235 | stub_request(:post, app_upload_path) 236 | 237 | stub_request(:put, app_path) 238 | 239 | # Both 'vmc push ..' and 'vmc update ..' ultimately end up calling upload_app_bits 240 | command.update('foo') 241 | 242 | a_request(:post, app_upload_path).should have_been_made.once 243 | a_request(:put, app_path).should have_been_made.once 244 | 245 | end 246 | 247 | it 'should clone an app' do 248 | 249 | @client = VMC::Client.new(@local_target, @auth_token) 250 | 251 | login_path = "#{@local_target}/users/#{@user}/tokens" 252 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 253 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 254 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 255 | 256 | options = { 257 | :url => 'bar.vcap.me', 258 | :nostart => true 259 | } 260 | command = VMC::Cli::Command::Apps.new(options) 261 | command.client(@client) 262 | 263 | services_path = "#{@local_target}/#{VMC::SERVICES_PATH}" 264 | stub_request(:get,services_path).to_return(:body=>"[]") 265 | 266 | apps_path = "#{@local_target}/#{VMC::APPS_PATH}" 267 | stub_request(:post, apps_path) 268 | 269 | src_app_path = "#{@local_target}/#{VMC::APPS_PATH}/foo" 270 | stub_request(:get, src_app_path).to_return(File.new(spec_asset('standalone_app_info.txt'))) 271 | app_download_path = "#{@local_target}/#{VMC::APPS_PATH}/foo/application" 272 | stub_request(:get, app_download_path).to_return(:status => 200, 273 | :body => File.new(spec_asset('tests/standalone/java_app/target/zip/standalone-java-app-1.0.0.BUILD-SNAPSHOT-jar.zip')).binmode) 274 | 275 | dest_app_path = "#{@local_target}/#{VMC::APPS_PATH}/bar" 276 | stub_request(:get, dest_app_path).to_return(File.new(spec_asset('app_not_found.txt'))) 277 | 278 | app_upload_path = "#{@local_target}/#{VMC::APPS_PATH}/bar/application" 279 | stub_request(:post, app_upload_path) 280 | 281 | command.clone('foo','bar','rs') 282 | a_request(:post, apps_path).should have_been_made.once # to create the clone 283 | a_request(:post, app_upload_path).should have_been_made.once # to upload the code 284 | 285 | end 286 | 287 | it 'should clone services for an app' do 288 | pending 289 | end 290 | 291 | end 292 | -------------------------------------------------------------------------------- /spec/unit/command_info_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'stringio' 3 | 4 | describe 'VMC::Cli::Command::Misc' do 5 | 6 | include WebMock::API 7 | 8 | before(:all) do 9 | @target = VMC::DEFAULT_TARGET 10 | @local_target = VMC::DEFAULT_LOCAL_TARGET 11 | @user = 'derek@gmail.com' 12 | @password = 'foo' 13 | @auth_token = spec_asset('sample_token.txt') 14 | end 15 | 16 | before(:each) do 17 | # make sure these get cleared so we don't have tests pass that shouldn't 18 | RestClient.proxy = nil 19 | ENV['http_proxy'] = nil 20 | ENV['https_proxy'] = nil 21 | end 22 | 23 | it 'should not raise exception for user with no apps deployed' do 24 | @client = VMC::Client.new(@local_target, @auth_token) 25 | 26 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 27 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_nil_usage.txt'))) 28 | 29 | command = VMC::Cli::Command::Misc.new() 30 | command.client(@client) 31 | 32 | expect {command.info()}.to_not raise_error(/undefined/) 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/unit/command_services_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'VMC::Cli::Command::Services' do 4 | 5 | include WebMock::API 6 | 7 | before(:all) do 8 | @target = VMC::DEFAULT_TARGET 9 | @local_target = VMC::DEFAULT_LOCAL_TARGET 10 | @user = 'derek@gmail.com' 11 | @password = 'foo' 12 | @auth_token = spec_asset('sample_token.txt') 13 | end 14 | 15 | before(:each) do 16 | # make sure these get cleared so we don't have tests pass that shouldn't 17 | RestClient.proxy = nil 18 | ENV['http_proxy'] = nil 19 | ENV['https_proxy'] = nil 20 | end 21 | 22 | describe "import and export" do 23 | before(:each) do 24 | @client = VMC::Client.new(@local_target, @auth_token) 25 | 26 | login_path = "#{@local_target}/users/#{@user}/tokens" 27 | stub_request(:post, login_path).to_return(File.new(spec_asset('login_success.txt'))) 28 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 29 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_authenticated.txt'))) 30 | end 31 | 32 | it 'should export a mysql service' do 33 | 34 | command = VMC::Cli::Command::Services.new() 35 | command.client(@client) 36 | 37 | service_path = File.join(@local_target,VMC::SERVICE_EXPORT_PATH,"data") 38 | stub_request(:get,service_path).to_return(:body=>'{ "uri": "data.zip" }') 39 | 40 | command.export_service('data') 41 | a_request(:get, service_path).should have_been_made.once 42 | end 43 | 44 | it 'should import a mysql service' do 45 | command = VMC::Cli::Command::Services.new() 46 | command.client(@client) 47 | 48 | service_path = File.join(@local_target,VMC::SERVICE_IMPORT_PATH,"data") 49 | stub_request(:post,service_path) 50 | 51 | command.import_service('data','dl.vcap.me/data') 52 | a_request(:post, service_path).should have_been_made.once 53 | end 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /spec/unit/command_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'stringio' 3 | 4 | describe 'VMC::Cli::Command::User' do 5 | 6 | include WebMock::API 7 | 8 | before(:all) do 9 | @target = VMC::DEFAULT_TARGET 10 | @local_target = VMC::DEFAULT_LOCAL_TARGET 11 | @user = 'derek@gmail.com' 12 | @password = 'foo' 13 | @auth_token = spec_asset('sample_token.txt') 14 | end 15 | 16 | before(:each) do 17 | # make sure these get cleared so we don't have tests pass that shouldn't 18 | RestClient.proxy = nil 19 | ENV['http_proxy'] = nil 20 | ENV['https_proxy'] = nil 21 | end 22 | 23 | it 'should throw an error when a changed password contains a right curly brace' do 24 | @client = VMC::Client.new(@local_target, @auth_token) 25 | 26 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 27 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_nil_usage.txt'))) 28 | 29 | command = VMC::Cli::Command::User.new(:noprompts => 1) 30 | command.client(@client) 31 | 32 | expect {command.change_password('right}brace]')}.to raise_error(/Passwords may not contain braces/) 33 | end 34 | 35 | it 'should throw an error when a changed password contains a left curly brace' do 36 | @client = VMC::Client.new(@local_target, @auth_token) 37 | 38 | info_path = "#{@local_target}/#{VMC::INFO_PATH}" 39 | stub_request(:get, info_path).to_return(File.new(spec_asset('info_nil_usage.txt'))) 40 | 41 | command = VMC::Cli::Command::User.new(:noprompts => 1) 42 | command.client(@client) 43 | 44 | expect {command.change_password('left{brace]')}.to raise_error(/Passwords may not contain braces/) 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /spec/unit/console_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'VMC::Cli::ConsoleHelper' do 4 | 5 | include VMC::Cli::ConsoleHelper 6 | 7 | before(:each) do 8 | @client = mock("client") 9 | @telnet_client = mock("telnet_client") 10 | end 11 | 12 | it 'should return connection info for apps that have a console ip and port' do 13 | @client.should_receive(:app_info).with("foo").and_return(:staging=>{:model=>'rails3'}) 14 | @client.should_receive(:app_instances).with("foo").and_return({:instances=>[{:console_ip=>'192.168.1.1', :console_port=>3344}]}) 15 | foo_info = console_connection_info('foo') 16 | foo_info['hostname'].should == '192.168.1.1' 17 | foo_info['port'].should == 3344 18 | end 19 | 20 | it 'should output a message when no app instances found' do 21 | @client.should_receive(:app_info).with("foo").and_return(:staging=>{:model=>'rails3'}) 22 | @client.should_receive(:app_instances).with("foo").and_return({:instances=>[]}) 23 | errmsg = nil 24 | begin 25 | console_connection_info('foo') 26 | rescue VMC::Cli::CliExit=>e 27 | errmsg = e.message 28 | end 29 | errmsg.should == "Error: No running instances for [foo]" 30 | end 31 | 32 | it 'should output a message when app does not have console access b/c files are missing' do 33 | @client.should_receive(:app_info).with("foo").and_return(:staging=>{:model=>'rails3'}) 34 | @client.should_receive(:app_instances).with("foo").and_return({:instances=>[{}]}) 35 | @client.should_receive(:app_files).with('foo','/app/cf-rails-console').and_raise(VMC::Client::TargetError) 36 | errmsg = nil 37 | begin 38 | console_connection_info('foo') 39 | rescue VMC::Cli::CliExit=>e 40 | errmsg = e.message 41 | end 42 | errmsg.should == "Error: Console access not supported for [foo]. " + 43 | "Please redeploy your app to enable support." 44 | end 45 | 46 | it 'should output a message when app does not have console access b/c port is not bound' do 47 | @client.should_receive(:app_info).with("foo").and_return(:staging=>{:model=>'rails3'}) 48 | @client.should_receive(:app_instances).with("foo").and_return({:instances=>[{}]}) 49 | @client.should_receive(:app_files).with('foo','/app/cf-rails-console').and_return("files") 50 | errmsg = nil 51 | begin 52 | console_connection_info('foo') 53 | rescue VMC::Cli::CliExit=>e 54 | errmsg = e.message 55 | end 56 | errmsg.should == "Error: Console port not provided for [foo]. Try restarting the app." 57 | end 58 | 59 | it 'should output a message when console is not supported for app type' do 60 | @client.should_receive(:app_info).with("foo").and_return(:staging=>{:model=>'sinatra'}) 61 | errmsg = nil 62 | begin 63 | console_connection_info('foo') 64 | rescue VMC::Cli::CliExit=>e 65 | errmsg = e.message 66 | end 67 | errmsg.should == "Error: 'foo' is a sinatra application. " + 68 | "Console access is not supported for sinatra applications." 69 | end 70 | 71 | it 'should start console and process a command if authentication succeeds' do 72 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 73 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("Switch to inspect mode\nirb():001:0> ") 74 | verify_console_exit "irb():001:0> " 75 | start_local_console(3344,'foo') 76 | end 77 | 78 | it 'should output a message if console authentication information cannot be obtained' do 79 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('invalid_console_access.txt'))) 80 | errmsg = nil 81 | begin 82 | start_local_console(3344,'foo') 83 | rescue VMC::Cli::CliExit=>e 84 | errmsg = e.message 85 | end 86 | errmsg.should == "Error: Unable to verify console credentials." 87 | end 88 | 89 | it 'should exit if authentication fails' do 90 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 91 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("Login failed.") 92 | @telnet_client.should_receive(:close) 93 | errmsg = nil 94 | begin 95 | start_local_console(3344,'foo') 96 | rescue VMC::Cli::CliExit=>e 97 | errmsg = e.message 98 | end 99 | errmsg.should == "Error: Login failed." 100 | end 101 | 102 | it 'should retry authentication on timeout' do 103 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 104 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_raise(TimeoutError) 105 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("Switch to inspect mode\nirb():001:0> ") 106 | verify_console_exit "irb():001:0> " 107 | start_local_console(3344,'foo') 108 | end 109 | 110 | it 'should retry authentication on EOF' do 111 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 112 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_raise(EOFError) 113 | @telnet_client.should_receive(:close) 114 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("irb():001:0> ") 115 | verify_console_exit "irb():001:0> " 116 | start_local_console(3344,'foo') 117 | end 118 | 119 | it 'should operate console interactively' do 120 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 121 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("irb():001:0> ") 122 | Readline.should_receive(:readline).with("irb():001:0> ").and_return("puts 'hi'") 123 | Readline::HISTORY.should_receive(:push).with("puts 'hi'") 124 | @telnet_client.should_receive(:cmd).with("puts 'hi'").and_return("nil" + "\n" + "irb():002:0> ") 125 | verify_console_exit "irb():002:0> " 126 | start_local_console(3344,'foo') 127 | end 128 | 129 | it 'should not crash if command times out' do 130 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 131 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("irb():001:0> ") 132 | Readline.should_receive(:readline).with("irb():001:0> ").and_return("puts 'hi'") 133 | Readline::HISTORY.should_receive(:push).with("puts 'hi'") 134 | @telnet_client.should_receive(:cmd).with("puts 'hi'").and_raise(TimeoutError) 135 | verify_console_exit "irb():001:0> " 136 | start_local_console(3344,'foo') 137 | end 138 | 139 | it 'should exit with error message if an EOF is received' do 140 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 141 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("Switch to inspect mode\nirb():001:0> ") 142 | Readline.should_receive(:readline).with("irb():001:0> ").and_return("puts 'hi'") 143 | Readline::HISTORY.should_receive(:push).with("puts 'hi'") 144 | @telnet_client.should_receive(:cmd).with("puts 'hi'").and_raise(EOFError) 145 | errmsg = nil 146 | begin 147 | start_local_console(3344,'foo') 148 | rescue VMC::Cli::CliExit=>e 149 | errmsg = e.message 150 | end 151 | errmsg.should == "Error: The console connection has been terminated. " + 152 | "Perhaps the app was stopped or deleted?" 153 | end 154 | 155 | it 'should not keep blank lines in history' do 156 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 157 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("irb():001:0> ") 158 | Readline.should_receive(:readline).with("irb():001:0> ").and_return("") 159 | Readline::HISTORY.should_not_receive(:push).with("") 160 | @telnet_client.should_receive(:cmd).with("").and_return("irb():002:0*> ") 161 | verify_console_exit "irb():002:0*> " 162 | start_local_console(3344,'foo') 163 | end 164 | 165 | it 'should not keep identical commands in history' do 166 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 167 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("irb():001:0> ") 168 | Readline.should_receive(:readline).with("irb():001:0> ").and_return("puts 'hi'") 169 | Readline::HISTORY.should_receive(:to_a).and_return(["puts 'hi'"]) 170 | Readline::HISTORY.should_not_receive(:push).with("puts 'hi'") 171 | @telnet_client.should_receive(:cmd).with("puts 'hi'").and_return("nil" + "\n" + "irb():002:0> ") 172 | verify_console_exit "irb():002:0> " 173 | start_local_console(3344,'foo') 174 | end 175 | 176 | it 'should return remote tab completion data' do 177 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 178 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("Switch to inspect mode\nirb():001:0> ") 179 | @telnet_client.should_receive(:cmd).with({"String"=>"app.\t", "Match"=>/\S*\n$/, "Timeout"=>10}).and_return("to_s,nil?\n") 180 | verify_console_exit "irb():001:0> " 181 | start_local_console(3344,'foo') 182 | Readline.completion_proc.yield("app.").should == ["to_s","nil?"] 183 | end 184 | 185 | it 'should return remote tab completion data on receipt of empty completion string' do 186 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 187 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("irb():001:0> ") 188 | @telnet_client.should_receive(:cmd).with({"String"=>"app.\t", "Match"=>/\S*\n$/, "Timeout"=>10}).and_return("\n") 189 | verify_console_exit "irb():001:0> " 190 | start_local_console(3344,'foo') 191 | Readline.completion_proc.yield("app.").should == [] 192 | end 193 | 194 | it 'should not crash on timeout of remote tab completion data' do 195 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 196 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("Switch to inspect mode\nirb():001:0> ") 197 | @telnet_client.should_receive(:cmd).with({"String"=>"app.\t", "Match"=>/\S*\n$/, "Timeout"=>10}).and_raise(TimeoutError) 198 | verify_console_exit "irb():001:0> " 199 | start_local_console(3344,'foo') 200 | Readline.completion_proc.yield("app.").should == [] 201 | end 202 | 203 | it 'should properly initialize Readline for tab completion' do 204 | @client.should_receive(:app_files).with("foo", '/app/cf-rails-console/.consoleaccess', '0').and_return(IO.read(spec_asset('console_access.txt'))) 205 | @telnet_client.should_receive(:login).with({"Name"=>"cfuser", "Password"=>"testpw"}).and_return("irb():001:0> ") 206 | Readline.should_receive(:respond_to?).with("basic_word_break_characters=").and_return(true) 207 | Readline.should_receive(:basic_word_break_characters=).with(" \t\n`><=;|&{(") 208 | Readline.should_receive(:completion_append_character=).with(nil) 209 | Readline.should_receive(:completion_proc=) 210 | verify_console_exit "irb():001:0> " 211 | start_local_console(3344,'foo') 212 | end 213 | 214 | def client(cli=nil) 215 | @client 216 | end 217 | 218 | def display(message, nl=true) 219 | end 220 | 221 | def telnet_client(port) 222 | @telnet_client 223 | end 224 | 225 | def verify_console_exit(prompt) 226 | Readline.should_receive(:readline).with(prompt).and_return("exit") 227 | @telnet_client.should_receive(:cmd).with(({"String"=>"exit", "Timeout"=>1})).and_raise(TimeoutError) 228 | @telnet_client.should_receive(:close) 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /spec/unit/file_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'VMC::Cli::FileHelper' do 4 | 5 | include VMC::Cli::FileHelper 6 | 7 | before(:each) do 8 | end 9 | 10 | describe "afignore" do 11 | 12 | before :each do 13 | @af = VMC::Cli::FileHelper 14 | end 15 | 16 | it 'should ignore blank lines' do 17 | afi = VMC::Cli::FileHelper::AppFogIgnore.new([ "" ]) 18 | files = %W( index.html ) 19 | afi.included_files(files).should == %W( index.html ) 20 | end 21 | 22 | it 'should ignore lines starting with #' do 23 | afi = VMC::Cli::FileHelper::AppFogIgnore.new([ "# index.html" ]) 24 | files = %W(index.html) 25 | afi.included_files(files).should == %W( index.html ) 26 | end 27 | 28 | it 'should ignore literal matches' do 29 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W(index1.html index3.html)) 30 | files = %W(index1.html index2.html index3.html) 31 | afi.included_files(files).should == %W( index2.html ) 32 | end 33 | 34 | it 'should not match / in pattern with wildcard' do 35 | afi = VMC::Cli::FileHelper::AppFogIgnore.new([ "*.html" ]) 36 | files = %W(index.html public/index.html) 37 | afi.included_files(files).should == %W( public/index.html ) 38 | end 39 | 40 | it 'should ignore directories for patterns ending in slash' do 41 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W( public/ )) 42 | files = %W(index.html public public/first public/second script/foo.js) 43 | afi.included_files(files).should == %W( index.html script/foo.js) 44 | end 45 | 46 | it 'should reverse previous matches for patterns starting with !' do 47 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W( *.html !index[23].html )) 48 | files = %W( index.html index2.html index3.html index4.html lib/shared.so) 49 | afi.included_files(files).should == %W(index2.html index3.html lib/shared.so) 50 | end 51 | 52 | it 'should not reverse later matches for patterns starting with !' do 53 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W( !index[23].html *.html )) 54 | files = %W( index.html index2.html index3.html index4.html lib/shared.so) 55 | afi.included_files(files).should == %W(lib/shared.so) 56 | end 57 | 58 | it 'should match beginning of path for leading /' do 59 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W( /*.c )) 60 | files = %W( foo.c lib/foo.c ) 61 | afi.included_files(files).should == %W(lib/foo.c) 62 | end 63 | 64 | it 'can return files excluded by .afignore' do 65 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W(index1.html index3.html)) 66 | files = %W(index1.html index2.html index3.html) 67 | afi.excluded_files(files).should == %W( index1.html index3.html ) 68 | end 69 | 70 | it 'should ignore .git directory by default' do 71 | afi = VMC::Cli::FileHelper::AppFogIgnore.new([]) 72 | files = %W(index.html .git/config ) 73 | afi.included_files(files).should == %W( index.html ) 74 | end 75 | 76 | it 'should read patterns from .afignore' do 77 | files = %W(index.html index2.html index3.html index4.html) 78 | File.should_receive(:exists?).with('./.afignore').and_return(true) 79 | File.should_receive(:read).with('./.afignore').and_return("index2.html\nindex3.html") 80 | afi = VMC::Cli::FileHelper::AppFogIgnore.from_file('.') 81 | afi.included_files(files).should == %W(index.html index4.html) 82 | end 83 | 84 | it 'should ignore project directory when matching patterns' do 85 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W(about.html),"/project") 86 | files = %W(/project/index.html /project/about.html) 87 | afi.included_files(files).should == %W(/project/index.html) 88 | end 89 | 90 | it 'should anchor directory patterns' do 91 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W(data/)) 92 | files = %W(data/one.html data/two.html another/data/one.html) 93 | afi.included_files(files).should == %W(another/data/one.html) 94 | end 95 | 96 | it 'should anchor directory patterns with regex' do 97 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W(da.*/)) 98 | files = %W(data/one.html data/two.html another/data/one.html) 99 | afi.included_files(files).should == %W(another/data/one.html) 100 | end 101 | 102 | it 'should anchor directory patterns with regex' do 103 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W(.*da.*/)) 104 | files = %W(data/one.html data/two.html another/data/one.html index.html) 105 | afi.included_files(files).should == %W(index.html) 106 | end 107 | 108 | it 'should anchor file patterns' do 109 | afi = VMC::Cli::FileHelper::AppFogIgnore.new(%W(one.html)) 110 | files = %W(one.html sub/one.html) 111 | afi.included_files(files).should == %W(sub/one.html) 112 | end 113 | 114 | end 115 | 116 | describe "sockets" do 117 | it 'should ignore socket files' do 118 | File.should_receive(:socket?).with('a-socket').and_return(true) 119 | File.should_receive(:socket?).with('not-a-socket').and_return(false) 120 | results = ignore_sockets(%W(a-socket not-a-socket)) 121 | results.should == %W(not-a-socket) 122 | end 123 | end 124 | 125 | describe "unreachable links" do 126 | 127 | it 'raise exception for links outside project directory' do 128 | @project = double('pathname', 129 | :realpath => "/project", 130 | :relative_path_from => "." 131 | ) 132 | @internal = double('pathname', 133 | :realpath => "/project/internal", 134 | :symlink? => true 135 | ) 136 | @external = double('pathname', 137 | :realpath => "/somewhere/else", 138 | :symlink? => true, 139 | :relative_path_from => "external" 140 | ) 141 | 142 | Pathname.should_receive(:new).with("/project").and_return(@project) 143 | Pathname.should_receive(:new).with("/project/internal").and_return(@internal) 144 | Pathname.should_receive(:new).with("/project/external").and_return(@external) 145 | 146 | expect { 147 | check_unreachable_links('/project',%W(/project/internal /project/external)) 148 | }.to raise_error(VMC::Cli::CliExit) 149 | end 150 | end 151 | 152 | end -------------------------------------------------------------------------------- /spec/unit/frameworks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tmpdir' 3 | require 'rbconfig' 4 | 5 | describe 'VMC::Cli::Framework' do 6 | 7 | before(:all) do 8 | is_windows = RbConfig::CONFIG['host_os'] =~ /mswin|windows|mingw|cygwin/i 9 | VMC::Cli::Config.nozip = is_windows 10 | end 11 | 12 | it 'should be able to detect a Java web app war' do 13 | app = spec_asset('tests/java_web/java_tiny_app/target') 14 | framework(app).to_s.should =~ /Java Web/ 15 | end 16 | 17 | it 'should be able to detect an exploded Java web app' do 18 | app = spec_asset('tests/java_web/java_tiny_app/target') 19 | framework(get_war_file(app), true).to_s.should =~ /Java Web/ 20 | end 21 | 22 | it 'should be able to detect a Spring web app war' do 23 | app = spec_asset('tests/spring/roo-guestbook/target') 24 | framework(app).to_s.should =~ /Spring/ 25 | end 26 | 27 | it 'should be able to detect an exploded Spring web app' do 28 | app = spec_asset('tests/spring/roo-guestbook/target/') 29 | framework(get_war_file(app), true).to_s.should =~ /Spring/ 30 | end 31 | 32 | it 'should be able to detect a Spring web app war that uses OSGi-style jars' do 33 | app = spec_asset('tests/spring/spring-osgi-hello/target') 34 | framework(app).to_s.should =~ /Spring/ 35 | end 36 | 37 | it 'should be able to detect an exploded Spring web app that uses OSGi-style jars' do 38 | app = spec_asset('tests/spring/spring-osgi-hello/target') 39 | framework(get_war_file(app), true).to_s.should =~ /Spring/ 40 | end 41 | 42 | it 'should be able to detect a Lift web app war' do 43 | app = spec_asset('tests/lift/hello_lift/target') 44 | framework(app).to_s.should =~ /Lift/ 45 | end 46 | 47 | it 'should be able to detect a Lift web app war file' do 48 | app = spec_asset('tests/lift/hello_lift/target/scala_lift-1.0.war') 49 | framework(app).to_s.should =~ /Lift/ 50 | end 51 | 52 | it 'should be able to detect an exploded Lift web app' do 53 | app = spec_asset('tests/lift/hello_lift/target') 54 | framework(get_war_file(app), true).to_s.should =~ /Lift/ 55 | end 56 | 57 | it 'should be able to detect a Grails web app war' do 58 | pending "Availability of a fully functional maven plugin for grails" 59 | app = spec_asset('tests/grails/guestbook/target') 60 | framework(app).to_s.should =~ /Grails/ 61 | end 62 | 63 | it 'should be able to detect an exploded Grails web app' do 64 | pending "Availability of a fully functional maven plugin for grails" 65 | app = spec_asset('tests/grails/guestbook/target') 66 | framework(get_war_file(app), true).to_s.should =~ /Grails/ 67 | end 68 | 69 | it 'should be able to detect a Rails3 app' do 70 | app = spec_asset('tests/rails3/hello_vcap') 71 | framework(app).to_s.should =~ /Rails/ 72 | end 73 | 74 | it 'should be able to detect a Sinatra app' do 75 | app = spec_asset('tests/sinatra/hello_vcap') 76 | framework(app).to_s.should =~ /Sinatra/ 77 | end 78 | 79 | it 'should be able to detect a Rack app' do 80 | app = spec_asset('tests/rack/app_rack_service') 81 | framework(app,false,[["rack"]]).to_s.should =~ /Rack/ 82 | end 83 | 84 | it 'should fall back to Sinatra detection if Rack framework not supported' do 85 | app = spec_asset('tests/rack/app_rack_service') 86 | framework(app,false).to_s.should =~ /Sinatra/ 87 | end 88 | 89 | it 'should be able to detect a Node.js app' do 90 | app = spec_asset('tests/node/hello_vcap') 91 | framework(app).to_s.should=~ /Node.js/ 92 | end 93 | 94 | it 'should be able to detect a Play app' do 95 | VMC::Cli::Framework.detect_framework_from_zip_contents("lib/play.play_2.9.1-2.1-SNAPSHOT.jar", 96 | [["play"],["standalone"]]) 97 | end 98 | 99 | it 'should return correct list of available frameworks' do 100 | VMC::Cli::Framework.known_frameworks([["standalone"],["rails3"]]).should == ["Rails","Standalone"] 101 | end 102 | 103 | describe 'standalone app support' do 104 | it 'should fall back to Standalone app from single non-WAR file' do 105 | app = spec_asset("tests/standalone/java_app/target/" + 106 | "standalone-java-app-1.0.0.BUILD-SNAPSHOT.jar") 107 | framework(app,false,[["standalone"]]).to_s.should=~ /Standalone/ 108 | end 109 | 110 | it 'should fall back to nil if Standalone framework not supported for single non-WAR file' do 111 | app = spec_asset("tests/standalone/java_app/target/" + 112 | "standalone-java-app-1.0.0.BUILD-SNAPSHOT.jar") 113 | framework(app).should == nil 114 | end 115 | 116 | it 'should detect Standalone app from single zip file' do 117 | app = spec_asset("tests/standalone/java_app/target/zip/" + 118 | "standalone-java-app-1.0.0.BUILD-SNAPSHOT-jar.zip") 119 | framework(app,false,[["standalone"],["play"]]).to_s.should=~ /Standalone/ 120 | end 121 | 122 | it 'should detect Standalone app from dir containing a single zip file' do 123 | app = spec_asset("tests/standalone/java_app/target/zip/") 124 | framework(app,false,[["standalone"],["play"]]).to_s.should=~ /Standalone/ 125 | end 126 | 127 | it 'should fall back to nil if Standalone framework not supported for zip file' do 128 | app = spec_asset("tests/standalone/java_app/target/zip/" + 129 | "standalone-java-app-1.0.0.BUILD-SNAPSHOT-jar.zip") 130 | framework(app).should == nil 131 | end 132 | 133 | it 'should fall back to Standalone app if dir does not match other frameworks' do 134 | app = spec_asset('tests/standalone/python_app') 135 | framework(app,false,[["standalone"]]).to_s.should=~ /Standalone/ 136 | end 137 | 138 | it 'should detect default Java runtime with a zip of jars' do 139 | app = spec_asset("tests/standalone/java_app/target/zip/" + 140 | "standalone-java-app-1.0.0.BUILD-SNAPSHOT-jar.zip") 141 | framework(app,false,[["standalone"]]).default_runtime(app).should == "java" 142 | end 143 | 144 | it 'should fall back to nil if Standalone framework not supported for dir' do 145 | app = spec_asset('tests/standalone/python_app') 146 | framework(app).should == nil 147 | end 148 | 149 | it 'should detect default Java runtime with a single jar' do 150 | app = spec_asset("tests/standalone/java_app/target/" + 151 | "standalone-java-app-1.0.0.BUILD-SNAPSHOT.jar") 152 | framework(app,false,[["standalone"]]).default_runtime(app).should == "java" 153 | end 154 | 155 | it 'should detect default Java runtime with a zip of jars' do 156 | app = spec_asset("tests/standalone/java_app/target/zip/" + 157 | "standalone-java-app-1.0.0.BUILD-SNAPSHOT-jar.zip") 158 | framework(app,false,[["standalone"]]).default_runtime(app).should == "java" 159 | end 160 | 161 | it 'should detect default Java runtime with a dir containing zip of jar files' do 162 | app = spec_asset('tests/standalone/java_app/target/zip') 163 | framework(app,false,[["standalone"]]).default_runtime(app).should == "java" 164 | end 165 | 166 | it 'should detect default Java runtime with a dir containing jar files' do 167 | app = spec_asset('tests/standalone/java_app/target') 168 | framework(app,false,[["standalone"]]).default_runtime(app).should == "java" 169 | end 170 | 171 | it 'should detect default Java runtime with a single class' do 172 | app = spec_asset('tests/standalone/java_app/target/classes/HelloCloud.class') 173 | framework(app,false,[["standalone"]]).default_runtime(app).should == "java" 174 | end 175 | 176 | it 'should detect default Java runtime with a dir containing class files' do 177 | app = spec_asset('tests/standalone/java_app/target/classes') 178 | framework(app,false,[["standalone"]]).default_runtime(app).should == "java" 179 | end 180 | 181 | it 'should detect default Ruby runtime with a single rb file' do 182 | app = spec_asset('tests/standalone/ruby_app/main.rb') 183 | framework(app,false,[["standalone"]]).default_runtime(app).should == "ruby18" 184 | end 185 | 186 | it 'should detect default Ruby runtime with a dir containing rb files' do 187 | app = spec_asset('tests/standalone/simple_ruby_app') 188 | framework(app,false,[["standalone"]]).default_runtime(app).should == "ruby18" 189 | end 190 | 191 | it 'should return nil for default runtime if framework is not standalone' do 192 | app = spec_asset('tests/lift/hello_lift/target') 193 | framework(app,false,[["standalone"]]).default_runtime(app).should == nil 194 | end 195 | 196 | it 'should return nil for default runtime if zip does not contain jars' do 197 | app = spec_asset("tests/standalone/python_app/target/zip/" + 198 | "standalone-python-1.0.0.BUILD-SNAPSHOT-script.zip") 199 | framework(app,false,[["standalone"]]).default_runtime(app).should == nil 200 | end 201 | 202 | it 'should return nil for default runtime if dir contains zip with no jars' do 203 | app = spec_asset('tests/standalone/python_app/target/zip') 204 | framework(app,false,[["standalone"]]).default_runtime(app).should == nil 205 | end 206 | 207 | it 'should return nil for default runtime if file does not match any rules' do 208 | app = spec_asset('tests/standalone/python_app') 209 | framework(app,false,[["standalone"]]).default_runtime(app).should == nil 210 | end 211 | 212 | it 'should return expected default memory for standalone Java apps' do 213 | app = spec_asset('tests/standalone/java_app/target') 214 | framework(app,false,[["standalone"]]).memory("java").should == '512M' 215 | end 216 | 217 | it 'should return expected default memory for standalone Java 7 apps' do 218 | app = spec_asset('tests/standalone/java_app/target') 219 | framework(app,false,[["standalone"]]).memory("java7").should == '512M' 220 | end 221 | 222 | it 'should return expected default memory for standalone Ruby 1.8 apps' do 223 | app = spec_asset('tests/standalone/ruby_app/main.rb') 224 | framework(app,false,[["standalone"]]).memory("ruby18").should == '128M' 225 | end 226 | 227 | it 'should return expected default memory for standalone Ruby 1.9 apps' do 228 | app = spec_asset('tests/standalone/ruby_app/main.rb') 229 | framework(app,false,[["standalone"]]).memory("ruby19").should == '128M' 230 | end 231 | 232 | it 'should return expected default memory for standalone PHP apps' do 233 | app = spec_asset('tests/standalone/php_app') 234 | framework(app,false,[["standalone"]]).memory("php").should == '128M' 235 | end 236 | 237 | it 'should return expected default memory for standalone apps with other runtimes' do 238 | app = spec_asset('tests/standalone/python_app') 239 | framework(app,false,[["standalone"]]).memory("python").should == '64M' 240 | end 241 | 242 | it 'should return expected default memory for non-standalone apps' do 243 | app = spec_asset('tests/rails3/hello_vcap') 244 | framework(app).mem.should == '256M' 245 | end 246 | end 247 | 248 | def framework app, explode=false, available_frameworks=[] 249 | unless explode == true 250 | return VMC::Cli::Framework.detect(app, available_frameworks) 251 | end 252 | Dir.mktmpdir {|dir| 253 | exploded_dir = File.join(dir, "exploded") 254 | VMC::Cli::ZipUtil.unpack(app, exploded_dir) 255 | VMC::Cli::Framework.detect(exploded_dir, available_frameworks) 256 | } 257 | end 258 | 259 | def get_war_file app 260 | Dir.chdir(app) 261 | war_file = Dir.glob('*.war').first 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /spec/unit/manifests_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'manifests' do 4 | MY_MANIFEST = spec_asset("manifests/my-manifest.yml") 5 | SYM_MANIFEST = spec_asset("manifests/sym-manifest.yml") 6 | SUB_MANIFEST = spec_asset("manifests/sub-manifest.yml") 7 | BAD_MANIFEST = spec_asset("manifests/bad-manifest.yml") 8 | 9 | it "loads the specified manifest file" do 10 | app = VMC::Cli::Command::Base.new(:manifest => MY_MANIFEST) 11 | app.manifest("foo").should == 1 12 | app.manifest("bar").should == 2 13 | end 14 | 15 | it "loads the nearest manifest.yml file if none specified" do 16 | Dir.chdir(spec_asset("manifests/someapp")) do 17 | app = VMC::Cli::Command::Base.new 18 | app.manifest_file.should == File.expand_path("manifest.yml") 19 | app.manifest("fizz").should == 1 20 | app.manifest("buzz").should == 2 21 | end 22 | end 23 | 24 | it "searches upward for the manifest.yml file" do 25 | Dir.chdir(spec_asset("manifests/someapp/somedir/somesubdir")) do 26 | app = VMC::Cli::Command::Base.new 27 | app.manifest_file.should == File.expand_path("../../manifest.yml") 28 | app.manifest("fizz").should == 1 29 | app.manifest("buzz").should == 2 30 | end 31 | end 32 | 33 | it "has an empty manifest if none found" do 34 | Dir.chdir(spec_asset("manifests/somenomanifestapp")) do 35 | app = VMC::Cli::Command::Base.new 36 | app.manifest_file.should == nil 37 | end 38 | end 39 | 40 | describe 'symbol resolution' do 41 | before(:all) do 42 | @cli = VMC::Cli::Command::Base.new(:manifest => SYM_MANIFEST) 43 | end 44 | 45 | it "fails if there is an unknown symbol" do 46 | proc { 47 | VMC::Cli::Command::Base.new(:manifest => BAD_MANIFEST) 48 | }.should raise_error 49 | end 50 | 51 | it "searches under properties hash" do 52 | @cli.manifest("a").should == 43 53 | @cli.manifest("b").should == "baz" 54 | @cli.manifest("c").should == 42 55 | @cli.manifest("d").should == "bar" 56 | 57 | @cli.manifest("foo").should == "foo baz baz" 58 | @cli.manifest("bar").should == "fizz 43" 59 | end 60 | 61 | it "searches from the toplevel if not found in properties" do 62 | @cli.manifest("fizz").should == "foo bar baz" 63 | @cli.manifest("buzz").should == "fizz 42" 64 | end 65 | 66 | it "resolves lexically" do 67 | @cli.manifest("some-hash", "hello", "foo").should == 1 68 | @cli.manifest("some-hash", "hello", "bar").should == "1-2" 69 | @cli.manifest("some-hash", "goodbye", "fizz").should == 3 70 | @cli.manifest("some-hash", "goodbye", "buzz").should == 4 71 | 72 | @cli.manifest("parent", "foo").should == 0 73 | @cli.manifest("parent", "bar").should == "0" 74 | @cli.manifest("parent", "sub", "foo").should == 1 75 | @cli.manifest("parent", "sub", "bar").should == "1" 76 | @cli.manifest("parent", "sub", "baz").should == "-1" 77 | @cli.manifest("parent", "sub2", "foo").should == 2 78 | @cli.manifest("parent", "sub2", "bar").should == "2" 79 | @cli.manifest("parent", "sub2", "baz").should == "-2" 80 | end 81 | 82 | it "predefines a few helpers" do 83 | @cli.manifest("base").should == "somecloud.com" 84 | @cli.manifest("url").should == "api.somecloud.com" 85 | @cli.manifest("random").should be_a(String) 86 | end 87 | 88 | it "resolves recursively" do 89 | @cli.manifest("third").should == "baz" 90 | @cli.manifest("second").should == "bar baz" 91 | @cli.manifest("first").should == "foo bar baz" 92 | end 93 | end 94 | 95 | describe 'extension manifests' do 96 | before(:all) do 97 | @cli = VMC::Cli::Command::Base.new(:manifest => SUB_MANIFEST) 98 | end 99 | 100 | it "inherits values from a parent manifest" do 101 | @cli.manifest("a").should == 43 102 | @cli.manifest("b").should == "baz" 103 | @cli.manifest("c").should == 42 104 | end 105 | 106 | it "overrides values set in the parent" do 107 | @cli.manifest("d").should == "subbed bar" 108 | end 109 | 110 | it "merges before symbol resolution" do 111 | @cli.manifest("third").should == "baz" 112 | @cli.manifest("second").should == "subbed baz" 113 | @cli.manifest("first").should == "foo subbed baz" 114 | end 115 | 116 | it "merges depth-first" do 117 | @cli.manifest("some-hash", "hello", "foo").should == "one" 118 | @cli.manifest("some-hash", "hello", "bar").should == "one-2" 119 | @cli.manifest("some-hash", "goodbye", "fizz").should == 3 120 | @cli.manifest("some-hash", "goodbye", "buzz").should == 4 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/unit/micro_cmd_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe VMC::Cli::Command::Micro do 4 | VMRUN = "/path/to/vmrun" 5 | VMX = "/path/to/micro.vmx" 6 | PASSWORD = "password" 7 | 8 | describe "#build_config" do 9 | it "should ask for info when the config is empty" do 10 | VMC::Cli::Config.should_receive(:micro).and_return({}) 11 | cmd = VMC::Cli::Command::Micro.new({}) 12 | 13 | cmd.should_receive(:locate_vmx).and_return(VMX) 14 | VMC::Micro::VMrun.should_receive(:locate).and_return(VMRUN) 15 | cmd.should_receive(:ask).and_return(PASSWORD) 16 | 17 | config = cmd.build_config 18 | config['vmx'].should == VMX 19 | config['vmrun'].should == VMRUN 20 | config['password'].should == PASSWORD 21 | end 22 | 23 | it "should override stored config with command line arguments" do 24 | VMC::Cli::Config.should_receive(:micro).and_return({}) 25 | options = {:password => PASSWORD, :vmx => VMX, :vmrun => VMRUN} 26 | cmd = VMC::Cli::Command::Micro.new(options) 27 | 28 | config = cmd.build_config 29 | config['vmx'].should == VMX 30 | config['vmrun'].should == VMRUN 31 | config['password'].should == PASSWORD 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/unit/services_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'VMC::Cli::ServicesHelper' do 4 | 5 | include VMC::Cli::ServicesHelper 6 | 7 | before(:each) do 8 | end 9 | 10 | describe "generated service name" do 11 | 12 | it 'should replace app name' do 13 | new_name = generate_cloned_service_name('first','second','first-mysql','aws') 14 | new_name.should == 'second-mysql' 15 | end 16 | 17 | it 'should replace random hex number' do 18 | new_name = generate_cloned_service_name('first','second','mysql-01a94','aws') 19 | new_name.should =~ /mysql-\h+/ 20 | end 21 | 22 | it 'should append infra name' do 23 | new_name = generate_cloned_service_name('first','second','my-database','aws') 24 | new_name.should == 'my-database-aws' 25 | end 26 | 27 | end 28 | 29 | end -------------------------------------------------------------------------------- /spec/unit/switcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe VMC::Micro::Switcher::Base do 4 | it "should go online" do 5 | vmrun = double(VMC::Micro::VMrun) 6 | vmrun.should_receive(:running?).and_return(true) 7 | vmrun.should_receive(:ready?).and_return(true) 8 | vmrun.should_receive(:offline?).and_return(false) 9 | VMC::Micro::VMrun.should_receive(:new).and_return(vmrun) 10 | switcher = VMC::Micro::Switcher::Dummy.new({}) 11 | switcher.online 12 | end 13 | it "should go offline" do 14 | vmrun = double(VMC::Micro::VMrun) 15 | vmrun.should_receive(:running?).and_return(true) 16 | vmrun.should_receive(:ready?).and_return(true) 17 | vmrun.should_receive(:offline?).and_return(true) 18 | VMC::Micro::VMrun.should_receive(:new).and_return(vmrun) 19 | switcher = VMC::Micro::Switcher::Dummy.new({}) 20 | switcher.offline 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/vmrun_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe VMC::Micro::VMrun do 4 | 5 | before(:all) do 6 | platform = VMC::Cli::Command::Micro.platform 7 | @config = {'platform' => platform, 'password' => 'pass', 'vmrun' => 'vmrun', 'vmx' => 'vmx'} 8 | end 9 | 10 | it "should list all VMs running" do 11 | v = VMC::Micro::VMrun.new(@config) 12 | v.should_receive(:run).and_return(["Total ...", "/foo.vmx", "/bar.vmx"]) 13 | v.list.should == ["/foo.vmx", "/bar.vmx"] 14 | end 15 | 16 | describe "connection type" do 17 | it "should list the connection type" do 18 | vmrun = VMC::Micro::VMrun.new(@config) 19 | vmrun.should_receive(:run).and_return(["bridged"]) 20 | vmrun.connection_type == "bridged" 21 | end 22 | end 23 | end 24 | --------------------------------------------------------------------------------