├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib └── sshkit │ ├── sudo.rb │ └── sudo │ ├── backends │ ├── abstract.rb │ └── netssh.rb │ ├── interaction_handler.rb │ └── version.rb └── sshkit-sudo.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in sshkit-sudo.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Kentaro Imai 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. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSHKit::Sudo 2 | 3 | SSHKit extension, for sudo operation with password input. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem 'sshkit-sudo' 11 | ``` 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | If you're using Capistrano, add the following to your Capfile: 18 | 19 | ```ruby 20 | require 'sshkit/sudo' 21 | ``` 22 | 23 | ## Usage 24 | 25 | This gem adds `sudo` and `execute!` command to SSHKit backends. 26 | 27 | To execute a command with sudo, call `sudo` instead of `execute`. 28 | 29 | ```ruby 30 | sudo :cp, '~/something', '/something' 31 | 32 | # Or as follows: 33 | execute! :sudo, :cp, '~/something', '/something' 34 | ``` 35 | 36 | ### Examples in Capistrano tasks 37 | 38 | ```ruby 39 | # Executing a command with sudo in Capistrano task 40 | namespace :nginx do 41 | desc 'Reload nginx' 42 | task :reload do 43 | on roles(:web), in: :sequence do 44 | sudo :service, :nginx, :reload 45 | end 46 | end 47 | 48 | desc 'Restart nginx' 49 | task :restart do 50 | on roles(:web), in: :sequence do 51 | execute! :sudo, :service, :nginx, :restart 52 | end 53 | end 54 | end 55 | 56 | namespace :prov do 57 | desc 'Install nginx' 58 | task :nginx do 59 | on roles(:web), in: :sequence do 60 | 61 | within '/etc/apt' do 62 | unless test :grep, '-Fxq', '"deb http://nginx.org/packages/debian/ wheezy nginx"', 'sources.list' 63 | execute! :echo, '"deb http://nginx.org/packages/debian/ wheezy nginx"', '|', 'sudo tee -a sources.list' 64 | execute! :echo, '"deb-src http://nginx.org/packages/debian/ wheezy nginx"', '|', 'sudo tee -a sources.list' 65 | 66 | execute! :wget, '-q0 - http://nginx.org/keys/nginx_signing.key', '|', 'sudo apt-key add -' 67 | 68 | sudo :'apt-get', :update 69 | end 70 | end 71 | 72 | sudo :'apt-get', '-y install nginx' 73 | end 74 | end 75 | end 76 | ``` 77 | 78 | ### Configuration 79 | Available in sshkit-sudo 0.1.0 and later. 80 | 81 | #### Same password across servers 82 | If you are using a same password across all servers, you can skip inputting the password for the second server or after 83 | by using `use_same_password!` method in your `deploy.rb` as follows: 84 | ```ruby 85 | class SSHKit::Sudo::InteractionHandler 86 | use_same_password! 87 | end 88 | ``` 89 | 90 | #### Password prompt and wrong password matchers 91 | You can set your own matchers in your `deploy.rb` as follows: 92 | ```ruby 93 | class SSHKit::Sudo::InteractionHandler 94 | password_prompt_regexp /[Pp]assword.*:/ 95 | wrong_password_regexp /Sorry.*\stry\sagain/ 96 | end 97 | ``` 98 | 99 | #### Making your own handler 100 | You can write your own handler in your `deploy.rb` as follows: 101 | ```ruby 102 | class SSHKit::Sudo::InteractionHandler 103 | def on_data(command, stream_name, data, channel) 104 | if data =~ wrong_password 105 | SSHKit::Sudo.password_cache[password_cache_key(command.host)] = nil 106 | end 107 | if data =~ password_prompt 108 | key = password_cache_key(command.host) 109 | pass = SSHKit::Sudo.password_cache[key] 110 | unless pass 111 | print data 112 | pass = $stdin.noecho(&:gets) 113 | puts '' 114 | SSHKit::Sudo.password_cache[key] = pass 115 | end 116 | channel.send_data(pass) 117 | end 118 | end 119 | end 120 | ``` 121 | 122 | ## Contributing 123 | 124 | 1. Fork it ( https://github.com/[my-github-username]/sshkit-sudo/fork ) 125 | 2. Create your feature branch (`git checkout -b my-new-feature`) 126 | 3. Commit your changes (`git commit -am 'Add some feature'`) 127 | 4. Push to the branch (`git push origin my-new-feature`) 128 | 5. Create a new Pull Request 129 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /lib/sshkit/sudo.rb: -------------------------------------------------------------------------------- 1 | require "sshkit/sudo/version" 2 | require 'sshkit' 3 | require 'sshkit/sudo/interaction_handler' 4 | require 'sshkit/sudo/backends/abstract' 5 | require 'sshkit/sudo/backends/netssh' 6 | 7 | module SSHKit 8 | module Sudo 9 | def self.password_cache 10 | @password_cache ||= {} 11 | end 12 | end 13 | 14 | Backend::Abstract.send(:include, ::SSHKit::Sudo::Backend::Abstract) 15 | Backend::Netssh.send(:include, ::SSHKit::Sudo::Backend::Netssh) 16 | end 17 | -------------------------------------------------------------------------------- /lib/sshkit/sudo/backends/abstract.rb: -------------------------------------------------------------------------------- 1 | module SSHKit 2 | module Sudo 3 | module Backend 4 | module Abstract 5 | def sudo(*args) 6 | execute!(:sudo, *args) 7 | end 8 | 9 | def execute!(*args) 10 | options = args.extract_options! 11 | options[:interaction_handler] ||= SSHKit::Sudo::InteractionHandler.new 12 | create_command_and_execute!(args, options).success? 13 | end 14 | 15 | private 16 | def execute_command!(*args) 17 | execute_command(*args) 18 | end 19 | 20 | def create_command_and_execute!(args, options) 21 | command(args, options).tap { |cmd| execute_command!(cmd) } 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/sshkit/sudo/backends/netssh.rb: -------------------------------------------------------------------------------- 1 | module SSHKit 2 | module Sudo 3 | module Backend 4 | module Netssh 5 | private 6 | def execute_command!(cmd) 7 | output.log_command_start(cmd) 8 | cmd.started = true 9 | exit_status = nil 10 | with_ssh do |ssh| 11 | ssh.open_channel do |chan| 12 | chan.request_pty 13 | chan.exec cmd.to_command do |_ch, _success| 14 | chan.on_data do |ch, data| 15 | cmd.on_stdout(ch, data) 16 | output.log_command_data(cmd, :stdout, data) 17 | end 18 | chan.on_extended_data do |ch, _type, data| 19 | cmd.on_stderr(ch, data) 20 | output.log_command_data(cmd, :stderr, data) 21 | end 22 | chan.on_request("exit-status") do |_ch, data| 23 | exit_status = data.read_long 24 | end 25 | #chan.on_request("exit-signal") do |ch, data| 26 | # # TODO: This gets called if the program is killed by a signal 27 | # # might also be a worthwhile thing to report 28 | # exit_signal = data.read_string.to_i 29 | # warn ">>> " + exit_signal.inspect 30 | # output.log_command_killed(cmd, exit_signal) 31 | #end 32 | chan.on_open_failed do |_ch| 33 | # TODO: What do do here? 34 | # I think we should raise something 35 | end 36 | chan.on_process do |_ch| 37 | # TODO: I don't know if this is useful 38 | end 39 | chan.on_eof do |_ch| 40 | # TODO: chan sends EOF before the exit status has been 41 | # writtend 42 | end 43 | end 44 | chan.wait 45 | end 46 | ssh.loop 47 | end 48 | # Set exit_status and log the result upon completion 49 | if exit_status 50 | cmd.exit_status = exit_status 51 | output.log_command_exit(cmd) 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/sshkit/sudo/interaction_handler.rb: -------------------------------------------------------------------------------- 1 | module SSHKit 2 | module Sudo 3 | class DefaultInteractionHandler 4 | def wrong_password; self.class.wrong_password; end 5 | def password_prompt; self.class.password_prompt; end 6 | 7 | def on_data(command, stream_name, data, channel) 8 | if data =~ wrong_password 9 | puts data if defined?(Airbrussh) and 10 | Airbrussh.configuration.command_output != :stdout and 11 | data !~ password_prompt 12 | SSHKit::Sudo.password_cache[password_cache_key(command.host)] = nil 13 | end 14 | if data =~ password_prompt 15 | key = password_cache_key(command.host) 16 | pass = SSHKit::Sudo.password_cache[key] 17 | unless pass 18 | print data 19 | pass = $stdin.noecho(&:gets) 20 | puts '' 21 | SSHKit::Sudo.password_cache[key] = pass 22 | end 23 | channel.send_data(pass) 24 | end 25 | end 26 | 27 | def password_cache_key(host) 28 | "#{host.user}@#{host.hostname}" 29 | end 30 | 31 | class << self 32 | def wrong_password 33 | @wrong_password ||= /Sorry.*\stry\sagain/ 34 | end 35 | 36 | def password_prompt 37 | @password_prompt ||= /[Pp]assword.*:/ 38 | end 39 | 40 | def wrong_password_regexp(regexp) 41 | @wrong_password = regexp 42 | end 43 | 44 | def password_prompt_regexp(regexp) 45 | @password_prompt = regexp 46 | end 47 | 48 | def use_same_password! 49 | class_eval <<-METHOD, __FILE__, __LINE__ + 1 50 | def password_cache_key(host) 51 | '0' 52 | end 53 | METHOD 54 | end 55 | end 56 | end 57 | 58 | class InteractionHandler < DefaultInteractionHandler 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/sshkit/sudo/version.rb: -------------------------------------------------------------------------------- 1 | module SSHKit 2 | module Sudo 3 | VERSION = "0.1.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /sshkit-sudo.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'sshkit/sudo/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "sshkit-sudo" 8 | spec.version = SSHKit::Sudo::VERSION 9 | spec.authors = ["Kentaro Imai"] 10 | spec.email = ["kentaroi@gmail.com"] 11 | spec.summary = %q{SSHKit extension, for sudo operation with password input.} 12 | spec.description = %q{SSHKit extension, for sudo operation with password input.} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.required_ruby_version = ">= 1.9.3" 22 | 23 | spec.add_dependency "sshkit", "~> 1.8" 24 | spec.add_development_dependency "bundler", "~> 1.7" 25 | spec.add_development_dependency "rake", "~> 10.0" 26 | end 27 | --------------------------------------------------------------------------------