loom-core 0.0.1

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