tunnel-vmc-plugin 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []