runbook 0.12.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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/CHANGELOG.md +46 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +6 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +999 -0
  12. data/Rakefile +6 -0
  13. data/TODO.md +38 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +8 -0
  16. data/exe/runbook +5 -0
  17. data/images/runbook_anatomy_diagram.png +0 -0
  18. data/images/runbook_example.gif +0 -0
  19. data/images/runbook_execution_modes.png +0 -0
  20. data/lib/hacks/ssh_kit.rb +58 -0
  21. data/lib/runbook/cli.rb +90 -0
  22. data/lib/runbook/configuration.rb +110 -0
  23. data/lib/runbook/dsl.rb +21 -0
  24. data/lib/runbook/entities/book.rb +17 -0
  25. data/lib/runbook/entities/section.rb +7 -0
  26. data/lib/runbook/entities/step.rb +7 -0
  27. data/lib/runbook/entity.rb +127 -0
  28. data/lib/runbook/errors.rb +7 -0
  29. data/lib/runbook/extensions/add.rb +13 -0
  30. data/lib/runbook/extensions/description.rb +14 -0
  31. data/lib/runbook/extensions/sections.rb +15 -0
  32. data/lib/runbook/extensions/shared_variables.rb +51 -0
  33. data/lib/runbook/extensions/ssh_config.rb +76 -0
  34. data/lib/runbook/extensions/statements.rb +26 -0
  35. data/lib/runbook/extensions/steps.rb +14 -0
  36. data/lib/runbook/extensions/tmux.rb +13 -0
  37. data/lib/runbook/helpers/format_helper.rb +11 -0
  38. data/lib/runbook/helpers/ssh_kit_helper.rb +143 -0
  39. data/lib/runbook/helpers/tmux_helper.rb +174 -0
  40. data/lib/runbook/hooks.rb +88 -0
  41. data/lib/runbook/node.rb +23 -0
  42. data/lib/runbook/run.rb +283 -0
  43. data/lib/runbook/runner.rb +64 -0
  44. data/lib/runbook/runs/ssh_kit.rb +186 -0
  45. data/lib/runbook/statement.rb +22 -0
  46. data/lib/runbook/statements/ask.rb +11 -0
  47. data/lib/runbook/statements/assert.rb +25 -0
  48. data/lib/runbook/statements/capture.rb +14 -0
  49. data/lib/runbook/statements/capture_all.rb +14 -0
  50. data/lib/runbook/statements/command.rb +11 -0
  51. data/lib/runbook/statements/confirm.rb +10 -0
  52. data/lib/runbook/statements/description.rb +9 -0
  53. data/lib/runbook/statements/download.rb +12 -0
  54. data/lib/runbook/statements/layout.rb +10 -0
  55. data/lib/runbook/statements/note.rb +10 -0
  56. data/lib/runbook/statements/notice.rb +10 -0
  57. data/lib/runbook/statements/ruby_command.rb +9 -0
  58. data/lib/runbook/statements/tmux_command.rb +11 -0
  59. data/lib/runbook/statements/upload.rb +12 -0
  60. data/lib/runbook/statements/wait.rb +10 -0
  61. data/lib/runbook/toolbox.rb +43 -0
  62. data/lib/runbook/util/repo.rb +56 -0
  63. data/lib/runbook/util/runbook.rb +25 -0
  64. data/lib/runbook/util/sticky_hash.rb +26 -0
  65. data/lib/runbook/util/stored_pose.rb +54 -0
  66. data/lib/runbook/version.rb +3 -0
  67. data/lib/runbook/view.rb +24 -0
  68. data/lib/runbook/viewer.rb +24 -0
  69. data/lib/runbook/views/markdown.rb +109 -0
  70. data/lib/runbook.rb +110 -0
  71. data/runbook.gemspec +48 -0
  72. data/samples/hooks_runbook.rb +72 -0
  73. data/samples/layout_runbook.rb +26 -0
  74. data/samples/restart_nginx.rb +26 -0
  75. data/samples/simple_runbook.rb +41 -0
  76. 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,13 @@
1
+ module Runbook::Extensions
2
+ module Tmux
3
+ module LayoutDSL
4
+ def layout(layout)
5
+ Runbook::Statements::Layout.new(layout).tap do |layout|
6
+ parent.add(layout)
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ Runbook::Entities::Book::DSL.prepend(Tmux::LayoutDSL)
13
+ 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
@@ -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