harrison 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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