loom-core 0.0.5 → 0.0.6

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/bin/loom +5 -1
  4. data/lib/loom/config.rb +10 -3
  5. data/lib/loom/facts/fact_set.rb +7 -1
  6. data/lib/loom/mods/module.rb +3 -4
  7. data/lib/loom/pattern/definition_context.rb +1 -1
  8. data/lib/loom/pattern/dsl.rb +113 -32
  9. data/lib/loom/pattern/reference_set.rb +7 -7
  10. data/lib/loom/runner.rb +8 -0
  11. data/lib/loom/shell/api.rb +2 -0
  12. data/lib/loom/version.rb +1 -1
  13. data/lib/loomext/corefacts/system_info_provider.rb +2 -1
  14. data/lib/loomext/coremods/all.rb +2 -1
  15. data/lib/loomext/coremods/exec.rb +10 -0
  16. data/lib/loomext/coremods/git.rb +19 -0
  17. data/lib/loomext/coremods/package/adapter.rb +1 -1
  18. data/lib/loomext/coremods/systemd.rb +1 -0
  19. data/lib/loomext/coremods/systemd/all.rb +2 -0
  20. data/lib/loomext/coremods/{system.rb → systemd/systemd.rb} +19 -5
  21. data/lib/loomext/coremods/systemd/systemd_units.rb +69 -0
  22. data/scripts/harness.sh +1 -0
  23. data/spec/.loom/error_handling.loom +15 -0
  24. data/spec/.loom/fail.loom +20 -0
  25. data/spec/.loom/files.loom +39 -0
  26. data/spec/.loom/net.loom +21 -0
  27. data/spec/.loom/pattern_context.loom +78 -0
  28. data/spec/.loom/pkg.loom +33 -0
  29. data/spec/.loom/shell.loom +46 -0
  30. data/spec/.loom/test.loom +73 -0
  31. data/spec/.loom/user.loom +29 -0
  32. data/spec/.loom/vms.loom +22 -0
  33. data/spec/loom/facts/fact_set_spec.rb +57 -0
  34. data/spec/loom/pattern/dsl_spec.rb +58 -9
  35. data/spec/loom/shell/harness_blob_spec.rb +1 -1
  36. data/spec/loom/shell/harness_command_builder_spec.rb +1 -1
  37. data/spec/loomext/coremods/systemd_spec.rb +31 -0
  38. data/spec/runloom.sh +12 -19
  39. data/spec/scripts/harness_spec.rb +1 -1
  40. data/spec/shared/loom_internals_helper.rb +41 -0
  41. data/spec/spec_helper.rb +5 -1
  42. data/spec/systemd.loom +22 -0
  43. data/spec/test_loom_spec.rb +95 -17
  44. data/test +2 -0
  45. metadata +26 -5
  46. data/spec/test.loom +0 -370
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4dd2b1e4959e4556b67f1369c3e8de87bdca21c0082ec446d629319cc58ee748
4
- data.tar.gz: edb2d6b2f14a01549b6b75820cf3d456d401fb396f7c13469b812922ae700e2b
3
+ metadata.gz: 11bd9487c4e4752e294000493fe1fe04537f97bfcea9385b1cf16e90c2c79a71
4
+ data.tar.gz: 926227cb27d65b64103dd0f541acb1f523afdfac2df832180450f1aaaff4397f
5
5
  SHA512:
6
- metadata.gz: 8459bc0b442c21f59217a8d4646418c17a82a77fa0321f56f1877e8892bc84be56ee16043f9fc362acd437b713296c66f3a8542df0fe9be64c9a08070453bf55
7
- data.tar.gz: 3fb25060598c49d64efcc60e23a44e1ca927304ae861ddf45baa3ded0481da9da8d0f40f358b7c2e8df4c6ec2a1bf9cb2f017285cb0cf63a8ade311d7b149282
6
+ metadata.gz: 687255010d7a4c709cb95b2022151a95175ae88decb368cfac503dd3bfdb24c677abdbd2ef055f16a80a783bad2d24762ab856bf20c085ec08d1f51734c78c01
7
+ data.tar.gz: b789683f5aefd5a1997536e3f930f143bb7c12fc24be4e40d10fb0147e0c997c5af84798fad9e2da79d3cdbb7f5fe4a854d3f981f6deabe43ff08ab0092e3c68
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- loom-core (0.0.5)
4
+ loom-core (0.0.6)
5
5
  bcrypt_pbkdf (>= 1.0, < 2.0)
6
6
  commander (~> 4.4)
7
7
  ed25519 (>= 1.0, < 2.0)
data/bin/loom CHANGED
@@ -65,9 +65,13 @@ EOS
65
65
  Loom.configure { |c| c.inventory_all_hosts = flag }
66
66
  end
67
67
 
68
+ global_option "-L", "Adds localhost to the active inventory" do |flag|
69
+ Loom.configure { |c| c.inventory_hosts << "localhost" }
70
+ end
71
+
68
72
  global_option "-H", "--hosts host1,h2,h3", Array,
69
73
  "Adds HOSTS to the active inventory" do |hosts|
70
- Loom.configure { |c| c.inventory_hosts = hosts }
74
+ Loom.configure { |c| c.inventory_hosts.concat hosts }
71
75
  end
72
76
 
73
77
  global_option "-G", "--groups group1,g2,g3", Array,
@@ -12,6 +12,12 @@ module Loom
12
12
  CONFIG_VARS = {
13
13
  :loom_search_paths => ['/etc/loom', File.join(ENV['HOME'], '.loom'), './.loom'],
14
14
  :loom_files => ['site.loom'],
15
+ :loom_file_patterns => ['*.loom'],
16
+
17
+ :loomfile_autoloads => [
18
+ 'loomext/corefacts',
19
+ 'loomext/coremods',
20
+ ],
15
21
 
16
22
  :inventory_all_hosts => false,
17
23
  :inventory_hosts => [],
@@ -49,6 +55,8 @@ module Loom
49
55
  end
50
56
  alias_method :dump, :to_yaml # aliased to dump for debugging purposes
51
57
 
58
+ # TODO: disallow CONFIG_VAR properties named after Config methods.... like
59
+ # files. this is shitty, but I don't want to do a larger change.
52
60
  def files
53
61
  @file_manager
54
62
  end
@@ -68,11 +76,10 @@ module Loom
68
76
  private
69
77
  class FileManager
70
78
 
71
- LOOM_FILE_PATTERNS = ["*.loom"]
72
-
73
79
  def initialize(config)
74
80
  @loom_search_paths = config.loom_search_paths
75
81
  @loom_files = config.loom_files
82
+ @loom_file_patterns = config.loom_file_patterns
76
83
  end
77
84
 
78
85
  def find(glob_patterns)
@@ -80,7 +87,7 @@ module Loom
80
87
  end
81
88
 
82
89
  def loom_files
83
- [@loom_files + search_loom_paths(LOOM_FILE_PATTERNS)].flatten.uniq
90
+ [@loom_files + search_loom_paths(@loom_file_patterns)].flatten.uniq
84
91
  end
85
92
 
86
93
  private
@@ -1,5 +1,11 @@
1
1
  module Loom::Facts
2
2
 
3
+ class << self
4
+ def is_empty?(fact_set)
5
+ EMPTY.equal? fact_set
6
+ end
7
+ end
8
+
3
9
  EMPTY = {}
4
10
  class << EMPTY
5
11
  def [](*args)
@@ -104,7 +110,7 @@ NB
104
110
  v = @fact_map[fact_name.to_sym]
105
111
  dup = v.dup rescue v
106
112
  if dup.nil?
107
- EMPTY
113
+ Loom::Facts::EMPTY
108
114
  else
109
115
  dup
110
116
  end
@@ -19,10 +19,9 @@ module Loom::Mods
19
19
  @mods = shell.mod_loader
20
20
  @loom_config = loom_config
21
21
 
22
- # The action proxy is a facade for the mod provided to patterns by the
23
- # ShellApi (the 'loom' object). The ShellApi calls back to the mod_loader
24
- # on method missing which instantiates a new Module object and returns the
25
- # action_proxy.
22
+ # The action proxy is a facade (or is it a proxy? i don't know) for the mod provided to
23
+ # patterns by the ShellApi (the 'loom' object). The ShellApi calls back to the mod_loader on
24
+ # method missing which instantiates a new Module object and returns the action_proxy.
26
25
  @action_proxy = self.class.action_proxy self, shell.shell_api
27
26
  @action_args = nil
28
27
  @action_block = nil
@@ -45,7 +45,7 @@ module Loom::Pattern
45
45
  Loom.log.debug1(self) { "let[:#{let_key}] => #{value}" }
46
46
 
47
47
  if value.nil? || value.equal?(Loom::Facts::EMPTY)
48
- Loom.log.e "value of let expression[:#{let_key}] is nil"
48
+ Loom.log.error "value of let expression[:#{let_key}] is nil"
49
49
  raise NilLetValueError, let_key
50
50
  end
51
51
  scope_object.define_singleton_method(let_key) { value }
@@ -1,36 +1,63 @@
1
- =begin
2
-
3
1
  # TODO: DSL extensions:
4
- - Pattern+non_idempotent+ marks a pattern as explicitly not idempotent, this
5
- let's additional warnings and checks to be added
6
- - A history module, store a log of each executed command, a hash of the .loom
7
- file, and the requisite facts (the let declarations) for each executed pattern
8
- on the host it executes. /var/log/loom/history? Create this log on startup.
9
- -- add a new set of history commands through the CLI and a history
10
- FactProvider exposing host/loom/pattern_slug execution stats.
11
- - Provide automatic command reversion support with a =Module= DSL that ties in
12
- with local revision history.
13
- -- allow Module actions/mods to define an "undo" command of itself given the
14
- original inputs to the action
15
- -- using the history to pull previous params (let defns) into a revert command.
16
- -- usages of the raw shell, such as `loom.x` and `loom.capture` would be
17
- unsupported. so would accesses to the fact_set in any before/after/pattern
18
- blocks.
19
- -- however patterns that only used let blocks, and used all "revertable"
20
- module methods, could have automatic state reversion and integrity checking
21
- managed.
22
- -- best practices (encouraged through warnings) to be to heavily discourage
23
- uses of loom.execute and loom.capture in .loom files and encourage all
24
- accesses to fact_set be done in let expressions (enforce this maybe?)
25
- -- Later... before/after hooks can ensure the entire loom execution sequence
26
- was "revertable"
2
+ # - a way to test and verify pattern execution.... I still don't trust this
3
+ # enough. this starts with fixing error reporting.
4
+ # - Pattern+non_idempotent+ marks a pattern as explicitly not idempotent, this
5
+ # let's additional warnings and checks to be added
6
+ # - A history module, store a log of each executed command, a hash of the .loom
7
+ # file, and the requisite facts (the let declarations) for each executed pattern
8
+ # on the host it executes. /var/log/loom/history? Create this log on startup.
9
+ # -- add a new set of history commands through the CLI and a history
10
+ # FactProvider exposing host/loom/pattern_slug execution stats.
11
+ # - Provide automatic command reversion support with a =Module= DSL that ties in
12
+ # with local revision history.
13
+ # -- allow Module actions/mods to define an "undo" command of itself given the
14
+ # original inputs to the action
15
+ # -- using the history to pull previous params (let defns) into a revert command.
16
+ # -- usages of the raw shell, such as `loom.x` and `loom.capture` would be
17
+ # unsupported. so would accesses to the fact_set in any before/after/pattern
18
+ # blocks.
19
+ # -- however patterns that only used let blocks, and used all "revertable"
20
+ # module methods, could have automatic state reversion and integrity checking
21
+ # managed.
22
+ # -- best practices (encouraged through warnings) to be to heavily discourage
23
+ # uses of loom.execute and loom.capture in .loom files and encourage all
24
+ # accesses to fact_set be done in let expressions (enforce this maybe?)
25
+ # -- Later... before/after hooks can ensure the entire loom execution sequence
26
+ # was "revertable"
27
+ # - A mechanism to allow mods to register CLI flags and actions. Using an action predicate
28
+ # mechanisms can add flags at the global or action levels. All CLI flags set config values.
29
+ # -- Mods can also register for action namespaces similar to git. This is consistent with mod
30
+ # namespaces on the loom object.
31
+ # -- Best way is to migrate loom/mods/module and loom/mods/action_proxy into base classes of
32
+ # themselves. The isolate the shell specific behavior into a subclass of each to preserve the
33
+ # current behavior. A new "cli" module and "cli" action proxy would enable the implementation.
34
+ # - Add a phase to the pattern execution sequence to collect calls to factset and loom
35
+ # objects. Results collected from this ""pre-execute"" can be analyzed for errors, optimization,
36
+ # success assertions, verification, etc. The loom file then executes in 2 passes, analyze &
37
+ # execute.
38
+ # -- pre-fact collection - inject the facts (or loom) object as a recorder (like a mock in record mode)
39
+ # instead of the factual fact set. no need to change any loom files.
40
+ # -- only run fact providers which are accessed in the pattern set
41
+ # -- only load modules accessed in the pattern set
42
+ # - Replace Pattern "mods" and "mod specs" in Loom::Pattern::ReferenceSet with usages of a builder
43
+ # instead. Currently the internal data model in ReferenceSet is confusing, but luckily it's the
44
+ # only client of pattern modules, that have used Loom::Pattern::DSL. Change calls to
45
+ # DSL#pattern/report/weave (anythin else that creates a pattern) to add a new PatternBuilder to
46
+ # the module. Use the builder to implement the TODO above ("Add a phase..."). Implement analysis
47
+ # on the builder.
48
+
49
+ =begin
27
50
 
28
51
  ## .loom File DSL
29
52
 
30
- See specs/test.loom for a valid .loom file.
53
+ See:
54
+ * spec/test.loom for a valid .loom file.
55
+ * spec/loom/pattern/dsl_spec.rb for other examples
31
56
 
32
- I've tried to take inspriation from the RSpec DSL for .loom, so hopefully it
33
- feels comfortable.
57
+
58
+ I've tried to take inspriation from several ruby DSLs, including (but not
59
+ limited to) RSpec, Thor, Commander, Sinatra... so hopefully it feels
60
+ comfortable.
34
61
 
35
62
  Loom::Pattern::DSL is the mixin that defines the declarative API for all .loom
36
63
  file defined modules. It is included into Loom::Pattern by default. The outer
@@ -40,7 +67,7 @@ default. Submodules must explicitly include Loom::Pattern, and will receive DSL.
40
67
  For example, given the following .loom file:
41
68
 
42
69
  ``` ~ruby
43
- def top_level; end
70
+ pattern :cmd do |loom, facts| puts loom.xe :uptime end
44
71
 
45
72
  module Outer
46
73
 
@@ -76,6 +103,11 @@ file. A ReferenseSet being a collection of references with uniquely named
76
103
  slugs. The slug of a reference is computed from the module namespace and
77
104
  instance method name.
78
105
 
106
+ ### `report`
107
+
108
+ Use `report` to create a pattern that outputs a fact, other value, or result of
109
+ a block to yaml, json, or any other format.
110
+
79
111
  ### `let`, `before`, and `after`
80
112
 
81
113
  Module::Inner, from above, inherits all +let+ declarations from its outer
@@ -172,7 +204,7 @@ invoked via Loom::Runner+load+. Expansion happens on read via
172
204
  Loom::Pattern::Loader+patterns+, thus the list of patterns is constant
173
205
  throughout all phases of pattern execution.
174
206
 
175
- #### Pattern Execution Phases
207
+ #### Pattern Execution Sequence
176
208
 
177
209
  Once hosts and patterns are identified in earlier Loom::Runner phases,
178
210
  Loom::Runner+run_internal+, per host, initiates an SSH session and command
@@ -210,8 +242,14 @@ TODO
210
242
  ##
211
243
 
212
244
  =end
245
+
246
+ require "yaml"
247
+ require "json"
248
+
213
249
  module Loom::Pattern
214
250
 
251
+ PatternDefinitionError = Class.new Loom::LoomError
252
+
215
253
  # TODO: clarify this DSL to only export:
216
254
  # - description
217
255
  # - pattern
@@ -246,6 +284,39 @@ module Loom::Pattern
246
284
  define_pattern_internal(name, &block)
247
285
  end
248
286
 
287
+ ##
288
+ # @param format[:yaml|:json|:raw] default is :yaml
289
+ def report(name, format: :yaml, &block)
290
+ Loom.log.debug1(self) { "defining reporting pattern => #{name}" }
291
+ define_pattern_internal(name) do |loom, facts|
292
+ # TODO: I don't like all of this logic in the dsl.
293
+ result = if block_given?
294
+ Loom.log.debug(self) { "report[#{name}] from block" }
295
+ self.instance_exec(loom, facts, &block)
296
+ elsif !Loom::Facts.is_empty?(facts[name])
297
+ Loom.log.debug(self) { "report[#{name}] from facts[#{name}]" }
298
+ facts[name]
299
+ elsif self.respond_to?(name) && !self.send(name).nil?
300
+ Loom.log.debug(self) { "report[#{name}] from let{#{name}}" }
301
+ self.send name
302
+ else
303
+ err_msg = "no facts to report for fact[#{name}:#{name.class}]"
304
+ raise PatternDefinitionError, err_msg
305
+ end
306
+ result = result.stdout if result.is_a? Loom::Shell::CmdResult
307
+
308
+ puts case format
309
+ when :yaml then result.to_yaml
310
+ when :json then result.to_json
311
+ when :raw then result
312
+ else
313
+ err_msg = "invalid report format: #{format.inspect}"
314
+ err_msg << "valid options: yaml,json,raw"
315
+ raise PatternDefinitionError, err_msg
316
+ end
317
+ end
318
+ end
319
+
249
320
  def weave(name, pattern_slugs)
250
321
  Loom.log.debug1(self) { "defining weave => #{name}" }
251
322
  @weave_slugs ||= {}
@@ -300,7 +371,7 @@ module Loom::Pattern
300
371
  end
301
372
 
302
373
  private
303
- def define_pattern_internal(name, &block)
374
+ def define_pattern_internal(name, &loom_file_block)
304
375
  @pattern_methods ||= []
305
376
  @pattern_method_map ||= {}
306
377
  @pattern_descriptions ||= {}
@@ -312,7 +383,17 @@ module Loom::Pattern
312
383
  @pattern_descriptions[method_name] = @next_description
313
384
  @next_description = nil
314
385
 
315
- define_method method_name, &block
386
+ ##
387
+ # ```ruby
388
+ # pattern :xyz do |loom, facts|
389
+ # loom.x :dostuff
390
+ # end
391
+ # ```
392
+ # Patterns declared in the .loom file are defined here:
393
+ define_method method_name do |loom, facts|
394
+ Loom.log.debug2(self) { "calling .loom file pattern: #{name}" }
395
+ self.instance_exec(loom, facts, &loom_file_block)
396
+ end
316
397
  end
317
398
 
318
399
  def hook(scope, &block)
@@ -56,22 +56,22 @@ module Loom::Pattern
56
56
  using Loom::CoreExt # using demodulize for namespace creation
57
57
 
58
58
  class << self
59
- def create(ruby_code, source)
59
+ def create(ruby_code, source_file)
60
60
  shell_module = Module.new
61
61
  shell_module.include Loom::Pattern
62
62
  # TODO: I think this is my black magic for capturing stacktrace
63
63
  # info... I forget the details. Add documentation.
64
- shell_module.module_eval ruby_code, source, 1
64
+ shell_module.module_eval ruby_code, source_file, 1
65
65
  shell_module.namespace ""
66
66
 
67
- self.new(shell_module, source).build
67
+ self.new(shell_module, source_file).build
68
68
  end
69
69
  end
70
70
 
71
- def initialize(shell_module, source)
71
+ def initialize(shell_module, source_file)
72
72
  @shell_module = shell_module
73
73
  @pattern_mod_specs = pattern_mod_specs
74
- @source = source
74
+ @source_file = source_file
75
75
  end
76
76
 
77
77
  def build
@@ -106,7 +106,7 @@ module Loom::Pattern
106
106
  desc = mod.pattern_description weave_name
107
107
  Loom.log.warn "no descripiton for weave => #{slug}" unless desc
108
108
 
109
- ExpandingReference.new slug, mod.weave_slugs[weave_name], @source, desc
109
+ ExpandingReference.new slug, mod.weave_slugs[weave_name], @source_file, desc
110
110
  end
111
111
 
112
112
  def build_pattern_reference(pattern_name, slug, context, mod)
@@ -114,7 +114,7 @@ module Loom::Pattern
114
114
  desc = mod.pattern_description pattern_name
115
115
  Loom.log.warn "no descripiton for pattern => #{slug}" unless desc
116
116
 
117
- Reference.new slug, method, @source, context, desc
117
+ Reference.new slug, method, @source_file, context, desc
118
118
  end
119
119
 
120
120
  def context_for_mod_spec(mod_spec)
@@ -78,6 +78,10 @@ module Loom
78
78
  rescue => e
79
79
  Loom.log.fatal "fatal error => #{e.inspect}"
80
80
  Loom.log.fatal e.backtrace.join "\n\t"
81
+
82
+ loom_files = @loom_config.files.loom_files
83
+ loom_errors = e.backtrace.select { |line| line =~ /(#{loom_files.join("|")})/ }
84
+ Loom.log.error "Loom file errors: \n\t" + loom_errors.join("\n\t")
81
85
  exit 99
82
86
  end
83
87
  end
@@ -114,6 +118,10 @@ module Loom
114
118
  pattern_loader = Loom::Pattern::Loader.load @loom_config
115
119
  @pattern_refs = pattern_loader.patterns @pattern_slugs
116
120
 
121
+ @loom_config[:loomfile_autoloads].each do |path|
122
+ Loom.log.debug { "autoloading: #{path}" }
123
+ require path
124
+ end
117
125
  @mod_loader = Loom::Mods::ModLoader.new @loom_config
118
126
  end
119
127
 
@@ -19,6 +19,7 @@ module Loom::Shell
19
19
  @shell.local.shell_api
20
20
  end
21
21
 
22
+ # This is the entry point for `loom.foo` calls from .loom files.
22
23
  def method_missing(name, *args, &block)
23
24
  Loom.log.debug3(self) { "shell api => #{name} #{args} #{block}" }
24
25
  # TODO: The relationship between shell and mod_loader seems leaky here, a
@@ -42,6 +43,7 @@ module Loom::Shell
42
43
  def method_missing(name, *args, &block)
43
44
  @cmd_executions.push name
44
45
  @cmd_execution_args.push args
46
+ self
45
47
  end
46
48
  end
47
49
 
@@ -1,3 +1,3 @@
1
1
  module Loom
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end
@@ -83,8 +83,9 @@ module LoomExt::CoreFacts
83
83
  facts = {}
84
84
  os_release.each_line do |l|
85
85
  l = l.strip
86
+ next if l.empty?
86
87
  key, value = l.split("=")
87
- unquoted_value = value.gsub(/^"(.+)"$/) { |m| $1 }
88
+ unquoted_value = value.gsub(/^"(.+)"$/) { |m| $1 } rescue ""
88
89
  facts[key.to_sym] = unquoted_value
89
90
  end
90
91
  facts
@@ -7,8 +7,9 @@ require "loom"
7
7
 
8
8
  require_relative "exec"
9
9
  require_relative "files"
10
+ require_relative "git"
10
11
  require_relative "net"
11
12
  require_relative "user"
12
13
  require_relative "package/package"
13
- require_relative "system"
14
+ require_relative "systemd"
14
15
  require_relative "vm/all"
@@ -1,3 +1,4 @@
1
+ # TODO: rename this file -> oneliners.rb
1
2
  module LoomExt::CoreMods
2
3
 
3
4
  FailError = Class.new Loom::ExecutionError
@@ -14,8 +15,16 @@ module LoomExt::CoreMods
14
15
  end
15
16
  end
16
17
 
18
+ class ExecEcho < Loom::Mods::Module
19
+ register_mod :exec_echo, :alias => [:xe, :"<<e"] do |*cmd, **opts|
20
+ shell.capture *cmd, **opts
21
+ end
22
+ end
23
+
17
24
  class ChangeDirectory < Loom::Mods::Module
18
25
  register_mod :change_directory, :alias => :cd do |path, &block|
26
+ # TODO: I think this block binding is to the Module instead of the RunContext. Find out and
27
+ # fix it.
19
28
  shell.cd path, &block
20
29
  end
21
30
  end
@@ -34,6 +43,7 @@ module LoomExt::CoreMods
34
43
 
35
44
  class Sudo < Loom::Mods::Module
36
45
  register_mod :sudo do |user: :root, cmd: nil, &block|
46
+ # TODO: ditto re: binding to Module instead of RunContext.
37
47
  shell.sudo user, cmd, &block
38
48
  end
39
49
  end