├── spec
├── assets
│ ├── manifests
│ │ ├── somenomanifestapp
│ │ │ └── .gitignore
│ │ ├── bad-manifest.yml
│ │ ├── my-manifest.yml
│ │ ├── someapp
│ │ │ ├── somedir
│ │ │ │ └── somesubdir
│ │ │ │ │ └── .gitignore
│ │ │ └── manifest.yml
│ │ ├── sub-manifest.yml
│ │ └── sym-manifest.yml
│ ├── invalid_console_access.txt
│ ├── console_access.txt
│ ├── sample_token.txt
│ ├── app_not_found.txt
│ ├── login_fail.txt
│ ├── delete_app.txt
│ ├── user_info.txt
│ ├── good_create_app.txt
│ ├── service_not_found.txt
│ ├── good_create_service.txt
│ ├── login_success.txt
│ ├── service_gateway_fail.txt
│ ├── service_already_exists.txt
│ ├── resources_return.txt
│ ├── bad_create_app.txt
│ ├── info_return.txt
│ ├── standalone_app_info.txt
│ ├── app_info.txt
│ ├── app_listings.txt
│ ├── service_listings.txt
│ ├── info_return_bad.txt
│ ├── global_service_listings.txt
│ ├── info_nil_usage.txt
│ ├── info_authenticated.txt
│ └── list_users.txt
├── spec_helper.rb
└── unit
│ ├── vmrun_spec.rb
│ ├── services_helper_spec.rb
│ ├── switcher_spec.rb
│ ├── command_info_spec.rb
│ ├── micro_cmd_spec.rb
│ ├── command_admin_spec.rb
│ ├── command_user_spec.rb
│ ├── command_services_spec.rb
│ ├── cli_opts_spec.rb
│ ├── manifests_spec.rb
│ ├── file_helper_spec.rb
│ ├── frameworks_spec.rb
│ ├── command_apps_spec.rb
│ ├── console_helper_spec.rb
│ └── client_spec.rb
├── config
├── micro
│ ├── offline.conf
│ ├── paths.yml
│ └── refresh_ip.rb
└── clients.yml
├── Gemfile
├── lib
├── vmc.rb
├── cli
│ ├── version.rb
│ ├── errors.rb
│ ├── commands
│ │ ├── manifest.rb
│ │ ├── user.rb
│ │ ├── admin.rb
│ │ ├── micro.rb
│ │ ├── misc.rb
│ │ ├── base.rb
│ │ └── services.rb
│ ├── core_ext.rb
│ ├── zip_util.rb
│ ├── services_helper.rb
│ ├── file_helper.rb
│ ├── config.rb
│ ├── console_helper.rb
│ ├── usage.rb
│ ├── frameworks.rb
│ ├── manifest_helper.rb
│ └── tunnel_helper.rb
├── vmc
│ ├── micro
│ │ ├── switcher
│ │ │ ├── dummy.rb
│ │ │ ├── linux.rb
│ │ │ ├── darwin.rb
│ │ │ ├── windows.rb
│ │ │ └── base.rb
│ │ └── vmrun.rb
│ ├── const.rb
│ └── micro.rb
└── cli.rb
├── .gitignore
├── bin
└── af
├── .gitmodules
├── caldecott_helper
├── Gemfile
├── server.rb
└── Gemfile.lock
├── TODO
├── af.gemspec
├── LICENSE
├── Rakefile
└── README.md
/spec/assets/manifests/somenomanifestapp/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/micro/offline.conf:
--------------------------------------------------------------------------------
1 | no-resolv
2 | log-queries
3 |
--------------------------------------------------------------------------------
/spec/assets/invalid_console_access.txt:
--------------------------------------------------------------------------------
1 | username: cfuser
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gemspec
4 |
5 |
--------------------------------------------------------------------------------
/lib/vmc.rb:
--------------------------------------------------------------------------------
1 | module VMC; end
2 |
3 | require 'vmc/client'
4 |
--------------------------------------------------------------------------------
/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/somedir/somesubdir/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/assets/console_access.txt:
--------------------------------------------------------------------------------
1 | username: cfuser
2 | password: testpw
--------------------------------------------------------------------------------
/spec/assets/manifests/someapp/manifest.yml:
--------------------------------------------------------------------------------
1 | fizz: 1
2 | buzz: 2
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 | !caldecott_helper/Gemfile.lock
3 | .build
4 | *.gem
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/spec/assets/sample_token.txt:
--------------------------------------------------------------------------------
1 | 04085b082214646572656b40676d61696c2e636f6d6c2b07f014794d2219b3dd205faeaefc0a8c70bb3ba786628c8cb8d667
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "spec/assets/tests"]
2 | path = spec/assets/tests
3 | url = https://github.com/cloudfoundry/vcap-test-assets.git
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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"}
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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_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/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/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/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/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/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}]
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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"}}}}
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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_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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------