opswalrus 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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