loom-core 0.0.1

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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +99 -0
  6. data/Guardfile +54 -0
  7. data/Rakefile +6 -0
  8. data/bin/loom +185 -0
  9. data/lib/env/development.rb +1 -0
  10. data/lib/loom.rb +44 -0
  11. data/lib/loom/all.rb +20 -0
  12. data/lib/loom/config.rb +106 -0
  13. data/lib/loom/core_ext.rb +37 -0
  14. data/lib/loom/dsl.rb +60 -0
  15. data/lib/loom/facts.rb +13 -0
  16. data/lib/loom/facts/all.rb +2 -0
  17. data/lib/loom/facts/fact_file_provider.rb +86 -0
  18. data/lib/loom/facts/fact_set.rb +138 -0
  19. data/lib/loom/host_spec.rb +32 -0
  20. data/lib/loom/inventory.rb +124 -0
  21. data/lib/loom/logger.rb +141 -0
  22. data/lib/loom/method_signature.rb +174 -0
  23. data/lib/loom/mods.rb +4 -0
  24. data/lib/loom/mods/action_proxy.rb +105 -0
  25. data/lib/loom/mods/all.rb +3 -0
  26. data/lib/loom/mods/mod_loader.rb +80 -0
  27. data/lib/loom/mods/module.rb +113 -0
  28. data/lib/loom/pattern.rb +15 -0
  29. data/lib/loom/pattern/all.rb +7 -0
  30. data/lib/loom/pattern/definition_context.rb +74 -0
  31. data/lib/loom/pattern/dsl.rb +176 -0
  32. data/lib/loom/pattern/hook.rb +28 -0
  33. data/lib/loom/pattern/loader.rb +48 -0
  34. data/lib/loom/pattern/reference.rb +71 -0
  35. data/lib/loom/pattern/reference_set.rb +169 -0
  36. data/lib/loom/pattern/result_reporter.rb +77 -0
  37. data/lib/loom/runner.rb +209 -0
  38. data/lib/loom/shell.rb +12 -0
  39. data/lib/loom/shell/all.rb +10 -0
  40. data/lib/loom/shell/api.rb +48 -0
  41. data/lib/loom/shell/cmd_result.rb +33 -0
  42. data/lib/loom/shell/cmd_wrapper.rb +164 -0
  43. data/lib/loom/shell/core.rb +226 -0
  44. data/lib/loom/shell/harness_blob.rb +26 -0
  45. data/lib/loom/shell/harness_command_builder.rb +50 -0
  46. data/lib/loom/shell/session.rb +25 -0
  47. data/lib/loom/trap.rb +44 -0
  48. data/lib/loom/version.rb +3 -0
  49. data/lib/loomext/all.rb +4 -0
  50. data/lib/loomext/corefacts.rb +6 -0
  51. data/lib/loomext/corefacts/all.rb +8 -0
  52. data/lib/loomext/corefacts/facter_provider.rb +24 -0
  53. data/lib/loomext/coremods.rb +5 -0
  54. data/lib/loomext/coremods/all.rb +13 -0
  55. data/lib/loomext/coremods/exec.rb +50 -0
  56. data/lib/loomext/coremods/files.rb +104 -0
  57. data/lib/loomext/coremods/net.rb +33 -0
  58. data/lib/loomext/coremods/package/adapter.rb +100 -0
  59. data/lib/loomext/coremods/package/package.rb +62 -0
  60. data/lib/loomext/coremods/user.rb +82 -0
  61. data/lib/loomext/coremods/vm.rb +0 -0
  62. data/lib/loomext/coremods/vm/all.rb +6 -0
  63. data/lib/loomext/coremods/vm/vbox.rb +84 -0
  64. data/loom.gemspec +39 -0
  65. data/loom/inventory.yml +13 -0
  66. data/scripts/harness.sh +242 -0
  67. data/spec/loom/host_spec_spec.rb +101 -0
  68. data/spec/loom/inventory_spec.rb +154 -0
  69. data/spec/loom/method_signature_spec.rb +275 -0
  70. data/spec/loom/pattern/dsl_spec.rb +207 -0
  71. data/spec/loom/shell/cmd_wrapper_spec.rb +239 -0
  72. data/spec/loom/shell/harness_blob_spec.rb +42 -0
  73. data/spec/loom/shell/harness_command_builder_spec.rb +36 -0
  74. data/spec/runloom.sh +35 -0
  75. data/spec/scripts/harness_spec.rb +385 -0
  76. data/spec/spec_helper.rb +94 -0
  77. data/spec/test.loom +370 -0
  78. data/spec/test_loom_spec.rb +57 -0
  79. metadata +287 -0
@@ -0,0 +1,62 @@
1
+ require "forwardable"
2
+ require_relative "adapter"
3
+
4
+ module LoomExt::CoreMods
5
+ class Package < Loom::Mods::Module
6
+
7
+ UnsupportedPackageManager = Class.new Loom::Mods::ModActionError
8
+
9
+ attr_reader :pkg_adapter
10
+
11
+ register_mod :pkg
12
+
13
+ def init_action
14
+ @pkg_adapter = default_adapter
15
+ end
16
+
17
+ def get(adapter)
18
+ case adapter.to_sym
19
+ when :dnf
20
+ DnfAdapter.new loom
21
+ when :rpm
22
+ RpmAdapter.new loom
23
+ when :apt
24
+ AptAdapter.new loom
25
+ when :dpkg
26
+ DpkgAdapter.new loom
27
+ when :gem
28
+ GemAdapter.new loom
29
+ else
30
+ raise UnsupportedPackageManager, adapter
31
+ end
32
+ end
33
+ alias_method :[], :get
34
+
35
+ def default_adapter
36
+ if loom.test :which, "dnf"
37
+ DnfAdapter.new loom
38
+ elsif loom.test :which, "rpm"
39
+ RpmAdapter.new loom
40
+ elsif loom.test :which, "apt"
41
+ AptAdapter.new loom
42
+ elsif loom.test :which, "dpkg"
43
+ DpkgAdapter.new loom
44
+ else
45
+ raise UnsupportedPackageManager
46
+ end
47
+ end
48
+
49
+ module Actions
50
+ extend Forwardable
51
+ def_delegators :@pkg_adapter, :installed?, :install, :uninstall,
52
+ :update_cache, :upgrade, :ensure_installed
53
+
54
+ def [](*args)
55
+ get(*args)
56
+ end
57
+ end
58
+
59
+ import_actions Package::Actions
60
+
61
+ end
62
+ end
@@ -0,0 +1,82 @@
1
+ module LoomExt::CoreMods
2
+ class User < Loom::Mods::Module
3
+
4
+ SudoersDNoExistError = Class.new Loom::Mods::ModActionError
5
+ SudoersDNotIncluded = Class.new Loom::Mods::ModActionError
6
+
7
+ register_mod :user
8
+ required_commands :useradd, :userdel, :getent
9
+
10
+ SUDOERS_FILE = "/etc/sudoers"
11
+ SUDOERS_DIR = "/etc/sudoers.d"
12
+ LOOM_SUDOERS_FILE = SUDOERS_DIR + "/90-loom-sudoers"
13
+
14
+ def user_exists?(user)
15
+ shell.test :getent, :passwd, user
16
+ end
17
+
18
+ def includes_sudoers?
19
+ shell.test :grep, :"-e", :"'^#includedir #{SUDOERS_DIR}$'", SUDOERS_FILE
20
+ end
21
+
22
+ def sudoersd_exists?
23
+ shell.test :test, %Q[-d #{SUDOERS_DIR}]
24
+ end
25
+
26
+ module Actions
27
+ def add(user,
28
+ home_dir: nil,
29
+ login_shell: "/bin/bash",
30
+ uid: nil,
31
+ gid: nil,
32
+ groups: [],
33
+ is_system_user: nil)
34
+ if user_exists? user
35
+ Loom.log.warn "add_user skipping existing user => #{user}"
36
+ return
37
+ end
38
+
39
+ flags = []
40
+ flags << ["--home-dir", home_dir] if home_dir
41
+ flags << ["--create-home"] if home_dir
42
+ flags << ["--shell", login_shell] if login_shell
43
+ flags << ["--uid", uid] if uid
44
+ flags << ["--gid", gid] if gid
45
+ flags << ["--groups", groups] unless groups.empty?
46
+ flags << "--system" if is_system_user
47
+
48
+ loom.exec :useradd, flags, user
49
+ end
50
+
51
+ def add_system_user(user, **user_fields)
52
+ if user_exists? user
53
+ Loom.log.warn "add_system_user skipping existing user => #{user}"
54
+ return
55
+ end
56
+
57
+ add user, is_system_user: true, login_shell: "/bin/false", **user_fields
58
+ end
59
+
60
+ def remove(user)
61
+ unless user_exists? user
62
+ Loom.log.warn "remove_user skipping non-existant user => #{user}"
63
+ return
64
+ end
65
+
66
+ loom.exec :userdel, "-r", user
67
+ end
68
+
69
+ def make_sudoer(user)
70
+ raise SudoersDNotIncluded unless includes_sudoers?
71
+ raise SudoersDNoExistError unless sudoersd_exists?
72
+
73
+ sudoer_conf = :"#{user} ALL=(ALL) NOPASSWD:ALL"
74
+
75
+ loom.files(LOOM_SUDOERS_FILE).append sudoer_conf
76
+ loom.exec :chmod, "0440", LOOM_SUDOERS_FILE
77
+ end
78
+ end
79
+ end
80
+
81
+ User.import_actions User::Actions
82
+ end
File without changes
@@ -0,0 +1,6 @@
1
+ module LoomExt::CoreMods
2
+ module VM
3
+ end
4
+ end
5
+
6
+ require_relative "vbox"
@@ -0,0 +1,84 @@
1
+ module LoomExt::CoreMods::VM
2
+ class Virtualbox < Loom::Mods::Module
3
+
4
+ DuplicateVMImport = Class.new Loom::ExecutionError
5
+ UnknownVM = Class.new Loom::ExecutionError
6
+
7
+ register_mod :vbox
8
+ required_commands :vboxmanage
9
+
10
+ module Actions
11
+ def check_exists(vm)
12
+ loom.test "vboxmanage showvminfo #{vm}".split
13
+ end
14
+
15
+ def check_running(vm)
16
+ loom.test "vboxmanage list runningvms".split, :piped_cmds => [
17
+ "grep \"#{vm}\"".split
18
+ ]
19
+ end
20
+
21
+ def list
22
+ loom << "vboxmanage list vms".split
23
+ end
24
+
25
+ def snapshot(vm, action: :take, snapshot_name: nil)
26
+ raise UnknownVM, vm unless check_exists(vm)
27
+
28
+ cmd = ["vboxmanage snapshot #{vm} #{action}"]
29
+ cmd << snapshot_name if snapshot_name
30
+ cmd = cmd.join " "
31
+
32
+ loom << cmd.split
33
+ end
34
+
35
+ def import(ova_file, vm, disk, take_snapshot: true)
36
+ raise DuplicateVMImport, vm if check_exists(vm)
37
+
38
+ loom << "vboxmanage import #{ova_file} \
39
+ --vsys 0 --vmname #{vm} \
40
+ --vsys 0 --unit 12 --disk '#{disk}'".split
41
+
42
+ if take_snapshot
43
+ snapshot vm, action: :take, snapshot_name: "#{vm}:import"
44
+ end
45
+ end
46
+
47
+ def clone(src_vm, dst_vm, options: :link, snapshot: nil, take_snapshot: true)
48
+ raise DuplicateVMImport, "VM already exists => #{dst_vm}" if check_exists(dst_vm)
49
+ raise UnknownVM, src_vm unless check_exists(src_vm)
50
+
51
+ cmd = ["vboxmanage clonevm #{src_vm}"]
52
+ cmd << "--snapshot #{snapshot}" if snapshot
53
+ cmd << "--options #{options}" if options
54
+ cmd << "--name #{dst_vm}"
55
+ cmd << "--register"
56
+ cmd = cmd.join " "
57
+
58
+ loom << cmd.split
59
+
60
+ if take_snapshot
61
+ snapshot dst_vm, action: :take, snapshot_name: "#{dst_vm}:clone"
62
+ end
63
+ end
64
+
65
+ def up(vm)
66
+ unless check_running(vm)
67
+ loom << "vboxmanage startvm #{vm} --type headless".split
68
+ else
69
+ Loom.log.warn "VM #{vm} already running, nothing to do"
70
+ end
71
+ end
72
+
73
+ def down(vm)
74
+ if check_running(vm)
75
+ loom << "vboxmanage controlvm #{vm} acpipowerbutton".split
76
+ else
77
+ Loom.log.warn "VM #{vm} not running, nothing to do"
78
+ end
79
+ end
80
+ end
81
+
82
+ import_actions Actions
83
+ end
84
+ end
data/loom.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ $LOAD_PATH.push File.expand_path '../lib/', __FILE__
2
+ require 'loom/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'loom-core'
6
+ s.description = 'Repeatable management of remote hosts over SSH'
7
+ s.summary = s.description
8
+ s.version = Loom::VERSION
9
+ s.license = 'MIT'
10
+ s.authors = ['Erick Johnson']
11
+ s.email = 'ejohnson82@gmail.com'
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- spec/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
16
+ s.require_paths = %w[lib]
17
+
18
+ s.add_dependency 'sshkit', '~> 1.11'
19
+ s.add_dependency 'commander', '~> 4.4'
20
+
21
+ # Need net-ssh beta and its explicit requirements until for ed25519
22
+ # elliptic curve key support
23
+ # https://github.com/net-ssh/net-ssh/issues/214
24
+
25
+ # grrr.. 4.x.beta won't work in `gem install` until the official
26
+ # release due to net-scp gem dependencies.
27
+ # I can manually `gem install net-ssh --version 4.0.0.beta3` for now.
28
+ # s.add_dependency 'net-ssh', '>= 4.0.0.beta3'
29
+ s.add_dependency 'net-ssh', '>= 3'
30
+ s.add_dependency 'rbnacl-libsodium', '1.0.10'
31
+ s.add_dependency 'bcrypt_pbkdf', '1.0.0.alpha1'
32
+
33
+ s.add_development_dependency 'bundler', '~> 1.13'
34
+ s.add_development_dependency 'rake', '~> 11.3'
35
+ s.add_development_dependency 'rspec', '~> 3.5'
36
+ s.add_development_dependency 'guard-rspec', '~> 4.7'
37
+ s.add_development_dependency 'pry', '~> 0.10'
38
+ s.add_development_dependency 'pry-byebug'
39
+ end
@@ -0,0 +1,13 @@
1
+ ---
2
+ - ejjohnson.org
3
+
4
+ - :local-vms:
5
+ - vm-ubuntu-db
6
+ - vm-ubuntu-base
7
+ - :digitalocean:
8
+ - ejjohnson.net
9
+ # - vos.io
10
+ - quotes.vos.io
11
+ - :vos.io:
12
+ - quotes.vos.io
13
+ # - vos.io
@@ -0,0 +1,242 @@
1
+ # The Harness script for encoding, checksum'ing and running loom
2
+ # patterns.
3
+ #
4
+ # The point of the harness is to safely encode arbitrary commands as
5
+ # base64 strings and execute them in another shell, usually on a
6
+ # remote machine over SSH. The flow for running the harness is:
7
+ #
8
+ # 1. base64 encode an arbitrary shell script, this is the encoded
9
+ # script
10
+ # 2. get a checksum for the encoded script
11
+ # 3. send the encoded script and checksum to a shell in another
12
+ # process (local or remote) to invoke the encoded script via this
13
+ # harness script
14
+ #
15
+ # Given 2 hosts, [local] and [remote] the process looks like this:
16
+ #
17
+ # [local]$ encoded=$(./scripts/harness.sh --print_base64 - <<'EOS'
18
+ # echo my sweet script
19
+ # EOS
20
+ # )
21
+ # [local]$ checksum=$(./scripts/harness --print_checksum $encoded)
22
+ #
23
+ # ... SCP harness.sh to some/path on remote ...
24
+ #
25
+ # [local]$ ssh user@remote \
26
+ # some/path/harness.sh --run - $checksum <<EOS
27
+ # $encoded
28
+ # EOS
29
+ #
30
+ # There are 2 different shells that the harness deals with. The
31
+ # harness shell, and the command shell.
32
+ #
33
+ # The harness shell is the shell used to run the harness script (this
34
+ # file). Only POSIX features are supported in the harness
35
+ # script. Officially, `bash`, `bash --posix`, and `dash` are suported
36
+ # via the specs, (see spec/scripts/harness_spec.rb). Unofficially, any
37
+ # POSIX compliant shell should work.
38
+ #
39
+ # The command shell is the shell used by the harness to execute the
40
+ # encoded script. By default the command shell is `/bin/sh`. The
41
+ # command shell can be whatever you choose by passing an additonal
42
+ # parameter to `harness.sh --run`. For example, to run the encoded
43
+ # script in dash:
44
+ #
45
+ # [local]$ harness.sh --run - $checksum --cmd_shell /bin/dash <<EOS
46
+ # $encoded
47
+ # EOS
48
+ #
49
+ # To run the harness script in bash POSIX mode and the command script
50
+ # in plain old bash, the following will work:
51
+ #
52
+ # [local]$ (bash --posix -) <<HARNESS_EOS
53
+ # harness.sh --run - $checksum --cmd_shell /bin/bash <<COMMAND_EOS
54
+ # $encoded
55
+ # COMMAND_EOS
56
+ # HARNESS_EOS
57
+ #
58
+ # Commands will be recored as they are executed in the record file. By
59
+ # default the record file is /dev/null. To use a record file pass the
60
+ # record_file argument to --run, e.g.:
61
+ #
62
+ # harness.sh --run - $checksum --record_file /opt/loom/cmds <<CMDS...
63
+ #
64
+
65
+ declare -r DEFAULT_COMMAND_SHELL="/bin/sh"
66
+ declare -r DEFAULT_RECORD_FILE="/dev/null"
67
+
68
+ declare -r TRUE=0
69
+ declare -r FALSE=1
70
+
71
+ declare -r SUCCESS=0
72
+
73
+ declare -r EXIT_INVALID_BASE64=9
74
+ declare -r EXIT_BAD_CHECKSUM=8
75
+ declare -r EXIT_MISSING_ARG=2
76
+ declare -r EXIT_GENERIC=1
77
+
78
+ exit_with_usage() {
79
+ script=$(basename "$0")
80
+ echo "Usages:"
81
+ echo " ${script} --check <base64_blob|-> <golden_checksum>"
82
+ echo " ${script} --run <base64_blob|-> <golden_checksum> \\"
83
+ echo " [--cmd_shell shell] \\"
84
+ echo " [--record_file record_file]"
85
+ echo " ${script} --print_checksum <base64_blob|->"
86
+ echo " ${script} --print_base64 <raw_cmds|->"
87
+ exit $EXIT_GENERIC
88
+ }
89
+
90
+ ##
91
+ # If $0 equals "-", then consume and return STDIN, otherwise return
92
+ # the value.
93
+ value_or_stdin() {
94
+ local value="${1}"
95
+
96
+ if [ "${value}" = "-" ]; then
97
+ echo "read stdin" 1>&2
98
+ (cat)<&0
99
+ else
100
+ echo "read value arg" 1>&2
101
+ echo -n $value
102
+ fi
103
+ }
104
+
105
+ base64_encode_cmds() {
106
+ local raw_cmds="$1"
107
+ echo $(base64 -w0 <<BASE64_EOF
108
+ ${raw_cmds}
109
+ BASE64_EOF
110
+ )
111
+ }
112
+
113
+ validate_base64_blob() {
114
+ local unknown_blob="$1"
115
+ (base64 -d <<BASE64_EOF
116
+ ${unknown_blob}
117
+ BASE64_EOF
118
+ ) > /dev/null
119
+ if [ ! "$?" -eq 0 ]; then
120
+ exit $EXIT_INVALID_BASE64
121
+ fi
122
+ }
123
+
124
+ validate_arg_is_present() {
125
+ arg="$1"
126
+ msg="$2"
127
+ if [ -z "${arg}" ]; then
128
+ echo "${msg}" 1>&2
129
+ exit $EXIT_MISSING_ARG
130
+ fi
131
+ }
132
+
133
+ print_checksum() {
134
+ local chksum_blob="$1"
135
+
136
+ echo "checksum'ing base64 blob: +${chksum_blob}+" 1>&2
137
+ echo $(sha1sum - <<CHECKSUM_EOF | cut -d' ' -f1
138
+ ${chksum_blob}
139
+ CHECKSUM_EOF
140
+ )
141
+ }
142
+
143
+ check_cmds() {
144
+ local base64_blob="$1"
145
+ local golden_sha1="$2"
146
+ local actual_sha1=$(print_checksum "${base64_blob}")
147
+
148
+ test "${golden_sha1}" = "${actual_sha1}"
149
+ }
150
+
151
+ run_cmds() {
152
+ local base64_blob="$1"
153
+ local cmd_shell="${2:-$DEFAULT_COMMAND_SHELL}"
154
+ local record_file="${3:-$DEFAULT_RECORD_FILE}"
155
+ (
156
+ base64 -d | tee -a ${record_file} | ${cmd_shell} -
157
+ ) <<RUN_EOS
158
+ ${base64_blob}
159
+ RUN_EOS
160
+ }
161
+
162
+ main() {
163
+ set -xv
164
+ local flag="$1"
165
+ local should_run=$FALSE
166
+ shift
167
+
168
+ if [ -z "${flag}" ]; then
169
+ exit_with_usage
170
+ fi
171
+
172
+ case $flag in
173
+ --print_base64)
174
+ declare -r raw_cmds=$(value_or_stdin "$1")
175
+ declare -r base64_blob=$(base64_encode_cmds "${raw_cmds}")
176
+ validate_base64_blob "${base64_blob}"
177
+
178
+ printf $base64_blob
179
+ exit $SUCCESS
180
+ ;;
181
+ --print_checksum)
182
+ declare -r base64_blob=$(value_or_stdin "$1")
183
+ validate_base64_blob "${base64_blob}"
184
+
185
+ printf $(print_checksum "${base64_blob}")
186
+ exit $SUCCESS
187
+ ;;
188
+ --check)
189
+ declare -r base64_blob=$(value_or_stdin "$1")
190
+ declare -r golden_sha1="$2"
191
+ shift
192
+ shift
193
+ ;;
194
+ --run)
195
+ should_run=$TRUE
196
+ declare -r base64_blob=$(value_or_stdin "$1")
197
+ declare -r golden_sha1="$2"
198
+ shift
199
+ shift
200
+
201
+ while (( "$#" >= 2 )); do
202
+ case "$1" in
203
+ --cmd_shell)
204
+ declare -r cmd_shell="$2"
205
+ shift
206
+ shift
207
+ ;;
208
+ --record_file)
209
+ declare -r record_file="$2"
210
+ shift
211
+ shift
212
+ ;;
213
+ *)
214
+ echo "unknown arg for --run: ${1}" 1>&2
215
+ exit_with_usage
216
+ shift
217
+ ;;
218
+ esac
219
+ shift
220
+ done
221
+ ;;
222
+ *)
223
+ exit_with_usage
224
+ ;;
225
+ esac
226
+
227
+ validate_arg_is_present "${base64_blob}" "missing base64_blob"
228
+ validate_arg_is_present "${golden_sha1}" "missing golden_sha1"
229
+ validate_base64_blob "${base64_blob}"
230
+
231
+ if ! (check_cmds "${base64_blob}" "${golden_sha1}"); then
232
+ echo "checksum failed, expected ${golden_sha1}" 1>&2
233
+ exit $EXIT_BAD_CHECKSUM
234
+ fi
235
+
236
+ if [ "${should_run}" -eq $TRUE ]; then
237
+ echo "running commands" 1>&2
238
+ run_cmds "${base64_blob}" "${cmd_shell}" "${record_file}"
239
+ fi
240
+ }
241
+
242
+ main "$@"