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
data/lib/loom/shell.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Loom
2
+ module Shell
3
+
4
+ VerifyError = Class.new Loom::LoomError
5
+
6
+ def self.create(*args)
7
+ Loom::Shell::Core.new *args
8
+ end
9
+ end
10
+ end
11
+
12
+ require_relative "shell/all"
@@ -0,0 +1,10 @@
1
+ require_relative "harness_blob"
2
+ require_relative "harness_command_builder"
3
+
4
+ require_relative "cmd_result"
5
+ require_relative "cmd_wrapper"
6
+
7
+ require_relative "session"
8
+ require_relative "api"
9
+
10
+ require_relative "core"
@@ -0,0 +1,48 @@
1
+ module Loom::Shell
2
+
3
+ ##
4
+ # A facade for the shell API exposed to Loom files. This is the +loom+ object
5
+ # passed to patterns.
6
+ class Api
7
+
8
+ def initialize(shell)
9
+ @shell = shell
10
+ @mod_loader = shell.mod_loader
11
+ @dry_run = shell.dry_run
12
+ end
13
+
14
+ def dry_run?
15
+ @dry_run
16
+ end
17
+
18
+ def local
19
+ @shell.local.shell_api
20
+ end
21
+
22
+ def method_missing(name, *args, &block)
23
+ Loom.log.debug3(self) { "shell api => #{name} #{args} #{block}" }
24
+ # TODO: The relationship between shell and mod_loader seems leaky here, a
25
+ # Shell::Api should have a shell and not care about the mod_loader,
26
+ # currently it seems to violate Demeter. The shell should dispatch to the
27
+ # mod_loader only as an implementation detail. Otherwise this is harder to
28
+ # test.
29
+ @mod_loader.send name, @shell, *args, &block
30
+ end
31
+ end
32
+
33
+ class FakeApi < Api
34
+
35
+ # Fake Override
36
+ def initialize
37
+ @cmd_executions = []
38
+ @cmd_execution_args = []
39
+ end
40
+ attr_reader :cmd_executions, :cmd_execution_args
41
+
42
+ def method_missing(name, *args, &block)
43
+ @cmd_executions.push name
44
+ @cmd_execution_args.push args
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,33 @@
1
+ module Loom::Shell
2
+ class CmdResult
3
+ def initialize(command, stdout, stderr, exit_status, is_test, shell)
4
+ @command = command
5
+ @stdout = stdout
6
+ @stderr = stderr
7
+ @exit_status = exit_status
8
+ @is_test = is_test
9
+ @time = Time.now
10
+ @shell = shell
11
+ end
12
+
13
+ attr_reader :command, :stdout, :stderr, :exit_status, :time, :is_test
14
+
15
+ def success?
16
+ @exit_status == 0
17
+ end
18
+
19
+ def pipe(*cmd, fd: :stdout)
20
+ puts "stdout >>> " + @stdout.inspect
21
+ @shell.pipe [:"/bin/echo", "-e", @stdout], [*cmd]
22
+ end
23
+
24
+ def self.create_from_sshkit_command(cmd, is_test, shell)
25
+ CmdResult.new cmd.command,
26
+ cmd.full_stdout,
27
+ cmd.full_stderr,
28
+ cmd.exit_status,
29
+ is_test,
30
+ shell
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,164 @@
1
+ require "shellwords"
2
+
3
+ module Loom::Shell
4
+
5
+ class CmdWrapper
6
+
7
+ class << self
8
+ # Escapes a shell command.
9
+ # @param cmd [CmdWrapper|String]
10
+ # @return [String]
11
+ def escape(cmd)
12
+ if cmd.is_a? CmdWrapper
13
+ cmd.escape_cmd
14
+ else
15
+ Shellwords.escape(cmd)
16
+ end
17
+ end
18
+
19
+ # Wraps a command in another command. See `CmdWrapper.new'
20
+ # @param cmd_parts [CmdWrapper|String|Symbol]
21
+ # @param should_quote [Boolean]
22
+ def wrap_cmd(*cmd_parts, should_quote: false)
23
+ cmd_parts = cmd_parts.map do |parts|
24
+ if parts.respond_to? :cmd_parts
25
+ parts.cmd_parts
26
+ else
27
+ parts
28
+ end
29
+ end
30
+ CmdWrapper.new *cmd_parts.flatten, {
31
+ :should_quote => should_quote,
32
+ :is_wrapped => true
33
+ }
34
+ end
35
+ end
36
+
37
+ # @param cmd [Array<[#to_s]>] Command parts that will be shell escaped.
38
+ # @param :should_quote [Boolean] Whether wrapped commands should be quoted.
39
+ # @param :redirc [Array<CmdRedirect>] STDIO redirection for the command
40
+ # in quotes.
41
+ def initialize(*cmd, should_quote: false, is_wrapped: false, redirect: [])
42
+ @cmd_parts = cmd.flatten
43
+ @should_quote = should_quote
44
+ @is_wrapped = is_wrapped
45
+ @redirects = [redirect].flatten.compact
46
+ Loom.log.debug2(self) { "CmdWrapper.new {#{cmd}} => #{self.escape_cmd}" }
47
+ end
48
+
49
+ attr_reader :cmd_parts
50
+
51
+ # Shell escapes each part of `@cmd_parts` and joins them with spaces.
52
+ # @return [String]
53
+ def escape_cmd
54
+ escaped_cmd = escape_inner
55
+
56
+ cmd_with_redirects = [escaped_cmd].concat @redirects.map(&:to_s)
57
+ cmd_with_redirects.join " "
58
+ end
59
+ alias_method :to_s, :escape_cmd
60
+
61
+ # @param wrapped_cmd [String]
62
+ # @return [Array<#to_s>] The `wrapped_cmd` wrapped by `#escape_cmd`
63
+ def wrap(*wrapped_cmd)
64
+ wrapped_cmd =
65
+ CmdWrapper.wrap_cmd(*wrapped_cmd, should_quote: @should_quote)
66
+ CmdWrapper.new(self, wrapped_cmd)
67
+ end
68
+
69
+ private
70
+ def escape_inner
71
+ escaped_parts = escape_parts(@cmd_parts)
72
+
73
+ # Don't fuck with this unless you really want to fix it.
74
+ if @should_quote && @is_wrapped
75
+ double_escaped = escape_parts(escaped_parts).join " "
76
+
77
+ # Shellwords escapes spaces, but I'm wrapping this string in another set
78
+ # of quotes here, so it's unnecessary.
79
+ double_escaped.gsub!(/\\(\s)/, "\\1") while double_escaped.match(/\\\s/)
80
+
81
+ "\"#{double_escaped}\""
82
+ else
83
+ escaped_parts.join " "
84
+ end
85
+ end
86
+
87
+ # Maps each entry of #{cmd_parts} to the escaped form of itself, except if
88
+ # the part is frozen (like a Symbol)
89
+ # @param cmd_parts [Array<String|Symbol|CmdWrapper>]
90
+ # @return [Array<String|Symbol>]
91
+ def escape_parts(cmd_parts)
92
+ cmd_parts.map do |part|
93
+ part.cmd_parts rescue part
94
+ end.flatten
95
+
96
+ cmd_parts.map do |part|
97
+ unless part.frozen?
98
+ CmdWrapper.escape part
99
+ else
100
+ part
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ class CmdRedirect
107
+
108
+ class << self
109
+ def append_stdout(word)
110
+ CmdRedirect.new(word, mode: Mode::APPEND)
111
+ end
112
+ end
113
+
114
+ # See `man bash` under REDIRECTION
115
+ module Mode
116
+ INPUT = :input
117
+ OUTPUT = :output
118
+ APPEND = :append
119
+ OUTPUT_12 = :output_1_and_2
120
+ APPEND_12 = :append_1_and_2
121
+ end
122
+
123
+ def initialize(word, fd: nil, mode: Mode::OUTPUT)
124
+ @fd = fd
125
+ @word = word
126
+ @mode = mode
127
+ end
128
+
129
+ def to_s
130
+ case @mode
131
+ when Mode::INPUT
132
+ "%s<%s" % [@fd, @word]
133
+ when Mode::OUTPUT
134
+ "%s>%s" % [@fd, @word]
135
+ when Mode::APPEND
136
+ "%s>>%s" % [@fd, @word]
137
+ when Mode::OUTPUT_12
138
+ "&>%s" % [@word]
139
+ when Mode::APPEND_12
140
+ "&>>%s" % [@word]
141
+ else
142
+ raise "invalid shell redirection mode: #{@mode}"
143
+ end
144
+ end
145
+
146
+ end
147
+
148
+ class CmdPipeline
149
+ def initialize(piped_cmds)
150
+ @piped_cmds = piped_cmds
151
+ end
152
+
153
+ def to_s
154
+ @piped_cmds.map do |cmd|
155
+ if cmd.respond_to? :escape_cmd
156
+ cmd.escape_cmd
157
+ else
158
+ cmd
159
+ end
160
+ end.join " | "
161
+ end
162
+ end
163
+
164
+ end
@@ -0,0 +1,226 @@
1
+ require "forwardable"
2
+ require "shellwords"
3
+ require "sshkit"
4
+
5
+ module Loom::Shell
6
+
7
+ class Core
8
+
9
+ def initialize(mod_loader, sshkit_backend, dry_run=false)
10
+ @dry_run = dry_run
11
+ @mod_loader = mod_loader
12
+ @sshkit_backend = sshkit_backend
13
+
14
+ @session = Session.new
15
+ @shell_api = Api.new self
16
+
17
+ @cmd_wrappers = []
18
+ @sudo_users = []
19
+
20
+ # TODO: @sudo_dirs is a smelly workaround for not having a better
21
+ # understanding of sudo security policies and inheriting environments.
22
+ @sudo_dirs = []
23
+ end
24
+
25
+ attr_reader :session, :shell_api, :mod_loader, :dry_run
26
+
27
+ def local
28
+ @local ||= LocalShell.new @mod_loader, @session, @dry_run
29
+ end
30
+
31
+ def test(*cmd, check: :exit_status, **cmd_opts)
32
+ # TODO: is_test smells like a hack. I can't rely on Command#is_success?
33
+ # here (returned from execute) because I'm overriding it with :is_test =>
34
+ # true. Fix Command#is_success? to not be a lie.. that is a lazy hack for
35
+ # result reporting (I think the fix & feature) is to define Command
36
+ # objects and declare style of reporting & error code handling it
37
+ # has. Commands can be defined to ignore errors and just return their
38
+ # results.
39
+ execute *cmd, :is_test => true, **cmd_opts
40
+
41
+ case check
42
+ when :exit_status
43
+ @session.last.exit_status == 0
44
+ when :stderr
45
+ @session.last.stderr.empty?
46
+ else
47
+ raise "unknown test check => #{check}"
48
+ end
49
+ end
50
+
51
+ def verify(*check)
52
+ raise VerifyError, check unless test *check
53
+ end
54
+
55
+ def verify_which(command)
56
+ verify :which, command
57
+ end
58
+
59
+ def wrap(*wrapper, first: false, should_quote: true, &block)
60
+ raise "missing block for +wrap+" unless block_given?
61
+
62
+ cmd_wrapper = CmdWrapper.new(*wrapper, should_quote: should_quote)
63
+
64
+ if first
65
+ @cmd_wrappers.unshift(cmd_wrapper)
66
+ else
67
+ @cmd_wrappers.push(cmd_wrapper)
68
+ end
69
+
70
+ begin
71
+ yield
72
+ ensure
73
+ first ? @cmd_wrappers.shift : @cmd_wrappers.pop
74
+ end
75
+ end
76
+
77
+ def sudo(user=nil, *sudo_cmd, &block)
78
+ user ||= :root
79
+ Loom.log.debug1(self) { "sudo => #{user} #{sudo_cmd} #{block}" }
80
+
81
+ is_new_sudoer = @sudo_users.last.to_sym != user.to_sym rescue true
82
+
83
+ @sudo_dirs.push(capture :pwd)
84
+ @sudo_users.push << user if is_new_sudoer
85
+
86
+ sudo_wrapper = [:sudo, "-u", user, "--", "/bin/sh", "-c"]
87
+ sudo_cmd.compact!
88
+ begin
89
+ wrap *sudo_wrapper, :should_quote => true do
90
+ execute *sudo_cmd unless sudo_cmd.empty?
91
+ yield if block_given?
92
+ end
93
+ ensure
94
+ @sudo_users.pop if is_new_sudoer
95
+ @sudo_dirs.pop
96
+ end
97
+ end
98
+
99
+ def cd(path, &block)
100
+ Loom.log.debug1(self) { "cd => #{path} #{block}" }
101
+
102
+ # TODO: this might creates problems with relative paths, e.g.
103
+ # loom.cd foo => cd ./foo
104
+ # loom.sudo user => cd ./foo; sudo user
105
+ @sudo_dirs.push path
106
+ begin
107
+ @sshkit_backend.within path, &block
108
+ ensure
109
+ @sudo_dirs.pop
110
+ end
111
+ end
112
+
113
+ def capture(*cmd_parts)
114
+ if @dry_run
115
+ # TODO: I'm not sure what to do about this.
116
+ Loom.log.warn "`capture` during dry run won't do what you want"
117
+ end
118
+ execute *cmd_parts
119
+ @session.last.stdout.strip
120
+ end
121
+
122
+ def pipe(*cmds)
123
+ cmd = CmdWrapper.pipe *cmds.map { |*cmd| CmdWrapper.new *cmd }
124
+ execute cmd
125
+ end
126
+
127
+ def execute(*cmd_parts, is_test: false, **cmd_opts)
128
+ cmd_parts.compact!
129
+ raise "empty command passed to execute" if cmd_parts.empty?
130
+
131
+ result = if @dry_run
132
+ wrap :printf, :first => true do
133
+ cmd_result = execute_internal *cmd_parts, **cmd_opts
134
+ Loom.log.info do
135
+ "\t%s" % prompt_fmt(cmd_result.full_stdout.strip)
136
+ end
137
+ cmd_result
138
+ end
139
+ else
140
+ execute_internal *cmd_parts, **cmd_opts
141
+ end
142
+ @session << CmdResult.create_from_sshkit_command(result, is_test, self)
143
+
144
+ Loom.log.debug @session.last.stdout unless @session.last.stdout.empty?
145
+ Loom.log.debug @session.last.stderr unless @session.last.stderr.empty?
146
+ @session.last
147
+ end
148
+ alias_method :exec, :execute
149
+
150
+ protected
151
+ def prompt_label
152
+ # TODO: get the real hostname.
153
+ "remote"
154
+ end
155
+
156
+ private
157
+ def prompt_fmt(*cmd_parts)
158
+ output = Shellwords.join(cmd_parts).gsub /\\/, ''
159
+ "[%s]:$ %s" % [prompt_label, output]
160
+ end
161
+
162
+ def execute_internal(*cmd_parts, piped_cmds: [])
163
+ primary_cmd = create_command *cmd_parts
164
+ piped_cmds = piped_cmds.map { |cmd_parts| CmdWrapper.new *cmd_parts }
165
+
166
+ cmd = CmdPipeline.new([primary_cmd].concat(piped_cmds)).to_s
167
+ # Tests if the command looks like "echo\ hi", the trailing slash after
168
+ # echo indicates that just 1 big string was passed in and we can't really
169
+ # isolate the execuatable part of the command. This might be fine, but
170
+ # it's better to be strict now and relax this later if it's OK.
171
+ if cmd.match /^[\w\-\[]+\\/i
172
+ raise "use array parts for command escaping => #{cmd}"
173
+ end
174
+
175
+ Loom.log.debug1(self) { "executing => #{cmd}" }
176
+
177
+ # This is a big hack to get access to the SSHKit command
178
+ # object and avoid the automatic errors thrown on non-zero
179
+ # error codes
180
+ @sshkit_backend.send(
181
+ :create_command_and_execute,
182
+ cmd,
183
+ :raise_on_non_zero_exit => false)
184
+ end
185
+
186
+ # Here be dragons.
187
+ # @return [String|Loom::Shell::CmdWrapper]
188
+ def create_command(*cmd_parts)
189
+ cmd_wrapper = if cmd_parts.is_a? CmdWrapper
190
+ cmd_parts
191
+ else
192
+ Loom.log.debug3(self) { "new cmd from parts => #{cmd_parts}" }
193
+ CmdWrapper.new *cmd_parts
194
+ end
195
+
196
+ # Useful for sudo, dry runs, timing a set of commands, or
197
+ # timeout... anytime you want to prefix a group of commands. Reverses the
198
+ # array to wrap from inner most call to `#{wrap}` to outer most.
199
+ cmd = @cmd_wrappers.reverse.reduce(cmd_wrapper) do |cmd_or_wrapper, wrapper|
200
+ Loom.log.debug3(self) { "wrapping cmds => #{wrapper} => #{cmd_or_wrapper}"}
201
+ wrapper.wrap cmd_or_wrapper
202
+ end
203
+
204
+ unless @sudo_dirs.empty? || @dry_run
205
+ cmd = "cd #{@sudo_dirs.last}; " << cmd.to_s
206
+ end
207
+ cmd
208
+ end
209
+
210
+ # A shell object restricted to localhost.
211
+ class LocalShell < Core
212
+ def initialize(mod_loader, session, dry_run)
213
+ super mod_loader, SSHKit::Backend::Local.new, dry_run
214
+ @session = session
215
+ end
216
+
217
+ def local
218
+ raise 'already in a local shell'
219
+ end
220
+
221
+ def prompt_label
222
+ "local"
223
+ end
224
+ end
225
+ end
226
+ end