appfog-tunnel-vmc-plugin 0.0.1 → 0.0.1.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.
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