runbook 0.12.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +46 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +999 -0
- data/Rakefile +6 -0
- data/TODO.md +38 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/runbook +5 -0
- data/images/runbook_anatomy_diagram.png +0 -0
- data/images/runbook_example.gif +0 -0
- data/images/runbook_execution_modes.png +0 -0
- data/lib/hacks/ssh_kit.rb +58 -0
- data/lib/runbook/cli.rb +90 -0
- data/lib/runbook/configuration.rb +110 -0
- data/lib/runbook/dsl.rb +21 -0
- data/lib/runbook/entities/book.rb +17 -0
- data/lib/runbook/entities/section.rb +7 -0
- data/lib/runbook/entities/step.rb +7 -0
- data/lib/runbook/entity.rb +127 -0
- data/lib/runbook/errors.rb +7 -0
- data/lib/runbook/extensions/add.rb +13 -0
- data/lib/runbook/extensions/description.rb +14 -0
- data/lib/runbook/extensions/sections.rb +15 -0
- data/lib/runbook/extensions/shared_variables.rb +51 -0
- data/lib/runbook/extensions/ssh_config.rb +76 -0
- data/lib/runbook/extensions/statements.rb +26 -0
- data/lib/runbook/extensions/steps.rb +14 -0
- data/lib/runbook/extensions/tmux.rb +13 -0
- data/lib/runbook/helpers/format_helper.rb +11 -0
- data/lib/runbook/helpers/ssh_kit_helper.rb +143 -0
- data/lib/runbook/helpers/tmux_helper.rb +174 -0
- data/lib/runbook/hooks.rb +88 -0
- data/lib/runbook/node.rb +23 -0
- data/lib/runbook/run.rb +283 -0
- data/lib/runbook/runner.rb +64 -0
- data/lib/runbook/runs/ssh_kit.rb +186 -0
- data/lib/runbook/statement.rb +22 -0
- data/lib/runbook/statements/ask.rb +11 -0
- data/lib/runbook/statements/assert.rb +25 -0
- data/lib/runbook/statements/capture.rb +14 -0
- data/lib/runbook/statements/capture_all.rb +14 -0
- data/lib/runbook/statements/command.rb +11 -0
- data/lib/runbook/statements/confirm.rb +10 -0
- data/lib/runbook/statements/description.rb +9 -0
- data/lib/runbook/statements/download.rb +12 -0
- data/lib/runbook/statements/layout.rb +10 -0
- data/lib/runbook/statements/note.rb +10 -0
- data/lib/runbook/statements/notice.rb +10 -0
- data/lib/runbook/statements/ruby_command.rb +9 -0
- data/lib/runbook/statements/tmux_command.rb +11 -0
- data/lib/runbook/statements/upload.rb +12 -0
- data/lib/runbook/statements/wait.rb +10 -0
- data/lib/runbook/toolbox.rb +43 -0
- data/lib/runbook/util/repo.rb +56 -0
- data/lib/runbook/util/runbook.rb +25 -0
- data/lib/runbook/util/sticky_hash.rb +26 -0
- data/lib/runbook/util/stored_pose.rb +54 -0
- data/lib/runbook/version.rb +3 -0
- data/lib/runbook/view.rb +24 -0
- data/lib/runbook/viewer.rb +24 -0
- data/lib/runbook/views/markdown.rb +109 -0
- data/lib/runbook.rb +110 -0
- data/runbook.gemspec +48 -0
- data/samples/hooks_runbook.rb +72 -0
- data/samples/layout_runbook.rb +26 -0
- data/samples/restart_nginx.rb +26 -0
- data/samples/simple_runbook.rb +41 -0
- metadata +324 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
module Runbook::Extensions
|
2
|
+
module SSHConfig
|
3
|
+
def self.blank_ssh_config
|
4
|
+
{
|
5
|
+
servers: [],
|
6
|
+
parallelization: {},
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
def ssh_config
|
11
|
+
@ssh_config ||= Runbook::Extensions::SSHConfig.blank_ssh_config
|
12
|
+
end
|
13
|
+
|
14
|
+
module DSL
|
15
|
+
def ssh_config(&block)
|
16
|
+
config = Class.new do
|
17
|
+
attr_reader :dsl
|
18
|
+
prepend Runbook::Extensions::SSHConfig
|
19
|
+
end.new
|
20
|
+
dsl_class = Runbook::DSL.class(
|
21
|
+
Runbook::Extensions::SSHConfig::DSL,
|
22
|
+
)
|
23
|
+
config.instance_variable_set(:@dsl, dsl_class.new(config))
|
24
|
+
config.dsl.instance_eval(&block)
|
25
|
+
config.ssh_config
|
26
|
+
end
|
27
|
+
|
28
|
+
def parallelization(strategy: , limit: 2, wait: 2)
|
29
|
+
parent.ssh_config[:parallelization] = {
|
30
|
+
strategy: strategy,
|
31
|
+
limit: limit,
|
32
|
+
wait: wait,
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def server(server)
|
37
|
+
parent.ssh_config[:servers].clear
|
38
|
+
parent.ssh_config[:servers] << server
|
39
|
+
end
|
40
|
+
|
41
|
+
def servers(*servers)
|
42
|
+
parent.ssh_config[:servers].clear
|
43
|
+
servers.flatten.each do |server|
|
44
|
+
parent.ssh_config[:servers] << server
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def path(path)
|
49
|
+
parent.ssh_config[:path] = path
|
50
|
+
end
|
51
|
+
|
52
|
+
def user(user)
|
53
|
+
parent.ssh_config[:user] = user
|
54
|
+
end
|
55
|
+
|
56
|
+
def group(group)
|
57
|
+
parent.ssh_config[:group] = group
|
58
|
+
end
|
59
|
+
|
60
|
+
def env(env)
|
61
|
+
parent.ssh_config[:env] = env
|
62
|
+
end
|
63
|
+
|
64
|
+
def umask(umask)
|
65
|
+
parent.ssh_config[:umask] = umask
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Runbook::Entities::Step.prepend(SSHConfig)
|
71
|
+
Runbook::Entities::Step::DSL.prepend(SSHConfig::DSL)
|
72
|
+
Runbook::Entities::Section.prepend(SSHConfig)
|
73
|
+
Runbook::Entities::Section::DSL.prepend(SSHConfig::DSL)
|
74
|
+
Runbook::Entities::Book.prepend(SSHConfig)
|
75
|
+
Runbook::Entities::Book::DSL.prepend(SSHConfig::DSL)
|
76
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Runbook::Extensions
|
2
|
+
module Statements
|
3
|
+
module DSL
|
4
|
+
def method_missing(name, *args, &block)
|
5
|
+
if (klass = Statements::DSL._statement_class(name))
|
6
|
+
klass.new(*args, &block).tap do |statement|
|
7
|
+
parent.add(statement)
|
8
|
+
end
|
9
|
+
else
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def respond_to?(name, include_private = false)
|
15
|
+
!!(Statements::DSL._statement_class(name) || super)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self._statement_class(name)
|
19
|
+
"Runbook::Statements::#{name.to_s.camelize}".constantize
|
20
|
+
rescue NameError
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Runbook::Entities::Step::DSL.prepend(Statements::DSL)
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Runbook::Extensions
|
2
|
+
module Steps
|
3
|
+
module DSL
|
4
|
+
def step(title=nil, &block)
|
5
|
+
Runbook::Entities::Step.new(title).tap do |step|
|
6
|
+
parent.add(step)
|
7
|
+
step.dsl.instance_eval(&block) if block
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Runbook::Entities::Section::DSL.prepend(Steps::DSL)
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Runbook::Helpers
|
2
|
+
module FormatHelper
|
3
|
+
def deindent(str, padding: 0)
|
4
|
+
lines = str.split("\n")
|
5
|
+
indentations = lines.map { |l| l.size - l.gsub(/^\s+/, "").size }
|
6
|
+
min_indentation = indentations.min
|
7
|
+
lines.map! { |line| " " * padding + line[min_indentation..-1] }
|
8
|
+
lines.join("\n")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Runbook::Helpers
|
2
|
+
module SSHKitHelper
|
3
|
+
def ssh_kit_command(cmd, raw: false)
|
4
|
+
return [cmd] if raw
|
5
|
+
cmd, args = cmd.split(" ", 2)
|
6
|
+
[cmd.to_sym, args]
|
7
|
+
end
|
8
|
+
|
9
|
+
def ssh_kit_command_options(ssh_config)
|
10
|
+
{}.tap do |options|
|
11
|
+
if ssh_config[:user] && Runbook.configuration.enable_sudo_prompt
|
12
|
+
options[:interaction_handler] ||= ::SSHKit::Sudo::InteractionHandler.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_ssh_config(object, ssh_config_method=:ssh_config)
|
18
|
+
blank_config = Runbook::Extensions::SSHConfig.blank_ssh_config
|
19
|
+
nil_or_blank = ->(config) { config.nil? || config == blank_config }
|
20
|
+
ssh_config = object.send(ssh_config_method)
|
21
|
+
return ssh_config unless nil_or_blank.call(ssh_config)
|
22
|
+
object = object.parent
|
23
|
+
|
24
|
+
while object
|
25
|
+
ssh_config = object.ssh_config
|
26
|
+
return ssh_config unless nil_or_blank.call(ssh_config)
|
27
|
+
object = object.parent
|
28
|
+
end
|
29
|
+
|
30
|
+
return blank_config
|
31
|
+
end
|
32
|
+
|
33
|
+
def render_ssh_config_output(ssh_config)
|
34
|
+
"".tap do |output|
|
35
|
+
if (servers = ssh_config[:servers]).any?
|
36
|
+
server_str = servers.join(", ")
|
37
|
+
if server_str.size > 80
|
38
|
+
server_str = "#{server_str[0..38]}...#{server_str[-38..-1]}"
|
39
|
+
end
|
40
|
+
output << " on: #{server_str}\n"
|
41
|
+
end
|
42
|
+
|
43
|
+
if (strategy = ssh_config[:parallelization][:strategy])
|
44
|
+
limit = ssh_config[:parallelization][:limit]
|
45
|
+
wait = ssh_config[:parallelization][:wait]
|
46
|
+
in_str = " in: #{strategy}"
|
47
|
+
in_str << ", limit: #{limit}" if strategy == :groups
|
48
|
+
in_str << ", wait: #{wait}" if [:sequence, :groups].include?(strategy)
|
49
|
+
output << "#{in_str}\n"
|
50
|
+
end
|
51
|
+
|
52
|
+
if ssh_config[:user] || ssh_config[:group]
|
53
|
+
user = ssh_config[:user]
|
54
|
+
group = ssh_config[:group]
|
55
|
+
as_str = " as:"
|
56
|
+
as_str << " user: #{user}" if user
|
57
|
+
as_str << " group: #{group}" if group
|
58
|
+
output << "#{as_str}\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
if (path = ssh_config[:path])
|
62
|
+
output << " within: #{path}\n"
|
63
|
+
end
|
64
|
+
|
65
|
+
if (env = ssh_config[:env])
|
66
|
+
env_str = env.map do |k, v|
|
67
|
+
"#{k.to_s.upcase}=#{v}"
|
68
|
+
end.join(" ")
|
69
|
+
output << " with: #{env_str}\n"
|
70
|
+
end
|
71
|
+
|
72
|
+
if (umask = ssh_config[:umask])
|
73
|
+
output << " umask: #{umask}\n"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def with_ssh_config(ssh_config, &exec_block)
|
79
|
+
user = ssh_config[:user]
|
80
|
+
group = ssh_config[:group]
|
81
|
+
as_block = _as_block(user, group, &exec_block)
|
82
|
+
within_block = _within_block(ssh_config[:path], &as_block)
|
83
|
+
with_block = _with_block(ssh_config[:env], &within_block)
|
84
|
+
|
85
|
+
_with_umask(ssh_config[:umask]) do
|
86
|
+
servers = _servers(ssh_config[:servers])
|
87
|
+
parallelization = ssh_config[:parallelization]
|
88
|
+
coordinator_options = _coordinator_options(parallelization)
|
89
|
+
SSHKit::Coordinator.new(servers).each(coordinator_options) do
|
90
|
+
instance_exec(&with_block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def _servers(ssh_config_servers)
|
96
|
+
return :local if ssh_config_servers.empty?
|
97
|
+
return :local if ssh_config_servers == [:local]
|
98
|
+
ssh_config_servers
|
99
|
+
end
|
100
|
+
|
101
|
+
def _coordinator_options(ssh_config_parallelization)
|
102
|
+
ssh_config_parallelization.clone.tap do |options|
|
103
|
+
if options[:strategy]
|
104
|
+
options[:in] = options.delete(:strategy)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def _as_block(user, group, &block)
|
110
|
+
if user || group
|
111
|
+
-> { as({user: user, group: group}) { instance_exec(&block) } }
|
112
|
+
else
|
113
|
+
-> { instance_exec(&block) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def _with_block(env, &block)
|
118
|
+
if env
|
119
|
+
-> { with(env) { instance_exec(&block) } }
|
120
|
+
else
|
121
|
+
-> { instance_exec(&block) }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def _within_block(path, &block)
|
126
|
+
if path
|
127
|
+
-> { within(path) { instance_exec(&block) } }
|
128
|
+
else
|
129
|
+
-> { instance_exec(&block) }
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def _with_umask(umask, &block)
|
134
|
+
old_umask = SSHKit.config.umask
|
135
|
+
begin
|
136
|
+
SSHKit.config.umask = umask if umask
|
137
|
+
instance_exec(&block)
|
138
|
+
ensure
|
139
|
+
SSHKit.config.umask = old_umask
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module Runbook::Helpers
|
2
|
+
module TmuxHelper
|
3
|
+
def setup_layout(structure, runbook_title:)
|
4
|
+
_remove_stale_layouts
|
5
|
+
layout_file = _layout_file(_slug(runbook_title))
|
6
|
+
if File.exists?(layout_file)
|
7
|
+
stored_layout = ::YAML::load_file(layout_file)
|
8
|
+
if _all_panes_exist?(stored_layout)
|
9
|
+
return stored_layout
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
_setup_layout(structure).tap do |layout_panes|
|
14
|
+
File.open(layout_file, 'w') do |f|
|
15
|
+
f.write(layout_panes.to_yaml)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def send_keys(command, target)
|
21
|
+
`tmux send-keys -t #{target} #{_pager_escape_sequence} '#{command}' C-m`
|
22
|
+
end
|
23
|
+
|
24
|
+
def kill_all_panes(layout_panes)
|
25
|
+
runbook_pane = _runbook_pane
|
26
|
+
layout_panes.values.each do |pane_id|
|
27
|
+
_kill_pane(pane_id) unless pane_id == runbook_pane
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def _pager_escape_sequence
|
32
|
+
"q C-u"
|
33
|
+
end
|
34
|
+
|
35
|
+
def _kill_pane(pane_id)
|
36
|
+
`tmux kill-pane -t #{pane_id}`
|
37
|
+
end
|
38
|
+
|
39
|
+
def _layout_file(runbook_title)
|
40
|
+
`tmux display-message -p -t $TMUX_PANE "#{Dir.tmpdir}/runbook_layout_\#{pid}_\#{session_name}_\#{pane_pid}_\#{pane_id}_#{runbook_title}.yml"`.strip
|
41
|
+
end
|
42
|
+
|
43
|
+
def _slug(title)
|
44
|
+
title.titleize.gsub(/\s+/, "").underscore.dasherize
|
45
|
+
end
|
46
|
+
|
47
|
+
def _all_panes_exist?(stored_layout)
|
48
|
+
(stored_layout.values - _session_panes).empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
def _remove_stale_layouts
|
52
|
+
session_panes = _session_panes
|
53
|
+
session_layout_files = _session_layout_files
|
54
|
+
session_layout_files.each do |file|
|
55
|
+
File.delete(file) unless session_panes.any? { |pane| /_#{pane}_/ =~ file }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def _session_panes
|
60
|
+
`tmux list-panes -s -F '#D'`.split("\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
def _session_layout_files
|
64
|
+
session_layout_glob = `tmux display-message -p "#{Dir.tmpdir}/runbook_layout_\#{pid}_\#{session_name}_*.yml"`.strip
|
65
|
+
Dir[session_layout_glob]
|
66
|
+
end
|
67
|
+
|
68
|
+
def _setup_layout(structure)
|
69
|
+
current_pane = _runbook_pane
|
70
|
+
panes_to_init = []
|
71
|
+
{}.tap do |layout_panes|
|
72
|
+
if structure.is_a?(Hash)
|
73
|
+
first_window = true
|
74
|
+
structure.each do |name, window|
|
75
|
+
if first_window
|
76
|
+
_rename_window(name)
|
77
|
+
first_window = false
|
78
|
+
else
|
79
|
+
current_pane = _new_window(name)
|
80
|
+
end
|
81
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, window)
|
82
|
+
end
|
83
|
+
else
|
84
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure)
|
85
|
+
end
|
86
|
+
_swap_runbook_pane(panes_to_init, layout_panes)
|
87
|
+
_initialize_panes(panes_to_init, layout_panes)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def _setup_panes(layout_panes, panes_to_init, current_pane, structure, depth=0)
|
92
|
+
return if structure.empty?
|
93
|
+
case structure
|
94
|
+
when Array
|
95
|
+
case structure.size
|
96
|
+
when 1
|
97
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.shift, depth+1)
|
98
|
+
else
|
99
|
+
size = 100 - 100 / structure.size
|
100
|
+
new_pane = _split(current_pane, depth, size)
|
101
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.shift, depth+1)
|
102
|
+
_setup_panes(layout_panes, panes_to_init, new_pane, structure, depth)
|
103
|
+
end
|
104
|
+
when Hash
|
105
|
+
if structure.values.all? { |v| v.is_a?(Numeric) }
|
106
|
+
total_size = structure.values.reduce(:+)
|
107
|
+
case structure.size
|
108
|
+
when 1
|
109
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.keys[0], depth+1)
|
110
|
+
else
|
111
|
+
size = (total_size - structure.values[0]) * 100 / total_size
|
112
|
+
new_pane = _split(current_pane, depth, size)
|
113
|
+
first_struct = structure.keys[0]
|
114
|
+
structure.delete(first_struct)
|
115
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, first_struct, depth+1)
|
116
|
+
_setup_panes(layout_panes, panes_to_init, new_pane, structure, depth)
|
117
|
+
end
|
118
|
+
else
|
119
|
+
layout_panes[structure[:name]] = current_pane
|
120
|
+
panes_to_init << structure
|
121
|
+
end
|
122
|
+
when Symbol
|
123
|
+
layout_panes[structure] = current_pane
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def _swap_runbook_pane(panes_to_init, layout_panes)
|
128
|
+
if (runbook_pane = panes_to_init.find { |pane| pane[:runbook_pane] })
|
129
|
+
current_runbook_pane_name = layout_panes.keys.find do |k|
|
130
|
+
layout_panes[k] == _runbook_pane
|
131
|
+
end
|
132
|
+
target_pane_id = layout_panes[runbook_pane[:name]]
|
133
|
+
layout_panes[runbook_pane[:name]] = _runbook_pane
|
134
|
+
layout_panes[current_runbook_pane_name] = target_pane_id
|
135
|
+
_swap_panes(target_pane_id, _runbook_pane)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def _initialize_panes(panes_to_init, layout_panes)
|
140
|
+
panes_to_init.each do |pane|
|
141
|
+
target = layout_panes[pane[:name]]
|
142
|
+
_set_directory(pane[:directory], target) if pane[:directory]
|
143
|
+
send_keys(pane[:command], target) if pane[:command]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def _runbook_pane
|
148
|
+
@runbook_pane ||= `tmux display-message -p '#D'`.strip
|
149
|
+
end
|
150
|
+
|
151
|
+
def _rename_window(name)
|
152
|
+
`tmux rename-window "#{name}"`
|
153
|
+
end
|
154
|
+
|
155
|
+
def _new_window(name)
|
156
|
+
`tmux new-window -n "#{name}" -P -F '#D' -d`.strip
|
157
|
+
end
|
158
|
+
|
159
|
+
def _split(current_pane, depth, size)
|
160
|
+
direction = depth.even? ? "h" : "v"
|
161
|
+
command = "tmux split-window"
|
162
|
+
args = "-#{direction} -t #{current_pane} -p #{size} -P -F '#D' -d"
|
163
|
+
`#{command} #{args}`.strip
|
164
|
+
end
|
165
|
+
|
166
|
+
def _swap_panes(target_pane, source_pane)
|
167
|
+
`tmux swap-pane -d -t #{target_pane} -s #{source_pane}`
|
168
|
+
end
|
169
|
+
|
170
|
+
def _set_directory(directory, target)
|
171
|
+
send_keys("cd #{directory}; clear", target)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Runbook
|
2
|
+
module Hooks
|
3
|
+
def hooks
|
4
|
+
@hooks ||= []
|
5
|
+
end
|
6
|
+
|
7
|
+
def register_hook(name, type, klass, before: nil, &block)
|
8
|
+
hook = {
|
9
|
+
name: name,
|
10
|
+
type: type,
|
11
|
+
klass: klass,
|
12
|
+
block: block,
|
13
|
+
}
|
14
|
+
|
15
|
+
if before
|
16
|
+
hooks.insert(_hook_index(before), hook)
|
17
|
+
else
|
18
|
+
hooks << hook
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def hooks_for(type, klass)
|
23
|
+
hooks.select do |hook|
|
24
|
+
hook[:type] == type && klass <= hook[:klass]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def _hook_index(hook_name)
|
29
|
+
hooks.index { |hook| hook[:name] == hook_name } || -1
|
30
|
+
end
|
31
|
+
|
32
|
+
module Invoker
|
33
|
+
def invoke_with_hooks(executor, object, *args, &block)
|
34
|
+
skip_before = skip_around = skip_after = false
|
35
|
+
if executor <= Runbook::Run
|
36
|
+
if executor.should_skip?(args[0])
|
37
|
+
if executor.start_at_is_substep?(object, args[0])
|
38
|
+
skip_before = skip_around = true
|
39
|
+
else
|
40
|
+
skip_before = skip_around = skip_after = true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
unless skip_before
|
46
|
+
_execute_before_hooks(executor, object, *args)
|
47
|
+
end
|
48
|
+
|
49
|
+
if skip_around
|
50
|
+
block.call
|
51
|
+
else
|
52
|
+
_execute_around_hooks(executor, object, *args, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
unless skip_after
|
56
|
+
_execute_after_hooks(executor, object, *args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def _execute_before_hooks(executor, object, *args)
|
61
|
+
executor.hooks_for(:before, object.class).each do |hook|
|
62
|
+
executor.instance_exec(object, *args, &hook[:block])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def _execute_around_hooks(executor, object, *args)
|
67
|
+
executor.hooks_for(:around, object.class).reverse.reduce(
|
68
|
+
-> (object, *args) {
|
69
|
+
yield
|
70
|
+
}
|
71
|
+
) do |inner_method, hook|
|
72
|
+
-> (object, *args) {
|
73
|
+
inner_block = Proc.new do |object, *args|
|
74
|
+
inner_method.call(object, *args)
|
75
|
+
end
|
76
|
+
executor.instance_exec(object, *args, inner_block, &hook[:block])
|
77
|
+
}
|
78
|
+
end.call(object, *args)
|
79
|
+
end
|
80
|
+
|
81
|
+
def _execute_after_hooks(executor, object, *args)
|
82
|
+
executor.hooks_for(:after, object.class).each do |hook|
|
83
|
+
executor.instance_exec(object, *args, &hook[:block])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
data/lib/runbook/node.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Runbook
|
2
|
+
class Node
|
3
|
+
def initialize
|
4
|
+
raise "Should not be initialized"
|
5
|
+
end
|
6
|
+
|
7
|
+
def dynamic!
|
8
|
+
@dynamic = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def visited!
|
12
|
+
@visited = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def dynamic?
|
16
|
+
@dynamic
|
17
|
+
end
|
18
|
+
|
19
|
+
def visited?
|
20
|
+
@visited
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|