opswalrus 1.0.0

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.
@@ -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