canals 0.8.0
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 +7 -0
- data/.gitignore +38 -0
- data/.rvmrc +1 -0
- data/Gemfile +8 -0
- data/LICENSE +21 -0
- data/README.md +11 -0
- data/Rakefile +3 -0
- data/assets/canals.sh +53 -0
- data/bin/canal +14 -0
- data/canals.gemspec +27 -0
- data/lib/canals/cli/environment.rb +44 -0
- data/lib/canals/cli/helpers.rb +38 -0
- data/lib/canals/cli/list.rb +43 -0
- data/lib/canals/cli/session.rb +75 -0
- data/lib/canals/cli/setup.rb +98 -0
- data/lib/canals/cli.rb +90 -0
- data/lib/canals/config.rb +37 -0
- data/lib/canals/core.rb +69 -0
- data/lib/canals/core_ext/shell_colors.rb +15 -0
- data/lib/canals/core_ext/string.rb +6 -0
- data/lib/canals/environment.rb +40 -0
- data/lib/canals/options.rb +75 -0
- data/lib/canals/repository.rb +87 -0
- data/lib/canals/session.rb +64 -0
- data/lib/canals/tools/assets.rb +21 -0
- data/lib/canals/tools/completion.rb +47 -0
- data/lib/canals/version.rb +6 -0
- data/lib/canals.rb +39 -0
- data/spec/canals/environment_spec.rb +77 -0
- data/spec/canals/options_spec.rb +203 -0
- data/spec/spec_helper.rb +4 -0
- metadata +119 -0
data/lib/canals/core.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Canals
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def create_tunnel(tunnel_opts)
|
7
|
+
Canals.repository.add tunnel_opts
|
8
|
+
end
|
9
|
+
|
10
|
+
def start(tunnel_opts)
|
11
|
+
if tunnel_opts.instance_of? String
|
12
|
+
tunnel_opts = Canals.repository.get(tunnel_opts)
|
13
|
+
end
|
14
|
+
tunnel_start(tunnel_opts)
|
15
|
+
pid = tunnel_pid(tunnel_opts)
|
16
|
+
Canals.session.add({name: tunnel_opts.name, pid: pid, socket: socket_file(tunnel_opts)})
|
17
|
+
pid.to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop(tunnel_opts)
|
21
|
+
if tunnel_opts.instance_of? String
|
22
|
+
tunnel_opts = Canals.repository.get(tunnel_opts)
|
23
|
+
end
|
24
|
+
tunnel_close(tunnel_opts)
|
25
|
+
Canals.session.del(tunnel_opts.name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def restart(tunnel_opts)
|
29
|
+
stop(tunnel_opts)
|
30
|
+
start(tunnel_opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
def isalive?(tunnel_opts)
|
34
|
+
if tunnel_opts.instance_of? String
|
35
|
+
tunnel_opts = Canals.repository.get(tunnel_opts)
|
36
|
+
end
|
37
|
+
!!tunnel_pid(tunnel_opts)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def socket_file(tunnel_opts)
|
43
|
+
"/tmp/canals/canal.#{tunnel_opts.name}.sock"
|
44
|
+
end
|
45
|
+
|
46
|
+
def tunnel_start(tunnel_opts)
|
47
|
+
FileUtils.mkdir_p("/tmp/canals")
|
48
|
+
cmd = "ssh -M -S #{socket_file(tunnel_opts)} -fnNT -L #{tunnel_opts.bind_address}:#{tunnel_opts.local_port}:#{tunnel_opts.remote_host}:#{tunnel_opts.remote_port} #{tunnel_opts.proxy}"
|
49
|
+
system(cmd)
|
50
|
+
end
|
51
|
+
|
52
|
+
def tunnel_check(tunnel_opts)
|
53
|
+
cmd = "ssh -S #{socket_file(tunnel_opts)} -O check #{tunnel_opts.proxy}"
|
54
|
+
Open3.capture2e(cmd)
|
55
|
+
end
|
56
|
+
|
57
|
+
def tunnel_close(tunnel_opts)
|
58
|
+
cmd = "ssh -S #{socket_file(tunnel_opts)} -O exit #{tunnel_opts.proxy}"
|
59
|
+
Open3.capture2e(cmd)
|
60
|
+
end
|
61
|
+
|
62
|
+
def tunnel_pid(tunnel_opts)
|
63
|
+
stdout, _ = tunnel_check(tunnel_opts)
|
64
|
+
m = /\(pid=(.*)\)/.match(stdout)
|
65
|
+
m[1].to_i if m
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
class Thor
|
4
|
+
module Shell
|
5
|
+
class Color < Basic
|
6
|
+
# The start of an ANSI dim sequence.
|
7
|
+
DIM = "\e[2m"
|
8
|
+
# The start of an ANSI underline sequence.
|
9
|
+
UNDERLINE = "\e[4m"
|
10
|
+
|
11
|
+
# Set the terminal's foreground ANSI color to dark gray.
|
12
|
+
DARK_GRAY = "\e[90m"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'psych'
|
2
|
+
|
3
|
+
module Canals
|
4
|
+
class CanalEnvironmentError < StandardError; end
|
5
|
+
|
6
|
+
class Environment
|
7
|
+
attr_reader :name, :user, :hostname, :pem
|
8
|
+
def initialize(args)
|
9
|
+
@args = validate?(args)
|
10
|
+
@name = @args["name"]
|
11
|
+
@user = @args["user"]
|
12
|
+
@hostname = @args["hostname"]
|
13
|
+
@pem = @args["pem"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate?(args)
|
17
|
+
vargs = args.dup
|
18
|
+
raise CanalEnvironmentError.new("Missing option: \"name\" in environment creation") if args["name"].nil?
|
19
|
+
vargs
|
20
|
+
end
|
21
|
+
|
22
|
+
def default=(val)
|
23
|
+
@args["default"] = !!val
|
24
|
+
end
|
25
|
+
|
26
|
+
def is_default?
|
27
|
+
!!@args["default"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_yaml
|
31
|
+
Psych.dump(@args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_hash
|
35
|
+
@args.dup
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'psych'
|
2
|
+
|
3
|
+
module Canals
|
4
|
+
class CanalOptionError < StandardError; end
|
5
|
+
|
6
|
+
class CanalOptions
|
7
|
+
BIND_ADDRESS = "127.0.0.1"
|
8
|
+
attr_reader :name, :remote_host, :remote_port, :local_port, :env_name, :env
|
9
|
+
|
10
|
+
def initialize(args)
|
11
|
+
@args = validate?(args)
|
12
|
+
@name = @args["name"]
|
13
|
+
@remote_host = @args["remote_host"]
|
14
|
+
@remote_port = @args["remote_port"]
|
15
|
+
@local_port = @args["local_port"]
|
16
|
+
@env_name = @args['env']
|
17
|
+
@env = Canals.repository.environment(@env_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def bind_address
|
21
|
+
return @args["bind_address"] if @args["bind_address"]
|
22
|
+
return BIND_ADDRESS
|
23
|
+
end
|
24
|
+
|
25
|
+
def hostname
|
26
|
+
get_env_var("hostname")
|
27
|
+
end
|
28
|
+
|
29
|
+
def user
|
30
|
+
get_env_var("user")
|
31
|
+
end
|
32
|
+
|
33
|
+
def pem
|
34
|
+
get_env_var("pem")
|
35
|
+
end
|
36
|
+
|
37
|
+
def proxy
|
38
|
+
prxy = ""
|
39
|
+
prxy += "-i #{pem} " if pem
|
40
|
+
prxy += "#{user}@#{hostname}"
|
41
|
+
prxy
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_yaml
|
45
|
+
Psych.dump(@args)
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_hash
|
49
|
+
@args.dup
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def validate?(args)
|
55
|
+
vargs = args.dup
|
56
|
+
raise CanalOptionError.new("Missing option: \"name\" in canal creation") if args["name"].nil?
|
57
|
+
raise CanalOptionError.new("Missing option: \"remote_host\" in canal creation") if args["remote_host"].nil?
|
58
|
+
raise CanalOptionError.new("Missing option: \"remote_port\" in canal creation") if args["remote_port"].nil?
|
59
|
+
vargs["remote_port"] = vargs["remote_port"].to_i
|
60
|
+
if vargs["local_port"].nil?
|
61
|
+
vargs["local_port"] = vargs["remote_port"]
|
62
|
+
else
|
63
|
+
vargs["local_port"] = vargs["local_port"].to_i
|
64
|
+
end
|
65
|
+
vargs
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_env_var(var)
|
69
|
+
return @args[var] if @args[var]
|
70
|
+
return @env.send var.to_sym if @env
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'psych'
|
2
|
+
require 'pathname'
|
3
|
+
require 'forwardable'
|
4
|
+
require 'canals/environment'
|
5
|
+
|
6
|
+
module Canals
|
7
|
+
class Repository
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
ENVIRONMENTS = :environments
|
12
|
+
TUNNELS = :tunnels
|
13
|
+
|
14
|
+
def initialize(root = nil)
|
15
|
+
@root = root
|
16
|
+
@repo = load_repository(repo_file)
|
17
|
+
end
|
18
|
+
|
19
|
+
def_delegator :@repo, :[]
|
20
|
+
|
21
|
+
def each(&block)
|
22
|
+
@repo[TUNNELS].map{ |n, r| Canals::CanalOptions.new(r) }.each(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def empty?
|
26
|
+
@repo[TUNNELS].empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def add(options, save=true)
|
30
|
+
@repo[TUNNELS][options.name] = options.to_hash
|
31
|
+
if options.env_name.nil? && !options.env.nil? && options.env.is_default?
|
32
|
+
@repo[TUNNELS][options.name]["env"] = options.env.name
|
33
|
+
end
|
34
|
+
save! if save
|
35
|
+
end
|
36
|
+
|
37
|
+
def get(name)
|
38
|
+
CanalOptions.new(@repo[:tunnels][name])
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_environment(environment, save=true)
|
42
|
+
if environment.is_default?
|
43
|
+
@repo[ENVIRONMENTS].each { |name, env| env.delete("default") }
|
44
|
+
end
|
45
|
+
if @repo[ENVIRONMENTS].empty?
|
46
|
+
environment.default = true
|
47
|
+
end
|
48
|
+
@repo[ENVIRONMENTS][environment.name] = environment.to_hash
|
49
|
+
save! if save
|
50
|
+
end
|
51
|
+
|
52
|
+
def save!
|
53
|
+
FileUtils.mkdir_p(repo_file.dirname)
|
54
|
+
File.open(repo_file, 'w') do |file|
|
55
|
+
file.write(Psych.dump(@repo))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def environment(name=nil)
|
60
|
+
if name.nil?
|
61
|
+
args = @repo[ENVIRONMENTS].select{ |n,e| e["default"] }.values[0]
|
62
|
+
else
|
63
|
+
args = @repo[ENVIRONMENTS][name]
|
64
|
+
end
|
65
|
+
Canals::Environment.new(args) if !args.nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
def environments
|
69
|
+
@repo[ENVIRONMENTS].map { |n, e| Canals::Environment.new(e) }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def repo_file
|
75
|
+
file = File.join(Dir.home, '.canals/repository')
|
76
|
+
Pathname.new(file)
|
77
|
+
end
|
78
|
+
|
79
|
+
def load_repository(repository_file)
|
80
|
+
valid_file = repository_file && repository_file.exist? && !repository_file.size.zero?
|
81
|
+
return { ENVIRONMENTS => {}, TUNNELS => {} } if !valid_file
|
82
|
+
return Psych.load_file(repository_file)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'psych'
|
2
|
+
require 'pathname'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Canals
|
6
|
+
class Session
|
7
|
+
include Enumerable
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@session = load_session(session_file)
|
12
|
+
end
|
13
|
+
|
14
|
+
def_delegator :@session, :[]
|
15
|
+
|
16
|
+
def each(&block)
|
17
|
+
@session.each(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def empty?
|
21
|
+
@session.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
def add(session, save=true)
|
25
|
+
@session.push(session)
|
26
|
+
save! if save
|
27
|
+
end
|
28
|
+
|
29
|
+
def del(name, save=true)
|
30
|
+
@session.delete_if{ |s| s[:name] == name }
|
31
|
+
save! if save
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(session_id)
|
35
|
+
case session_id
|
36
|
+
when Numeric
|
37
|
+
@session.find{ |s| s[:pid] == session_id }
|
38
|
+
when String
|
39
|
+
@session.find{ |s| s[:name] == session_id }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def save!
|
44
|
+
FileUtils.mkdir_p(session_file.dirname)
|
45
|
+
File.open(session_file, 'w') do |file|
|
46
|
+
file.write(Psych.dump(@session))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def session_file
|
53
|
+
file = File.join(Dir.home, '.canals/session')
|
54
|
+
Pathname.new(file)
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_session(_session_file)
|
58
|
+
valid_file = _session_file && _session_file.exist? && !_session_file.size.zero?
|
59
|
+
return [] if !valid_file
|
60
|
+
return Psych.load_file(_session_file)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Canals
|
2
|
+
module Tools
|
3
|
+
module Assets
|
4
|
+
BASE = File.expand_path('../../../../assets', __FILE__)
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def asset_path(file)
|
8
|
+
File.expand_path(file, BASE)
|
9
|
+
end
|
10
|
+
|
11
|
+
def asset(file)
|
12
|
+
File.read(asset_path(file))
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
alias [] asset_path
|
17
|
+
alias read asset
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'canals'
|
2
|
+
require 'canals/tools/assets'
|
3
|
+
|
4
|
+
module Canals
|
5
|
+
module Tools
|
6
|
+
module Completion
|
7
|
+
include FileUtils
|
8
|
+
extend self
|
9
|
+
|
10
|
+
def config_path
|
11
|
+
File.expand_path(".canals", ENV['HOME'])
|
12
|
+
end
|
13
|
+
|
14
|
+
def cmp_file
|
15
|
+
File.expand_path('canals.sh', config_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def install_completion
|
19
|
+
update_completion
|
20
|
+
source = "source " << cmp_file
|
21
|
+
|
22
|
+
rcfile = File.expand_path('.bashrc', ENV['HOME'])
|
23
|
+
return if File.read(rcfile).include? source
|
24
|
+
File.open(rcfile, 'a') { |f| f.puts("", "# added by canals gem", "[ -f #{cmp_file} ] && #{source}") }
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_completion
|
28
|
+
mkdir_p(config_path)
|
29
|
+
cp(Assets['canals.sh'], cmp_file)
|
30
|
+
update_config
|
31
|
+
end
|
32
|
+
|
33
|
+
def update_config
|
34
|
+
Canals.config[:completion_version] = Canals::VERSION
|
35
|
+
Canals.config.save!
|
36
|
+
end
|
37
|
+
|
38
|
+
def completion_installed?
|
39
|
+
source = "source " << cmp_file
|
40
|
+
rcfile = File.expand_path('.bashrc', ENV['HOME'])
|
41
|
+
return false unless File.read(rcfile).include? source
|
42
|
+
true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
data/lib/canals.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'canals/core'
|
3
|
+
|
4
|
+
# a gem for managing ssh tunnel connections
|
5
|
+
module Canals
|
6
|
+
extend self
|
7
|
+
|
8
|
+
autoload :Repository, "canals/repository"
|
9
|
+
autoload :Session, "canals/session"
|
10
|
+
autoload :Config, 'canals/config'
|
11
|
+
autoload :Version, 'canals/version'
|
12
|
+
|
13
|
+
|
14
|
+
attr_accessor :logger
|
15
|
+
|
16
|
+
def config
|
17
|
+
return @config if defined?(@config)
|
18
|
+
@config = Config.new(File.join(Dir.home, '.canals'))
|
19
|
+
end
|
20
|
+
|
21
|
+
def repository
|
22
|
+
return @repository if defined?(@repository)
|
23
|
+
@repository = Repository.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def environments
|
27
|
+
return @repository.environments if defined?(@repository)
|
28
|
+
@repository = Repository.new
|
29
|
+
@repository.environments
|
30
|
+
end
|
31
|
+
|
32
|
+
def session
|
33
|
+
return @session if defined?(@session)
|
34
|
+
@session = Session.new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# default logger
|
39
|
+
Canals.logger = Logger.new(STDERR)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'canals/environment'
|
3
|
+
require 'psych'
|
4
|
+
|
5
|
+
describe Canals::Environment do
|
6
|
+
let(:name) { "blah" }
|
7
|
+
let(:hostname) { "nat.example.com" }
|
8
|
+
let(:user) { "user" }
|
9
|
+
let(:pem) { "/tmp/file.pem" }
|
10
|
+
|
11
|
+
describe "name" do
|
12
|
+
it "contains 'name'" do
|
13
|
+
args = {"name" => name}
|
14
|
+
env = Canals::Environment.new(args)
|
15
|
+
expect(env.name).to eq name
|
16
|
+
end
|
17
|
+
|
18
|
+
it "raises error when 'name' is not availble" do
|
19
|
+
args = {"hostname" => hostname}
|
20
|
+
expect{Canals::Environment.new(args)}.to raise_error(Canals::CanalEnvironmentError)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "hostname" do
|
25
|
+
it "contains 'hostname'" do
|
26
|
+
args = {"name" => name, "hostname" => hostname}
|
27
|
+
env = Canals::Environment.new(args)
|
28
|
+
expect(env.hostname).to eq hostname
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "user" do
|
33
|
+
it "contains 'user'" do
|
34
|
+
args = {"name" => name, "user" => user}
|
35
|
+
env = Canals::Environment.new(args)
|
36
|
+
expect(env.user).to eq user
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "pem" do
|
41
|
+
it "contains 'pem'" do
|
42
|
+
args = {"name" => name, "pem" => pem}
|
43
|
+
env = Canals::Environment.new(args)
|
44
|
+
expect(env.pem).to eq pem
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "default" do
|
49
|
+
it "returns 'defualt' true if default" do
|
50
|
+
args = {"name" => name, "default" => true}
|
51
|
+
env = Canals::Environment.new(args)
|
52
|
+
expect(env.is_default?).to eq true
|
53
|
+
end
|
54
|
+
|
55
|
+
it "returns 'defualt' false if default" do
|
56
|
+
args = {"name" => name, "default" => false}
|
57
|
+
env = Canals::Environment.new(args)
|
58
|
+
expect(env.is_default?).to eq false
|
59
|
+
end
|
60
|
+
|
61
|
+
it "returns 'defualt' false if default not availble" do
|
62
|
+
args = {"name" => name}
|
63
|
+
env = Canals::Environment.new(args)
|
64
|
+
expect(env.is_default?).to eq false
|
65
|
+
end
|
66
|
+
|
67
|
+
it "default can be changed" do
|
68
|
+
args = {"name" => name, "default" => true}
|
69
|
+
env = Canals::Environment.new(args)
|
70
|
+
expect(env.is_default?).to eq true
|
71
|
+
expect(env.to_hash["default"]).to eq true
|
72
|
+
env.default = false
|
73
|
+
expect(env.is_default?).to eq false
|
74
|
+
expect(env.to_hash["default"]).to eq false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|