uoregon-multissh 0.2.2

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.
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: []