minestrone 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/.github/workflows/main.yml +32 -0
- data/.gitignore +5 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/bin/capify +89 -0
- data/bin/min +5 -0
- data/docs/lib-codebase-map.md +162 -0
- data/docs/lib-dependency-graph.svg +129 -0
- data/lib/minestrone/callback.rb +45 -0
- data/lib/minestrone/cli/help.rb +131 -0
- data/lib/minestrone/cli/help.txt +72 -0
- data/lib/minestrone/cli/options.rb +232 -0
- data/lib/minestrone/cli.rb +159 -0
- data/lib/minestrone/command.rb +177 -0
- data/lib/minestrone/configuration/actions/file_transfer.rb +53 -0
- data/lib/minestrone/configuration/actions/inspect.rb +46 -0
- data/lib/minestrone/configuration/actions/invocation.rb +202 -0
- data/lib/minestrone/configuration/alias_task.rb +29 -0
- data/lib/minestrone/configuration/callbacks.rb +129 -0
- data/lib/minestrone/configuration/connections.rb +66 -0
- data/lib/minestrone/configuration/execution.rb +139 -0
- data/lib/minestrone/configuration/loading.rb +207 -0
- data/lib/minestrone/configuration/log_formatters.rb +75 -0
- data/lib/minestrone/configuration/namespaces.rb +225 -0
- data/lib/minestrone/configuration/servers.rb +70 -0
- data/lib/minestrone/configuration/variables.rb +115 -0
- data/lib/minestrone/configuration.rb +69 -0
- data/lib/minestrone/errors.rb +17 -0
- data/lib/minestrone/ext/string.rb +7 -0
- data/lib/minestrone/extensions.rb +56 -0
- data/lib/minestrone/logger.rb +171 -0
- data/lib/minestrone/processable.rb +50 -0
- data/lib/minestrone/recipes/deploy/assets.rb +194 -0
- data/lib/minestrone/recipes/deploy/bundler.rb +81 -0
- data/lib/minestrone/recipes/deploy/dependencies.rb +44 -0
- data/lib/minestrone/recipes/deploy/local_dependency.rb +45 -0
- data/lib/minestrone/recipes/deploy/remote_dependency.rb +119 -0
- data/lib/minestrone/recipes/deploy/scm/base.rb +204 -0
- data/lib/minestrone/recipes/deploy/scm/git.rb +284 -0
- data/lib/minestrone/recipes/deploy/scm/none.rb +54 -0
- data/lib/minestrone/recipes/deploy/scm.rb +22 -0
- data/lib/minestrone/recipes/deploy/strategy/base.rb +87 -0
- data/lib/minestrone/recipes/deploy/strategy/copy.rb +353 -0
- data/lib/minestrone/recipes/deploy/strategy/remote_cache.rb +80 -0
- data/lib/minestrone/recipes/deploy/strategy.rb +22 -0
- data/lib/minestrone/recipes/deploy.rb +639 -0
- data/lib/minestrone/recipes/standard.rb +23 -0
- data/lib/minestrone/recipes/templates/maintenance.rhtml +53 -0
- data/lib/minestrone/server_definition.rb +56 -0
- data/lib/minestrone/ssh.rb +81 -0
- data/lib/minestrone/task_definition.rb +82 -0
- data/lib/minestrone/transfer.rb +205 -0
- data/lib/minestrone/version.rb +11 -0
- data/lib/minestrone.rb +3 -0
- data/minestrone.gemspec +32 -0
- data/test/cli/execute_test.rb +130 -0
- data/test/cli/help_test.rb +178 -0
- data/test/cli/options_test.rb +315 -0
- data/test/cli/ui_test.rb +26 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +305 -0
- data/test/configuration/actions/file_transfer_test.rb +61 -0
- data/test/configuration/actions/inspect_test.rb +76 -0
- data/test/configuration/actions/invocation_test.rb +258 -0
- data/test/configuration/alias_task_test.rb +110 -0
- data/test/configuration/callbacks_test.rb +201 -0
- data/test/configuration/connections_test.rb +192 -0
- data/test/configuration/execution_test.rb +176 -0
- data/test/configuration/loading_test.rb +149 -0
- data/test/configuration/namespace_dsl_test.rb +325 -0
- data/test/configuration/servers_test.rb +100 -0
- data/test/configuration/variables_test.rb +191 -0
- data/test/configuration_test.rb +77 -0
- data/test/deploy/local_dependency_test.rb +61 -0
- data/test/deploy/remote_dependency_test.rb +146 -0
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/scm/git_test.rb +260 -0
- data/test/deploy/scm/none_test.rb +26 -0
- data/test/deploy/strategy/copy_test.rb +360 -0
- data/test/extensions_test.rb +69 -0
- data/test/fixtures/cli_integration.rb +5 -0
- data/test/fixtures/config.rb +4 -0
- data/test/fixtures/custom.rb +3 -0
- data/test/logger_formatting_test.rb +149 -0
- data/test/logger_test.rb +134 -0
- data/test/recipes_test.rb +26 -0
- data/test/server_definition_test.rb +121 -0
- data/test/ssh_test.rb +99 -0
- data/test/task_definition_test.rb +117 -0
- data/test/transfer_test.rb +172 -0
- data/test/utils.rb +28 -0
- data/test/version_test.rb +11 -0
- metadata +258 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'benchmark'
|
|
4
|
+
require 'minestrone/errors'
|
|
5
|
+
require 'minestrone/processable'
|
|
6
|
+
|
|
7
|
+
module Minestrone
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# This class encapsulates a single command to be executed on a remote machine.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
class Command
|
|
14
|
+
include Processable
|
|
15
|
+
|
|
16
|
+
attr_reader :command, :session, :options, :callback, :channel
|
|
17
|
+
|
|
18
|
+
def self.process(command, session, options = {}, &block)
|
|
19
|
+
new(command, session, options, &block).process!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Instantiates a new command object. The +command+ must be a string
|
|
23
|
+
# containing the command to execute. +session+ is a Net::SSH session
|
|
24
|
+
# instance, and +options+ must be a hash containing any of the following keys:
|
|
25
|
+
#
|
|
26
|
+
# * +logger+: (optional), a Minestrone::Logger instance
|
|
27
|
+
# * +data+: (optional), a string to be sent to the command via it's stdin
|
|
28
|
+
# * +env+: (optional), a string or hash to be interpreted as environment
|
|
29
|
+
# variables that should be defined for this command invocation.
|
|
30
|
+
|
|
31
|
+
def initialize(command, session, options = {}, &block)
|
|
32
|
+
@command = command.strip.gsub(/\r?\n/, "\\\n")
|
|
33
|
+
@session = session
|
|
34
|
+
@options = options
|
|
35
|
+
@callback = block || Minestrone::Configuration.default_io_proc
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Processes the command. If the command fails (non-zero return code), this
|
|
39
|
+
# will raise a Minestrone::CommandError.
|
|
40
|
+
|
|
41
|
+
def process!
|
|
42
|
+
elapsed = Benchmark.realtime do
|
|
43
|
+
open_channel(session)
|
|
44
|
+
|
|
45
|
+
loop do
|
|
46
|
+
break unless process_iteration { !channel[:closed] }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
logger.trace "command finished in #{(elapsed * 1000).round}ms" if logger
|
|
51
|
+
|
|
52
|
+
if channel[:status] != 0
|
|
53
|
+
message = "#{channel[:command].inspect} on #{channel[:server]}"
|
|
54
|
+
error = CommandError.new("failed: #{message}")
|
|
55
|
+
error.host = channel[:server]
|
|
56
|
+
raise error
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Force the command to stop processing, by closing the open channel
|
|
63
|
+
# associated with this command.
|
|
64
|
+
|
|
65
|
+
def stop!
|
|
66
|
+
channel.close if channel && !channel[:closed]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def logger
|
|
73
|
+
options[:logger]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def open_channel(session)
|
|
77
|
+
server = session.xserver
|
|
78
|
+
opened = nil
|
|
79
|
+
|
|
80
|
+
returned = session.open_channel do |channel|
|
|
81
|
+
opened = channel
|
|
82
|
+
channel[:server] = server
|
|
83
|
+
channel[:host] = server.host
|
|
84
|
+
channel[:options] = options
|
|
85
|
+
channel[:callback] = callback
|
|
86
|
+
|
|
87
|
+
request_pty_if_necessary(channel) do |ch, success|
|
|
88
|
+
if success
|
|
89
|
+
logger.trace "executing command", ch[:server] if logger
|
|
90
|
+
cmd = replace_placeholders(command, ch)
|
|
91
|
+
|
|
92
|
+
if options[:shell] == false
|
|
93
|
+
shell = nil
|
|
94
|
+
else
|
|
95
|
+
shell = "#{options[:shell] || "sh"} -c"
|
|
96
|
+
cmd = cmd.gsub(/'/) { |m| "'\\''" }
|
|
97
|
+
cmd = "'#{cmd}'"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
command_line = [environment, shell, cmd].compact.join(" ")
|
|
101
|
+
ch[:command] = command_line
|
|
102
|
+
|
|
103
|
+
ch.exec(command_line)
|
|
104
|
+
ch.send_data(options[:data]) if options[:data]
|
|
105
|
+
ch.eof! if options[:eof]
|
|
106
|
+
else
|
|
107
|
+
# just log it, don't actually raise an exception, since the
|
|
108
|
+
# process method will see that the status is not zero and will
|
|
109
|
+
# raise an exception then.
|
|
110
|
+
logger.important "could not open channel", ch[:server] if logger
|
|
111
|
+
ch.close
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
channel.on_data do |ch, data|
|
|
116
|
+
ch[:callback][ch, :out, data]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
channel.on_extended_data do |ch, type, data|
|
|
120
|
+
ch[:callback][ch, :err, data]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
channel.on_request("exit-status") do |ch, data|
|
|
124
|
+
ch[:status] = data.read_long
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
channel.on_request("exit-signal") do |ch, data|
|
|
128
|
+
if logger
|
|
129
|
+
exit_signal = data.read_string
|
|
130
|
+
logger.important "command received signal #{exit_signal}", ch[:server]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
channel.on_close do |ch|
|
|
135
|
+
ch[:closed] = true
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
@channel = returned || opened
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def request_pty_if_necessary(channel)
|
|
143
|
+
if options[:pty]
|
|
144
|
+
channel.request_pty do |ch, success|
|
|
145
|
+
yield ch, success
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
yield channel, true
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def replace_placeholders(command, channel)
|
|
153
|
+
command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# prepare a space-separated sequence of variables assignments
|
|
157
|
+
# intended to be prepended to a command, so the shell sets
|
|
158
|
+
# the environment before running the command.
|
|
159
|
+
# i.e.: options[:env] = {'PATH' => '/opt/ruby/bin:$PATH',
|
|
160
|
+
# 'TEST' => '( "quoted" )'}
|
|
161
|
+
# environment returns:
|
|
162
|
+
# "env TEST=(\ \"quoted\"\ ) PATH=/opt/ruby/bin:$PATH"
|
|
163
|
+
|
|
164
|
+
def environment
|
|
165
|
+
return if options[:env].nil? || options[:env].empty?
|
|
166
|
+
|
|
167
|
+
@environment ||= if String === options[:env]
|
|
168
|
+
"env #{options[:env]}"
|
|
169
|
+
else
|
|
170
|
+
options[:env].inject("env".dup) do |string, (name, value)|
|
|
171
|
+
value = value.to_s.gsub(/[ "]/) { |m| "\\#{m}" }
|
|
172
|
+
string << " #{name}=#{value}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minestrone/transfer'
|
|
4
|
+
|
|
5
|
+
module Minestrone
|
|
6
|
+
class Configuration
|
|
7
|
+
module Actions
|
|
8
|
+
module FileTransfer
|
|
9
|
+
|
|
10
|
+
# Store the given data at the given location on the configured server.
|
|
11
|
+
# If <tt>:mode</tt> is specified it is used to set the mode on the file.
|
|
12
|
+
|
|
13
|
+
def put(data, path, options = {})
|
|
14
|
+
opts = options.dup
|
|
15
|
+
upload(StringIO.new(data), path, opts)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get file remote_path from the configured server and transfer it to
|
|
19
|
+
# local machine as path.
|
|
20
|
+
#
|
|
21
|
+
# get "#{deploy_to}/current/log/production.log", "log/production.log.web"
|
|
22
|
+
|
|
23
|
+
def get(remote_path, path, options = {}, &block)
|
|
24
|
+
download(remote_path, path, options, &block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def upload(from, to, options = {}, &block)
|
|
28
|
+
mode = options.delete(:mode)
|
|
29
|
+
transfer(:up, from, to, options, &block)
|
|
30
|
+
|
|
31
|
+
if mode
|
|
32
|
+
mode = mode.is_a?(Numeric) ? mode.to_s(8) : mode.to_s
|
|
33
|
+
run "chmod #{mode} #{to}", options
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def download(from, to, options = {}, &block)
|
|
38
|
+
transfer(:down, from, to, options, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def transfer(direction, from, to, options = {}, &block)
|
|
42
|
+
if dry_run
|
|
43
|
+
return logger.debug "transfering: #{[direction, from, to] * ', '}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
execute_on_server do
|
|
47
|
+
Transfer.process(direction, from, to, session, options.merge(:logger => logger), &block)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minestrone/errors'
|
|
4
|
+
|
|
5
|
+
module Minestrone
|
|
6
|
+
class Configuration
|
|
7
|
+
module Actions
|
|
8
|
+
module Inspect
|
|
9
|
+
|
|
10
|
+
# Streams the result of the command from the configured server.
|
|
11
|
+
# The command is invoked via #invoke_command.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
#
|
|
15
|
+
# desc "Run a tail on multiple log files at the same time"
|
|
16
|
+
# task :tail_fcgi do
|
|
17
|
+
# stream "tail -f #{shared_path}/log/fastcgi.crash.log"
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
def stream(command, options = {})
|
|
21
|
+
invoke_command(command, options.merge(:eof => !command.include?(sudo))) do |ch, stream, out|
|
|
22
|
+
puts out if stream == :out
|
|
23
|
+
warn "[err :: #{ch[:server]}] #{out}" if stream == :err
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Executes the given command on the first server targetted by the
|
|
28
|
+
# current task, collects it's stdout into a string, and returns the
|
|
29
|
+
# string. The command is invoked via #invoke_command.
|
|
30
|
+
|
|
31
|
+
def capture(command, options = {})
|
|
32
|
+
output = "".dup
|
|
33
|
+
|
|
34
|
+
invoke_command(command, options.merge(:once => true, :eof => !command.include?(sudo))) do |ch, stream, data|
|
|
35
|
+
case stream
|
|
36
|
+
when :out then output << data
|
|
37
|
+
when :err then warn "[err :: #{ch[:server]}] #{data}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
output
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minestrone/command'
|
|
4
|
+
|
|
5
|
+
module Minestrone
|
|
6
|
+
class Configuration
|
|
7
|
+
module Actions
|
|
8
|
+
module Invocation
|
|
9
|
+
def self.included(base) #:nodoc:
|
|
10
|
+
base.extend(ClassMethods)
|
|
11
|
+
|
|
12
|
+
base.default_io_proc = Proc.new do |ch, stream, out|
|
|
13
|
+
level = (stream == :err) ? :important : :info
|
|
14
|
+
ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:server]}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module ClassMethods
|
|
19
|
+
attr_accessor :default_io_proc
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize_invocation #:nodoc:
|
|
23
|
+
set :default_environment, {}
|
|
24
|
+
set :default_run_options, {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Invokes the given command. If a +via+ key is given, it will be used
|
|
28
|
+
# to determine what method to use to invoke the command. It defaults
|
|
29
|
+
# to :run, but may be :sudo, or any other method that conforms to the
|
|
30
|
+
# same interface as run and sudo.
|
|
31
|
+
|
|
32
|
+
def invoke_command(cmd, options = {}, &block)
|
|
33
|
+
options = options.dup
|
|
34
|
+
via = options.delete(:via) || :run
|
|
35
|
+
send(via, cmd, options, &block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Execute the given command on the configured server. If a block is
|
|
39
|
+
# given, it is invoked for all output
|
|
40
|
+
# generated by the command, and should accept three parameters: the SSH
|
|
41
|
+
# channel (which may be used to send data back to the remote process),
|
|
42
|
+
# the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for
|
|
43
|
+
# stdout), and the data that was received.
|
|
44
|
+
#
|
|
45
|
+
# The +options+ hash may include any of the following keys:
|
|
46
|
+
#
|
|
47
|
+
# * :shell - says which shell should be used to invoke commands. This
|
|
48
|
+
# defaults to "sh". Setting this to false causes Minestrone to invoke
|
|
49
|
+
# the commands directly, without wrapping them in a shell invocation.
|
|
50
|
+
# * :data - if not nil (the default), this should be a string that will
|
|
51
|
+
# be passed to the command's stdin stream.
|
|
52
|
+
# * :pty - if true, a pseudo-tty will be allocated for each command. The
|
|
53
|
+
# default is false. Note that there are benefits and drawbacks both ways.
|
|
54
|
+
# Empirically, it appears that if a pty is allocated, the SSH server daemon
|
|
55
|
+
# will _not_ read user shell start-up scripts (e.g. bashrc, etc.). However,
|
|
56
|
+
# if a pty is _not_ allocated, some commands will refuse to run in
|
|
57
|
+
# interactive mode and will not prompt for (e.g.) passwords.
|
|
58
|
+
# * :env - a hash of environment variable mappings that should be made
|
|
59
|
+
# available to the command. The keys should be environment variable names,
|
|
60
|
+
# and the values should be their corresponding values. The default is
|
|
61
|
+
# empty, but may be modified by changing the +default_environment+
|
|
62
|
+
# Minestrone variable.
|
|
63
|
+
# * :eof - if true, the standard input stream will be closed after sending
|
|
64
|
+
# any data specified in the :data option. If false, the input stream is
|
|
65
|
+
# left open. The default is to close the input stream only if no block is
|
|
66
|
+
# passed.
|
|
67
|
+
#
|
|
68
|
+
# Note that if you set these keys in the +default_run_options+ Minestrone
|
|
69
|
+
# variable, they will apply for all invocations of #run and
|
|
70
|
+
# #invoke_command.
|
|
71
|
+
|
|
72
|
+
def run(cmd, options = {}, &block)
|
|
73
|
+
if options[:eof].nil? && !cmd.include?(sudo)
|
|
74
|
+
options = options.merge(:eof => !block_given?)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
block ||= self.class.default_io_proc
|
|
78
|
+
options = add_default_command_options(options)
|
|
79
|
+
|
|
80
|
+
if cmd.nil? || cmd.empty?
|
|
81
|
+
raise ArgumentError, "attempt to execute without specifying a command"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
logger.debug "executing #{cmd.inspect}" unless options[:silent]
|
|
85
|
+
|
|
86
|
+
return if dry_run || (debug && continue_execution(cmd) == false)
|
|
87
|
+
|
|
88
|
+
block = sudo_behavior_callback(block) if cmd.include?(sudo)
|
|
89
|
+
|
|
90
|
+
execute_on_server do
|
|
91
|
+
Command.process(cmd, session, options.merge(:logger => logger, :configuration => self), &block)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns the command string used by minestrone to invoke a comamnd via
|
|
96
|
+
# sudo.
|
|
97
|
+
#
|
|
98
|
+
# run "#{sudo :as => 'bob'} mkdir /path/to/dir"
|
|
99
|
+
#
|
|
100
|
+
# It can also be invoked like #run, but executing the command via sudo.
|
|
101
|
+
# If sudo requires a password, set the <tt>:password</tt> variable or
|
|
102
|
+
# pass <tt>-p</tt> to prompt for it before running.
|
|
103
|
+
#
|
|
104
|
+
# sudo "mkdir /path/to/dir"
|
|
105
|
+
#
|
|
106
|
+
# Also, this method understands a <tt>:sudo</tt> configuration variable,
|
|
107
|
+
# which (if specified) will be used as the full path to the sudo
|
|
108
|
+
# executable on the remote machine:
|
|
109
|
+
#
|
|
110
|
+
# set :sudo, "/opt/local/bin/sudo"
|
|
111
|
+
#
|
|
112
|
+
# If you know what you're doing, you can also set <tt>:sudo_prompt</tt>,
|
|
113
|
+
# which tells minestrone which prompt sudo should use when asking for
|
|
114
|
+
# a password. (This is so that minestrone knows what prompt to look for
|
|
115
|
+
# in the output.) If you set :sudo_prompt to an empty string, Minestrone
|
|
116
|
+
# will not send a preferred prompt.
|
|
117
|
+
|
|
118
|
+
def sudo(*parameters, &block)
|
|
119
|
+
options = parameters.last.is_a?(Hash) ? parameters.pop.dup : {}
|
|
120
|
+
command = parameters.first
|
|
121
|
+
user = options[:as] && "-u #{options.delete(:as)}"
|
|
122
|
+
|
|
123
|
+
sudo_prompt_option = "-p '#{sudo_prompt}'" unless sudo_prompt.empty?
|
|
124
|
+
sudo_command = [fetch(:sudo, "sudo"), sudo_prompt_option, user].compact.join(" ")
|
|
125
|
+
|
|
126
|
+
if command
|
|
127
|
+
command = sudo_command + " " + command
|
|
128
|
+
run(command, options, &block)
|
|
129
|
+
else
|
|
130
|
+
return sudo_command
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns a Proc object that defines the behavior of the sudo
|
|
135
|
+
# callback. The returned Proc will defer to the +fallback+ argument
|
|
136
|
+
# (which should also be a Proc) for any output it does not
|
|
137
|
+
# explicitly handle.
|
|
138
|
+
|
|
139
|
+
def sudo_behavior_callback(fallback) #:nodoc:
|
|
140
|
+
# in order to prevent _each host_ from prompting when the password
|
|
141
|
+
# was wrong, let's track which host prompted first and only allow
|
|
142
|
+
# subsequent prompts from that host.
|
|
143
|
+
prompt_host = nil
|
|
144
|
+
|
|
145
|
+
Proc.new do |ch, stream, out|
|
|
146
|
+
if out.to_s =~ /^Sorry, try again/
|
|
147
|
+
if prompt_host.nil? || prompt_host == ch[:server]
|
|
148
|
+
prompt_host = ch[:server]
|
|
149
|
+
logger.important out, "#{stream} :: #{ch[:server]}"
|
|
150
|
+
reset! :password
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if out.to_s =~ /^#{Regexp.escape(sudo_prompt)}/
|
|
155
|
+
ch.send_data "#{self[:password]}\n"
|
|
156
|
+
elsif fallback
|
|
157
|
+
fallback.call(ch, stream, out)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Merges the various default command options into the options hash and
|
|
163
|
+
# returns the result. The default command options that are understand
|
|
164
|
+
# are:
|
|
165
|
+
#
|
|
166
|
+
# * :default_environment: If the :env key already exists, the :env
|
|
167
|
+
# key is merged into default_environment and then added back into
|
|
168
|
+
# options.
|
|
169
|
+
# * :default_shell: if the :shell key already exists, it will be used.
|
|
170
|
+
# Otherwise, if the :default_shell key exists in the configuration,
|
|
171
|
+
# it will be used. Otherwise, no :shell key is added.
|
|
172
|
+
|
|
173
|
+
def add_default_command_options(options)
|
|
174
|
+
defaults = self[:default_run_options]
|
|
175
|
+
options = defaults.merge(options)
|
|
176
|
+
|
|
177
|
+
env = self[:default_environment]
|
|
178
|
+
env = env.merge(options[:env]) if options[:env]
|
|
179
|
+
options[:env] = env unless env.empty?
|
|
180
|
+
|
|
181
|
+
shell = options[:shell] || self[:default_shell]
|
|
182
|
+
options[:shell] = shell unless shell.nil?
|
|
183
|
+
|
|
184
|
+
options
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Returns the prompt text to use with sudo
|
|
188
|
+
def sudo_prompt
|
|
189
|
+
fetch(:sudo_prompt, "sudo password: ")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def continue_execution(command)
|
|
193
|
+
case Minestrone::CLI.debug_prompt(command.inspect)
|
|
194
|
+
when "y" then true
|
|
195
|
+
when "n" then false
|
|
196
|
+
when "a" then exit(-1)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minestrone
|
|
4
|
+
class Configuration
|
|
5
|
+
module AliasTask
|
|
6
|
+
|
|
7
|
+
# Attempts to find the task at the given fully-qualified path, and
|
|
8
|
+
# alias it. If arguments don't have correct task names, an ArgumentError
|
|
9
|
+
# will be raised. If no such task exists, a Minestrone::NoSuchTaskError
|
|
10
|
+
# will be raised.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
#
|
|
14
|
+
# alias_task :original_deploy, :deploy
|
|
15
|
+
|
|
16
|
+
def alias_task(new_name, old_name)
|
|
17
|
+
if !new_name.respond_to?(:to_sym) || !old_name.respond_to?(:to_sym)
|
|
18
|
+
raise ArgumentError, "expected a valid task name"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
original_task = find_task(old_name) or raise NoSuchTaskError, "the task `#{old_name}' does not exist"
|
|
22
|
+
task = original_task.dup # Duplicate task to avoid modify original task
|
|
23
|
+
task.name = new_name
|
|
24
|
+
|
|
25
|
+
define_task(task)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minestrone/callback'
|
|
4
|
+
|
|
5
|
+
module Minestrone
|
|
6
|
+
class Configuration
|
|
7
|
+
module Callbacks
|
|
8
|
+
def self.included(base) #:nodoc:
|
|
9
|
+
base.send :alias_method, :invoke_task_directly_without_callbacks, :invoke_task_directly
|
|
10
|
+
base.send :alias_method, :invoke_task_directly, :invoke_task_directly_with_callbacks
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The hash of callbacks that have been registered for this configuration
|
|
14
|
+
attr_reader :callbacks
|
|
15
|
+
|
|
16
|
+
def initialize_callbacks #:nodoc:
|
|
17
|
+
@callbacks = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def invoke_task_directly_with_callbacks(task) #:nodoc:
|
|
21
|
+
trigger :before, task
|
|
22
|
+
result = invoke_task_directly_without_callbacks(task)
|
|
23
|
+
trigger :after, task
|
|
24
|
+
|
|
25
|
+
return result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Defines a callback to be invoked before the given task. You must
|
|
29
|
+
# specify the fully-qualified task name, both for the primary task, and
|
|
30
|
+
# for the task(s) to be executed before. Alternatively, you can pass a
|
|
31
|
+
# block to be executed before the given task.
|
|
32
|
+
#
|
|
33
|
+
# before "deploy:update_code", :record_difference
|
|
34
|
+
# before :deploy, "custom:log_deploy"
|
|
35
|
+
# before :deploy, :this, "then:this", "and:then:this"
|
|
36
|
+
# before :some_task do
|
|
37
|
+
# puts "an anonymous hook!"
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# This just provides a convenient interface to the more general #on method.
|
|
41
|
+
|
|
42
|
+
def before(task_name, *args, &block)
|
|
43
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
44
|
+
args << options.merge(:only => task_name)
|
|
45
|
+
|
|
46
|
+
on :before, *args, &block
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Defines a callback to be invoked after the given task. You must
|
|
50
|
+
# specify the fully-qualified task name, both for the primary task, and
|
|
51
|
+
# for the task(s) to be executed after. Alternatively, you can pass a
|
|
52
|
+
# block to be executed after the given task.
|
|
53
|
+
#
|
|
54
|
+
# after "deploy:update_code", :log_difference
|
|
55
|
+
# after :deploy, "custom:announce"
|
|
56
|
+
# after :deploy, :this, "then:this", "and:then:this"
|
|
57
|
+
# after :some_task do
|
|
58
|
+
# puts "an anonymous hook!"
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# This just provides a convenient interface to the more general #on method.
|
|
62
|
+
|
|
63
|
+
def after(task_name, *args, &block)
|
|
64
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
65
|
+
args << options.merge(:only => task_name)
|
|
66
|
+
|
|
67
|
+
on :after, *args, &block
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Defines one or more callbacks to be invoked in response to some event.
|
|
71
|
+
# Minestrone currently understands the following events:
|
|
72
|
+
#
|
|
73
|
+
# * :before, triggered before a task is invoked
|
|
74
|
+
# * :after, triggered after a task is invoked
|
|
75
|
+
# * :start, triggered before a top-level task is invoked via the command-line
|
|
76
|
+
# * :finish, triggered when a top-level task completes
|
|
77
|
+
# * :load, triggered after all recipes have loaded
|
|
78
|
+
# * :exit, triggered after all tasks have completed
|
|
79
|
+
#
|
|
80
|
+
# Specify the (fully-qualified) task names that you want invoked in
|
|
81
|
+
# response to the event. Alternatively, you can specify a block to invoke
|
|
82
|
+
# when the event is triggered. You can also pass a hash of options as the
|
|
83
|
+
# last parameter, which may include either of two keys:
|
|
84
|
+
#
|
|
85
|
+
# * :only, should specify an array of task names. Restricts this callback
|
|
86
|
+
# so that it will only fire when the event applies to those tasks.
|
|
87
|
+
# * :except, should specify an array of task names. Restricts this callback
|
|
88
|
+
# so that it will never fire when the event applies to those tasks.
|
|
89
|
+
#
|
|
90
|
+
# Usage:
|
|
91
|
+
#
|
|
92
|
+
# on :before, "some:hook", "another:hook", :only => "deploy:update"
|
|
93
|
+
# on :after, "some:hook", :except => "deploy:create_symlink"
|
|
94
|
+
# on :before, "global:hook"
|
|
95
|
+
# on :after, :only => :deploy do
|
|
96
|
+
# puts "after deploy here"
|
|
97
|
+
# end
|
|
98
|
+
|
|
99
|
+
def on(event, *args, &block)
|
|
100
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
101
|
+
callbacks[event] ||= []
|
|
102
|
+
|
|
103
|
+
if args.empty? && block.nil?
|
|
104
|
+
raise ArgumentError, "please specify either a task name or a block to invoke"
|
|
105
|
+
elsif args.any? && block
|
|
106
|
+
raise ArgumentError, "please specify only a task name or a block, but not both"
|
|
107
|
+
elsif block
|
|
108
|
+
callbacks[event] << ProcCallback.new(block, options)
|
|
109
|
+
else
|
|
110
|
+
callbacks[event].concat(args.map { |name| TaskCallback.new(self, name, options) })
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Trigger the named event for the named task. All associated callbacks
|
|
115
|
+
# will be fired, in the order they were defined.
|
|
116
|
+
|
|
117
|
+
def trigger(event, task = nil)
|
|
118
|
+
pending = Array(callbacks[event]).select { |c| c.applies_to?(task) }
|
|
119
|
+
|
|
120
|
+
if pending.any?
|
|
121
|
+
msg = "triggering #{event} callbacks"
|
|
122
|
+
msg << " for `#{task.fully_qualified_name}'" if task
|
|
123
|
+
logger.trace(msg)
|
|
124
|
+
pending.each { |callback| callback.call }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|