opswalrus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # there are probably some services that need restarting because they're using old libraries, so we'll just do the easy thing and reboot
4
+ sudo DEBIAN_FRONTEND=noninteractive apt install -yq needrestart
5
+
6
+ # install basic development tools
7
+ sudo DEBIAN_FRONTEND=noninteractive apt install -yq build-essential
8
+
9
+ # install ruby dependencies
10
+ sudo DEBIAN_FRONTEND=noninteractive apt install -yq autoconf patch rustc libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev uuid-dev
11
+
12
+ # restart services that need it
13
+ sudo needrestart -q -r a
14
+
15
+ # vagrant@ubuntu-jammy:~$ sudo needrestart -q -r a
16
+ # systemctl restart unattended-upgrades.service
17
+
18
+ # vagrant@ubuntu-jammy:~$ sudo needrestart -r l
19
+ # Scanning processes...
20
+ # Scanning candidates...
21
+ # Scanning linux images...
22
+ #
23
+ # Running kernel seems to be up-to-date.
24
+ #
25
+ # Services to be restarted:
26
+ #
27
+ # Service restarts being deferred:
28
+ # systemctl restart unattended-upgrades.service
29
+ #
30
+ # No containers need to be restarted.
31
+ #
32
+ # No user sessions are running outdated binaries.
33
+ #
34
+ # No VM guests are running outdated hypervisor (qemu) binaries on this host.
35
+
36
+ # reboot just in case
37
+ # sudo reboot
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # install homebrew
4
+ NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
5
+ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
6
+
7
+ # install gcc, rust, ruby
8
+ brew install gcc
9
+ brew install rust
10
+ brew install ruby
11
+
12
+ # brew install rtx
13
+ # eval "$(rtx activate bash)" # register a shell hook
14
+ # rtx use -g ruby@3.2 # install ruby via rtx
15
+
16
+ # download frum for ruby version management
17
+ # curl -L -o frum.tar.gz https://github.com/TaKO8Ki/frum/releases/download/v0.1.2/frum-v0.1.2-x86_64-unknown-linux-musl.tar.gz
18
+ # tar -zxf frum.tar.gz
19
+ # mv frum-v0.1.2-x86_64-unknown-linux-musl/frum ~/bin
20
+ # chmod 755 ~/bin/frum
21
+
@@ -0,0 +1,175 @@
1
+ require 'git'
2
+ require 'set'
3
+
4
+ require_relative "package_file"
5
+ require_relative "traversable"
6
+ require_relative "zip"
7
+
8
+ module OpsWalrus
9
+ class Bundler
10
+ BUNDLE_DIR = "opswalrus_bundle"
11
+
12
+ include Traversable
13
+
14
+ attr_accessor :pwd
15
+
16
+ def initialize(working_directory_path)
17
+ @pwd = working_directory_path.to_pathname
18
+ @bundle_dir = @pwd.join(BUNDLE_DIR)
19
+ end
20
+
21
+ def bundle_dir
22
+ @bundle_dir
23
+ end
24
+
25
+ def ensure_package_bundle_directory_exists
26
+ FileUtils.mkdir_p(@bundle_dir)
27
+ end
28
+
29
+ # # returns the OpsFile within the bundle directory that represents the given ops_file (which is outside of the bundle directory)
30
+ # def build_bundle_for_ops_file(ops_file)
31
+ # if ops_file.package_file # ops_file is part of larger package
32
+ # self_pkg_dir = build_bundle_for_package(ops_file.package_file)
33
+ # relative_ops_file_path = ops_file.ops_file_path.relative_path_from(ops_file.package_file.dirname)
34
+ # else
35
+ # # ops_file is not part of a larger package
36
+ # # in this case, we want to bundle the ops_file.dirname into the bundle directory
37
+ # update
38
+ # self_pkg_dir = include_directory_in_bundle_as_self_pkg(ops_file.dirname)
39
+ # relative_ops_file_path = ops_file.ops_file_path.relative_path_from(ops_file.dirname)
40
+ # end
41
+ # ops_file_in_self_pkg = self_pkg_dir.join(relative_ops_file_path)
42
+ # end
43
+
44
+ # # we want to bundle the package represented by package_file into the bundle directory that
45
+ # # resides within the package directory that contains the package_file
46
+ # def build_bundle_for_package(package_file)
47
+ # bundler_for_package = Bundler.new(package_file.dirname)
48
+ # bundler_for_package.update
49
+ # bundler_for_package.include_directory_in_bundle_as_self_pkg(pwd)
50
+ # end
51
+
52
+ def update
53
+ ensure_package_bundle_directory_exists
54
+
55
+ package_yaml_files = pwd.glob("./**/package.yaml") - pwd.glob("./**/#{BUNDLE_DIR}/**/package.yaml")
56
+ package_files_within_pwd = package_yaml_files.map {|path| PackageFile.new(path.realpath) }
57
+
58
+ download_dependency_tree(*package_files_within_pwd)
59
+ end
60
+
61
+ # downloads all transitive package dependencies associated with ops_files
62
+ # all downloaded packages are placed into @bundle_dir
63
+ def download_dependency_tree(*ops_files_and_package_files)
64
+ package_files = ops_files_and_package_files.map(&:package_file).compact.uniq
65
+
66
+ package_files.each do |root_package_file|
67
+ pre_order_traverse(root_package_file) do |package_file|
68
+ download_package_dependencies(package_file).map do |downloaded_package_directory_path|
69
+ package_file_path = File.join(downloaded_package_directory_path, "package.yaml")
70
+ PackageFile.new(package_file_path)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # returns the array of the destination directories that the packages that ops_file depend on were downloaded to
77
+ # e.g. [dir_path1, dir_path2, dir_path3, ...]
78
+ def download_package_dependencies(package_file)
79
+ package_file.dependencies.map do |local_name, package_reference|
80
+ download_package(package_file, package_reference)
81
+ end
82
+ end
83
+
84
+ # returns the self_pkg directory within the bundle directory
85
+ # def include_directory_in_bundle_as_self_pkg(dirname = pwd)
86
+ # ensure_package_bundle_directory_exists
87
+
88
+ # destination_package_path = @bundle_dir.join("self_pkg")
89
+
90
+ # # recreate the destination package path - self_pkg
91
+ # FileUtils.remove_dir(destination_package_path) if destination_package_path.exist?
92
+ # FileUtils.mkdir_p(destination_package_path)
93
+
94
+ # # files in dirname except for the BUNDLE_DIR
95
+ # files = dirname.glob("*").reject {|f| f.basename.to_s == BUNDLE_DIR }
96
+ # files.each do |file|
97
+ # FileUtils.cp_r(file, destination_package_path)
98
+ # end
99
+
100
+ # destination_package_path
101
+ # end
102
+
103
+ # This method downloads a package_url that is a dependency referenced in the specified package_file
104
+ # returns the destination directory that the package was downloaded to
105
+ def download_package(package_file, package_reference)
106
+ ensure_package_bundle_directory_exists
107
+
108
+ local_name = package_reference.local_name
109
+ package_url = package_reference.package_uri
110
+ version = package_reference.version
111
+
112
+ destination_package_path = @bundle_dir.join(package_reference.dirname)
113
+ FileUtils.remove_dir(destination_package_path) if destination_package_path.exist?
114
+
115
+ case
116
+ when package_url =~ /\.git/ # git reference
117
+ download_git_package(package_url, version, destination_package_path)
118
+ when package_url.start_with?("file://") # local path
119
+ path = package_url.sub("file://", "")
120
+ path = path.to_pathname
121
+ package_path_to_download = if path.relative? # relative path
122
+ package_file.containing_directory.join(path)
123
+ else # absolute path
124
+ path.realpath
125
+ end
126
+
127
+ raise Error, "Package not found: #{package_path_to_download}" unless package_path_to_download.exist?
128
+ FileUtils.cp_r(package_path_to_download, destination_package_path)
129
+ when package_url.to_pathname.exist? || package_file.containing_directory.join(package_url).exist? # local path
130
+ path = package_url.to_pathname
131
+ package_path_to_download = if path.relative? # relative path
132
+ package_file.containing_directory.join(path)
133
+ else # absolute path
134
+ path.realpath
135
+ end
136
+
137
+ raise Error, "Package not found: #{package_path_to_download}" unless File.exist?(package_path_to_download)
138
+ FileUtils.cp_r(package_path_to_download, destination_package_path)
139
+ else # git reference
140
+ download_git_package(package_url, version, destination_package_path)
141
+ end
142
+
143
+ destination_package_path
144
+ end
145
+
146
+ def download_git_package(package_url, version = nil, destination_package_path = nil)
147
+ destination_package_path ||= begin
148
+ package_reference_dirname = sanitize_path(package_url)
149
+ bundle_dir.join(package_reference_dirname)
150
+ end
151
+ FileUtils.remove_dir(destination_package_path) if File.exist?(destination_package_path)
152
+ if version
153
+ Git.clone(package_url, destination_package_path, branch: version, config: ['submodule.recurse=true'])
154
+ else
155
+ Git.clone(package_url, destination_package_path, config: ['submodule.recurse=true'])
156
+ end
157
+ destination_package_path
158
+ end
159
+
160
+ def sanitize_path(path)
161
+ # found this at https://apidock.com/rails/v5.2.3/ActiveStorage/Filename/sanitized
162
+ path.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
163
+ end
164
+
165
+ # returns the directory that the zip file is unzipped into
166
+ def unzip(zip_bundle_file, output_dir = nil)
167
+ if zip_bundle_file.to_pathname.exist?
168
+ output_dir ||= Dir.mktmpdir.to_pathname
169
+
170
+ # unzip the bundle into the output_dir directory
171
+ DirZipper.unzip(zip_bundle_file, output_dir)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,143 @@
1
+ require "gli"
2
+
3
+ # require_relative "walrus_lang"
4
+ require_relative "app"
5
+
6
+ module OpsWalrus
7
+ class Cli
8
+ extend GLI::App
9
+
10
+ pre do |global_options, command, options, args|
11
+ $app = App.instance(Dir.pwd)
12
+ $app.set_local_hostname(ENV["WALRUS_LOCAL_HOSTNAME"]) if ENV["WALRUS_LOCAL_HOSTNAME"]
13
+ true
14
+ end
15
+
16
+ # this is invoked on an unhandled exception or a call to exit_now!
17
+ on_error do |exception|
18
+ puts "*" * 80
19
+ puts "catchall exception handler:"
20
+ puts exception.message
21
+ puts exception.backtrace.join("\n")
22
+ false # disable built-in exception handling
23
+ end
24
+
25
+ program_desc 'ops is an operation runner'
26
+
27
+ desc 'Be verbose'
28
+ switch [:v, :verbose]
29
+
30
+ desc 'Debug'
31
+ switch [:d, :debug]
32
+
33
+ flag [:h, :hosts], multiple: true, desc: "Specify the hosts.yaml file"
34
+ flag [:t, :tags], multiple: true, desc: "Specify a set of tags to filter the hosts by"
35
+
36
+ desc 'Report on the host inventory'
37
+ long_desc 'Report on the host inventory'
38
+ command :inventory do |c|
39
+ c.action do |global_options, options, args|
40
+ hosts = global_options[:hosts]
41
+ tags = global_options[:tags]
42
+
43
+ $app.set_verbose(global_options[:debug] || global_options[:verbose])
44
+
45
+ $app.report_inventory(hosts, tags: tags)
46
+ end
47
+ end
48
+
49
+ desc 'Run an operation from a package'
50
+ long_desc 'Run the specified operation found within the specified package'
51
+ arg_name 'args', :multiple
52
+ command :run do |c|
53
+ c.flag [:u, :user], desc: "Specify the user that the operation will run as"
54
+ c.switch :pass, desc: "Prompt for a sudo password"
55
+ c.flag [:p, :params], desc: "JSON string that represents the input parameters for the operation. The JSON string must conform to the params schema for the operation."
56
+
57
+ c.action do |global_options, options, args|
58
+ hosts = global_options[:hosts] || []
59
+ tags = global_options[:tags] || []
60
+
61
+ $app.set_inventory_hosts(hosts)
62
+ $app.set_inventory_tags(tags)
63
+
64
+ verbose = case
65
+ when global_options[:debug]
66
+ 2
67
+ when global_options[:verbose]
68
+ 1
69
+ end
70
+
71
+ user = options[:user]
72
+ params = options[:params]
73
+
74
+ $app.set_verbose(verbose)
75
+ $app.set_params(params)
76
+
77
+ $app.set_sudo_user(user) if user
78
+
79
+ if options[:pass]
80
+ $app.prompt_sudo_password
81
+ end
82
+
83
+ # puts "verbose"
84
+ # puts verbose.inspect
85
+ # puts "user"
86
+ # puts user.inspect
87
+ # puts "args"
88
+ # puts args.inspect
89
+
90
+ exit_status = $app.run(args)
91
+
92
+ exit_now!("error", exit_status) unless exit_status == 0
93
+ end
94
+ end
95
+
96
+ desc 'Bundle dependencies'
97
+ long_desc 'Download and bundle the dependencies for the operations found in the current directory'
98
+ command :bundle do |c|
99
+
100
+ desc 'Update bundle dependencies'
101
+ long_desc 'Download and bundle the latest versions of dependencies for the current package'
102
+ c.command :update do |update|
103
+ update.action do |global_options, options, args|
104
+ $app.bundle_update
105
+ end
106
+ end
107
+
108
+ desc 'List bundle dependencies'
109
+ long_desc 'List bundle dependencies'
110
+ c.command :status do |status|
111
+ status.action do |global_options, options, args|
112
+ $app.bundle_status
113
+ end
114
+ end
115
+
116
+ desc 'Unzip bundle'
117
+ long_desc 'Unzip the specified bundle zip file to the specified directory'
118
+ c.command :unzip do |unzip|
119
+ unzip.flag [:o, :output], desc: "Specify the output directory"
120
+
121
+ unzip.action do |global_options, options, args|
122
+ output_dir = options[:output]
123
+ zip_file_path = args.first
124
+
125
+ destination_dir = $app.unzip(zip_file_path, output_dir)
126
+
127
+ if destination_dir
128
+ puts destination_dir
129
+ end
130
+ end
131
+ end
132
+
133
+ c.default_command :status
134
+ end
135
+ end
136
+ end
137
+
138
+ def main
139
+ exit_status = OpsWalrus::Cli.run(ARGV)
140
+ exit exit_status
141
+ end
142
+
143
+ main if __FILE__ == $0
@@ -0,0 +1,177 @@
1
+ require "set"
2
+ require "sshkit"
3
+
4
+ require_relative "interaction_handlers"
5
+
6
+ module OpsWalrus
7
+
8
+ module HostDSL
9
+ # returns the stdout from the command
10
+ def sh(desc_or_cmd = nil, cmd = nil, stdin: nil, &block)
11
+ out, err, status = *shell!(desc_or_cmd, cmd, block, stdin: stdin)
12
+ out
13
+ end
14
+
15
+ # returns the tuple: [stdout, stderr, exit_status]
16
+ def shell(desc_or_cmd = nil, cmd = nil, stdin: nil, &block)
17
+ shell!(desc_or_cmd, cmd, block, stdin: stdin)
18
+ end
19
+
20
+ # returns the tuple: [stdout, stderr, exit_status]
21
+ def shell!(desc_or_cmd = nil, cmd = nil, block = nil, stdin: nil)
22
+ # description = nil
23
+
24
+ return ["", "", 0] if !desc_or_cmd && !cmd && !block # we were told to do nothing; like hitting enter at the bash prompt; we can do nothing successfully
25
+
26
+ description = desc_or_cmd if cmd || block
27
+ cmd = block.call if block
28
+ cmd ||= desc_or_cmd
29
+
30
+ if self.alias
31
+ print "[#{self.alias} | #{host}] "
32
+ else
33
+ print "[#{host}] "
34
+ end
35
+ print "#{description}: " if description
36
+ puts cmd
37
+
38
+ cmd = WalrusLang.render(cmd, block.binding) if block && cmd =~ /{{.*}}/
39
+ return unless cmd && !cmd.strip.empty?
40
+
41
+ #cmd = Shellwords.escape(cmd)
42
+
43
+ # puts "shell: #{cmd}"
44
+ # puts "shell: #{cmd.inspect}"
45
+ # puts "sudo_password: #{sudo_password}"
46
+
47
+ sshkit_cmd = execute_cmd(cmd)
48
+
49
+ [sshkit_cmd.full_stdout, sshkit_cmd.full_stderr, sshkit_cmd.exit_status]
50
+ end
51
+
52
+ # def init_brew
53
+ # execute('eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"')
54
+ # end
55
+
56
+ def run_ops(cmd)
57
+ shell!("/home/linuxbrew/.linuxbrew/bin/gem exec opswalrus ops #{cmd}")
58
+ end
59
+
60
+ end
61
+
62
+ class HostProxy
63
+ def initialize(host)
64
+ @host = host
65
+ end
66
+ def method_missing()
67
+ end
68
+ end
69
+
70
+ class Host
71
+ include HostDSL
72
+
73
+ def initialize(name_or_ip_or_cidr, tags = [], props = {})
74
+ @name_or_ip_or_cidr = name_or_ip_or_cidr
75
+ @tags = tags.to_set
76
+ @props = props.is_a?(Array) ? {"tags" => props} : props.to_h
77
+ end
78
+
79
+ def host
80
+ @name_or_ip_or_cidr
81
+ end
82
+
83
+ def ssh_port
84
+ @props["port"]
85
+ end
86
+
87
+ def ssh_user
88
+ @props["user"]
89
+ end
90
+
91
+ def ssh_password
92
+ @props["password"]
93
+ end
94
+
95
+ def ssh_keys
96
+ @props["keys"]
97
+ end
98
+
99
+ def hash
100
+ @name_or_ip_or_cidr.hash
101
+ end
102
+
103
+ def eql?(other)
104
+ self.class == other.class && self.hash == other.hash
105
+ end
106
+
107
+ def to_s
108
+ @name_or_ip_or_cidr
109
+ end
110
+
111
+ def alias
112
+ @props["alias"]
113
+ end
114
+
115
+ def tag!(*tags)
116
+ enumerables, scalars = tags.partition {|t| Enumerable === t }
117
+ @tags.merge(scalars)
118
+ enumerables.each {|enum| @tags.merge(enum) }
119
+ @tags
120
+ end
121
+
122
+ def tags
123
+ @tags
124
+ end
125
+
126
+ def summary(verbose = false)
127
+ report = "#{to_s}\n tags: #{tags.sort.join(', ')}"
128
+ if verbose
129
+ @props.reject{|k,v| k == 'tags' }.each {|k,v| report << "\n #{k}: #{v}" }
130
+ end
131
+ report
132
+ end
133
+
134
+ def sshkit_host
135
+ @sshkit_host ||= ::SSHKit::Host.new({
136
+ hostname: host,
137
+ port: ssh_port || 22,
138
+ user: ssh_user || raise("No ssh user specified to connect to #{host}"),
139
+ password: ssh_password,
140
+ keys: ssh_keys
141
+ })
142
+ end
143
+
144
+ def set_ssh_connection(sshkit_backend)
145
+ @sshkit_backend = sshkit_backend
146
+ end
147
+
148
+ def clear_ssh_connection
149
+ @sskit_backend = nil
150
+ end
151
+
152
+ def execute(*args)
153
+ # puts "interaction handler responds with: #{ssh_password}"
154
+ @sshkit_backend.capture(*args, interaction_handler: SudoPasswordMapper.new(ssh_password).interaction_handler, verbosity: :info)
155
+ # @sshkit_backend.capture(*args, interaction_handler: SudoPromptInteractionHandler.new, verbosity: :info)
156
+ end
157
+
158
+ def execute_cmd(*args)
159
+ @sshkit_backend.execute_cmd(*args, interaction_handler: SudoPasswordMapper.new(ssh_password).interaction_handler, verbosity: :info)
160
+ end
161
+
162
+ def upload(local_path_or_io, remote_path)
163
+ source = local_path_or_io.is_a?(IO) ? local_path_or_io : local_path_or_io.to_s
164
+ @sshkit_backend.upload!(source, remote_path.to_s)
165
+ end
166
+
167
+ def download(remote_path, local_path)
168
+ @sshkit_backend.download!(remote_path.to_s, local_path.to_s)
169
+ end
170
+
171
+ def run
172
+
173
+ end
174
+
175
+ end
176
+
177
+ end
@@ -0,0 +1,55 @@
1
+ require "pathname"
2
+ require "yaml"
3
+ require_relative "host"
4
+
5
+ module OpsWalrus
6
+ class HostsFile
7
+ attr_accessor :hosts_file_path
8
+ attr_accessor :yaml
9
+
10
+ def initialize(hosts_file_path)
11
+ @hosts_file_path = File.absolute_path(hosts_file_path)
12
+ @yaml = YAML.load(File.read(hosts_file_path)) if File.exist?(hosts_file_path)
13
+ # puts @yaml.inspect
14
+ end
15
+
16
+ def defaults
17
+ @defaults ||= (@yaml["defaults"] || @yaml["default"] || {})
18
+ end
19
+
20
+ # returns an Array(Host)
21
+ def hosts
22
+ # @yaml is a map of the form:
23
+ # {
24
+ # "198.23.249.13"=>{"hostname"=>"web1", "tags"=>["monopod", "racknerd", "vps", "2.5gb", "web1", "web", "ubuntu22.04"]},
25
+ # "107.175.91.150"=>{"tags"=>["monopod", "racknerd", "vps", "2.5gb", "pbx1", "pbx", "ubuntu22.04"]},
26
+ # "198.23.249.16"=>{"tags"=>["racknerd", "vps", "4gb", "kvm", "ubuntu20.04", "minecraft"]},
27
+ # "198.211.15.34"=>{"tags"=>["racknerd", "vps", "1.5gb", "kvm", "ubuntu20.04", "blog"]},
28
+ # "homeassistant.locallan.network"=>{"tags"=>["local", "homeassistant", "home", "rpi"]},
29
+ # "synology.locallan.network"=>{"tags"=>["local", "synology", "nas"]},
30
+ # "pfsense.locallan.network"=>false,
31
+ # "192.168.56.10"=>{"tags"=>["web", "vagrant"]}
32
+ # }
33
+ @yaml.map do |host_ref, host_attrs|
34
+ next if host_ref == "default" || host_ref == "defaults" # this maps to a nil
35
+
36
+ host_params = host_attrs.is_a?(Hash) ? host_attrs : {}
37
+
38
+ Host.new(host_ref, tags(host_ref), defaults.merge(host_params))
39
+ end.compact
40
+ end
41
+
42
+ def tags(host)
43
+ host_attrs = @yaml[host]
44
+
45
+ case host_attrs
46
+ when Array
47
+ tags = host_attrs
48
+ tags.compact.uniq
49
+ when Hash
50
+ tags = host_attrs["tags"] || []
51
+ tags.compact.uniq
52
+ end || []
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ require 'sshkit'
2
+
3
+ module OpsWalrus
4
+
5
+ class PasswdInteractionHandler
6
+ def on_data(command, stream_name, data, channel)
7
+ # puts data
8
+ case data
9
+ when '(current) UNIX password: '
10
+ channel.send_data("old_pw\n")
11
+ when 'Enter new UNIX password: ', 'Retype new UNIX password: '
12
+ channel.send_data("new_pw\n")
13
+ when 'passwd: password updated successfully'
14
+ else
15
+ raise "Unexpected stderr #{stderr}"
16
+ end
17
+ end
18
+ end
19
+
20
+ class SudoPasswordMapper
21
+ def initialize(sudo_password)
22
+ @sudo_password = sudo_password
23
+ end
24
+
25
+ def interaction_handler
26
+ SSHKit::MappingInteractionHandler.new({
27
+ /\[sudo\] password for .*?:\s*/ => "#{@sudo_password}\n",
28
+ # /\s+/ => nil, # unnecessary
29
+ }, :info)
30
+ end
31
+ end
32
+
33
+ class SudoPromptInteractionHandler
34
+ def on_data(command, stream_name, data, channel)
35
+ # puts "0" * 80
36
+ # puts data.inspect
37
+ case data
38
+ when /\[sudo\] password for/
39
+ if channel.respond_to?(:send_data) # Net::SSH channel
40
+ channel.send_data("conquer\n")
41
+ elsif channel.respond_to?(:write) # IO
42
+ channel.write("conquer\n")
43
+ end
44
+ when /\s+/
45
+ puts 'space, do nothing'
46
+ else
47
+ raise "Unexpected prompt: #{data} on stream #{stream_name} and channel #{channel.inspect}"
48
+ end
49
+ end
50
+ end
51
+
52
+
53
+ end