opswalrus 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +59 -0
- data/LICENSE +674 -0
- data/README.md +256 -0
- data/Rakefile +8 -0
- data/exe/ops +6 -0
- data/exe/run_ops_bundle +22 -0
- data/lib/opswalrus/app.rb +328 -0
- data/lib/opswalrus/bootstrap.sh +57 -0
- data/lib/opswalrus/bootstrap_linux_host1.sh +12 -0
- data/lib/opswalrus/bootstrap_linux_host2.sh +37 -0
- data/lib/opswalrus/bootstrap_linux_host3.sh +21 -0
- data/lib/opswalrus/bundler.rb +175 -0
- data/lib/opswalrus/cli.rb +143 -0
- data/lib/opswalrus/host.rb +177 -0
- data/lib/opswalrus/hosts_file.rb +55 -0
- data/lib/opswalrus/interaction_handlers.rb +53 -0
- data/lib/opswalrus/local_non_blocking_backend.rb +132 -0
- data/lib/opswalrus/local_pty_backend.rb +89 -0
- data/lib/opswalrus/operation_runner.rb +85 -0
- data/lib/opswalrus/ops_file.rb +235 -0
- data/lib/opswalrus/ops_file_script.rb +472 -0
- data/lib/opswalrus/package_file.rb +102 -0
- data/lib/opswalrus/runtime_environment.rb +258 -0
- data/lib/opswalrus/sshkit_ext.rb +51 -0
- data/lib/opswalrus/traversable.rb +15 -0
- data/lib/opswalrus/version.rb +3 -0
- data/lib/opswalrus/walrus_lang.rb +83 -0
- data/lib/opswalrus/zip.rb +57 -0
- data/lib/opswalrus.rb +10 -0
- data/opswalrus.gemspec +45 -0
- data/sig/opswalrus.rbs +4 -0
- metadata +178 -0
@@ -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
|