canals 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,6 @@
1
+
2
+ class String
3
+ def titleize
4
+ split(/(\W)/).map(&:capitalize).join
5
+ end
6
+ 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
+
@@ -0,0 +1,6 @@
1
+ module Canals
2
+
3
+ # Canals gem current version
4
+ VERSION = "0.8.0"
5
+
6
+ end
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