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