harrison 0.0.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.
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ .DS_Store
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ *.bundle
20
+ *.so
21
+ *.o
22
+ *.a
23
+ mkmf.log
24
+ /Harrisonfile
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in harrison.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jesse Scott
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Harrison
2
+
3
+ Simple artifact-based deployment for web applications.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'harrison'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install harrison
18
+
19
+ ## Usage
20
+
21
+ First, create a Harrisonfile in the root of your project. Here's an example:
22
+
23
+ ```ruby
24
+ # Project-wide Config
25
+ Harrison.config do |h|
26
+ h.project = 'harrison'
27
+ h.git_src = "git@github.com:scotje/harrison.git"
28
+ end
29
+
30
+ Harrison.package do |h|
31
+ # Where to build package.
32
+ h.host = 'build-server.example.com'
33
+ h.user = 'jesse'
34
+
35
+ # Things we don't want to package.
36
+ h.exclude = %w(.git ./config ./coverage ./examples ./log ./pkg ./tmp ./spec)
37
+
38
+ # Define the build process here.
39
+ h.run do |h|
40
+ # Bundle Install
41
+ h.remote_exec("cd #{h.commit} && bash -l -c \"bundle install --path=vendor --without=\\\"development packaging test doc\\\"\"")
42
+ end
43
+ end
44
+
45
+ Harrison.deploy do |h|
46
+ h.hosts = [ 'util-server-01.example.com', 'app-server-01.example.com', 'app-server-02.example.com' ]
47
+ h.user = 'jesse'
48
+ h.base_dir = '/opt'
49
+
50
+ # Run block will be invoked once for each host after new code is in place.
51
+ h.run do |h|
52
+ # You can interrogate h.host to see what host you are currently running on.
53
+ if h.host =~ /util/
54
+ # Do something on the util box.
55
+ else
56
+ puts "Reloading Unicorn on #{h.host}..."
57
+ h.remote_exec("sudo -- /etc/init.d/unicorn_#{h.project} reload")
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ Next, ensure that your SSH key is authorized to log in as the `user` you have specified in
64
+ the Harrisonfile for each task. (Or be ready to type the password a lot. :weary:)
65
+
66
+ ### Building a Release
67
+
68
+ Use the `harrison package` command:
69
+
70
+ ```
71
+ $ harrison package
72
+ ```
73
+
74
+ By default this will build and package `HEAD` of your current branch. You may specify another commit to
75
+ build using the `--commit` option:
76
+
77
+ ```
78
+ $ harrison package --commit mybranch
79
+ ```
80
+
81
+ The `--commit` option understands anything that `git rev-parse` understands. *NOTE: The commit you
82
+ reference must be pushed to the repository referenced as `git_src` in the Harrisonfile before
83
+ you can build it.*
84
+
85
+ The packaged release artifact will, by default, be saved into a 'pkg' subfolder:
86
+
87
+ ```
88
+ $ harrison package
89
+ Packaging 5a547d8 for "harrison" on build-server.example.com...
90
+ Sucessfully packaged 5a547d8 to pkg/20140711170226-5a547d8.tar.gz
91
+ ```
92
+
93
+ There are some additional options available, run `harrison package --help` to see everything available.
94
+
95
+
96
+ ### Deploying a Release
97
+
98
+ Use the `harrison deploy` command passing the artifact to be deployed as an argument:
99
+
100
+ ```
101
+ $ harrison deploy pkg/20140711170226-5a547d8.tar.gz
102
+ ```
103
+
104
+ By default, this will deploy to the list of hosts defined in your Harrisonfile.
105
+
106
+ You can override the target hosts by passing a `--hosts` option:
107
+
108
+ ```
109
+ $ harrison deploy pkg/20140711170226-5a547d8.tar.gz --hosts test-app-server-01.example.com test-app-server-02.example.com
110
+ ```
111
+
112
+ You can also pass an `--env` option to deploy into multi-stage environments:
113
+
114
+ ```
115
+ $ harrison deploy pkg/20140711170226-5a547d8.tar.gz --env prod
116
+ ```
117
+
118
+ This value can then be tested to alter the default target hosts in your Harrisonfile:
119
+
120
+ ```ruby
121
+ if h.env =~ /prod/
122
+ h.hosts = [ 'app-server-01.prod.example.com', 'app-server-02.prod.example.com' ]
123
+ else
124
+ h.hosts = [ 'app-server-01.stage.example.com', 'app-server-02.stage.example.com' ]
125
+ end
126
+ ```
127
+
128
+ There are some additional options available, run `harrison deploy --help` to see everything available.
129
+
130
+
131
+ ## Contributing
132
+
133
+ 1. Fork it ( https://github.com/scotje/harrison/fork )
134
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
135
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
136
+ 4. Push to the branch (`git push origin my-new-feature`)
137
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+
6
+ # If you want to make this the default task
7
+ task :default => :spec
data/TODO ADDED
@@ -0,0 +1,26 @@
1
+ Bugs:
2
+ -----
3
+ [ ] Test that artifact exists before doing anything for deploy task.
4
+
5
+ In Progress:
6
+ ------------
7
+ [ ] Unit Testing
8
+
9
+ Definitely:
10
+ -----------
11
+ [ ] Rollback action.
12
+ [ ] Error handling (with automatic rollback/undo)
13
+ [ ] Further de-compose actions so that specific parts can be easily overridden.
14
+ [ ] When deploying via a proxy, cache artifact on proxy before deploying to hosts.
15
+ [ ] Banner text in --help output.
16
+ [ ] Improve unit testing, try to minimize coupling to implementation details.
17
+ [ ] Include branch name in artifact file name.
18
+
19
+ Maybe:
20
+ ------
21
+ [ ] Allow more elaborate hosts config, e.g.: h.hosts = [ { host: '10.16.18.207', tags: %w(util migrate) } ]
22
+ [ ] Some kind of --dry-run option.
23
+ [ ] Allow deploy_via to include alternate user/connection options.
24
+ [ ] Rename "releases" => "builds" (and "deploys" => "releases"?)
25
+ [ ] Upload artifact to all hosts before the rest of the deployment process begins.
26
+ [ ] --force option for deploy task to overwrite an existing release
data/bin/harrison ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Exit cleanly from an early interrupt
4
+ Signal.trap("INT") { exit 1 }
5
+
6
+ require 'harrison'
7
+ Harrison.invoke(ARGV)
data/harrison.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'harrison/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "harrison"
8
+ spec.version = Harrison::VERSION
9
+ spec.authors = ["Jesse Scott"]
10
+ spec.email = ["jesse@puppetlabs.com"]
11
+ spec.summary = %q{Simple artifact-based deployment for web applications.}
12
+ spec.homepage = "https://github.com/scotje/harrison"
13
+ spec.license = "Apache 2.0"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = ["harrison"]
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.required_ruby_version = '>= 1.9.3'
21
+
22
+ spec.add_runtime_dependency "trollop"
23
+ spec.add_runtime_dependency "net-ssh"
24
+ spec.add_runtime_dependency "net-scp"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.6"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "rspec"
29
+ spec.add_development_dependency "debugger" if RUBY_VERSION < "2.0.0"
30
+ spec.add_development_dependency "byebug" if RUBY_VERSION >= "2.0.0"
31
+ spec.add_development_dependency "sourcify"
32
+ end
@@ -0,0 +1,101 @@
1
+ module Harrison
2
+ class Base
3
+ attr_reader :ssh
4
+ attr_accessor :options
5
+
6
+ def initialize(arg_opts=[], opts={})
7
+ # Config helpers for Harrisonfile.
8
+ self.class.option_helper(:user)
9
+
10
+ @arg_opts = arg_opts
11
+ @arg_opts << [ :debug, "Output debug messages.", :type => :boolean, :default => false ]
12
+
13
+ @options = opts
14
+ end
15
+
16
+ def self.option_helper(option)
17
+ send :define_method, option do
18
+ @options[option]
19
+ end
20
+
21
+ send :define_method, "#{option}=" do |val|
22
+ @options[option] = val
23
+ end
24
+ end
25
+
26
+ # Find config from Harrison.config if it's not on this class.
27
+ def method_missing(meth, *args, &block)
28
+ if Harrison.config.respond_to?(meth)
29
+ Harrison.config.send(meth, *args, &block)
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def exec(cmd)
36
+ result = `#{cmd}`
37
+ abort("ERROR: Unable to execute local command: \"#{cmd}\"") if !$?.success? || result.nil?
38
+ result.strip
39
+ end
40
+
41
+ def remote_exec(cmd)
42
+ result = ssh.exec(cmd)
43
+ abort("ERROR: Unable to execute remote command: \"#{cmd}\"") if result.nil?
44
+ result.strip
45
+ end
46
+
47
+ def parse(args)
48
+ opt_parser = Trollop::Parser.new
49
+
50
+ @arg_opts.each do |arg_opt|
51
+ opt_parser.opt(*arg_opt)
52
+ end
53
+
54
+ @options.merge!(Trollop::with_standard_exception_handling(opt_parser) do
55
+ opt_parser.parse(args)
56
+ end)
57
+
58
+ Harrison.send(:remove_const, "DEBUG") if Harrison.const_defined?("DEBUG")
59
+ Harrison.const_set("DEBUG", @options[:debug])
60
+ end
61
+
62
+ def run(&block)
63
+ if block_given?
64
+ # If called with a block, convert it to a proc and store.
65
+ @run_block = block
66
+ else
67
+ # Otherwise, invoke the previously stored block with self.
68
+ @run_block && @run_block.call(self)
69
+ end
70
+ end
71
+
72
+ def download(remote_path, local_path)
73
+ ssh.download(remote_path, local_path)
74
+ end
75
+
76
+ def upload(local_path, remote_path)
77
+ ssh.upload(local_path, remote_path)
78
+ end
79
+
80
+ def close
81
+ ssh.close if @ssh
82
+ end
83
+
84
+ protected
85
+
86
+ def ssh
87
+ @ssh ||= Harrison::SSH.new(host: @options[:host], user: @options[:user])
88
+ end
89
+
90
+ def ensure_local_dir(dir)
91
+ @_ensured_local ||= {}
92
+ @_ensured_local[dir] || (system("if [ ! -d #{dir} ] ; then mkdir -p #{dir} ; fi") && @_ensured_local[dir] = true) || abort("Error: Unable to create local directory \"#{dir}\".")
93
+ end
94
+
95
+ def ensure_remote_dir(host, dir)
96
+ @_ensured_remote ||= {}
97
+ @_ensured_remote[host] ||= {}
98
+ @_ensured_remote[host][dir] || (ssh.exec("if [ ! -d #{dir} ] ; then mkdir -p #{dir} ; fi") && @_ensured_remote[host][dir] = true) || abort("Error: Unable to create remote directory \"#{dir}\" on \"#{host}\".")
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,9 @@
1
+ module Harrison
2
+ class Config
3
+ attr_accessor :project
4
+ attr_accessor :git_src
5
+
6
+ def initialize(opts={})
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,110 @@
1
+ module Harrison
2
+ class Deploy < Base
3
+ attr_accessor :artifact
4
+ attr_accessor :host # The specific host among --hosts that we are currently working on.
5
+ attr_accessor :release_dir
6
+ attr_accessor :deploy_link
7
+
8
+ def initialize(opts={})
9
+ # Config helpers for Harrisonfile.
10
+ self.class.option_helper(:hosts)
11
+ self.class.option_helper(:env)
12
+ self.class.option_helper(:base_dir)
13
+ self.class.option_helper(:deploy_via)
14
+
15
+ # Command line opts for this action. Will be merged with common opts.
16
+ arg_opts = [
17
+ [ :hosts, "List of remote hosts to deploy to. Can also be specified in Harrisonfile.", :type => :strings ],
18
+ [ :env, "Environment to deploy to. This can be examined in your Harrisonfile to calculate target hosts.", :type => :string ],
19
+ ]
20
+
21
+ super(arg_opts, opts)
22
+ end
23
+
24
+ def parse(args)
25
+ super
26
+
27
+ # Preserve argv hosts if it's been passed.
28
+ @_argv_hosts = self.hosts.dup if self.hosts
29
+
30
+ # Make sure they passed an artifact.
31
+ self.artifact = args[1] || abort("ERROR: You must specify the artifact to be deployed as an argument to this command.")
32
+ end
33
+
34
+ def remote_exec(cmd)
35
+ super("cd #{remote_project_dir} && #{cmd}")
36
+ end
37
+
38
+ def run(&block)
39
+ return super if block_given?
40
+
41
+ # Override Harrisonfile hosts if it was passed on argv.
42
+ self.hosts = @_argv_hosts if @_argv_hosts
43
+
44
+ if !self.hosts || self.hosts.empty?
45
+ abort("ERROR: You must specify one or more hosts to deploy to, either in your Harrisonfile or via --hosts.")
46
+ end
47
+
48
+ # Default base_dir.
49
+ self.base_dir ||= '/opt'
50
+
51
+ puts "Deploying #{artifact} for \"#{project}\" onto #{hosts.size} hosts..."
52
+
53
+ self.release_dir = "#{remote_project_dir}/releases/" + File.basename(artifact, '.tar.gz')
54
+ self.deploy_link = "#{remote_project_dir}/deploys/" + Time.new.utc.strftime('%Y-%m-%d_%H%M%S')
55
+
56
+ hosts.each do |h|
57
+ self.host = h
58
+
59
+ ensure_remote_dir(self.host, "#{remote_project_dir}/deploys")
60
+ ensure_remote_dir(self.host, "#{remote_project_dir}/releases")
61
+
62
+ # Make folder for release or bail if it already exists.
63
+ remote_exec("mkdir #{release_dir}")
64
+
65
+ # Upload artifact to host.
66
+ upload(artifact, "#{remote_project_dir}/releases/")
67
+
68
+ # Unpack.
69
+ remote_exec("cd #{release_dir} && tar -xzf ../#{File.basename(artifact)}")
70
+
71
+ # Clean up artifact.
72
+ remote_exec("rm -f #{remote_project_dir}/releases/#{File.basename(artifact)}")
73
+
74
+ # Symlink a new deploy to this release.
75
+ remote_exec("ln -s #{release_dir} #{deploy_link}")
76
+
77
+ # Symlink current to new deploy.
78
+ remote_exec("ln -sfn #{deploy_link} #{remote_project_dir}/current")
79
+
80
+ # Run user supplied deploy code to restart server or whatever.
81
+ super
82
+
83
+ close(self.host)
84
+ end
85
+
86
+ puts "Sucessfully deployed #{artifact} to #{hosts.join(', ')}."
87
+ end
88
+
89
+ def close(host=nil)
90
+ if host
91
+ @_conns[host].close if @_conns && @_conns[host]
92
+ elsif @_conns
93
+ @_conns.keys.each do |host|
94
+ @_conns[host].close unless @_conns[host].closed?
95
+ end
96
+ end
97
+ end
98
+
99
+ protected
100
+
101
+ def ssh
102
+ @_conns ||= {}
103
+ @_conns[self.host] ||= Harrison::SSH.new(host: self.host, user: @options[:user], proxy: self.deploy_via)
104
+ end
105
+
106
+ def remote_project_dir
107
+ "#{base_dir}/#{project}"
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,87 @@
1
+ module Harrison
2
+ class Package < Base
3
+ def initialize(opts={})
4
+ # Config helpers for Harrisonfile.
5
+ self.class.option_helper(:host)
6
+ self.class.option_helper(:commit)
7
+ self.class.option_helper(:purge)
8
+ self.class.option_helper(:pkg_dir)
9
+ self.class.option_helper(:remote_dir)
10
+ self.class.option_helper(:exclude)
11
+
12
+ # Command line opts for this action. Will be merged with common opts.
13
+ arg_opts = [
14
+ [ :commit, "Specific commit to be packaged. Accepts anything that `git rev-parse` understands.", :type => :string, :default => "HEAD" ],
15
+ [ :purge, "Remove all previously packaged commits and working copies from the build host when finished.", :type => :boolean, :default => false ],
16
+ [ :pkg_dir, "Local folder to save package to.", :type => :string, :default => "pkg" ],
17
+ [ :remote_dir, "Remote working folder.", :type => :string, :default => "~/.harrison" ],
18
+ ]
19
+
20
+ super(arg_opts, opts)
21
+ end
22
+
23
+ def remote_exec(cmd)
24
+ ensure_remote_dir(self.host, "#{remote_project_dir}/package")
25
+
26
+ super("cd #{remote_project_dir}/package && #{cmd}")
27
+ end
28
+
29
+ def run(&block)
30
+ return super if block_given?
31
+
32
+ # Resolve commit ref to an actual short SHA.
33
+ resolve_commit!
34
+
35
+ puts "Packaging #{commit} for \"#{project}\" on #{host}..."
36
+
37
+ # Make sure the folder to save the artifact to locally exists.
38
+ ensure_local_dir(pkg_dir)
39
+
40
+ # Fetch/clone git repo on remote host.
41
+ remote_exec("if [ -d cached ] ; then cd cached && git fetch origin -p ; else git clone #{git_src} cached ; fi")
42
+
43
+ # Check out target commit.
44
+ remote_exec("cd cached && git reset --hard #{commit} && git clean -f -d")
45
+
46
+ # Make a build folder of the target commit.
47
+ remote_exec("rm -rf #{commit} && cp -a cached #{commit}")
48
+
49
+ # Run user supplied build code.
50
+ # TODO: alter remote_exec to set directory context to commit dir?
51
+ super
52
+
53
+ # Package build folder into tgz.
54
+ remote_exec("rm -f #{artifact_name(commit)}.tar.gz && cd #{commit} && tar #{excludes_for_tar} -czf ../#{artifact_name(commit)}.tar.gz .")
55
+
56
+ # Download (Expand remote path since Net::SCP doesn't expand ~)
57
+ download(remote_exec("readlink -m #{artifact_name(commit)}.tar.gz"), "#{pkg_dir}/#{artifact_name(commit)}.tar.gz")
58
+
59
+ if purge
60
+ remote_exec("cd .. && rm -rf package")
61
+ end
62
+
63
+ puts "Sucessfully packaged #{commit} to #{pkg_dir}/#{artifact_name(commit)}.tar.gz"
64
+ end
65
+
66
+ protected
67
+
68
+ def remote_project_dir
69
+ "#{remote_dir}/#{project}"
70
+ end
71
+
72
+ def resolve_commit!
73
+ self.commit = exec("git rev-parse --short #{self.commit} 2>/dev/null")
74
+ end
75
+
76
+ def excludes_for_tar
77
+ return '' if !exclude || exclude.empty?
78
+
79
+ "--exclude \"#{exclude.join('" --exclude "')}\""
80
+ end
81
+
82
+ def artifact_name(commit)
83
+ @_timestamp ||= Time.new.utc.strftime('%Y%m%d%H%M%S')
84
+ "#{@_timestamp}-#{commit}"
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,80 @@
1
+ require 'net/ssh'
2
+ require 'net/ssh/proxy/command'
3
+ require 'net/scp'
4
+
5
+ module Harrison
6
+ class SSH
7
+ def initialize(opts={})
8
+ if opts[:proxy]
9
+ @proxy = Net::SSH::Proxy::Command.new("ssh #{opts[:proxy]} \"nc %h %p\" 2>/dev/null")
10
+ @conn = Net::SSH.start(opts[:host], opts[:user], forward_agent: true, proxy: @proxy)
11
+ else
12
+ @conn = Net::SSH.start(opts[:host], opts[:user], forward_agent: true)
13
+ end
14
+ end
15
+
16
+ # Helper to catch non-zero exit status and report errors.
17
+ def exec(command)
18
+ puts "INFO (ssh-exec #{desc}): #{command}" if Harrison::DEBUG
19
+
20
+ stdout_data = ""
21
+ stderr_data = ""
22
+ exit_code = nil
23
+
24
+ @conn.open_channel do |channel|
25
+ channel.exec(command) do |ch, success|
26
+ warn "Couldn't execute command (ssh.channel.exec) on remote host: #{command}" unless success
27
+
28
+ channel.on_data do |ch,data|
29
+ stdout_data += data
30
+ end
31
+
32
+ channel.on_extended_data do |ch,type,data|
33
+ stderr_data += data
34
+ end
35
+
36
+ channel.on_request("exit-status") do |ch,data|
37
+ exit_code = data.read_long
38
+ end
39
+ end
40
+ end
41
+
42
+ @conn.loop
43
+
44
+ if Harrison::DEBUG || exit_code != 0
45
+ warn "STDERR (ssh-exec #{desc}): #{stderr_data.strip}" unless stderr_data.empty?
46
+ warn "STDOUT (ssh-exec #{desc}): #{stdout_data.strip}" unless stdout_data.empty?
47
+ end
48
+
49
+ (exit_code == 0) ? stdout_data : nil
50
+ end
51
+
52
+ def download(remote_path, local_path)
53
+ puts "INFO (scp-down #{desc}): #{local_path} <<< #{remote_path}" if Harrison::DEBUG
54
+ @conn.scp.download!(remote_path, local_path)
55
+ end
56
+
57
+ def upload(local_path, remote_path)
58
+ puts "INFO (scp-up #{desc}): #{local_path} >>> #{remote_path}" if Harrison::DEBUG
59
+ @conn.scp.upload!(local_path, remote_path)
60
+ end
61
+
62
+ def close
63
+ # net-ssh doesn't seem to know how to close proxy::command connections
64
+ Process.kill("TERM", @conn.transport.socket.pid) if @proxy
65
+ @conn.close
66
+ end
67
+
68
+ def closed?
69
+ @conn.closed?
70
+ end
71
+
72
+ def desc
73
+ if @proxy
74
+ "#{@conn.host} (via #{@proxy.command_line.split(' ')[1]})"
75
+ else
76
+ @conn.host
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module Harrison
2
+ VERSION = "0.0.1"
3
+ end