uoregon-multissh 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b6c803a8837c4f704bfd43e4ee2b92e03afb75d419c9353d60bc8b596171ac03
4
+ data.tar.gz: 56dab86e92624866237d5e4f1f986e83961ac8660509ea4ae91d0bab3a9431b9
5
+ SHA512:
6
+ metadata.gz: 386e2b55b1bf09ed100083b108fd38d276f29dcf9b51858848eddafa002bf7a56463eca9ce2c6c4c02997024d9e3601cbb5344429b2a2c291113c4845ff3bb72
7
+ data.tar.gz: ce46bd0c1e12847e5440f1027f058b11c99be94c93e07760488091df6f4ec59c2318dd5dd277573643db0df44762f4e331cf7d1454c94e53e7aceefed524f612
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .vscode
2
+ tests.txt
3
+ command.txt
4
+ nodes.txt
5
+ multissh-*.gem
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.3
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gem 'net-ssh'
8
+ gem 'parallel'
9
+ gem 'colorize'
data/Gemfile.lock ADDED
@@ -0,0 +1,17 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ colorize (0.8.1)
5
+ net-ssh (5.1.0)
6
+ parallel (1.12.1)
7
+
8
+ PLATFORMS
9
+ ruby
10
+
11
+ DEPENDENCIES
12
+ colorize
13
+ net-ssh
14
+ parallel
15
+
16
+ BUNDLED WITH
17
+ 2.0.1
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # MultiSSH
2
+
3
+ Do all the things everywhere at the same time
4
+
5
+
6
+ ### Setup
7
+
8
+ Clone the repo and install the required gems with bundler
9
+ ```
10
+ git clone https://github.com/lcrownover/multissh
11
+ cd multissh
12
+ bundle install
13
+ ```
14
+
15
+ On the first run, you'll be prompted to generate a credential file.
16
+ This file is stored at *~/.ssh/multissh.yaml*, with the owner as the current user and mode of 600.
17
+
18
+ If you decline to generate this file, it will prompt for password if not provided via command line.
19
+
20
+ If you don't have ssh-agent configured with your keys, it will prompt for a private key password during credential file generation, or during run if you opted out of the credential file.
21
+
22
+
23
+ <br>
24
+
25
+ ### Usage
26
+
27
+ ```
28
+ Usage: multissh.rb --nodes "server1,server2" --command "echo 'hello'"
29
+ --nodes "NODES" REQUIRED: "server1,server2,server3" OR "@nodes.txt"
30
+ --command "COMMAND" REQUIRED: "echo 'hello'" OR @command.txt
31
+ --username "USERNAME" OPTIONAL: current user by default
32
+ --password "PASSWORD" OPTIONAL: will prompt if needed
33
+ --pkey_password "PASSWORD" OPTIONAL: will prompt if needed
34
+ --block OPTIONAL: block mode for command ouptut
35
+ --regenerate_config OPTIONAL: regenerate configuration file
36
+ --debug OPTIONAL: debug mode
37
+ ```
38
+
39
+ <br><br>
40
+
41
+ ### Examples
42
+
43
+ Run a command against a comma-separated list of nodes
44
+ ```bash
45
+ ruby multissh.rb --nodes "NODE1,NODE2" --command "COMMAND"
46
+ ```
47
+
48
+ <br>
49
+
50
+ Run a command against a file containing a newline-separated list of nodes
51
+ ```
52
+ node1.example.org
53
+ node2.example.org
54
+ ```
55
+
56
+ ```bash
57
+ ruby multissh.rb --nodes @nodes.txt --command "COMMAND"
58
+ ```
59
+
60
+ <br>
61
+
62
+ Run a list of newline-separated commands against a newline-separated list of nodes
63
+ ```
64
+ echo $(hostname)
65
+ yum install ruby
66
+ ruby -v
67
+ ```
68
+
69
+ ```bash
70
+ ruby multissh.rb --nodes @nodes.txt --command @commands.txt
71
+ ```
72
+
73
+ <br>
data/bin/multissh ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'multissh'
4
+
5
+ mssh = Multissh.new
6
+ mssh.run
7
+
data/lib/cli.rb ADDED
@@ -0,0 +1,134 @@
1
+ require_relative 'credential'
2
+
3
+ class Cli
4
+ attr_accessor :username, :password, :key_password, :nodes, :command, :block, :debug, :credential
5
+
6
+ def initialize
7
+
8
+ @options = {}
9
+ opt_parse = OptionParser.new do |opt|
10
+ opt.banner = 'Usage: multissh.rb --nodes "server1,server2" --command "echo \'hello\'"'
11
+ opt.on('--nodes "NODES"', 'REQUIRED: "server1,server2,server3" OR "@nodes.txt"') { |o| @options[:nodes] = o }
12
+ opt.on('--command "COMMAND"', 'REQUIRED: "echo \'hello\'" OR @command.txt') { |o| @options[:command] = o }
13
+ opt.on('--username "USERNAME"', 'OPTIONAL: current user by default') { |o| @options[:username] = o }
14
+ opt.on('--password "PASSWORD"', 'OPTIONAL: will prompt if needed') { |o| @options[:password] = o }
15
+ opt.on('--pkey_password "PASSWORD"', 'OPTIONAL: will prompt if needed') { |o| @options[:pkey_password] = o }
16
+ opt.on('--disable_sudo', 'OPTIONAL: disable_sudo requirement and run as current user') { @options[:disable_sudo] = true }
17
+ opt.on('--block', 'OPTIONAL: block mode for command ouptut') { @options[:block] = true }
18
+ opt.on('--regenerate_config', 'OPTIONAL: regenerate configuration file') { @options[:regenerate_config] = true }
19
+ opt.on('--debug', 'OPTIONAL: debug mode') { @options[:debug] = true }
20
+ end
21
+ opt_parse.parse!
22
+
23
+ begin
24
+ valid_options_set = false
25
+ valid_options_set = true if @options[:nodes] && @options[:command]
26
+ valid_options_set = true if @options[:regenerate_config]
27
+
28
+ raise OptionParser::MissingArgument unless valid_options_set
29
+ rescue
30
+ puts "\n"
31
+ abort(opt_parse.help)
32
+ end
33
+
34
+ @username = @options[:username]
35
+ @password = @options[:password]
36
+ @pkey_password = @options[:pkey_password]
37
+
38
+ @debug = true if @options[:debug]
39
+ @regenerate = true if @options[:regenerate_config]
40
+ @disable_sudo = true if @options[:disable_sudo]
41
+
42
+ credential = Credential.new(
43
+ username = @username,
44
+ password = @password,
45
+ pkey_password = @pkey_password,
46
+ regenerate = @regenerate,
47
+ debug = @debug
48
+ )
49
+ @username = credential.username
50
+ @password = credential.password
51
+ @pkey_password = if credential.pkey_password == "" then nil else credential.pkey_password end
52
+
53
+ abort() if @options[:regenerate_config]
54
+
55
+ @nodes = parse_nodes(@options[:nodes])
56
+ @command = parse_command(@options[:command])
57
+ @block = true if @options[:block]
58
+
59
+ rescue Interrupt
60
+ puts "\nCtrl+C Interrupt\n"
61
+ exit 1
62
+
63
+ end#initialize
64
+
65
+
66
+
67
+ def parse_nodes(nodes)
68
+ ##
69
+ # If '@' is used, return a list of nodes from a file
70
+ # Otherwise return a list of nodes parsed from comma-separated input from cli
71
+ #
72
+ if nodes.start_with?('@')
73
+ node_list = []
74
+ file_path = nodes[1..-1]
75
+ expanded_file_path = File.expand_path(file_path)
76
+ if File.exists? expanded_file_path
77
+ File.open(expanded_file_path, 'r') do |f|
78
+ f.each_line do |line|
79
+ line.chomp!.strip!
80
+ unless line.start_with?('#') || line.empty?
81
+ node_list.append(line)
82
+ end#unless
83
+ end#f.each_line
84
+ end#File.open
85
+ end#File.exists?
86
+ node_list
87
+ else
88
+ nodes.split(',').map(&:chomp)
89
+ end#if
90
+ end#parse_nodes
91
+
92
+
93
+ def parse_command(command)
94
+ ##
95
+ # If '@' is used, return a command string from a file
96
+ # Otherwise return specified command
97
+ #
98
+ if command.start_with?('@')
99
+ command_list = []
100
+ file_path = command[1..-1]
101
+ expanded_file_path = File.expand_path(file_path)
102
+ if File.exists? expanded_file_path
103
+ File.open(expanded_file_path, 'r') do |f|
104
+ f.each_line do |line|
105
+ line.chomp!.strip!
106
+ unless line.start_with?('#') || line.empty?
107
+ command_list.append(line)
108
+ end#unless
109
+ end#f.each_line
110
+ end#File.open
111
+ end#File.exists?
112
+ command_list.map! do |command|
113
+ command = format_command(command, @disable_sudo)
114
+ end
115
+ command = command_list.join('; ')
116
+ else
117
+ command = command.chomp
118
+ command = format_command(command, @disable_sudo)
119
+ end#if
120
+ end#parse_command
121
+
122
+
123
+ def format_command(command, disable_sudo=false)
124
+ pre_command = ". ~/.bash_profile; "\
125
+ "export PATH=$PATH:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin; "
126
+ unless command[0..3] == 'sudo'
127
+ unless disable_sudo
128
+ command = 'sudo ' + command
129
+ end
130
+ end
131
+ pre_command + command + ' 2>&1'
132
+ end
133
+
134
+ end
data/lib/credential.rb ADDED
@@ -0,0 +1,210 @@
1
+ class Credential
2
+ attr_accessor :config_file_path, :username, :password, :pkey_password, :sudo_password, :snowflakes
3
+
4
+ def initialize(username, password, pkey_password, regenerate, debug)
5
+ @debug = debug
6
+ @util = Util.new(@debug)
7
+
8
+ @config_file_path = "#{%x{echo ~}.chomp}/.ssh/multissh.yaml"
9
+ @username = set_username(username)
10
+ @password = set_password(password)
11
+ @pkey_password = pkey_password
12
+ @sudo_password = nil
13
+ @snowflakes = nil
14
+ @regenerate = true if regenerate
15
+
16
+ generate_config if @regenerate
17
+ process_config
18
+ end
19
+
20
+
21
+ def process_config
22
+ if File.exist? @config_file_path
23
+ @util.dbg('configuration file exists')
24
+ else
25
+ @util.dbg('couldnt find configuration file')
26
+ generate_config
27
+ end
28
+
29
+ begin
30
+ @yaml = YAML.load_file(@config_file_path)
31
+ rescue
32
+ raise 'Configuration file detected but unable to properly load. Please regenerate using "multissh.rb --regenerate_config"'
33
+ end
34
+
35
+ if @yaml['enabled']
36
+ @util.dbg('credential enabled')
37
+ @util.dbg(@yaml)
38
+
39
+ unless @yaml['credentials'][@username]
40
+ printf "No saved credential for #{@username}, create one? [yes]: "
41
+ if @util.check_affirmative
42
+ credential = generate_credential_entry
43
+ @yaml['credentials'][@username] = credential
44
+ save_config
45
+ load_config
46
+ end
47
+ end
48
+
49
+ unless @password
50
+ @password = @util.decrypt(@yaml['credentials'][@username]['password'])
51
+ @util.dbg("password - #{@password}")
52
+ end
53
+
54
+ unless @pkey_password
55
+ @pkey_password = @util.decrypt(@yaml['credentials'][@username]['pkey_password'])
56
+ @util.dbg("pkey_password - #{@pkey_password}")
57
+ end
58
+
59
+ @sudo_password = @util.decrypt(@yaml['credentials'][@username]['sudo_password'])
60
+ @util.dbg("sudo_password - #{@sudo_password}")
61
+
62
+ else
63
+ @util.dbg('credential disabled')
64
+ unless @password
65
+ printf "Enter System Password: "
66
+ @password = STDIN.noecho(&:gets).chomp
67
+ puts "\n"
68
+ end
69
+
70
+ unless ssh_agent_loaded?
71
+ unless @pkey_password
72
+ puts "ssh-agent is not in use, falling back to manual entry"
73
+ printf "Enter Private Key Password: "
74
+ @pkey_password = STDIN.noecho(&:gets).chomp
75
+ puts "\n"
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+
82
+ def set_username(username)
83
+ unless username.nil? or username == ''
84
+ @username = username
85
+ else
86
+ @username = %x{whoami}.chomp.to_s
87
+ end
88
+ end
89
+
90
+ def set_password(password)
91
+ if password == ""
92
+ printf "Enter System Password: "
93
+ password = STDIN.noecho(&:gets).chomp
94
+ password
95
+ else
96
+ password
97
+ end
98
+ end
99
+
100
+
101
+ def generate_config
102
+ @util.dbg('starting generation process')
103
+ unless @regenerate
104
+ puts "\n\n\n\n"
105
+ puts "* No existing configuration file found at path: ".red + "#{@config_file_path}".yellow
106
+ puts "*"
107
+ puts "* MultiSSH handles [sudo] prompts and private key failures by storing your credentials in its config file"
108
+ puts "* The owner of this config file will be set to #{@username.green} with a mode of #{'600'.green}"
109
+ puts "*"
110
+ puts "* If you opt out, you will be prompted for your password on every run"
111
+ puts "*"
112
+ printf "* Would you like MultiSSH to store your credentials? (y/n): "
113
+ ans = gets.chomp.downcase
114
+ @util.dbg(ans)
115
+ until ['y','n'].include? ans
116
+ printf "Please answer only with 'y' or 'n': "
117
+ ans = gets.chomp.downcase
118
+ end
119
+ generate = (ans == 'y') ? true : false
120
+
121
+ else
122
+ generate = true
123
+ puts 'MultiSSH called with "--regenerate_config", generating new configuration file'
124
+ end
125
+
126
+ if generate
127
+ credential = generate_credential_entry
128
+ @yaml = { "enabled" => true, "credentials" => { @username => credential } }
129
+ else
130
+ @yaml = { "enabled" => false }
131
+ end
132
+
133
+ save_config
134
+ set_secure_permissions
135
+
136
+ unless generate then puts "configuration file set to disabled" end
137
+
138
+ puts "Configuration file saved to #{@config_file_path}".yellow
139
+ @util.dbg('end generation process')
140
+ end
141
+
142
+
143
+ def ssh_agent_loaded?
144
+ begin
145
+ @util.dbg('ssh-agent begin check')
146
+ Net::SSH::Authentication::Agent.new.connect!
147
+ @util.dbg('ssh-agent loaded')
148
+ true
149
+ rescue Net::SSH::Authentication::AgentNotAvailable
150
+ @util.dbg('ssh-agent not loaded')
151
+ false
152
+ end
153
+ end
154
+
155
+ def private_key_exist?
156
+ if !Dir.glob("#{%x{echo ~}.chomp}/.ssh/id_*").empty?
157
+ false
158
+ else
159
+ true
160
+ end
161
+ end
162
+
163
+ def generate_credential_entry
164
+ @util.dbg("generating new entry for #{@username}")
165
+ print "#{@username} - Password: "
166
+ password = STDIN.noecho(&:gets).chomp
167
+ epassword = @util.encrypt(password)
168
+ puts "\n"
169
+
170
+ unless ssh_agent_loaded?
171
+ pkey_password = nil
172
+ if private_key_exist?
173
+ print "#{@username} - SSH Private Key Password: "
174
+ pkey_password = STDIN.noecho(&:gets).chomp
175
+ epkey_password = @util.encrypt(pkey_password)
176
+ puts "\n"
177
+ end
178
+ end
179
+
180
+ credential = { "password" => epassword, "pkey_password" => epkey_password }
181
+ @util.dbg("credential:")
182
+ @util.dbg(credential)
183
+ return credential
184
+ end
185
+
186
+ def save_config
187
+ @util.dbg("saving config to '#{@config_file_path}'")
188
+ File.open(@config_file_path, 'w') { |f| f.write @yaml.to_yaml }
189
+ end
190
+
191
+ def load_config
192
+ @util.dbg("loading config from '#{@config_file_path}'")
193
+ @yaml = YAML.load_file(@config_file_path)
194
+ end
195
+
196
+
197
+ def set_secure_permissions
198
+ %x{chown #{@username} #{@config_file_path}; chmod 600 #{@config_file_path} }
199
+ target_uid = %x{id}.split[0].match('\d+').to_s
200
+ target_mode = '600'
201
+ file_uid = File.stat(@config_file_path).uid.to_s
202
+ file_mode = File.stat(@config_file_path).mode.to_s(8)[-3..-1]
203
+ @util.dbg("target_uid: #{target_uid}, target_mode: #{target_mode}")
204
+ @util.dbg("file_uid: #{file_uid}, file_mode: #{file_mode}")
205
+ unless target_uid == file_uid && target_mode == file_mode
206
+ raise "Failed to set permissions on #{@config_file_path}".red
207
+ end
208
+ end
209
+
210
+ end
data/lib/multissh.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'net/ssh'
2
+ require 'parallel'
3
+ require 'optparse'
4
+ require 'io/console'
5
+ require 'colorize'
6
+ require 'yaml'
7
+
8
+ require_relative 'update'
9
+ require_relative 'cli'
10
+ require_relative 'worker'
11
+ require_relative 'util'
12
+
13
+
14
+ class Multissh < Cli
15
+
16
+ def run
17
+ tasks = []
18
+ @nodes.each do |node|
19
+ worker = Worker.new(
20
+ hostname=node.chomp,
21
+ username=@username,
22
+ password=@password,
23
+ pkey_password=@pkey_password,
24
+ sudo_password=@sudo_password,
25
+ command=@command,
26
+ block=@block,
27
+ debug=@debug,
28
+ )
29
+ tasks.append(worker)
30
+ end
31
+
32
+ results = Parallel.map(tasks) do |task|
33
+ task.go
34
+ end
35
+
36
+ rescue Interrupt
37
+ puts "\nCtrl+C Interrupt\n"
38
+ exit 1
39
+
40
+ end#run
41
+
42
+ end#class
43
+
data/lib/update.rb ADDED
@@ -0,0 +1,31 @@
1
+ class Update
2
+ attr_reader :local_revision, :remote_revision
3
+
4
+ def initialize(called_command)
5
+ begin
6
+ %x{git fetch -q}
7
+ @local_revision = %x{git rev-parse HEAD}
8
+ @remote_revision = (%x{git ls-remote --heads --tags origin}).split.first
9
+ rescue
10
+ @local_revision = nil
11
+ @remote_revision = nil
12
+ end
13
+
14
+ if update.local_revision != update.remote_revision
15
+ printf "MultiSSH update available. Would you like to update? (y/n): "
16
+ if ['y', 'Y'].include? gets.chomp
17
+ %x{git reset --hard origin/master && git pull}
18
+ at_exit do
19
+ %x{#{called_command}}
20
+ end#at_exit
21
+ exit 0
22
+ end#if
23
+ end#if
24
+ end#initialize
25
+
26
+ def show
27
+ puts "local: #{@local_revision}"
28
+ puts "remote: #{@remote_revision}"
29
+ end
30
+
31
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,86 @@
1
+ class Util
2
+ def initialize(debug)
3
+ @debug = debug
4
+ end
5
+
6
+
7
+ def debug_top(data)
8
+ "\n\n\n#{('-'*80).blue}\n#{'raw data:'.yellow}\n#{data.inspect.yellow}\n\n#{'formatted:'.green}\n"
9
+ end
10
+
11
+
12
+ def debug_bottom
13
+ "#{('-'*80).blue}"
14
+ end
15
+
16
+ def dbg(msg)
17
+ if @debug
18
+ puts "debug: #{msg}".blue
19
+ end
20
+ end
21
+
22
+ def display_data(header, data)
23
+ if @debug then puts debug_top(data) end
24
+
25
+ data.split(/\r\n?/).each do |line|
26
+ puts format_line(header, line)
27
+ end
28
+
29
+ if @debug then puts debug_bottom end
30
+ end
31
+
32
+ def check_affirmative
33
+ ans = ['y', 'yes', ''].include?(gets.chomp) ? true : false
34
+ dbg("affirmative = #{ans}")
35
+ puts "\n"
36
+ return ans
37
+ end
38
+
39
+ def format_line(header, line)
40
+ if not line.chomp.empty?
41
+ "#{header.blue}#{line}"
42
+ end
43
+ end
44
+
45
+ def display_error(error)
46
+ if @debug
47
+ puts error.backtrace
48
+ puts error
49
+ end
50
+ end
51
+
52
+
53
+ def show_summary(worker)
54
+ if @debug
55
+ puts "\n\n#{worker.to_s.blue}\n"
56
+ end
57
+ end
58
+
59
+ def encrypt(s)
60
+ if s
61
+ ecarr = []
62
+ s.chomp.each_byte do |c|
63
+ (33..126).to_a.include?(c + 20) ? ecarr << (c + 20).chr : ecarr << (c + 20 - 94).chr
64
+ end
65
+ ecarr.join
66
+ else
67
+ ''
68
+ end
69
+ end
70
+
71
+
72
+
73
+ def decrypt(s)
74
+ if s
75
+ carr = []
76
+ s.chomp.each_byte do |c|
77
+ (33..126).to_a.include?(c - 20) ? carr << (c - 20).chr : carr << (c - 20 + 94).chr
78
+ end
79
+ carr.join
80
+ else
81
+ ''
82
+ end
83
+ end
84
+
85
+
86
+ end
data/lib/worker.rb ADDED
@@ -0,0 +1,103 @@
1
+ class Worker
2
+ def initialize(hostname, username, password, pkey_password, sudo_password, command, block, debug)
3
+ @hostname = hostname
4
+ @username = username
5
+ @password = password
6
+ @pkey_password = pkey_password
7
+ @sudo_password = sudo_password
8
+ @command = command
9
+ @block = block
10
+
11
+ @header = "#{hostname} -- "
12
+ @util = Util.new(debug)
13
+ end
14
+
15
+
16
+ def go
17
+ @util.show_summary(self)
18
+
19
+ result = ''
20
+ begin
21
+ Net::SSH.start(@hostname, @username, :password => @password, :passphrase => @pkey_password, :non_interactive => true) do |ssh|
22
+ channel = ssh.open_channel do |channel, success|
23
+
24
+ # request a pseudo TTY formatted to screen width
25
+ cols = %x{tput cols}.chomp.to_i - @header.length
26
+ channel.request_pty(opts={:term=>'xterm',:chars_wide => cols})
27
+
28
+ @util.dbg("sending command: #{@command}")
29
+ channel.exec(@command)
30
+
31
+ channel.on_data do |channel, data|
32
+
33
+ attempts = 0
34
+
35
+ if attempts >= 2
36
+ raise 'failed to connect -- too many attempts'
37
+ end
38
+
39
+ if data =~ /Sorry, try again/
40
+ raise 'failed to connect -- incorrect sudo password'
41
+ end
42
+
43
+ if data =~ /#{@username}@#{@hostname}'s password:/
44
+ raise 'failed to connect -- password failed'
45
+ end
46
+
47
+ if data =~ /^\[sudo\] password for / and attempts == 0
48
+ if @sudo_password
49
+ channel.send_data "#{@sudo_password}\n"
50
+ elsif @password
51
+ channel.send_data "#{@password}\n"
52
+ else
53
+ raise 'failed to connect -- no sudo_password or password defined'
54
+ end
55
+ attempts += 1
56
+ @util.dbg("attempts: #{attempts}")
57
+ elsif data =~ /^\[sudo\] password for / and attempts == 1
58
+ channel.send_data "#{@password}\n"
59
+ attempts += 1
60
+ @util.dbg("attempts: #{attempts}")
61
+ end
62
+
63
+ unless @block
64
+ @util.display_data(@header, data)
65
+ else
66
+ result += data.to_s
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
73
+ channel.wait
74
+
75
+ if @block
76
+ @util.display_data(@header, result)
77
+ puts "\n"
78
+ end
79
+
80
+ end#start
81
+
82
+ rescue SocketError => e
83
+ @util.display_error(e)
84
+ puts "Failed to connect to #{@hostname}\n".red
85
+
86
+ rescue RuntimeError => e
87
+ @util.display_error(e)
88
+ puts "#{@hostname} -- incorrect password, failed to connect".red
89
+
90
+ rescue => e
91
+ @util.display_error(e)
92
+
93
+ end
94
+
95
+ end
96
+
97
+
98
+ def to_s
99
+ "Worker: {hostname:'#{@hostname}',username:'#{@username}',password:'#{@password}',command:'#{@command}',block:'#{@block}'"
100
+ end
101
+
102
+
103
+ end
data/multissh.gemspec ADDED
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'uoregon-multissh'
3
+ spec.version = '0.2.2'
4
+ spec.date = '2019-05-14'
5
+ spec.summary = "Do all the things everywhere at the same time"
6
+ spec.description = "Quickly run multiple commands on many boxes at the same time"
7
+ spec.authors = ["Lucas Crownover"]
8
+ spec.email = 'lcrownover127@gmail.com'
9
+ spec.homepage = 'https://www.savethemanatee.org/manatees/facts/'
10
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
11
+ spec.executables = ["multissh"]
12
+ spec.require_paths = ["lib"]
13
+ spec.license = 'MIT'
14
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uoregon-multissh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Lucas Crownover
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Quickly run multiple commands on many boxes at the same time
14
+ email: lcrownover127@gmail.com
15
+ executables:
16
+ - multissh
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".ruby-version"
22
+ - Gemfile
23
+ - Gemfile.lock
24
+ - README.md
25
+ - bin/multissh
26
+ - lib/cli.rb
27
+ - lib/credential.rb
28
+ - lib/multissh.rb
29
+ - lib/update.rb
30
+ - lib/util.rb
31
+ - lib/worker.rb
32
+ - multissh.gemspec
33
+ homepage: https://www.savethemanatee.org/manatees/facts/
34
+ licenses:
35
+ - MIT
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 2.7.6
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Do all the things everywhere at the same time
57
+ test_files: []