careacademy-runbook 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dockerignore +17 -0
- data/.github/workflows/ruby.yml +43 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Appraisals +8 -0
- data/CHANGELOG.md +143 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +1272 -0
- data/Rakefile +12 -0
- data/TODO.md +316 -0
- data/bin/console +16 -0
- data/bin/setup +8 -0
- data/dockerfiles/Dockerfile-runbook +18 -0
- data/dockerfiles/Dockerfile-sshd +4 -0
- data/examples/hooks_runbook.rb +72 -0
- data/examples/layout_runbook.rb +26 -0
- data/examples/restart_nginx.rb +26 -0
- data/examples/simple_runbook.rb +41 -0
- data/examples/suppress_capture_output.rb +47 -0
- data/exe/runbook +5 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/activesupport_5.gemfile +7 -0
- data/gemfiles/activesupport_6.gemfile +7 -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/airbrussh_context.rb +25 -0
- data/lib/runbook/cli.rb +115 -0
- data/lib/runbook/cli_base.rb +38 -0
- data/lib/runbook/configuration.rb +120 -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/setup.rb +7 -0
- data/lib/runbook/entities/step.rb +7 -0
- data/lib/runbook/entity.rb +129 -0
- data/lib/runbook/errors.rb +7 -0
- data/lib/runbook/extensions/add.rb +14 -0
- data/lib/runbook/extensions/description.rb +14 -0
- data/lib/runbook/extensions/sections.rb +19 -0
- data/lib/runbook/extensions/setup.rb +17 -0
- data/lib/runbook/extensions/shared_variables.rb +51 -0
- data/lib/runbook/extensions/ssh_config.rb +78 -0
- data/lib/runbook/extensions/statements.rb +31 -0
- data/lib/runbook/extensions/steps.rb +24 -0
- data/lib/runbook/extensions/tmux.rb +13 -0
- data/lib/runbook/generator.rb +38 -0
- data/lib/runbook/generators/base.rb +45 -0
- data/lib/runbook/generators/dsl_extension/dsl_extension.rb +29 -0
- data/lib/runbook/generators/dsl_extension/templates/dsl_extension.tt +25 -0
- data/lib/runbook/generators/generator/generator.rb +43 -0
- data/lib/runbook/generators/generator/templates/generator.tt +53 -0
- data/lib/runbook/generators/project/project.rb +301 -0
- data/lib/runbook/generators/project/templates/Gemfile.tt +6 -0
- data/lib/runbook/generators/project/templates/README.md.tt +29 -0
- data/lib/runbook/generators/project/templates/Runbookfile.tt +11 -0
- data/lib/runbook/generators/project/templates/base_file.rb.tt +8 -0
- data/lib/runbook/generators/runbook/runbook.rb +22 -0
- data/lib/runbook/generators/runbook/templates/runbook.tt +20 -0
- data/lib/runbook/generators/statement/statement.rb +18 -0
- data/lib/runbook/generators/statement/templates/statement.tt +34 -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 +176 -0
- data/lib/runbook/hooks.rb +88 -0
- data/lib/runbook/initializer.rb +85 -0
- data/lib/runbook/node.rb +33 -0
- data/lib/runbook/run.rb +290 -0
- data/lib/runbook/runner.rb +66 -0
- data/lib/runbook/runs/ssh_kit.rb +193 -0
- data/lib/runbook/statement.rb +20 -0
- data/lib/runbook/statements/ask.rb +12 -0
- data/lib/runbook/statements/assert.rb +34 -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 +37 -0
- data/lib/runbook/util/repo.rb +57 -0
- data/lib/runbook/util/runbook.rb +43 -0
- data/lib/runbook/util/sticky_hash.rb +26 -0
- data/lib/runbook/util/stored_pose.rb +55 -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 +117 -0
- data/lib/runbook.rb +140 -0
- data/runbook.gemspec +53 -0
- metadata +419 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# <%= name.titleize %>
|
2
|
+
|
3
|
+
## Deployment
|
4
|
+
|
5
|
+
This code is best deployed by cloning this project using git.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Set up this repository by running `bin/setup`. This will install all necessary dependencies for your runbooks.
|
10
|
+
|
11
|
+
Execute your runbooks using `bundle exec runbook exec <PATH_TO_YOUR_RUNBOOK>`
|
12
|
+
|
13
|
+
Get runbook help executing `bundle exec runbook help` or visiting https://github.com/braintree/runbook
|
14
|
+
|
15
|
+
## This Repository
|
16
|
+
|
17
|
+
Runbook code should live in the following locations:
|
18
|
+
|
19
|
+
`runbooks` - Place your runbooks in this directory
|
20
|
+
|
21
|
+
`<%= @shared_lib_dir %>` - Place code shared across your runbooks in this directory
|
22
|
+
|
23
|
+
`lib/runbook/extensions` - Place code that extends Runbook's functionality here. For example, adding a new statement to Runbook's DSL would live in this directory. Require these files in your `Runbookfile` so Runbook can load them when it is launched. See https://github.com/braintree/runbook#extending-runbook for more details on extending Runbook.
|
24
|
+
|
25
|
+
`lib/runbook/generators` - Place Runbook generators in this directory. Require these files in your `Runbookfile` to expose the generators via Runbook's command line interface.
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
Run `bin/setup` to install any necessary dependencies for this project. Run `bin/console` to use your code via an interactive prompt.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Your Runbookfile serves two purposes. Use your Runbookfile to
|
2
|
+
# (1) require any runbook plugins, extensions, or generators that
|
3
|
+
# you want available to your runbooks and (2) configure Runbook
|
4
|
+
# using a `Runbook.configure` block.
|
5
|
+
|
6
|
+
# Use this block to configure Runbook. See
|
7
|
+
# https://github.com/braintree/runbook#configuration for more
|
8
|
+
# details.
|
9
|
+
#
|
10
|
+
# Runbook.configure do |config|
|
11
|
+
# end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Runbook::Generators
|
2
|
+
class Runbook < Thor::Group
|
3
|
+
include ::Runbook::Generators::Base
|
4
|
+
|
5
|
+
source_root File.dirname(__FILE__)
|
6
|
+
|
7
|
+
def self.usage
|
8
|
+
"runbook NAME [options]"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.description
|
12
|
+
"Generate a runbook named NAME, e.x. deploy_nginx"
|
13
|
+
end
|
14
|
+
|
15
|
+
argument :name, desc: "The name of your runbook, e.x. deploy_nginx"
|
16
|
+
|
17
|
+
def create_runbook
|
18
|
+
target = File.join(options[:root], "#{name.underscore}.rb")
|
19
|
+
template('templates/runbook.tt', target)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "runbook"
|
3
|
+
|
4
|
+
runbook = Runbook.book "<%= name.titleize %>" do
|
5
|
+
description <<-DESC
|
6
|
+
This is a runbook that...
|
7
|
+
DESC
|
8
|
+
|
9
|
+
section "SECTION" do
|
10
|
+
step "STEP" do
|
11
|
+
# Add statements here
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
if __FILE__ == $0
|
17
|
+
Runbook::Runner.new(runbook).run
|
18
|
+
else
|
19
|
+
runbook
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Runbook::Generators
|
2
|
+
class Statement < Thor::Group
|
3
|
+
include ::Runbook::Generators::Base
|
4
|
+
|
5
|
+
source_root File.dirname(__FILE__)
|
6
|
+
|
7
|
+
def self.description
|
8
|
+
"Generate a statement named NAME (e.x. ruby_command) that can be used in your runbooks"
|
9
|
+
end
|
10
|
+
|
11
|
+
argument :name, desc: "The name of your statement, e.x. ruby_command"
|
12
|
+
|
13
|
+
def create_statement
|
14
|
+
target = File.join(parent_options[:root], "#{name.underscore}.rb")
|
15
|
+
template('templates/statement.tt', target)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Remember to require this file in a runbook config file
|
2
|
+
# or in your project so it is available in your runbooks
|
3
|
+
module Runbook::Statements
|
4
|
+
class <%= name.classify %> < Runbook::Statement
|
5
|
+
# Add all attributes for your statement
|
6
|
+
attr_reader :attr1, :attr2
|
7
|
+
|
8
|
+
# Define the initialize method signature to
|
9
|
+
# match the method signature of your dsl statement
|
10
|
+
def initialize(attr1, attr2)
|
11
|
+
@attr1 = attr1
|
12
|
+
@attr2 = attr2
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# scope this module using your project's namespace
|
18
|
+
module MyProject::RunbookExtensions
|
19
|
+
module <%= "#{name.underscore}_markdown".classify %>
|
20
|
+
def runbook__statements__<%= name.underscore %>(object, output, metadata)
|
21
|
+
# Format how your statement will be displayed when rendered with markdown
|
22
|
+
output << "#{object.attr1}#{object.attr2}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
Runbook::Views::Markdown.singleton_class.prepend(<%= "#{name.underscore}_markdown".classify %>)
|
26
|
+
|
27
|
+
module <%= "#{name.underscore}_sshkit".classify %>
|
28
|
+
def runbook__statements__<%= name.underscore %>(object, metadata)
|
29
|
+
# Execute your behavior using object which is your instantiated statement
|
30
|
+
# and the current metadata for this step of the execution
|
31
|
+
end
|
32
|
+
end
|
33
|
+
Runbook::Runs::SSHKit.singleton_class.prepend(<%= "#{name.underscore}_sshkit".classify %>)
|
34
|
+
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,176 @@
|
|
1
|
+
module Runbook::Helpers
|
2
|
+
module TmuxHelper
|
3
|
+
FILE_PERMISSIONS = 0600
|
4
|
+
|
5
|
+
def setup_layout(structure, runbook_title:)
|
6
|
+
_remove_stale_layouts
|
7
|
+
layout_file = _layout_file(_slug(runbook_title))
|
8
|
+
if File.exist?(layout_file)
|
9
|
+
stored_layout = ::YAML::load_file(layout_file)
|
10
|
+
if _all_panes_exist?(stored_layout)
|
11
|
+
return stored_layout
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
_setup_layout(structure).tap do |layout_panes|
|
16
|
+
File.open(layout_file, 'w', FILE_PERMISSIONS) do |f|
|
17
|
+
f.write(layout_panes.to_yaml)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def send_keys(command, target)
|
23
|
+
`tmux send-keys -t #{target} #{_pager_escape_sequence} #{Shellwords.escape(command)} C-m`
|
24
|
+
end
|
25
|
+
|
26
|
+
def kill_all_panes(layout_panes)
|
27
|
+
runbook_pane = _runbook_pane
|
28
|
+
layout_panes.values.each do |pane_id|
|
29
|
+
_kill_pane(pane_id) unless pane_id == runbook_pane
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def _pager_escape_sequence
|
34
|
+
"q C-u"
|
35
|
+
end
|
36
|
+
|
37
|
+
def _kill_pane(pane_id)
|
38
|
+
`tmux kill-pane -t #{pane_id}`
|
39
|
+
end
|
40
|
+
|
41
|
+
def _layout_file(runbook_title)
|
42
|
+
`tmux display-message -p -t $TMUX_PANE "#{Dir.tmpdir}/runbook_layout_\#{pid}_\#{session_name}_\#{pane_pid}_\#{pane_id}_#{runbook_title}.yml"`.strip
|
43
|
+
end
|
44
|
+
|
45
|
+
def _slug(title)
|
46
|
+
title.titleize.gsub(/[\/\s]+/, "").underscore.dasherize
|
47
|
+
end
|
48
|
+
|
49
|
+
def _all_panes_exist?(stored_layout)
|
50
|
+
(stored_layout.values - _session_panes).empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
def _remove_stale_layouts
|
54
|
+
session_panes = _session_panes
|
55
|
+
session_layout_files = _session_layout_files
|
56
|
+
session_layout_files.each do |file|
|
57
|
+
File.delete(file) unless session_panes.any? { |pane| /_#{pane}_/ =~ file }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def _session_panes
|
62
|
+
`tmux list-panes -s -F '#D'`.split("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
def _session_layout_files
|
66
|
+
session_layout_glob = `tmux display-message -p "#{Dir.tmpdir}/runbook_layout_\#{pid}_\#{session_name}_*.yml"`.strip
|
67
|
+
Dir[session_layout_glob]
|
68
|
+
end
|
69
|
+
|
70
|
+
def _setup_layout(structure)
|
71
|
+
current_pane = _runbook_pane
|
72
|
+
panes_to_init = []
|
73
|
+
{}.tap do |layout_panes|
|
74
|
+
if structure.is_a?(Hash)
|
75
|
+
first_window = true
|
76
|
+
structure.each do |name, window|
|
77
|
+
if first_window
|
78
|
+
_rename_window(name)
|
79
|
+
first_window = false
|
80
|
+
else
|
81
|
+
current_pane = _new_window(name)
|
82
|
+
end
|
83
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, window)
|
84
|
+
end
|
85
|
+
else
|
86
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure)
|
87
|
+
end
|
88
|
+
_swap_runbook_pane(panes_to_init, layout_panes)
|
89
|
+
_initialize_panes(panes_to_init, layout_panes)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def _setup_panes(layout_panes, panes_to_init, current_pane, structure, depth=0)
|
94
|
+
return if structure.empty?
|
95
|
+
case structure
|
96
|
+
when Array
|
97
|
+
case structure.size
|
98
|
+
when 1
|
99
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.shift, depth+1)
|
100
|
+
else
|
101
|
+
size = 100 - 100 / structure.size
|
102
|
+
new_pane = _split(current_pane, depth, size)
|
103
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.shift, depth+1)
|
104
|
+
_setup_panes(layout_panes, panes_to_init, new_pane, structure, depth)
|
105
|
+
end
|
106
|
+
when Hash
|
107
|
+
if structure.values.all? { |v| v.is_a?(Numeric) }
|
108
|
+
total_size = structure.values.reduce(:+)
|
109
|
+
case structure.size
|
110
|
+
when 1
|
111
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, structure.keys[0], depth+1)
|
112
|
+
else
|
113
|
+
size = (total_size - structure.values[0]) * 100 / total_size
|
114
|
+
new_pane = _split(current_pane, depth, size)
|
115
|
+
first_struct = structure.keys[0]
|
116
|
+
structure.delete(first_struct)
|
117
|
+
_setup_panes(layout_panes, panes_to_init, current_pane, first_struct, depth+1)
|
118
|
+
_setup_panes(layout_panes, panes_to_init, new_pane, structure, depth)
|
119
|
+
end
|
120
|
+
else
|
121
|
+
layout_panes[structure[:name]] = current_pane
|
122
|
+
panes_to_init << structure
|
123
|
+
end
|
124
|
+
when Symbol
|
125
|
+
layout_panes[structure] = current_pane
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def _swap_runbook_pane(panes_to_init, layout_panes)
|
130
|
+
if (runbook_pane = panes_to_init.find { |pane| pane[:runbook_pane] })
|
131
|
+
current_runbook_pane_name = layout_panes.keys.find do |k|
|
132
|
+
layout_panes[k] == _runbook_pane
|
133
|
+
end
|
134
|
+
target_pane_id = layout_panes[runbook_pane[:name]]
|
135
|
+
layout_panes[runbook_pane[:name]] = _runbook_pane
|
136
|
+
layout_panes[current_runbook_pane_name] = target_pane_id
|
137
|
+
_swap_panes(target_pane_id, _runbook_pane)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def _initialize_panes(panes_to_init, layout_panes)
|
142
|
+
panes_to_init.each do |pane|
|
143
|
+
target = layout_panes[pane[:name]]
|
144
|
+
_set_directory(pane[:directory], target) if pane[:directory]
|
145
|
+
send_keys(pane[:command], target) if pane[:command]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def _runbook_pane
|
150
|
+
@runbook_pane ||= `tmux display-message -p '#D'`.strip
|
151
|
+
end
|
152
|
+
|
153
|
+
def _rename_window(name)
|
154
|
+
`tmux rename-window "#{name}"`
|
155
|
+
end
|
156
|
+
|
157
|
+
def _new_window(name)
|
158
|
+
`tmux new-window -n "#{name}" -P -F '#D' -d`.strip
|
159
|
+
end
|
160
|
+
|
161
|
+
def _split(current_pane, depth, size)
|
162
|
+
direction = depth.even? ? "h" : "v"
|
163
|
+
command = "tmux split-window"
|
164
|
+
args = "-#{direction} -t #{current_pane} -p #{size} -P -F '#D' -d"
|
165
|
+
`#{command} #{args}`.strip
|
166
|
+
end
|
167
|
+
|
168
|
+
def _swap_panes(target_pane, source_pane)
|
169
|
+
`tmux swap-pane -d -t #{target_pane} -s #{source_pane}`
|
170
|
+
end
|
171
|
+
|
172
|
+
def _set_directory(directory, target)
|
173
|
+
send_keys("cd #{directory}; clear", target)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
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
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Runbook
|
2
|
+
class Initializer < Thor::Group
|
3
|
+
include Thor::Actions
|
4
|
+
|
5
|
+
source_root File.join(
|
6
|
+
File.dirname(__FILE__),
|
7
|
+
"generators",
|
8
|
+
"project",
|
9
|
+
)
|
10
|
+
|
11
|
+
add_runtime_options!
|
12
|
+
check_unknown_options!
|
13
|
+
|
14
|
+
def create_runbookfile
|
15
|
+
template(
|
16
|
+
"templates/Runbookfile.tt",
|
17
|
+
"Runbookfile",
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_runbooks_directory
|
22
|
+
target = "runbooks"
|
23
|
+
empty_directory(target)
|
24
|
+
_keep_dir(target)
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_lib_directory
|
28
|
+
dirs = [
|
29
|
+
"lib",
|
30
|
+
"runbook",
|
31
|
+
]
|
32
|
+
target = File.join(*dirs)
|
33
|
+
|
34
|
+
empty_directory(target)
|
35
|
+
_keep_dir(target)
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_extensions_directory
|
39
|
+
dirs = [
|
40
|
+
"lib",
|
41
|
+
"runbook",
|
42
|
+
"extensions",
|
43
|
+
]
|
44
|
+
target = File.join(*dirs)
|
45
|
+
|
46
|
+
empty_directory(target)
|
47
|
+
_keep_dir(target)
|
48
|
+
end
|
49
|
+
|
50
|
+
def create_generators_directory
|
51
|
+
dirs = [
|
52
|
+
"lib",
|
53
|
+
"runbook",
|
54
|
+
"generators",
|
55
|
+
]
|
56
|
+
target = File.join(*dirs)
|
57
|
+
|
58
|
+
empty_directory(target)
|
59
|
+
_keep_dir(target)
|
60
|
+
end
|
61
|
+
|
62
|
+
def runbook_initialization_overview
|
63
|
+
msg = [
|
64
|
+
"",
|
65
|
+
"Runbook was successfully initialized.",
|
66
|
+
"Add runbooks to the `runbooks` directory.",
|
67
|
+
"Add shared code to `lib/runbook`.",
|
68
|
+
"Execute runbooks using `bundle exec runbook exec <RUNBOOK_PATH>`",
|
69
|
+
"from your project root.",
|
70
|
+
"\n",
|
71
|
+
]
|
72
|
+
|
73
|
+
say(msg.join("\n"))
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def _keep_dir(dir)
|
79
|
+
create_file(
|
80
|
+
File.join(dir, ".keep"),
|
81
|
+
verbose: false,
|
82
|
+
)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/runbook/node.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Runbook
|
2
|
+
class Node
|
3
|
+
attr_accessor :parent
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
raise "Should not be initialized"
|
7
|
+
end
|
8
|
+
|
9
|
+
def dynamic!
|
10
|
+
@dynamic = true
|
11
|
+
end
|
12
|
+
|
13
|
+
def visited!
|
14
|
+
@visited = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def dynamic?
|
18
|
+
@dynamic
|
19
|
+
end
|
20
|
+
|
21
|
+
def visited?
|
22
|
+
@visited
|
23
|
+
end
|
24
|
+
|
25
|
+
def parent_entity
|
26
|
+
node = self
|
27
|
+
while(node && !node.is_a?(Runbook::Entity))
|
28
|
+
node = node.parent
|
29
|
+
end
|
30
|
+
node
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|