tunnel-vmc-plugin 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
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} > ${ask 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} ${ask 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}'
@@ -0,0 +1,10 @@
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'
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,43 @@
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"])
@@ -0,0 +1,87 @@
1
+ require "vmc/plugin"
2
+ require File.expand_path("../tunnel", __FILE__)
3
+
4
+ VMC.Plugin(VMC::Service) do
5
+ include VMCTunnel
6
+
7
+ desc "tunnel SERVICE [CLIENT]", "create a local tunnel to a service"
8
+ flag(:service) { |choices|
9
+ ask("Which service?", :choices => choices)
10
+ }
11
+ flag(:client)
12
+ flag(:port, :default => 10000)
13
+ def tunnel(service = nil, client_name = nil)
14
+ unless defined? Caldecott
15
+ $stderr.puts "To use `vmc tunnel', you must first install Caldecott:"
16
+ $stderr.puts ""
17
+ $stderr.puts "\tgem install caldecott"
18
+ $stderr.puts ""
19
+ $stderr.puts "Note that you'll need a C compiler. If you're on OS X, Xcode"
20
+ $stderr.puts "will provide one. If you're on Windows, try DevKit."
21
+ $stderr.puts ""
22
+ $stderr.puts "This manual step will be removed in the future."
23
+ $stderr.puts ""
24
+ err "Caldecott is not installed."
25
+ return
26
+ end
27
+
28
+ client_name ||= input(:client)
29
+
30
+ services = client.services
31
+ if services.empty?
32
+ err "No services available to tunnel to"
33
+ return
34
+ end
35
+
36
+ service ||= input(:service, services.collect(&:name).sort)
37
+
38
+ info = services.find { |s| s.name == service }
39
+
40
+ unless info
41
+ err "Unknown service '#{service}'"
42
+ return
43
+ end
44
+
45
+ clients = tunnel_clients[info.vendor] || {}
46
+
47
+ unless client_name
48
+ if clients.empty?
49
+ client_name = "none"
50
+ else
51
+ client_name = ask(
52
+ "Which client would you like to start?",
53
+ :choices => ["none"] + clients.keys)
54
+ end
55
+ end
56
+
57
+ tunnel = CFTunnel.new(client, info)
58
+ port = tunnel.pick_port!(input(:port))
59
+
60
+ conn_info =
61
+ with_progress("Opening tunnel on port #{c(port, :blue)}") do
62
+ tunnel.open!
63
+ end
64
+
65
+ if client_name == "none"
66
+ unless simple_output?
67
+ puts ""
68
+ display_tunnel_connection_info(conn_info)
69
+
70
+ puts ""
71
+ puts "Open another shell to run command-line clients or"
72
+ puts "use a UI tool to connect using the displayed information."
73
+ puts "Press Ctrl-C to exit..."
74
+ end
75
+
76
+ tunnel.wait_for_end
77
+ else
78
+ with_progress("Waiting for local tunnel to become available") do
79
+ tunnel.wait_for_start
80
+ end
81
+
82
+ unless start_local_prog(clients, client_name, conn_info, port)
83
+ err "'#{client_name}' execution failed; is it in your $PATH?"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,406 @@
1
+ require "addressable/uri"
2
+
3
+ begin
4
+ require "caldecott"
5
+ rescue LoadError
6
+ end
7
+
8
+ class CFTunnel
9
+ HELPER_NAME = "caldecott"
10
+ HELPER_APP = File.expand_path("../../../helper-app", __FILE__)
11
+
12
+ # bump this AND the version info reported by HELPER_APP/server.rb
13
+ # this is to keep the helper in sync with any updates here
14
+ HELPER_VERSION = "0.0.4"
15
+
16
+ def initialize(client, service, port = 10000)
17
+ @client = client
18
+ @service = service
19
+ @port = port
20
+ end
21
+
22
+ def open!
23
+ if helper.exists?
24
+ auth = helper_auth
25
+
26
+ unless helper_healthy?(auth)
27
+ delete_helper
28
+ auth = create_helper
29
+ end
30
+ else
31
+ auth = create_helper
32
+ end
33
+
34
+ bind_to_helper unless helper_already_binds?
35
+
36
+ info = get_connection_info(auth)
37
+
38
+ start_tunnel(info, auth)
39
+
40
+ info
41
+ end
42
+
43
+ def wait_for_start
44
+ 10.times do |n|
45
+ begin
46
+ TCPSocket.open("localhost", @port).close
47
+ return true
48
+ rescue => e
49
+ sleep 1
50
+ end
51
+ end
52
+
53
+ raise "Could not connect to local tunnel."
54
+ end
55
+
56
+ def wait_for_end
57
+ if @local_tunnel_thread
58
+ @local_tunnel_thread.join
59
+ else
60
+ raise "Tunnel wasn't started!"
61
+ end
62
+ end
63
+
64
+ PORT_RANGE = 10
65
+ def pick_port!(port = @port)
66
+ original = port
67
+
68
+ PORT_RANGE.times do |n|
69
+ begin
70
+ TCPSocket.open("localhost", port)
71
+ port += 1
72
+ rescue
73
+ return @port = port
74
+ end
75
+ end
76
+
77
+ @port = grab_ephemeral_port
78
+ end
79
+
80
+ private
81
+
82
+ def helper
83
+ @helper ||= @client.app(HELPER_NAME)
84
+ end
85
+
86
+ def create_helper
87
+ auth = UUIDTools::UUID.random_create.to_s
88
+ push_helper(auth)
89
+ start_helper
90
+ auth
91
+ end
92
+
93
+ def helper_auth
94
+ helper.env.each do |e|
95
+ name, val = e.split("=", 2)
96
+ return val if name == "CALDECOTT_AUTH"
97
+ end
98
+
99
+ nil
100
+ end
101
+
102
+ def helper_healthy?(token)
103
+ return false unless helper.healthy?
104
+
105
+ begin
106
+ response = RestClient.get(
107
+ "#{helper_url}/info",
108
+ "Auth-Token" => token
109
+ )
110
+
111
+ info = JSON.parse(response)
112
+ if info["version"] == HELPER_VERSION
113
+ true
114
+ else
115
+ stop_helper
116
+ false
117
+ end
118
+ rescue RestClient::Exception
119
+ stop_helper
120
+ false
121
+ end
122
+ end
123
+
124
+ def helper_already_binds?
125
+ helper.services.include? @service.name
126
+ end
127
+
128
+ def push_helper(token)
129
+ target_base = @client.target.sub(/^[^\.]+\./, "")
130
+
131
+ app = @client.app(HELPER_NAME)
132
+ app.framework = "sinatra"
133
+ app.url = "#{random_helper_url}.#{target_base}"
134
+ app.total_instances = 1
135
+ app.memory = 64
136
+ app.env = ["CALDECOTT_AUTH=#{token}"]
137
+ app.services = [@service.name]
138
+ app.create!
139
+
140
+ begin
141
+ app.upload(HELPER_APP)
142
+ invalidate_tunnel_app_info
143
+ rescue
144
+ app.delete!
145
+ raise
146
+ end
147
+ end
148
+
149
+ def delete_helper
150
+ helper.delete!
151
+ invalidate_tunnel_app_info
152
+ end
153
+
154
+ def stop_helper
155
+ helper.stop!
156
+ invalidate_tunnel_app_info
157
+ end
158
+
159
+ TUNNEL_CHECK_LIMIT = 60
160
+ def start_helper
161
+ helper.start!
162
+
163
+ seconds = 0
164
+ until helper.healthy?
165
+ sleep 1
166
+ seconds += 1
167
+ if seconds == TUNNEL_CHECK_LIMIT
168
+ raise "Helper application failed to start."
169
+ end
170
+ end
171
+
172
+ invalidate_tunnel_app_info
173
+ end
174
+
175
+ def bind_to_helper
176
+ helper.bind(@service.name)
177
+ helper.restart!
178
+ end
179
+
180
+ def invalidate_tunnel_app_info
181
+ @helper_url = nil
182
+ @helper = nil
183
+ end
184
+
185
+ def helper_url
186
+ return @helper_url if @helper_url
187
+
188
+ tun_url = helper.url
189
+
190
+ ["https", "http"].each do |scheme|
191
+ url = "#{scheme}://#{tun_url}"
192
+ begin
193
+ RestClient.get(url)
194
+
195
+ # https failed
196
+ rescue Errno::ECONNREFUSED
197
+
198
+ # we expect a 404 since this request isn't auth'd
199
+ rescue RestClient::ResourceNotFound
200
+ return @helper_url = url
201
+ end
202
+ end
203
+
204
+ raise "Cannot determine URL for #{tun_url}"
205
+ end
206
+
207
+ def get_connection_info(token)
208
+ response = nil
209
+ 10.times do
210
+ begin
211
+ response =
212
+ RestClient.get(
213
+ helper_url + "/" + safe_path("services", @service.name),
214
+ "Auth-Token" => token)
215
+
216
+ break
217
+ rescue RestClient::Exception => e
218
+ p [e, e.to_s]
219
+ sleep 1
220
+ end
221
+ end
222
+
223
+ unless response
224
+ raise "Remote tunnel helper is unaware of #{@service.name}!"
225
+ end
226
+
227
+ info = JSON.parse(response)
228
+ case @service.vendor
229
+ when "rabbitmq"
230
+ uri = Addressable::URI.parse info["url"]
231
+ info["hostname"] = uri.host
232
+ info["port"] = uri.port
233
+ info["vhost"] = uri.path[1..-1]
234
+ info["user"] = uri.user
235
+ info["password"] = uri.password
236
+ info.delete "url"
237
+
238
+ # we use "db" as the "name" for mongo
239
+ # existing "name" is junk
240
+ when "mongodb"
241
+ info["name"] = info["db"]
242
+ info.delete "db"
243
+
244
+ # our "name" is irrelevant for redis
245
+ when "redis"
246
+ info.delete "name"
247
+ end
248
+
249
+ ["hostname", "port", "password"].each do |k|
250
+ raise "Could not determine #{k} for #{@service.name}" if info[k].nil?
251
+ end
252
+
253
+ info
254
+ end
255
+
256
+ def start_tunnel(conn_info, auth)
257
+ @local_tunnel_thread = Thread.new do
258
+ Caldecott::Client.start({
259
+ :local_port => @port,
260
+ :tun_url => helper_url,
261
+ :dst_host => conn_info["hostname"],
262
+ :dst_port => conn_info["port"],
263
+ :log_file => STDOUT,
264
+ :log_level => ENV["VMC_TUNNEL_DEBUG"] || "ERROR",
265
+ :auth_token => auth,
266
+ :quiet => true
267
+ })
268
+ end
269
+
270
+ at_exit { @local_tunnel_thread.kill }
271
+ end
272
+
273
+ def random_helper_url
274
+ random = sprintf("%x", rand(1000000))
275
+ "caldecott-#{random}"
276
+ end
277
+
278
+ def safe_path(*segments)
279
+ segments.flatten.collect { |x|
280
+ URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
281
+ }.join("/")
282
+ end
283
+
284
+ def grab_ephemeral_port
285
+ socket = TCPServer.new("0.0.0.0", 0)
286
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
287
+ Socket.do_not_reverse_lookup = true
288
+ socket.addr[1]
289
+ ensure
290
+ socket.close
291
+ end
292
+ end
293
+
294
+ module VMCTunnel
295
+ CLIENTS_FILE = "#{VMC::CONFIG_DIR}/tunnel-clients.yml"
296
+ STOCK_CLIENTS = File.expand_path("../../../config/clients.yml", __FILE__)
297
+
298
+ def display_tunnel_connection_info(info)
299
+ puts "Service connection info:"
300
+
301
+ to_show = [nil, nil, nil] # reserved for user, pass, db name
302
+ info.keys.each do |k|
303
+ case k
304
+ when "host", "hostname", "port", "node_id"
305
+ # skip
306
+ when "user", "username"
307
+ # prefer "username" over "user"
308
+ to_show[0] = k unless to_show[0] == "username"
309
+ when "password"
310
+ to_show[1] = k
311
+ when "name"
312
+ to_show[2] = k
313
+ else
314
+ to_show << k
315
+ end
316
+ end
317
+ to_show.compact!
318
+
319
+ align_len = to_show.collect(&:size).max + 1
320
+
321
+ to_show.each do |k|
322
+ # TODO: modify the server services rest call to have explicit knowledge
323
+ # about the items to return. It should return all of them if
324
+ # the service is unknown so that we don't have to do this weird
325
+ # filtering.
326
+ print " #{k.ljust align_len}: "
327
+ puts c("#{info[k]}", :yellow)
328
+ end
329
+
330
+ puts ""
331
+ end
332
+
333
+ def start_local_prog(clients, command, info, port)
334
+ client = clients[File.basename(command)]
335
+
336
+ cmdline = "#{command} "
337
+
338
+ case client
339
+ when Hash
340
+ cmdline << resolve_symbols(client["command"], info, port)
341
+ client["environment"].each do |e|
342
+ if e =~ /([^=]+)=(["']?)([^"']*)\2/
343
+ ENV[$1] = resolve_symbols($3, info, port)
344
+ else
345
+ raise "Invalid environment variable: #{e}"
346
+ end
347
+ end
348
+ when String
349
+ cmdline << resolve_symbols(client, info, port)
350
+ else
351
+ raise "Unknown client info: #{client.inspect}."
352
+ end
353
+
354
+ if verbose?
355
+ puts ""
356
+ puts "Launching '#{cmdline}'"
357
+ end
358
+
359
+ system(cmdline)
360
+ end
361
+
362
+ def tunnel_clients
363
+ return @tunnel_clients if @tunnel_clients
364
+
365
+ stock = YAML.load_file(STOCK_CLIENTS)
366
+ clients = File.expand_path CLIENTS_FILE
367
+ if File.exists? clients
368
+ user = YAML.load_file(clients)
369
+ @tunnel_clients = deep_merge(stock, user)
370
+ else
371
+ @tunnel_clients = stock
372
+ end
373
+ end
374
+
375
+ def resolve_symbols(str, info, local_port)
376
+ str.gsub(/\$\{\s*([^\}]+)\s*\}/) do
377
+ sym = $1
378
+
379
+ case sym
380
+ when "host"
381
+ # TODO: determine proper host
382
+ "localhost"
383
+ when "port"
384
+ local_port
385
+ when "user", "username"
386
+ info["username"]
387
+ when /^ask (.+)/
388
+ ask($1)
389
+ else
390
+ info[sym] || raise("Unknown symbol in config: #{sym}")
391
+ end
392
+ end
393
+ end
394
+
395
+ def deep_merge(a, b)
396
+ merge = proc { |old, new|
397
+ if old === Hash && new === Hash
398
+ old.merge(new, &merge)
399
+ else
400
+ new
401
+ end
402
+ }
403
+
404
+ a.merge(b, &merge)
405
+ end
406
+ end
@@ -0,0 +1,3 @@
1
+ module VMCTunnel
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tunnel-vmc-plugin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alex Suraci
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: addressable
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.2.6
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.2.6
30
+ - !ruby/object:Gem::Dependency
31
+ name: eventmachine
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.0.0.beta
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.0.0.beta
46
+ - !ruby/object:Gem::Dependency
47
+ name: caldecott
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.0.5
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.0.5
62
+ description:
63
+ email:
64
+ - asuraci@vmware.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - Rakefile
70
+ - lib/tunnel-vmc-plugin/tunnel.rb
71
+ - lib/tunnel-vmc-plugin/plugin.rb
72
+ - lib/tunnel-vmc-plugin/version.rb
73
+ - helper-app/Gemfile.lock
74
+ - helper-app/server.rb
75
+ - helper-app/Gemfile
76
+ - config/clients.yml
77
+ homepage: http://cloudfoundry.com/
78
+ licenses: []
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project: tunnel-vmc-plugin
97
+ rubygems_version: 1.8.23
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: External access to your services on Cloud Foundry via a Caldecott HTTP tunnel.
101
+ test_files: []