runbook 0.12.1

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