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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +99 -0
- data/Guardfile +54 -0
- data/Rakefile +6 -0
- data/bin/loom +185 -0
- data/lib/env/development.rb +1 -0
- data/lib/loom.rb +44 -0
- data/lib/loom/all.rb +20 -0
- data/lib/loom/config.rb +106 -0
- data/lib/loom/core_ext.rb +37 -0
- data/lib/loom/dsl.rb +60 -0
- data/lib/loom/facts.rb +13 -0
- data/lib/loom/facts/all.rb +2 -0
- data/lib/loom/facts/fact_file_provider.rb +86 -0
- data/lib/loom/facts/fact_set.rb +138 -0
- data/lib/loom/host_spec.rb +32 -0
- data/lib/loom/inventory.rb +124 -0
- data/lib/loom/logger.rb +141 -0
- data/lib/loom/method_signature.rb +174 -0
- data/lib/loom/mods.rb +4 -0
- data/lib/loom/mods/action_proxy.rb +105 -0
- data/lib/loom/mods/all.rb +3 -0
- data/lib/loom/mods/mod_loader.rb +80 -0
- data/lib/loom/mods/module.rb +113 -0
- data/lib/loom/pattern.rb +15 -0
- data/lib/loom/pattern/all.rb +7 -0
- data/lib/loom/pattern/definition_context.rb +74 -0
- data/lib/loom/pattern/dsl.rb +176 -0
- data/lib/loom/pattern/hook.rb +28 -0
- data/lib/loom/pattern/loader.rb +48 -0
- data/lib/loom/pattern/reference.rb +71 -0
- data/lib/loom/pattern/reference_set.rb +169 -0
- data/lib/loom/pattern/result_reporter.rb +77 -0
- data/lib/loom/runner.rb +209 -0
- data/lib/loom/shell.rb +12 -0
- data/lib/loom/shell/all.rb +10 -0
- data/lib/loom/shell/api.rb +48 -0
- data/lib/loom/shell/cmd_result.rb +33 -0
- data/lib/loom/shell/cmd_wrapper.rb +164 -0
- data/lib/loom/shell/core.rb +226 -0
- data/lib/loom/shell/harness_blob.rb +26 -0
- data/lib/loom/shell/harness_command_builder.rb +50 -0
- data/lib/loom/shell/session.rb +25 -0
- data/lib/loom/trap.rb +44 -0
- data/lib/loom/version.rb +3 -0
- data/lib/loomext/all.rb +4 -0
- data/lib/loomext/corefacts.rb +6 -0
- data/lib/loomext/corefacts/all.rb +8 -0
- data/lib/loomext/corefacts/facter_provider.rb +24 -0
- data/lib/loomext/coremods.rb +5 -0
- data/lib/loomext/coremods/all.rb +13 -0
- data/lib/loomext/coremods/exec.rb +50 -0
- data/lib/loomext/coremods/files.rb +104 -0
- data/lib/loomext/coremods/net.rb +33 -0
- data/lib/loomext/coremods/package/adapter.rb +100 -0
- data/lib/loomext/coremods/package/package.rb +62 -0
- data/lib/loomext/coremods/user.rb +82 -0
- data/lib/loomext/coremods/vm.rb +0 -0
- data/lib/loomext/coremods/vm/all.rb +6 -0
- data/lib/loomext/coremods/vm/vbox.rb +84 -0
- data/loom.gemspec +39 -0
- data/loom/inventory.yml +13 -0
- data/scripts/harness.sh +242 -0
- data/spec/loom/host_spec_spec.rb +101 -0
- data/spec/loom/inventory_spec.rb +154 -0
- data/spec/loom/method_signature_spec.rb +275 -0
- data/spec/loom/pattern/dsl_spec.rb +207 -0
- data/spec/loom/shell/cmd_wrapper_spec.rb +239 -0
- data/spec/loom/shell/harness_blob_spec.rb +42 -0
- data/spec/loom/shell/harness_command_builder_spec.rb +36 -0
- data/spec/runloom.sh +35 -0
- data/spec/scripts/harness_spec.rb +385 -0
- data/spec/spec_helper.rb +94 -0
- data/spec/test.loom +370 -0
- data/spec/test_loom_spec.rb +57 -0
- metadata +287 -0
data/lib/loom/shell.rb
ADDED
@@ -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
|