appfog-tunnel-vmc-plugin 0.0.1 → 0.0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- OTZmMjcxMmM0ZGExOGM4YmMxZDFlNTIzODFiZjdiMTM3NDNlNmE0Mg==
4
+ OWNjYTczMmEwYTU5ZTdiOGE5OWM0OWFiNmFmYWRjMjFjZGZhOGFiZQ==
5
5
  data.tar.gz: !binary |-
6
- YjY2OWM3M2FmZmViOTE5M2IwZjIyMTA4OWRmYTg2YzBhMzI1M2U5NQ==
6
+ YmVlY2QxOWMwZjM1OWVkNzJlN2ZkYWNlNzVkYmQ4NWNiMTdlYWI4Yg==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- NjY5Y2MwN2NkODhhM2QyYjY2MTBlYzU4MmJiMjk1M2Q2OWZjNmM5NTMyMjk4
10
- MGU4NzRmNGNjODZjNzdiYjhkZGIyMWM3MDlmOTYwZTBlOTUyMzJlMGM4MDgx
11
- MWE5NzVhODc0NjY5YWYyYjM1NzcwNDQxZDJiOTM2NjQ3ZDdmZmU=
9
+ MjI1ZjNiYjBlMTg2NWU5ZGIyZjRmNzZjM2U2MTVlNWZlZTcyZGE0YmYyZjE5
10
+ YTM2ODEwMTFjMDg5Nzk0ZWI4NTdlMzUzYjBmZTA3M2E3NWE1NmUwNjJiNDNm
11
+ Y2RkNzhiMTVkNjBlM2QzODEzYWVlYzJhOGM4ODRkZTlhM2Q0MmQ=
12
12
  data.tar.gz: !binary |-
13
- NzU4NTYzOGM0Y2FiMmEzZDE0NDczYTIwMjZhY2Q2OGZmNTg1Y2RiZjQ0NDcw
14
- NDgwMzU4MGIwN2U2MWRmMzliZGZmNDlmN2Y3NjE1ODZhZTdkNWExMWU5N2M1
15
- Y2E5NmU0Njc0Y2ZiYTBkMzJhMDc5MjdiMmQxOWNiYTBkNWY0NmE=
13
+ MDdlZjk0ODhiNWJkOWJmNThjYmQyNjUwMjU0ODAyNmI5MDRmODY5MzdkZGIw
14
+ OWRlNjU3OTg5MmVmZDg5ZmI5Mzc4Y2RiNDhiMDk2NDdlMWUxYzM2OGEwOWM1
15
+ OTI3MTY3YTI4MjQ2MGU2ZWM5NjQyMjZlZDVjMTZhYzNjZjNhYzI=
@@ -0,0 +1,178 @@
1
+ require "vmc/cli"
2
+ require "tunnel-vmc-plugin/tunnel"
3
+
4
+ module VMCTunnel
5
+ class Tunnel < VMC::CLI
6
+ CLIENTS_FILE = "#{VMC::CONFIG_DIR}/tunnel-clients.yml"
7
+ STOCK_CLIENTS = File.expand_path("../../../config/clients.yml", __FILE__)
8
+
9
+ desc "Create a local tunnel to a service."
10
+ group :services, :manage
11
+ input(:instance, :argument => :optional,
12
+ :from_given => find_by_name("service instance"),
13
+ :desc => "Service instance to tunnel to") { |instances|
14
+ ask("Which service instance?", :choices => instances,
15
+ :display => proc(&:name))
16
+ }
17
+ input(:client, :argument => :optional,
18
+ :desc => "Client to automatically launch") { |clients|
19
+ if clients.empty?
20
+ "none"
21
+ else
22
+ ask("Which client would you like to start?",
23
+ :choices => clients.keys.unshift("none"))
24
+ end
25
+ }
26
+ input(:port, :default => 10000, :desc => "Port to bind the tunnel to")
27
+ def tunnel
28
+ instances = client.service_instances
29
+ fail "No services available for tunneling." if instances.empty?
30
+
31
+ instance = input[:instance, instances.sort_by(&:name)]
32
+ vendor = v2? ? instance.service_plan.service.label : instance.vendor
33
+ clients = tunnel_clients[vendor] || {}
34
+ client_name = input[:client, clients]
35
+
36
+ tunnel = CFTunnel.new(client, instance)
37
+ port = tunnel.pick_port!(input[:port])
38
+
39
+ conn_info =
40
+ with_progress("Opening tunnel on port #{c(port, :name)}") do
41
+ tunnel.open!
42
+ end
43
+
44
+ if client_name == "none"
45
+ unless quiet?
46
+ line
47
+ display_tunnel_connection_info(conn_info)
48
+
49
+ line
50
+ line "Open another shell to run command-line clients or"
51
+ line "use a UI tool to connect using the displayed information."
52
+ line "Press Ctrl-C to exit..."
53
+ end
54
+
55
+ tunnel.wait_for_end
56
+ else
57
+ with_progress("Waiting for local tunnel to become available") do
58
+ tunnel.wait_for_start
59
+ end
60
+
61
+ unless start_local_prog(clients, client_name, conn_info, port)
62
+ fail "'#{client_name}' execution failed; is it in your $PATH?"
63
+ end
64
+ end
65
+ end
66
+
67
+ def tunnel_clients
68
+ return @tunnel_clients if @tunnel_clients
69
+ stock_config = YAML.load_file(STOCK_CLIENTS)
70
+ custom_config_file = File.expand_path(CLIENTS_FILE)
71
+ if File.exists?(custom_config_file)
72
+ custom_config = YAML.load_file(custom_config_file)
73
+ @tunnel_clients = deep_merge(stock_config, custom_config)
74
+ else
75
+ @tunnel_clients = stock_config
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def display_tunnel_connection_info(info)
82
+ line "Service connection info:"
83
+
84
+ to_show = [nil, nil, nil] # reserved for user, pass, db name
85
+ info.keys.each do |k|
86
+ case k
87
+ when "host", "hostname", "port", "node_id"
88
+ # skip
89
+ when "user", "username"
90
+ # prefer "username" over "user"
91
+ to_show[0] = k unless to_show[0] == "username"
92
+ when "password"
93
+ to_show[1] = k
94
+ when "name"
95
+ to_show[2] = k
96
+ else
97
+ to_show << k
98
+ end
99
+ end
100
+ to_show.compact!
101
+
102
+ align_len = to_show.collect(&:size).max + 1
103
+
104
+ indented do
105
+ to_show.each do |k|
106
+ # TODO: modify the server services rest call to have explicit knowledge
107
+ # about the items to return. It should return all of them if
108
+ # the service is unknown so that we don't have to do this weird
109
+ # filtering.
110
+ line "#{k.ljust align_len}: #{b(info[k])}"
111
+ end
112
+ end
113
+
114
+ line
115
+ end
116
+
117
+ def start_local_prog(clients, command, info, port)
118
+ client = clients[File.basename(command)]
119
+
120
+ cmdline = "#{command} "
121
+
122
+ case client
123
+ when Hash
124
+ cmdline << resolve_symbols(client["command"], info, port)
125
+ client["environment"].each do |e|
126
+ if e =~ /([^=]+)=(["']?)([^"']*)\2/
127
+ ENV[$1] = resolve_symbols($3, info, port)
128
+ else
129
+ fail "Invalid environment variable: #{e}"
130
+ end
131
+ end
132
+ when String
133
+ cmdline << resolve_symbols(client, info, port)
134
+ else
135
+ raise "Unknown client info: #{client.inspect}."
136
+ end
137
+
138
+ if verbose?
139
+ line
140
+ line "Launching '#{cmdline}'"
141
+ end
142
+
143
+ system(cmdline)
144
+ end
145
+
146
+ def resolve_symbols(str, info, local_port)
147
+ str.gsub(/\$\{\s*([^\}]+)\s*\}/) do
148
+ sym = $1
149
+
150
+ case sym
151
+ when "host"
152
+ # TODO: determine proper host
153
+ "localhost"
154
+ when "port"
155
+ local_port
156
+ when "user", "username"
157
+ info["username"]
158
+ when /^ask (.+)/
159
+ ask($1)
160
+ else
161
+ info[sym] || raise("Unknown symbol in config: #{sym}")
162
+ end
163
+ end
164
+ end
165
+
166
+ def deep_merge(a, b)
167
+ merge = proc { |_, old, new|
168
+ if old.is_a?(Hash) && new.is_a?(Hash)
169
+ old.merge(new, &merge)
170
+ else
171
+ new
172
+ end
173
+ }
174
+
175
+ a.merge(b, &merge)
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,306 @@
1
+ require "addressable/uri"
2
+ require "restclient"
3
+ require "uuidtools"
4
+
5
+ require "caldecott-client"
6
+
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
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 if @service && !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_by_name("#{HELPER_NAME}-#{@service.infra.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["CALDECOTT_AUTH"]
95
+ end
96
+
97
+ def helper_healthy?(token)
98
+ return false unless helper.healthy?
99
+
100
+ begin
101
+ response = RestClient.get(
102
+ "#{helper_url}/info",
103
+ "Auth-Token" => token
104
+ )
105
+
106
+ info = JSON.parse(response)
107
+ if info["version"] == HELPER_VERSION
108
+ true
109
+ else
110
+ stop_helper
111
+ false
112
+ end
113
+ rescue RestClient::Exception
114
+ stop_helper
115
+ false
116
+ end
117
+ end
118
+
119
+ def helper_already_binds?
120
+ helper.binds? @service
121
+ end
122
+
123
+ def push_helper(token)
124
+ url = "#{random_helper_url}.#{@service.infra.name}.af.cm"
125
+ is_v2 = @client.is_a?(CFoundry::V2::Client)
126
+
127
+ app = @client.app
128
+ app.name = "#{HELPER_NAME}-#{@service.infra.name}"
129
+ app.infra = @service.infra
130
+ app.framework = @client.framework_by_name("sinatra")
131
+ app.runtime = @client.runtime_by_name("ruby192")
132
+ app.total_instances = 1
133
+ app.memory = 64
134
+ app.env = { "CALDECOTT_AUTH" => token }
135
+
136
+ if is_v2
137
+ app.space = @client.current_space
138
+ else
139
+ app.services = [@service] if @service
140
+ app.url = url
141
+ end
142
+
143
+ app.create!
144
+
145
+ if is_v2
146
+ app.bind(@service) if @service
147
+ app.create_route(url)
148
+ end
149
+
150
+ begin
151
+ app.upload(HELPER_APP)
152
+ invalidate_tunnel_app_info
153
+ rescue
154
+ app.delete!
155
+ raise
156
+ end
157
+ end
158
+
159
+ def delete_helper
160
+ helper.delete!
161
+ invalidate_tunnel_app_info
162
+ end
163
+
164
+ def stop_helper
165
+ helper.stop!
166
+ invalidate_tunnel_app_info
167
+ end
168
+
169
+ TUNNEL_CHECK_LIMIT = 60
170
+ def start_helper
171
+ helper.start!
172
+
173
+ seconds = 0
174
+ until helper.healthy?
175
+ sleep 1
176
+ seconds += 1
177
+ if seconds == TUNNEL_CHECK_LIMIT
178
+ raise "Helper application failed to start."
179
+ end
180
+ end
181
+
182
+ invalidate_tunnel_app_info
183
+ end
184
+
185
+ def bind_to_helper
186
+ helper.bind(@service)
187
+ helper.restart!
188
+ end
189
+
190
+ def invalidate_tunnel_app_info
191
+ @helper_url = nil
192
+ @helper = nil
193
+ end
194
+
195
+ def helper_url
196
+ return @helper_url if @helper_url
197
+
198
+ tun_url = helper.url
199
+
200
+ ["https", "http"].each do |scheme|
201
+ url = "#{scheme}://#{tun_url}"
202
+ begin
203
+ RestClient.get(url)
204
+
205
+ # https failed
206
+ rescue Errno::ECONNREFUSED
207
+
208
+ # we expect a 404 since this request isn't auth'd
209
+ rescue RestClient::ResourceNotFound
210
+ return @helper_url = url
211
+ end
212
+ end
213
+
214
+ raise "Cannot determine URL for #{tun_url}"
215
+ end
216
+
217
+ def get_connection_info(token)
218
+ response = nil
219
+ 10.times do
220
+ begin
221
+ response =
222
+ RestClient.get(
223
+ helper_url + "/" + safe_path("services", @service.name),
224
+ "Auth-Token" => token)
225
+
226
+ break
227
+ rescue RestClient::Exception => e
228
+ sleep 1
229
+ end
230
+ end
231
+
232
+ unless response
233
+ raise "Remote tunnel helper is unaware of #{@service.name}!"
234
+ end
235
+
236
+ is_v2 = @client.is_a?(CFoundry::V2::Client)
237
+
238
+ info = JSON.parse(response)
239
+ case (is_v2 ? @service.service_plan.service.label : @service.vendor)
240
+ when "rabbitmq"
241
+ uri = Addressable::URI.parse info["url"]
242
+ info["hostname"] = uri.host
243
+ info["port"] = uri.port
244
+ info["vhost"] = uri.path[1..-1]
245
+ info["user"] = uri.user
246
+ info["password"] = uri.password
247
+ info.delete "url"
248
+
249
+ # we use "db" as the "name" for mongo
250
+ # existing "name" is junk
251
+ when "mongodb"
252
+ info["name"] = info["db"]
253
+ info.delete "db"
254
+
255
+ # our "name" is irrelevant for redis
256
+ when "redis"
257
+ info.delete "name"
258
+
259
+ when "filesystem"
260
+ raise "Tunneling is not supported for this type of service"
261
+ end
262
+
263
+ ["hostname", "port", "password"].each do |k|
264
+ raise "Could not determine #{k} for #{@service.name}" if info[k].nil?
265
+ end
266
+
267
+ info
268
+ end
269
+
270
+ def start_tunnel(conn_info, auth)
271
+ @local_tunnel_thread = Thread.new do
272
+ Caldecott::Client.start({
273
+ :local_port => @port,
274
+ :tun_url => helper_url,
275
+ :dst_host => conn_info["hostname"],
276
+ :dst_port => conn_info["port"],
277
+ :log_file => STDOUT,
278
+ :log_level => ENV["VMC_TUNNEL_DEBUG"] || "ERROR",
279
+ :auth_token => auth,
280
+ :quiet => true
281
+ })
282
+ end
283
+
284
+ at_exit { @local_tunnel_thread.kill }
285
+ end
286
+
287
+ def random_helper_url
288
+ random = sprintf("%x", rand(1000000))
289
+ "caldecott-#{random}"
290
+ end
291
+
292
+ def safe_path(*segments)
293
+ segments.flatten.collect { |x|
294
+ URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
295
+ }.join("/")
296
+ end
297
+
298
+ def grab_ephemeral_port
299
+ socket = TCPServer.new("0.0.0.0", 0)
300
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
301
+ Socket.do_not_reverse_lookup = true
302
+ socket.addr[1]
303
+ ensure
304
+ socket.close
305
+ end
306
+ end
@@ -0,0 +1,3 @@
1
+ module VMCTunnel
2
+ VERSION = "0.0.1.1".freeze
3
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appfog-tunnel-vmc-plugin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Suraci, Tim Santeford
@@ -144,6 +144,9 @@ extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
146
  - Rakefile
147
+ - lib/appfog-tunnel-vmc-plugin/plugin.rb
148
+ - lib/appfog-tunnel-vmc-plugin/tunnel.rb
149
+ - lib/appfog-tunnel-vmc-plugin/version.rb
147
150
  - lib/tunnel-vmc-plugin/plugin.rb
148
151
  - lib/tunnel-vmc-plugin/tunnel.rb
149
152
  - lib/tunnel-vmc-plugin/version.rb