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.
@@ -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