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.
data/README.md ADDED
@@ -0,0 +1,256 @@
1
+ # opswalrus
2
+
3
+ opswalrus is a tool that runs scripts against hosts. It's kind of like Ansible, but aims to be simpler to use.
4
+
5
+ # Examples
6
+
7
+ ```bash
8
+ > ops run core/host info
9
+ {
10
+ success: true,
11
+ host: {
12
+ name: "davidlinux",
13
+ os: "Ubuntu 23.04 (lunar)",
14
+ kernel: "Linux 6.2.0-1007-lowlatency x86_64 GNU/Linux",
15
+ }
16
+ }
17
+ ```
18
+
19
+ # Packages and Imports
20
+
21
+ ## Ops Packages
22
+
23
+ An ops package is a directory containing a package.yml (or package.yaml) file.
24
+
25
+ - The package.yml (or package.yaml) file is called the package file.
26
+ - The directory containing the package file is called the package directory.
27
+
28
+ A package file looks like this:
29
+ ```yaml
30
+ author: David Ellis
31
+ license: MIT
32
+ version: 1.0.0
33
+ dependencies:
34
+ core: davidkellis/ops_core
35
+ apt: davidkellis/ops_apt
36
+ ```
37
+
38
+ ## Ops Files
39
+
40
+ An ops package may also consist of ops files, arranged in an arbitrary directory structure.
41
+
42
+ - An ops file is a script to do do something, very much like a shell script.
43
+ - An ops file should try to implement an idempotent operation, such that repeated execution of the script results in the same desired state.
44
+
45
+ An ops file looks like this:
46
+ ```
47
+ params:
48
+ ops_username: string
49
+ ops_ssh_public_key: string
50
+ hostname: string
51
+
52
+ output:
53
+ success: boolean,
54
+ error: string?
55
+
56
+ imports:
57
+ core: core # core references the bundled core package referenced in the package.yaml
58
+ svc: service # service references the bundled service package referenced in the package.yaml
59
+ ...
60
+
61
+ desc "create the admin group if it doens't exist"
62
+ core.create_group name: "admin"
63
+
64
+ desc "set up passwordless sudo for admin group users"
65
+ core.replace_line file: "/etc/sudoers",
66
+ pattern: "^%admin",
67
+ line: "%admin ALL=(ALL) NOPASSWD: ALL",
68
+ verify: "/usr/sbin/visudo -cf %s"
69
+
70
+ desc "create the ops user and make it an admin user"
71
+ core.create_user name: params.ops_username
72
+
73
+ desc "set up authorized key for id_ansible ssh key (root user)"
74
+ core.ssh.add_authorized_key user: "root", key: ops_ssh_public_key
75
+
76
+ desc "set up authorized key for id_ansible ssh key (ops user)"
77
+ core.ssh.add_authorized_key user: ops_username, key: ops_ssh_public_key
78
+
79
+ desc "disable password authentication for root"
80
+ core.replace_line file: '/etc/ssh/sshd_config',
81
+ pattern: '^#?PermitRootLogin',
82
+ line: 'PermitRootLogin prohibit-password'
83
+
84
+ desc "restart sshd"
85
+ svc.restart name: "sshd"
86
+
87
+ {
88
+ success: true,
89
+ }
90
+ ```
91
+
92
+ An ops file is broken up into two parts. The first part is an optional YAML block that describes the structure of the expected
93
+ input parameters, the structure of the expected JSON output message, and the package dependencies that the script
94
+ needs in order to run.
95
+
96
+ The YAML block is concluded with an elipsis, `...`, on a line by itself.
97
+
98
+ The YAML block and its associated trailing elipsis may be omitted.
99
+
100
+ Following the elipsis that concludes the YAML block is a block of Ruby code. The block of Ruby is executed with a number
101
+ of methods, constants, and libraries that are available as a kind of domain specific language (DSL). This DSL makes
102
+ writing ops scripts feel very much like writing standard bash shell scripts.
103
+
104
+ Ops file imports are a mapping consisting of a local name and a corresponding package reference.
105
+
106
+ ## Package Bundles
107
+
108
+ When an ops file is run, the ops runtime will first bundle up the invoked ops file as well as all package dependencies
109
+ and place the bundle of associated ops packages and ops files into an ops bundle directory.
110
+
111
+ The bundle directory is named ops_bundle, and contains everything needed to run the specified ops file on either the
112
+ local host or a remote host.
113
+
114
+ The ops command will place the bundle directory in the directory from which the ops command is being run. So, if `pwd`
115
+ returns `/home/david/foo` and the ops command is run from within that directory, then the bundle directory will be placed
116
+ at `/home/david/foo/ops_bundle`.
117
+
118
+ The one exception to the normal bundle directory placement rule described in the previous paragraph is when the ops
119
+ command is being run from within a directory that is contained within a package directory. In that case, the bundle directory
120
+ will be placed inside the package directory. So, for example, if the directory structure looks like:
121
+ ```
122
+ ❯ tree pkg
123
+ pkg
124
+ ├── apt
125
+ │   ├── install.ops
126
+ │   └── update.ops
127
+ ├── core
128
+ │   ├── echo.ops
129
+ │   ├── host
130
+ │   │   ├── info.ops
131
+ │   │   └── info.rb
132
+ │   ├── package.yaml
133
+ │   ├── ssh_copy_id.ops
134
+ │   ├── touch.ops
135
+ │   └── whoami.ops
136
+ ├── hardening
137
+ │   └── package.yaml
138
+ ├── motd
139
+ │   ├── motd.ops
140
+ │   └── package.yaml
141
+ └── service
142
+ └── restart.ops
143
+ ```
144
+ and the `pwd` command returns `pkg/core/host`, and the ops command is run from within `pkg/core/host`, then the bundle
145
+ directory will be placed at `pkg/core/ops_bundle`.
146
+
147
+ ### Bundle Directory Contents
148
+
149
+ A bundle directory contains all the dependencies for a given ops file invocation. There are two possible cases:
150
+ 1. The invoked ops file is part of a package
151
+ 2. The invoked ops file is not part of a package
152
+
153
+ In case (1), when the ops file being invoked is part of a package, we'll call it P, then the bundle directory will contain
154
+ a copy of the package directory associated with P, as well as all of the package directories associated with all
155
+ transitive package dependencies of P. For example, if the ops file foo.ops is contained within the package directory
156
+ for the Bar package, and if the Bar package depends on the core package and the service package, then the
157
+ directory structure of the bundle directory would be:
158
+ ```
159
+ ❯ tree ops_bundle
160
+ ops_bundle
161
+ ├── Bar
162
+ │   └── foo.ops
163
+ ├── core
164
+ │   ├── echo.ops
165
+ │   ├── host
166
+ │   │   ├── info.ops
167
+ │   │   └── info.rb
168
+ │   ├── package.yaml
169
+ │   ├── ssh_copy_id.ops
170
+ │   ├── touch.ops
171
+ │   └── whoami.ops
172
+ └── service
173
+ └── restart.ops
174
+ ```
175
+
176
+ In case (2), when the ops file being invoked is not part of a package, then the bundle directory will contain a copy
177
+ of the package directories associated with all transitive package dependencies of the ops file being invoked.
178
+ Additionally, the deepest nested directory containing all of the transitive ops file dependencies of the ops file being
179
+ invoked will be copied to the bundle directory.
180
+
181
+ ## Import and Symbol Resolution
182
+
183
+ When the ops command bundles and runs an ops file, the rules that the runtime uses to resolve references to other ops
184
+ files is as follows; assume the following sample project directory structure:
185
+
186
+ Project directory structure:
187
+ ```
188
+ davidinfra
189
+ ├── caddy
190
+ │   ├── install
191
+ │   │   └── debian.ops
192
+ │   └── install.ops
193
+ ├── hosts.yml
194
+ ├── main.ops
195
+ ├── prepare_host
196
+ │   ├── all.ops
197
+ │   ├── hostname.ops
198
+ │   └── ssh.ops
199
+ └── roles
200
+ └── web.ops
201
+ ```
202
+
203
+ Corresponding bundle directory structure:
204
+ ```
205
+ davidinfra
206
+ ├── ops_bundle
207
+ │ ├── core
208
+ │ │ └──...
209
+ │   └── davidinfra
210
+ │ ├── caddy
211
+ │ │   ├── install
212
+ │ │   │   └── debian.ops
213
+ │ │   └── install.ops
214
+ │ ├── hosts.yml
215
+ │ ├── main.ops
216
+ │ ├── prepare_host
217
+ │ │   ├── all.ops
218
+ │ │   ├── hostname.ops
219
+ │ │   └── ssh.ops
220
+ │ └── roles
221
+ │ └── web.ops
222
+ ├── caddy
223
+ │   ├── install
224
+ │   │   └── debian.ops
225
+ │   └── install.ops
226
+ ├── hosts.yml
227
+ ├── main.ops
228
+ ├── prepare_host
229
+ │   ├── all.ops
230
+ │   ├── hostname.ops
231
+ │   └── ssh.ops
232
+ └── roles
233
+ └── web.ops
234
+ ```
235
+
236
+ The import and symbol resolution rules are as follows:
237
+
238
+ 1. An ops file implicitly imports all sibling ops files and directories that reside within its same parent directory.
239
+ Within the lexical scope of an ops file's ruby script, any ops files or subdirectories that are implicitly imported
240
+ may be referenced by their name.
241
+ For example:
242
+ - main.ops may invoke caddy/install.ops with the expression `caddy.install(...)`
243
+ - all.ops may invoke hostname.ops with the expression `hostname(...)`
244
+ 2. If there is an ops file and a directory that share the same name (with the exception of the .ops file extension),
245
+ then only the install.ops file may be referenced and invoked by other ops files, while the directory of the same name,
246
+ and its contents, may only be referenced from within the ops file of the matching name. This allows the details of
247
+ an operation to be encapsulated away and a public API be exposed through the ops file, while the details of the
248
+ implementation are hidden away in the ops files within the directory.
249
+ For example:
250
+ - main.ops may invoke `caddy.install(...)`, but it may not invoke `caddy.install.debian(...)`
251
+ - install.ops may invoke `install.debian(...)`, and reference other files or subpackages within the caddy/install directory
252
+ 3. Ops files may import packages or relative paths:
253
+ 1. a package reference that matches one of the local package names in the dependencies captured in packages.yaml
254
+ 2. a package reference that resolves to a relative path pointing at a package directory
255
+ 3. a relative path that resolves to a directory containing ops files
256
+ 4. a relative path that resolves to an ops file
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/exe/ops ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'opswalrus'
4
+
5
+ exit_status = OpsWalrus::Cli.run(ARGV)
6
+ exit exit_status
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'opswalrus'
4
+
5
+ def main
6
+ zip_file_path = ARGV.shift.to_pathname
7
+
8
+ exit_status = 0
9
+
10
+ Dir.mktmpdir do |dir|
11
+ dir = dir.to_pathname
12
+
13
+ # unzip the bundle into the temp directory
14
+ OpsWalrus::DirZipper.unzip(zip_file_path, dir)
15
+
16
+ exit_status = OpsWalrus::Cli.run(ARGV)
17
+ end
18
+
19
+ exit exit_status
20
+ end
21
+
22
+ main
@@ -0,0 +1,328 @@
1
+ require "citrus"
2
+ require "git"
3
+ require "io/console"
4
+ require "json"
5
+ require "random/formatter"
6
+ require "shellwords"
7
+ require "socket"
8
+ require "stringio"
9
+ require "yaml"
10
+ require "pathname"
11
+ require_relative "host"
12
+ require_relative "hosts_file"
13
+ require_relative "operation_runner"
14
+ require_relative "bundler"
15
+ require_relative "package_file"
16
+
17
+ class String
18
+ def escape_single_quotes
19
+ gsub("'"){"\\'"}
20
+ end
21
+
22
+ def to_pathname
23
+ Pathname.new(self)
24
+ end
25
+ end
26
+
27
+ class Pathname
28
+ def to_pathname
29
+ self
30
+ end
31
+ end
32
+
33
+ module OpsWalrus
34
+ class Error < StandardError
35
+ end
36
+
37
+ class App
38
+ def self.instance(*args)
39
+ @instance ||= new(*args)
40
+ end
41
+
42
+
43
+ attr_reader :local_hostname
44
+
45
+ def initialize(pwd = Dir.pwd)
46
+ @verbose = false
47
+ @sudo_user = nil
48
+ @sudo_password = nil
49
+ @inventory_host_references = []
50
+ @inventory_tag_selections = []
51
+ @params = nil
52
+ @pwd = pwd.to_pathname
53
+ @bundler = Bundler.new(@pwd)
54
+ @local_hostname = "localhost"
55
+ end
56
+
57
+ def to_s
58
+ "" # return empty string because we won't want anyone accidentally printing or inspecting @sudo_password
59
+ end
60
+
61
+ def inspect
62
+ "" # return empty string because we won't want anyone accidentally printing or inspecting @sudo_password
63
+ end
64
+
65
+ def set_local_hostname(hostname)
66
+ hostname = hostname.strip
67
+ @local_hostname = hostname.empty? ? "localhost" : hostname
68
+ end
69
+
70
+ def set_inventory_hosts(*hosts)
71
+ hosts.flatten!.compact!
72
+ @inventory_host_references.concat(hosts).compact!
73
+ end
74
+
75
+ def set_inventory_tags(*tags)
76
+ tags.flatten!.compact!
77
+ @inventory_tag_selections.concat(tags).compact!
78
+ end
79
+
80
+ def bundler
81
+ @bundler
82
+ end
83
+
84
+ def bundle_dir
85
+ @bundler.bundle_dir
86
+ end
87
+
88
+ def set_verbose(verbose)
89
+ @verbose = verbose
90
+ end
91
+
92
+ def verbose?
93
+ @verbose
94
+ end
95
+
96
+ def debug?
97
+ @verbose == 2
98
+ end
99
+
100
+ def set_pwd(pwd)
101
+ @pwd = pwd.to_pathname
102
+ @bundler = Bundler.new(@pwd)
103
+ end
104
+
105
+ def pwd
106
+ @pwd || raise("No working directory specified")
107
+ end
108
+
109
+ def set_sudo_user(user)
110
+ @sudo_user = user
111
+ end
112
+
113
+ def sudo_user
114
+ @sudo_user || "root"
115
+ end
116
+
117
+ def set_sudo_password(password)
118
+ @sudo_password = password
119
+ end
120
+
121
+ def prompt_sudo_password
122
+ password = IO::console.getpass("[ops] Enter sudo password to run sudo in local environment: ")
123
+ set_sudo_password(password)
124
+ nil
125
+ end
126
+
127
+ def sudo_password
128
+ @sudo_password
129
+ end
130
+
131
+ # params must be a string representation of a JSON object: '{}' | '{"key1": ... , ...}'
132
+ def set_params(params)
133
+ json_hash = JSON.parse(params) rescue nil
134
+ json_hash = json_hash.is_a?(Hash) ? json_hash : nil
135
+
136
+ @params = json_hash # @params returns a Hash or nil
137
+ end
138
+
139
+ # args is of the form ["github.com/davidkellis/my-package/sub-package1", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
140
+ # if the first argument is the path to a .ops file, then treat it as a local path, and add the containing package
141
+ # to the load path
142
+ # otherwise, copy the
143
+ # returns the exit status code that the script should terminate with
144
+ def run(package_operation_and_args)
145
+ return 0 if package_operation_and_args.empty?
146
+
147
+ ops_file_path, operation_kv_args, tmp_dir = get_entry_point_ops_file_and_args(package_operation_and_args)
148
+ ops_file = OpsFile.new(self, ops_file_path)
149
+
150
+ # if the ops file is part of a package, then set the package directory as the app's pwd
151
+ # puts "run1: #{ops_file.ops_file_path}"
152
+ if ops_file.package_file && ops_file.package_file.dirname.to_s !~ /#{Bundler::BUNDLE_DIR}/
153
+ # puts "set pwd: #{ops_file.package_file.dirname}"
154
+ set_pwd(ops_file.package_file.dirname)
155
+ rebased_ops_file_relative_path = ops_file.ops_file_path.relative_path_from(ops_file.package_file.dirname)
156
+ # note: rebased_ops_file_relative_path is a relative path that is relative to ops_file.package_file.dirname
157
+ # puts "rebased path: #{rebased_ops_file_relative_path}"
158
+ absolute_ops_file_path = ops_file.package_file.dirname.join(rebased_ops_file_relative_path)
159
+ # puts "absolute path: #{absolute_ops_file_path}"
160
+ ops_file = OpsFile.new(self, absolute_ops_file_path)
161
+ end
162
+ # puts "run2: #{ops_file.ops_file_path}"
163
+
164
+ op = OperationRunner.new(self, ops_file)
165
+ # if op.requires_sudo?
166
+ # prompt_sudo_password unless sudo_password
167
+ # end
168
+ # exit_status, out, err, script_output_structure = op.run(operation_kv_args, params_json_hash: @params, verbose: @verbose)
169
+ result = op.run(operation_kv_args, params_json_hash: @params, verbose: @verbose)
170
+ exit_status = result.exit_status
171
+
172
+ if @verbose
173
+ puts "Op exit_status"
174
+ puts exit_status
175
+
176
+ # puts "Op stdout"
177
+ # puts out
178
+
179
+ # puts "Op stderr"
180
+ # puts err
181
+
182
+ puts "Op output"
183
+ # puts script_output_structure ? JSON.pretty_generate(script_output_structure) : nil.inspect
184
+ puts JSON.pretty_generate(result.value)
185
+ end
186
+
187
+ exit_status
188
+ ensure
189
+ FileUtils.remove_entry(tmp_dir) if tmp_dir
190
+ end
191
+
192
+ # package_operation_and_args can take one of the following forms:
193
+ # - ["github.com/davidkellis/my-package", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
194
+ # - ["foo.zip", "foo/myfile.ops", "arg1:val1", "arg2:val2", "arg3:val3"]
195
+ # - ["davidkellis/my-package", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
196
+ # - ["davidkellis/my-package", "operation1"]
197
+ # - ["my-package/operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
198
+ # - ["./my-package/operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
199
+ # - ["../../my-package/operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
200
+ # - ["../../my-package/operation1"]
201
+ #
202
+ # returns 3-tuple of the form: [ ops_file_path, operation_kv_args, optional_tmp_dir ]
203
+ # such that the third item - optional_tmp_dir - if present, should be deleted after the script has completed running
204
+ def get_entry_point_ops_file_and_args(package_operation_and_args)
205
+ package_operation_and_args = package_operation_and_args.dup
206
+ package_or_ops_file_reference = package_operation_and_args.slice!(0, 1).first
207
+ tmp_dir = nil
208
+
209
+ case
210
+ when File.exist?(package_or_ops_file_reference)
211
+ first_filepath = package_or_ops_file_reference.to_pathname.realpath
212
+ ops_file_path = case first_filepath.extname.downcase
213
+ when ".ops"
214
+ first_filepath
215
+ when ".zip"
216
+ tmp_dir = Dir.mktmpdir.to_pathname
217
+
218
+ # unzip the bundle into the temp directory
219
+ DirZipper.unzip(first_filepath, tmp_dir)
220
+
221
+ relative_ops_path = package_operation_and_args.slice!(0, 1).first
222
+
223
+ tmp_dir.join(relative_ops_path)
224
+ else
225
+ raise Error, "Unknown file type for entrypoint: #{first_filepath}"
226
+ end
227
+ operation_kv_args = package_operation_and_args
228
+ [ops_file_path, operation_kv_args, tmp_dir ]
229
+ when repo_url = git_repo?(package_or_ops_file_reference)
230
+ destination_package_path = bundler.download_git_package(repo_url)
231
+
232
+ ops_file_path = nil
233
+ base_path = Pathname.new(destination_package_path)
234
+ path_parts = 0
235
+ package_operation_and_args.each do |candidate_path_arg|
236
+ candidate_base_path = base_path.join(candidate_path_arg)
237
+ candidate_ops_file = candidate_base_path.sub_ext(".ops")
238
+ if candidate_ops_file.exist?
239
+ path_parts += 1
240
+ ops_file_path = candidate_ops_file
241
+ break
242
+ elsif candidate_base_path.exist?
243
+ path_parts += 1
244
+ else
245
+ raise Error, "Operation not found in #{repo_url}: #{candidate_base_path}"
246
+ end
247
+ base_path = candidate_base_path
248
+ end
249
+ operation_kv_args = package_operation_and_args.drop(path_parts)
250
+
251
+ # for an original package_operation_and_args of ["github.com/davidkellis/my-package", "operation1", "arg1:val1", "arg2:val2", "arg3:val3"]
252
+ # we return: [ "#{pwd}/#{Bundler::BUNDLE_DIR}/github-com-davidkellis-my-package/operation1.ops", ["arg1:val1", "arg2:val2", "arg3:val3"] ]
253
+ [ops_file_path, operation_kv_args, tmp_dir]
254
+ else
255
+ raise Error, "Unknown operation reference: #{package_or_ops_file_reference.inspect}"
256
+ end
257
+ end
258
+
259
+ # git_repo?("davidkellis/arborist") -> "https://github.com/davidkellis/arborist"
260
+ # returns the repo URL
261
+ def git_repo?(repo_reference)
262
+ candidate_repo_references = [
263
+ repo_reference,
264
+ repo_reference =~ /(\.(com|net|org|dev|io|local))\// && "https://#{repo_reference}",
265
+ repo_reference !~ /github\.com\// && repo_reference =~ /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}\/([\w\.@\:\-~]+)$/i && "https://github.com/#{repo_reference}" # this regex is from https://www.npmjs.com/package/github-username-regex and https://www.debuggex.com/r/H4kRw1G0YPyBFjfm
266
+ ].compact
267
+ working_repo_reference = candidate_repo_references.find {|reference| Git.ls_remote(reference) rescue nil }
268
+ working_repo_reference
269
+ end
270
+
271
+ def bundle_status
272
+ end
273
+
274
+ def bundle_update
275
+ bundler.update
276
+ end
277
+
278
+ def report_inventory(host_references, tags: nil)
279
+ selected_hosts = inventory(tags, host_references)
280
+
281
+ selected_hosts.each do |host|
282
+ puts host.summary(verbose?)
283
+ end
284
+ end
285
+
286
+ # tag_selection is an array of strings
287
+ def inventory(tag_selection = nil, host_references_override = nil)
288
+ host_references = host_references_override || @inventory_host_references
289
+ tags = @inventory_tag_selections + (tag_selection || [])
290
+ tags.uniq!
291
+
292
+ host_references = ["hosts.yml"] if (host_references.nil? || host_references.empty?) && File.exist?("hosts.yml")
293
+
294
+ hosts_files, host_strings = host_references.partition {|ref| File.exist?(ref) }
295
+ hosts_files = hosts_files.map {|file_path| HostsFile.new(file_path) }
296
+ untagged_hosts = host_strings.map(&:strip).uniq.map {|host| Host.new(host) }
297
+ inventory_file_hosts = hosts_files.reduce({}) do |host_map, hosts_file|
298
+ hosts_file.hosts.each do |host|
299
+ (host_map[host] ||= host).tag!(host.tags)
300
+ end
301
+
302
+ host_map
303
+ end.keys
304
+ all_hosts = untagged_hosts + inventory_file_hosts
305
+
306
+ selected_hosts = if tags.empty?
307
+ all_hosts
308
+ else
309
+ all_hosts.select do |host|
310
+ tags.all? {|t| host.tags.include? t }
311
+ end
312
+ end
313
+
314
+ selected_hosts.sort_by(&:to_s)
315
+ end
316
+
317
+ def unzip(zip_bundle_file = nil, output_dir = nil)
318
+ bundler.unzip(zip_bundle_file, output_dir)
319
+ end
320
+
321
+ def zip
322
+ tmpzip = pwd.join("tmpops.zip")
323
+ FileUtils.rm(tmpzip) if tmpzip.exist?
324
+ @zip_bundle_path ||= DirZipper.zip(pwd, tmpzip)
325
+ end
326
+
327
+ end
328
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # if brew is already installed, initialize this shell environment with brew
4
+ if [ -x "$(command -v /home/linuxbrew/.linuxbrew/bin/brew)" ]; then
5
+ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
6
+
7
+ # exit early if ruby already exists
8
+ if [ -x "$(command -v ruby)" ]; then
9
+ echo 'Ruby is already installed.' >&2
10
+ exit 0
11
+ fi
12
+ fi
13
+
14
+ OS=$(cat /etc/os-release | grep "^ID=")
15
+ if echo $OS | grep -q 'ubuntu'; then
16
+ # update package list
17
+ sudo apt update -qy
18
+
19
+ if [ -f /var/run/reboot-required ]; then
20
+ echo 'A system reboot is required!'
21
+ exit 1
22
+ fi
23
+
24
+ # there are probably some services that need restarting because they're using old libraries, so we'll just do the easy thing and reboot
25
+ sudo DEBIAN_FRONTEND=noninteractive apt install -yq needrestart
26
+
27
+ # install homebrew dependencies, per https://docs.brew.sh/Homebrew-on-Linux
28
+ sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq build-essential procps curl file git
29
+
30
+ # restart services that need it
31
+ sudo needrestart -q -r a
32
+ sudo needrestart -q -r a
33
+ sudo needrestart -q -r a
34
+ elif echo $OS | grep -q 'fedora'; then
35
+ sudo yum groupinstall -y 'Development Tools'
36
+ sudo yum install -y procps-ng curl file git
37
+ elif echo $OS | grep -q 'arch'; then
38
+ sudo pacman -Syu --noconfirm base-devel procps-ng curl file git
39
+ else
40
+ echo "unsupported OS"
41
+ exit 1
42
+ fi
43
+
44
+
45
+ # install homebrew
46
+ NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
47
+
48
+ # initialize brew in shell session
49
+ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
50
+
51
+ # install gcc, ruby, age
52
+ brew install gcc
53
+ brew install ruby
54
+ brew install age # https://github.com/FiloSottile/age
55
+
56
+ # install opswalrus gem
57
+ gem install opswalrus
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # update package list
4
+ sudo apt update -y
5
+
6
+ # update OS
7
+ # sudo DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confold" -o Dpkg::Options::="--force-confdef" dist-upgrade -q -y --allow-downgrades --allow-remove-essential --allow-change-held-packages
8
+
9
+ if [ -f /var/run/reboot-required ]; then
10
+ echo 'A system reboot is required!'
11
+ sudo reboot
12
+ fi