loom-core 0.0.5 → 0.0.6

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