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
data/README.md
ADDED
@@ -0,0 +1,999 @@
|
|
1
|
+
# Runbook
|
2
|
+
|
3
|
+
> Runbook is a framework for progressively automating system operations.
|
4
|
+
|
5
|
+
Runbook provides a DSL for specifying a series of steps to execute an operation. Once your runbook is specified, you can use it to generate a formatted representation of the book or to execute the runbook interactively. For example, you can export your runbook to markdown or use the same runbook to execute commands on remote servers.
|
6
|
+
|
7
|
+
<div align="center">
|
8
|
+
<img width="600" src="images/runbook_example.gif" alt="example of a runbook" />
|
9
|
+
</div>
|
10
|
+
<br>
|
11
|
+
|
12
|
+
Runbook provides two modes for evaluating your runbook. The first mode, view mode, allows you to export your runbook into various formats such as markdown. The second mode, run mode, allows you to execute behavior based on the statements in your runbook.
|
13
|
+
|
14
|
+
<div align="center">
|
15
|
+
<img width="600" src="images/runbook_execution_modes.png" alt="diagram of execution modes" />
|
16
|
+
</div>
|
17
|
+
<br>
|
18
|
+
|
19
|
+
Runbook provides a very flexible interface. It can be integrated into your existing projects to add orchestration functionality, installed on systems as a stand-alone executable, or runbooks can be defined as self-executable scripts. In addition to being useful for automating common tasks, runbooks are a perfect bridge for providing operations teams with step-by-step instructions to handle common issues (especially when solutions cannot be easily automated).
|
20
|
+
|
21
|
+
Lastly, Runbook provides an extendable interface for augmenting the DSL and defining your own behavior.
|
22
|
+
|
23
|
+
## Features
|
24
|
+
|
25
|
+
* **Remote Command Execution** - Runbook lets you execute commands on remote hosts using [SSHKit](https://github.com/capistrano/sshkit)
|
26
|
+
* **Dynamic Control Flow** - Runbooks can start execution at any step and can skip steps based on user input.
|
27
|
+
* **Resumable** - Runbooks save their state at each step. If your runbook encounters an error, you can resume your runbook at the previous step after addressing the error.
|
28
|
+
* **Noop and Auto Modes** - Runbooks can be executed in noop mode. This allows you to see what a runbook will do before it executes. Runbooks can be run in auto mode to eliminate the need for human interaction.
|
29
|
+
* **Execution Lifecycle Hooks** - Runbook provides before, after, around hooks to augment its execution behavior.
|
30
|
+
* **Tmux Integration** - Runbook integrates with [tmux](https://github.com/tmux/tmux). You can define terminal pane layouts and send commands to terminal panes.
|
31
|
+
* **Extendable DSL** - Runbook's DSL is designed to be extendable. You can extend its DSL to add your own behavior.
|
32
|
+
|
33
|
+
## Use Cases
|
34
|
+
|
35
|
+
Runbook is a very flexible tool. Though it can solve a myriad of problems, Runbook is best used for removing the need for repeated, rote developer operations. Runbook allows developers to execute processes at a higher level than that of individual command-line commands. Additionally, Runbook provides features to simply and safely execute operations in mission-critical environments.
|
36
|
+
|
37
|
+
Runbook is not intended to replace more special-purpose automation solutions such as configuration management solutions (Puppet, Chef, Ansible, Salt), deployment solutions (Capistrano, Kubernetes, Docker Swarm), monitoring solutions (Nagios, Datadog), or local command execution (shell scripts, Rake tasks, Make). Instead Runbook is best used as a glue when needing to accomplish a task that cuts across these domains.
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
Add this line to your application's Gemfile:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
gem 'runbook'
|
45
|
+
```
|
46
|
+
|
47
|
+
And then execute:
|
48
|
+
|
49
|
+
$ bundle
|
50
|
+
|
51
|
+
Or install it yourself as:
|
52
|
+
|
53
|
+
$ gem install runbook
|
54
|
+
|
55
|
+
## Contents
|
56
|
+
|
57
|
+
* [1. Runbook Anatomy](#runbook-anatomy)
|
58
|
+
* [1.1 Entities, Statements, and Setters](#entities-statements-and-setters)
|
59
|
+
* [1.1.1 Books, Sections, and Steps](#books-sections-and-steps)
|
60
|
+
* [1.1.1.1 Books](#books)
|
61
|
+
* [1.1.1.2 Sections](#sections)
|
62
|
+
* [1.1.1.3 Steps](#steps)
|
63
|
+
* [1.1.2 Statements](#statements)
|
64
|
+
* [1.1.2.1 Ask](#ask)
|
65
|
+
* [1.1.2.2 Assert](#assert)
|
66
|
+
* [1.1.2.3 Capture](#capture)
|
67
|
+
* [1.1.2.4 Capture All](#capture-all)
|
68
|
+
* [1.1.2.5 Command](#command)
|
69
|
+
* [1.1.2.6 Confirm](#confirm)
|
70
|
+
* [1.1.2.7 Description](#description)
|
71
|
+
* [1.1.2.8 Download](#download)
|
72
|
+
* [1.1.2.9 Layout](#layout)
|
73
|
+
* [1.1.2.10 Note](#note)
|
74
|
+
* [1.1.2.11 Notice](#notice)
|
75
|
+
* [1.1.2.12 Ruby Command](#ruby-command)
|
76
|
+
* [1.1.2.13 Tmux Command](#tmux-command)
|
77
|
+
* [1.1.2.14 Upload](#upload)
|
78
|
+
* [1.1.2.15 Wait](#wait)
|
79
|
+
* [1.1.2.16 Tmux Layouts](#tmux-layouts)
|
80
|
+
* [1.1.3 Setters](#setters)
|
81
|
+
* [2. Configuration](#configuration)
|
82
|
+
* [2.1 Configuration Files](#configuration-files)
|
83
|
+
* [3. Working With Runbooks](#working-with-runbooks)
|
84
|
+
* [3.1 From Within Your Project](#from-within-your-project)
|
85
|
+
* [3.2 Via The Command Line](#via-the-command-line)
|
86
|
+
* [3.3 Self-executable](#self-executable)
|
87
|
+
* [4. Best Practices](#best-practices)
|
88
|
+
* [4.1 Iterative Automation](#iterative-automation)
|
89
|
+
* [4.2 Parameterizing Runbooks](#parameterizing-runbooks)
|
90
|
+
* [4.3 Execution Best Practices](#execution-best-practices)
|
91
|
+
* [4.4 Composing Runbooks](#composing-runbooks)
|
92
|
+
* [4.5 Deep Nesting](#deep-nesting)
|
93
|
+
* [4.6 Load Vs. Eval](#load-vs-eval)
|
94
|
+
* [4.7 Passing State](#passing-state)
|
95
|
+
* [5. Extending Runbook](#extending-runbook)
|
96
|
+
* [5.1 Adding Runs and Views](#adding-runs-and-views)
|
97
|
+
* [5.2 DSL Extensions](#dsl-extensions)
|
98
|
+
* [5.3 Adding New Statements](#adding-new-statements)
|
99
|
+
* [5.4 Adding Run and View Functionality](#adding-run-and-view-functionality)
|
100
|
+
* [5.5 Augmenting Functionality With Hooks](#augmenting-functionality-with-hooks)
|
101
|
+
* [5.6 Adding New Run Behaviors](#adding-new-run-behaviors)
|
102
|
+
* [5.7 Adding to Runbook's Run Metadata](#adding-to-runbooks-run-metadata)
|
103
|
+
* [5.8 Adding to Runbook's Configuration](#adding-to-runbooks-configuration)
|
104
|
+
* [6. Known Issues](#known-issues)
|
105
|
+
* [7. Development](#development)
|
106
|
+
* [8. Contributing](#contributing)
|
107
|
+
* [9. Feature Requests](#feature-requests)
|
108
|
+
* [10. License](#license)
|
109
|
+
* [11. Code of Conduct](#code-of-conduct)
|
110
|
+
|
111
|
+
## Runbook Anatomy
|
112
|
+
|
113
|
+
Below is an example of a runbook:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Runbook.book "Restart Nginx" do
|
117
|
+
description <<-DESC
|
118
|
+
This is a simple runbook to restart nginx and verify
|
119
|
+
it starts successfully
|
120
|
+
DESC
|
121
|
+
|
122
|
+
section "Restart Nginx" do
|
123
|
+
server "app01.prod"
|
124
|
+
user "root"
|
125
|
+
|
126
|
+
step "Stop Nginx" do
|
127
|
+
note "Stopping Nginx..."
|
128
|
+
command "service nginx stop"
|
129
|
+
assert %q{service nginx status | grep "not running"}
|
130
|
+
end
|
131
|
+
|
132
|
+
step { wait 5 }
|
133
|
+
|
134
|
+
step "Start Nginx" do
|
135
|
+
note "Starting Nginx..."
|
136
|
+
command "service nginx start"
|
137
|
+
assert %q{service nginx status | grep "is running"}
|
138
|
+
confirm "Nginx is taking traffic"
|
139
|
+
notice "Make sure to report why you restarted nginx"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
Hierarchically, a runbook looks like this:
|
146
|
+
|
147
|
+
<div align="center">
|
148
|
+
<img width="600" src="images/runbook_anatomy_diagram.png" alt="diagram of a runbook" />
|
149
|
+
</div>
|
150
|
+
<br>
|
151
|
+
|
152
|
+
### Entities, Statements, and Setters
|
153
|
+
|
154
|
+
A runbook is composed of entities, statements, and setters. Runbook entities contain either other entities or statements. Examples of entities include Books, Sections, and Steps. They define the structure of the runbook and can be considered the "nodes" of the tree structure. As entities are the nodes of the tree structure, statements are the "leaves" of the structure and comprise the various behaviors or commands of the runbook. Setters, typically referenced from within steps, associate state with the node, which can be accessed by its children.
|
155
|
+
|
156
|
+
#### Books, Sections, and Steps
|
157
|
+
|
158
|
+
Entities are composed of a title and a list of items which are their children. Each entity can be rendered with a specific view or executed with a specific run.
|
159
|
+
|
160
|
+
##### Books
|
161
|
+
|
162
|
+
Books are the root of a runbook. They are initialized as follows:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
Runbook.book "Unbalance node" do
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
Every book requires a title. Books can have description, layout, and section children. Descriptions describe the book and are declared with the `description` keyword.
|
170
|
+
|
171
|
+
##### Sections
|
172
|
+
|
173
|
+
A book is broken up into sections. Every section requires a title. Sections can have descriptions, other sections, or steps as children.
|
174
|
+
|
175
|
+
##### Steps
|
176
|
+
|
177
|
+
Steps hold state and group together a set of statements. Steps do not require titles or children. This allows runbooks to be very flexible. You can fill out steps as needed, or be terse when the behavior of the step is self-evident. Steps without titles will not prompt to continue when running in paranoid mode.
|
178
|
+
|
179
|
+
#### Statements
|
180
|
+
|
181
|
+
Statements are the workhorses of runbooks. They comprise all the behavior runbooks execute. Runbook comes with the following statements:
|
182
|
+
|
183
|
+
##### Ask
|
184
|
+
|
185
|
+
Prompts the user for a string and stores its value on the containing step entity. Once this statement is executed, its value is accessed as an instance variable under the `into` parameter. This value can be referenced in later statements such as the `ruby_command` statement.
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
ask "What percentage of requests are failing?", into: :failing_request_percentage, default: "100"
|
189
|
+
```
|
190
|
+
|
191
|
+
##### Assert
|
192
|
+
|
193
|
+
Runs the provided `cmd` repeatedly until it returns true. A timeout and maximum number of attempts can be set. You can specify a command to be run if a timeout occurs or the maximum number of attempts is hit. Commands can optionally be specified as `raw`. This tells SSHKit to not perform auto-wrapping of the commands, but execute the exact string on the remote server. See SSHKit's documentation for more details.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
assert(
|
197
|
+
'service nginx status | grep "is running"',
|
198
|
+
cmd_ssh_config: {servers: ["host1.prod"], parallelization: {strategy: :parallel}},
|
199
|
+
cmd_raw: false,
|
200
|
+
interval: 3, # seconds
|
201
|
+
timeout: 300, # seconds
|
202
|
+
attempts: 3,
|
203
|
+
timeout_statement: Runbook::Statements::Command.new(
|
204
|
+
"echo 'help' | mail -s 'need help' page-me@page-me.com",
|
205
|
+
ssh_config: {servers: [:local], parallelization: {strategy: :parallel}},
|
206
|
+
raw: false
|
207
|
+
)
|
208
|
+
)
|
209
|
+
```
|
210
|
+
|
211
|
+
##### Capture
|
212
|
+
|
213
|
+
Runs the provided `cmd` and captures its output into `into`. An optional `ssh_config` can be specified to configure how the capture command gets run. Capture commands take an optional `strip` parameter that indicates if the returned output should have leading and trailing whitespace removed. Capture commands also take an optional `raw` parameter that tells SSHKit whether the command should be executed as is, or to include the auto-wrapping of the ssh_config.
|
214
|
+
|
215
|
+
```ruby
|
216
|
+
capture %Q{wc -l file.txt | cut -d " " -f 1}, into: :num_lines, strip: true, ssh_config: {user: "root"}
|
217
|
+
```
|
218
|
+
|
219
|
+
##### Capture All
|
220
|
+
|
221
|
+
Accepts the same parameters as `capture`, but returns a hash of server names to capture results. `capture_all` should be used whenever multiple servers are specified because the returned result of `capture` is non-deterministic when specifying multiple servers.
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
capture_all %Q{wc -l file.txt | cut -d " " -f 1}, into: :num_lines, strip: true, ssh_config: {servers: ["host1.stg", "host2.stg"]}
|
225
|
+
```
|
226
|
+
|
227
|
+
##### Command
|
228
|
+
|
229
|
+
Runs the provided `cmd`. An optional `ssh_config` can be specified to configure how the command gets run. Commands also take an optional `raw` parameter that tells SSHKit whether the command should be executed as is, or to include the auto-wrapping of the ssh_config.
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
command "service nginx start", ssh_config: {servers: ["host1.prod", "host2.prod"], parallelization: {strategy: :groups}}
|
233
|
+
```
|
234
|
+
|
235
|
+
##### Confirm
|
236
|
+
|
237
|
+
Proposes the prompt to the user and exits if the user does not confirm the prompt.
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
confirm "Asset requests have started trickling to the box"
|
241
|
+
```
|
242
|
+
|
243
|
+
##### Description
|
244
|
+
|
245
|
+
Prints the description in an unformatted manner to the user
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
description <<-DESC
|
249
|
+
This message will print directly to the user as written, without
|
250
|
+
additional formatting.
|
251
|
+
DESC
|
252
|
+
```
|
253
|
+
|
254
|
+
##### Download
|
255
|
+
|
256
|
+
Downloads the specified file to `to`. An optional `ssh_config` can be specified to configure how the download command gets run, for example specifying the remote host and remote directory to download from. Optional `options` can be specified that get passed down to the underlying sshkit implementation
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
download /home/pblesi/rad_file.txt, to: my_rad_file.txt, ssh_config: {servers: ["host1.prod"]}, options: {log_percent: 10}
|
260
|
+
```
|
261
|
+
|
262
|
+
##### Layout
|
263
|
+
|
264
|
+
Defines a tmux layout to be used by your runbook. When executing the runbook, the specified layout will be initialized. This statement can only be specified at the book level. See [Tmux Layouts](#tmux-layouts) for more details.
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
layout [[
|
268
|
+
[:runbook, :deploy],
|
269
|
+
[:monitor_1, :monitor_2, :monitor_3],
|
270
|
+
]]
|
271
|
+
```
|
272
|
+
|
273
|
+
##### Note
|
274
|
+
|
275
|
+
Prints a short note to the user.
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
note "This operation kills all zombie processes"
|
279
|
+
```
|
280
|
+
|
281
|
+
##### Notice
|
282
|
+
|
283
|
+
Prints out an important message to the user.
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
notice "There be dragons!"
|
287
|
+
```
|
288
|
+
|
289
|
+
##### Ruby Command
|
290
|
+
|
291
|
+
Executes its block in the context of the parent step. The block is passed the ruby_command statement and the execution metadata as arguments.
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
ruby_command do |rb_cmd, metadata|
|
295
|
+
if (failure_rate = rb_cmd.parent.failing_request_percentage) > 25
|
296
|
+
`echo 'Help! failure rate at #{failure_rate}' | mail -s 'High failure rate!' page-me@page-me.com`
|
297
|
+
else
|
298
|
+
`echo "Experienced failure rate of #{failure_rate}" | mail -s 'Help me eventually' not-urgent@my_site.com`
|
299
|
+
end
|
300
|
+
notice "Email sent!"
|
301
|
+
end
|
302
|
+
```
|
303
|
+
|
304
|
+
Metadata at execution time is structured as follows:
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
{
|
308
|
+
book_title: "Restart Nginx", # The title of the current runbook
|
309
|
+
depth: 1, # The depth within the tree (book starts at depth 1)
|
310
|
+
index: 0, # The index of the item in terms of it's parent's children (starts at 0 for first child)
|
311
|
+
position: "1.1", # A string representing your current position within the tree
|
312
|
+
noop: false, # A boolean indicating if you are running in noop mode. ruby_command blocks are never evaluated in noop mode
|
313
|
+
auto: false, # A boolean indicating if you are running in auto mode
|
314
|
+
paranoid: true, # A boolean indicating if you are running in paranoid mode (prompting before each step)
|
315
|
+
start_at: 0, # A string representing the step where nodes should start being processed
|
316
|
+
toolbox: Runbook::Toolbox.new, # A collection of methods to invoke side-effects such as printing and collecting input
|
317
|
+
layout_panes: {}, # A map of pane names to pane ids. `layout_panes` is used by the `tmux_command` to identify which tmux pane to send the command to
|
318
|
+
repo: {}, # A repository for storing data and retrieving it between ruby_commands. Any data stored in the repo is persisted if a runbook is stopped and later resumed.
|
319
|
+
}
|
320
|
+
```
|
321
|
+
|
322
|
+
Additional methods that the `ruby_command` block has access to are:
|
323
|
+
|
324
|
+
* `metadata[:toolbox].prompt`: A `TTY::Prompt` for retrieving input from the user
|
325
|
+
* `metadata[:toolbox].ask(msg)`: retrieve user input
|
326
|
+
* `metadata[:toolbox].yes?(msg)`: provide the user with a yes/no prompt
|
327
|
+
* `metadata[:toolbox].output(msg)`: output text to the user
|
328
|
+
* `metadata[:toolbox].warn(msg)`: output warning text to the user
|
329
|
+
* `metadata[:toolbox].error(msg)`: output error text to the user
|
330
|
+
* `metadata[:toolbox].exit(return_value)`: exit the process with the specified response code
|
331
|
+
|
332
|
+
##### Tmux Command
|
333
|
+
|
334
|
+
Runs the provided `cmd` in the specified `pane`.
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
tmux_command "tail -Fn 100 /var/log/nginx.log", :monitor_1
|
338
|
+
```
|
339
|
+
|
340
|
+
##### Upload
|
341
|
+
|
342
|
+
Uploads the specified file to `to`. An optional `ssh_config` can be specified to configure how the upload command gets run, for example specifying the remote host and remote directory to upload to. Optional `options` can be specified that get passed down to the underlying sshkit implementation
|
343
|
+
|
344
|
+
```ruby
|
345
|
+
upload my_secrets.yml, to: secrets.yml, ssh_config: {servers: ["host1.prod"]}, options: {log_percent: 10}
|
346
|
+
```
|
347
|
+
|
348
|
+
##### Wait
|
349
|
+
|
350
|
+
Sleeps for the specified amount of time (in seconds)
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
wait 5
|
354
|
+
```
|
355
|
+
|
356
|
+
##### Tmux Layouts
|
357
|
+
|
358
|
+
Runbook provides native support for defining tmux layouts and executing commands in separate tmux panes. Layouts are specified by passing an array or hash to the `layout` statement in book blocks.
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
Runbook.book "My Book" do
|
362
|
+
layout [
|
363
|
+
:left,
|
364
|
+
{name: :middle, directory: "/var/log", command: "tail -Fn 100 auth.log"},
|
365
|
+
[:top_right, {name: :bottom_right, runbook_pane: true}]
|
366
|
+
]
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
When layout is passed as an array, each element of the array represents a pane stacked side-by-side with the other elements. Elements of the array can be symbols, hashes, or arrays.
|
371
|
+
|
372
|
+
Symbols and hashes represent panes. Hash keys for a pane include `name`, `directory`, `command`, and `runbook_pane`. `name` is the identifier used for the pane. This is used when specifying what pane you want to execute tmux commands in. `directory` indicates the starting directory of the pane. `command` is the initial command to execute in the pane when it is created. `runbook_pane` indicates which pane in the layout should hold the executing runbook. Only one pane should be designated as the `runbook_pane` and the runbook pane should not have a directory or command specified.
|
373
|
+
|
374
|
+
Arrays nested underneath the initial array split the pane from top to bottom. Arrays nested under these arrays split the pane from side to side, ad infinitum. You can start spliting panes from top to bottom as opposed to side-by-side by immediately nesting an array.
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
Runbook.book "Stacked Layout" do
|
378
|
+
layout [[
|
379
|
+
:top,
|
380
|
+
:middle,
|
381
|
+
:bottom,
|
382
|
+
]]
|
383
|
+
end
|
384
|
+
```
|
385
|
+
|
386
|
+
When a hash is passed to `layout`, the keys of the hash represent window names and the values represent pane layouts.
|
387
|
+
|
388
|
+
```ruby
|
389
|
+
Runbook.book "Multi Window Layout" do
|
390
|
+
layout({
|
391
|
+
:web_monitor => [
|
392
|
+
:left, :middle, :right,
|
393
|
+
],
|
394
|
+
:db_monitor => [[
|
395
|
+
:top, :middle, :bottom,
|
396
|
+
]]
|
397
|
+
})
|
398
|
+
end
|
399
|
+
```
|
400
|
+
|
401
|
+
Notice in the example that parenthesis are used to wrap the hash. Ruby will raise a syntax error if `layout`'s argument is not wrapped in parenthesis when passing a hash. Runbook expects that it is running in the last window in a tmux session. If you are running a runbook that uses a multi-window layout, the layout will not work unless runbook is running in the last window in the session.
|
402
|
+
|
403
|
+
If you want panes to be un-evenly spaced, you can replace the array of panes with a hash where the keys are panes and the values are numbers. The panes will be spaced according to the specified numbers.
|
404
|
+
|
405
|
+
```ruby
|
406
|
+
Runbook.book "Uneven Layout" do
|
407
|
+
layout [[
|
408
|
+
{:left => 20, {name: :middle, runbook_pane: true} => 60, :right => 20},
|
409
|
+
{:bottom_left => 5, :bottom_right => 5},
|
410
|
+
]]
|
411
|
+
end
|
412
|
+
```
|
413
|
+
|
414
|
+
Tmux layouts are persisted between runs of the same runbook. As long as none of the panes initially created by the runbook are closed, running the same runbook in the same pane will not recreate the tmux layout, but will reuse the existing layout. This is helpful when a runbook does not complete and must be restarted. When a runbook finishes, it asks if you want to close all opened panes. If your runbook is running in auto mode it will automatically close all panes when finished.
|
415
|
+
|
416
|
+
#### Setters
|
417
|
+
|
418
|
+
Setters set state on the parent item, typically the containing step. Runbook comes with the following setters:
|
419
|
+
|
420
|
+
**parallelization**: Specifies the SSHKit parallelization parameters for all commands in the entity. The default parallelization strategy is `:parallel`. Other strategies include `:sequence` and `:groups`. See [SSHKit](https://github.com/capistrano/sshkit#parallel) for more details on these options.
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
parallelization strategy: :parallel, limit: 2, wait: 2
|
424
|
+
```
|
425
|
+
|
426
|
+
**server**: Specifies the server to use for all commands in the entity. This command in conjunction with `servers` are declarative and overwrite each other. So if you specify `server` once, `servers` twice and finally, `server` again, only the last designation will be used to run the commands.
|
427
|
+
|
428
|
+
```ruby
|
429
|
+
server "db01.qa"
|
430
|
+
```
|
431
|
+
|
432
|
+
**servers**: Used to specify a list of servers for the entity. All commands contained in this entity will be run against this list of servers (unless they have been overridden by a lower config.)
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
servers "app01.qa", "app02.qa"
|
436
|
+
```
|
437
|
+
|
438
|
+
**path**: Specify the path from which commands in this step will execute.
|
439
|
+
|
440
|
+
```ruby
|
441
|
+
path "/home/sholmes"
|
442
|
+
```
|
443
|
+
|
444
|
+
**user**: Specify the user that the command will be run as
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
user "root"
|
448
|
+
```
|
449
|
+
|
450
|
+
**group**: Specify the effective group the commands will be run as
|
451
|
+
|
452
|
+
```ruby
|
453
|
+
group "devs"
|
454
|
+
```
|
455
|
+
|
456
|
+
**env**: Specify the environment for the commands
|
457
|
+
|
458
|
+
```ruby
|
459
|
+
env {rails_env: :production}
|
460
|
+
```
|
461
|
+
|
462
|
+
**umask**: Specify the umask the commands will be run with
|
463
|
+
|
464
|
+
```ruby
|
465
|
+
umask "077"
|
466
|
+
```
|
467
|
+
|
468
|
+
Additionally, `Step` provides an `ssh_config` helper method for generating ssh_configs that can be passed to command statements.
|
469
|
+
|
470
|
+
```ruby
|
471
|
+
step do
|
472
|
+
cmd_ssh_config = ssh_config do
|
473
|
+
server "host1.qa"
|
474
|
+
user "root"
|
475
|
+
end
|
476
|
+
command "echo $USER", ssh_config: cmd_ssh_config
|
477
|
+
end
|
478
|
+
```
|
479
|
+
|
480
|
+
## Configuration
|
481
|
+
|
482
|
+
Runbook is configured using its configuration object. Below is an example of how to configure Runbook.
|
483
|
+
|
484
|
+
```ruby
|
485
|
+
Runbook.configure do |config|
|
486
|
+
config.ssh_kit.umask = "077"
|
487
|
+
config.ssh_kit.default_runner_config = {in: :groups, limit: 5}
|
488
|
+
config.ssh_kit.default_env = {rails_env: :staging}
|
489
|
+
|
490
|
+
config.enable_sudo_prompt = true
|
491
|
+
config.use_same_sudo_password = true
|
492
|
+
end
|
493
|
+
```
|
494
|
+
|
495
|
+
If the `ssh_kit` configuration looks familiar, that's because it's an SSHKit Configuration object. Any configuration options set on `SSHKit.config` can be set on `config.ssh_kit`.
|
496
|
+
|
497
|
+
### Configuration Files
|
498
|
+
|
499
|
+
Runbook automatically loads configuration from a number of predefined files. Runbook will attempt to load configuration from the following locations on startup: `/etc/runbook.conf`, a `Runbookfile` in a parent directory from the current directory, a `.runbook.conf` file in the current user's home directory, a file specified with `--config` on the command line, any configuration specified in a runbook. Runbook will also load configuration from these files in this order of preference, respectively. That is, configuration values specified at the project level (`Runbookfile`) will override configuration values set at the global level (`/etc/runbook.conf`), etc.
|
500
|
+
|
501
|
+
## Working With Runbooks
|
502
|
+
|
503
|
+
You can integrate with Runbook in several different ways. You can create your own project or incorporate Runbook into your existing projects. You can use Runbook via the command line. And you can even create self-executing runbooks.
|
504
|
+
|
505
|
+
### From Within Your Project
|
506
|
+
|
507
|
+
Runbooks can be executed using the `Runbook::Viewer` and `Runbook::Runner` classes.
|
508
|
+
|
509
|
+
#### Executing a runbook using `Runbook::Viewer`
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
Runbook::Viewer.new(book).generate(view: :markdown)
|
513
|
+
```
|
514
|
+
|
515
|
+
In this case book is a `Runbook::Entities::Book` and `:markdown` refers to the specific view type (`Runbook::Views::Markdown`).
|
516
|
+
|
517
|
+
#### Executing a runbook using `Runbook::Runner`
|
518
|
+
|
519
|
+
```ruby
|
520
|
+
Runbook::Runner.new(book).run(run: :ssh_kit, noop: false, auto: false, paranoid: true, start_at: "0")
|
521
|
+
```
|
522
|
+
|
523
|
+
This will execute `book` using the `Runbook::Runs::SSHKit` run type. It will not run the book in `noop` mode. It will not run the book in `auto` mode. It will run the book in `paranoid` mode. And it will start at the beginning of the book. Noop mode runs the book without side-effects outside of printing what it will execute. Auto mode will skip any prompts in the runbook. If there are any required prompts in the runbook (such as the `ask` statement), then the run will fail. Paranoid mode will prompt the user for whether they should continue at every step. Finally `start_at` can be used to skip parts of the runbook or to restart at a certain point in the event of failures, stopping and starting the runbook, etc.
|
524
|
+
|
525
|
+
### Via The Command Line
|
526
|
+
|
527
|
+
Runbook can be used to write stand-alone runbook files that can be executed via the command line. Below is a list of examples of how to use Runbook via the command line.
|
528
|
+
|
529
|
+
Get Runbook usage instructions
|
530
|
+
|
531
|
+
```sh
|
532
|
+
$ runbook help
|
533
|
+
```
|
534
|
+
|
535
|
+
Render `my_runbook.rb` in the default view format (markdown)
|
536
|
+
|
537
|
+
```sh
|
538
|
+
$ runbook view my_runbook.rb
|
539
|
+
```
|
540
|
+
|
541
|
+
Execute `my_runbook.rb` using the default executor (ssh_kit)
|
542
|
+
|
543
|
+
```sh
|
544
|
+
$ runbook exec my_runbook.rb
|
545
|
+
```
|
546
|
+
|
547
|
+
Execute `my_runbook.rb` in no-op mode, preventing commands from executing.
|
548
|
+
|
549
|
+
```sh
|
550
|
+
$ runbook exec --noop my_runbook.rb
|
551
|
+
```
|
552
|
+
|
553
|
+
Execute `my_runbook.rb` in auto mode. Runbooks that are executed in auto mode do not prompt the user for input.
|
554
|
+
|
555
|
+
```sh
|
556
|
+
$ runbook exec --auto my_runbook.rb
|
557
|
+
```
|
558
|
+
|
559
|
+
Execute `my_runbook.rb` starting at position 1.2.1. All prior steps in the runbook will be skipped
|
560
|
+
|
561
|
+
```sh
|
562
|
+
$ runbook exec --start-at 1.2.1 my_runbook.rb
|
563
|
+
```
|
564
|
+
|
565
|
+
Execute `my_runbook.rb` without confirmation between each step
|
566
|
+
|
567
|
+
```sh
|
568
|
+
$ runbook exec --no-paranoid my_runbook.rb
|
569
|
+
```
|
570
|
+
|
571
|
+
Environment variables can be specified via the command line, modifying the behavior of
|
572
|
+
the runbook at runtime.
|
573
|
+
|
574
|
+
```sh
|
575
|
+
$ HOSTS="appbox{01..30}.prod" ENV="production" runbook exec --start-at 1.2.1 my_runbook.rb
|
576
|
+
```
|
577
|
+
|
578
|
+
### Self-executable
|
579
|
+
|
580
|
+
Runbooks can be written to be self-executable
|
581
|
+
|
582
|
+
```ruby
|
583
|
+
#!/usr/bin/env ruby
|
584
|
+
# my_runbook.rb
|
585
|
+
require "runbook"
|
586
|
+
|
587
|
+
runbook = Runbook.book "Say hello to world" do
|
588
|
+
section "Address the world" do
|
589
|
+
step { command "echo 'hello world!'" }
|
590
|
+
step { confirm "Has the world received your greeting?" }
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
if __FILE__ == $0
|
595
|
+
Runbook::Runner.new(runbook).run
|
596
|
+
else
|
597
|
+
runbook
|
598
|
+
end
|
599
|
+
```
|
600
|
+
|
601
|
+
This runbook can be executed via the command line or evaluated from within an existing project
|
602
|
+
|
603
|
+
```sh
|
604
|
+
$ ./my_runbook.rb
|
605
|
+
```
|
606
|
+
|
607
|
+
```ruby
|
608
|
+
load "my_runbook.rb"
|
609
|
+
runbook = Runbook.books.last # Runbooks register themselves to Runbook.books when they are defined
|
610
|
+
# (Or alternatively `runbook = eval(File.read("my_runbook.rb"))`)
|
611
|
+
Runbook::Runner.new(runbook).run(auto: true)
|
612
|
+
```
|
613
|
+
|
614
|
+
## Best Practices
|
615
|
+
|
616
|
+
The following are best practices when developing your own runbooks.
|
617
|
+
|
618
|
+
### Iterative Automation
|
619
|
+
|
620
|
+
Runbooks allow for a very gradual transition from entirely manual operations to full automation. Runbooks can start out as a simple outline of all steps required to carry out an operation. From there, commands and prompts can be added to the runbook, actually carrying out and replacing the manual processes.
|
621
|
+
|
622
|
+
Monitoring can transition from a process required by a human into something that can be codified and executed by your runbook. Eventually, the runner's `auto` flag can be used to allow the runbook to run uninterrupted without any human intervention. These runbooks can be triggered automatically in response to detected events. This will allow you to do more important things with your time, like eat ice cream.
|
623
|
+
|
624
|
+
### Parameterizing Runbooks
|
625
|
+
|
626
|
+
You will typically want to parameterize your runbooks so they can be run against different hosts or in different environments. Because runbooks are Ruby, you have a multitude of options for parameterizing your runbooks, from config files, to getting host information via shell commands, to using environment variables. Here's an example of a few of these methods:
|
627
|
+
|
628
|
+
```ruby
|
629
|
+
host = ENV["HOST"] || "<host>"
|
630
|
+
replication_host = ENV["REPLICATION_HOST"] || "<replication_host>"
|
631
|
+
env = `facter environment`
|
632
|
+
rails_env = `facter rails_env`
|
633
|
+
customer_list = File.read("/tmp/customer_list.txt")
|
634
|
+
```
|
635
|
+
|
636
|
+
### Execution Best Practices
|
637
|
+
|
638
|
+
As a best practice, Runbooks should always be nooped before they are run. This will allow you to catch runtime errors such as using the ask statement when running in auto mode, typos in your runbooks, and to visually confirm what will be executed.
|
639
|
+
|
640
|
+
Additionally, it can be nice to have a generated view of the runbook you are executing to have a good high-level overview of the steps in the runbook.
|
641
|
+
|
642
|
+
### Composing Runbooks
|
643
|
+
|
644
|
+
Runbooks can be composed using the `add` keyword. Below is an example of composing a runbook from smaller, reusable components.
|
645
|
+
|
646
|
+
```ruby
|
647
|
+
restart_services_section = Runbook.section "Restart all services" do
|
648
|
+
step "Restart nginx"
|
649
|
+
step "Restart postgres"
|
650
|
+
end
|
651
|
+
|
652
|
+
Runbook.book "Update configuration" do
|
653
|
+
section "Change config" do
|
654
|
+
command "sed -i 's/listen 8080;/listen 80;/' /etc/nginx/nginx.conf"
|
655
|
+
end
|
656
|
+
|
657
|
+
add restart_services_section
|
658
|
+
end
|
659
|
+
```
|
660
|
+
|
661
|
+
### Deep Nesting
|
662
|
+
|
663
|
+
Because the Runbook DSL is declarative, it is generally discouraged to develop elaborate nested decision trees. For example, it is discouraged to use the `ask` statement to gather user feedback, branch on this information in a `ruby_command`, and follow completely separate sets of steps. This is because deep nesting eliminates the benefits of the declarative DSL. You can no longer noop the deeply nested structure for example.
|
664
|
+
|
665
|
+
If you are looking to make a complex decision tree, it is recommended that you do this by composing separate runbooks or entities and nooping those entities separately to ensure they work as expected. Below is an example of a few different ways to compose nested runbooks
|
666
|
+
|
667
|
+
```ruby
|
668
|
+
step "Inspect plate" do
|
669
|
+
ask "What's on the plate?", into: :vegetable
|
670
|
+
ruby_command do |rb_cmd, metadata|
|
671
|
+
case (veggie = @vegetable)
|
672
|
+
when "carrots"
|
673
|
+
add carrots_book
|
674
|
+
when "peas"
|
675
|
+
system("runbook exec samples/print_peas.rb")
|
676
|
+
else
|
677
|
+
metadata[:toolbox].warn("Found #{veggie}!")
|
678
|
+
end
|
679
|
+
end
|
680
|
+
end
|
681
|
+
```
|
682
|
+
|
683
|
+
The first delegation `add carrots_book` adds the book to the execution tree of the current runbook. Sections and steps become sub-sections and sub-steps of the current step. The second delegation spins up an entirely new process to run the `print_peas` runbook in isolation. Either delegation could be preferred, depending on your needs.
|
684
|
+
|
685
|
+
### Load vs. Eval
|
686
|
+
|
687
|
+
Runbooks can be loaded from files using `load` or `eval`:
|
688
|
+
|
689
|
+
```ruby
|
690
|
+
load "my_runbook.rb"
|
691
|
+
runbook = Runbook.books.last # Runbooks register themselves to Runbook.books when they are defined
|
692
|
+
```
|
693
|
+
|
694
|
+
```ruby
|
695
|
+
runbook = eval(File.read("my_runbook.rb"))
|
696
|
+
```
|
697
|
+
|
698
|
+
Loading your runbook file is more ideal, but adds slight complexity. This method is prefered because the Ruby mechanism for retrieving source code does not work for code that has been `eval`ed. This means that you will not see `ruby_command` code blocks in view and noop output when using the `eval` method. You will see an "Unable to retrieve source code" message instead.
|
699
|
+
|
700
|
+
### Passing State
|
701
|
+
|
702
|
+
Runbook provides a number of different mechanisms for passing state throughout a runbook. For any data that is known at compile time, local variables can be used because Runbooks are lexically scoped.
|
703
|
+
|
704
|
+
```ruby
|
705
|
+
home_planet = "Krypton"
|
706
|
+
Runbook.book "Book Using Local Variables" do
|
707
|
+
hometown = "Smallville"
|
708
|
+
|
709
|
+
section "My Biography" do
|
710
|
+
step do
|
711
|
+
note "Home Planet: #{home_planet}"
|
712
|
+
note "Home Town: #{hometown}"
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|
716
|
+
```
|
717
|
+
|
718
|
+
When looking to pass data generated at runtime, for example data from `ruby_command`, `ask`, or `capture` statements, Runbook persists and synchronizes instance variables for these commands.
|
719
|
+
|
720
|
+
```ruby
|
721
|
+
Runbook.book "Book Using Instance Variables" do
|
722
|
+
section "The Transported Man" do
|
723
|
+
step do
|
724
|
+
ask "Who's the greatest magician?", into: :greatest, default: "Alfred Borden"
|
725
|
+
ruby_command { @magician = "Robert Angier" }
|
726
|
+
end
|
727
|
+
|
728
|
+
step do
|
729
|
+
ruby_command {
|
730
|
+
note "Magician: #{@magician}"
|
731
|
+
note "Greatest Magician: #{@greatest}"
|
732
|
+
}
|
733
|
+
end
|
734
|
+
end
|
735
|
+
end
|
736
|
+
```
|
737
|
+
|
738
|
+
Instance variables are only passed between statements such as `ruby_command`. They should not be set on entities such as steps, sections, or books. Instance variables are persisted using `metadata[:repo]`. They are copied to the repo after each statement finishes executing and copied from the repo before each statement starts executing. Because instance variables utilize the repo, they are persisted if the runbook is stopped and restarted at the same step.
|
739
|
+
|
740
|
+
Be careful with your naming of instance variables as it is possible to clobber the step's DSL methods because they share the same namespace.
|
741
|
+
|
742
|
+
## Extending Runbook
|
743
|
+
|
744
|
+
Runbook can be extended to add custom functionality.
|
745
|
+
|
746
|
+
### Adding Runs and Views
|
747
|
+
|
748
|
+
You can add new run and view types by defining modules under `Runbook:::Runs` and `Runbook::Views` respectively. They will automatically be accessible from the command line or via the `Runner` and `Viewer` classes. See `lib/runbook/runs/ssh_kit.rb` or `lib/runbook/views/markdown.rb` for examples of how to implement runs and views.
|
749
|
+
|
750
|
+
```ruby
|
751
|
+
module Runbook::Views
|
752
|
+
module Yaml
|
753
|
+
include Runbook::View
|
754
|
+
|
755
|
+
# handler names correspond to the entity or statement class name
|
756
|
+
# Everything is underscored and "::" is replaced by "__"
|
757
|
+
def self.runbook__entities__book(object, output, metadata)
|
758
|
+
output << "---\n"
|
759
|
+
output << "book:\n"
|
760
|
+
output << " title: #{object.title}\n"
|
761
|
+
end
|
762
|
+
|
763
|
+
# Add other handlers here
|
764
|
+
end
|
765
|
+
end
|
766
|
+
```
|
767
|
+
|
768
|
+
### DSL Extensions
|
769
|
+
|
770
|
+
You can add arbitrary keywords to your entity DSLs. For example, you could add an alias to Runbook's Book DSL as follows:
|
771
|
+
|
772
|
+
```ruby
|
773
|
+
module MyRunbook::Extensions
|
774
|
+
module Aliases
|
775
|
+
module DSL
|
776
|
+
def s(title, &block)
|
777
|
+
section(title, &block)
|
778
|
+
end
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
Runbook::Entities::Book::DSL.prepend(Aliases::DSL)
|
783
|
+
end
|
784
|
+
```
|
785
|
+
|
786
|
+
### Adding New Statements
|
787
|
+
|
788
|
+
In order to add a new statement to your DSL, create a class under `Runbook::Statements` that inherits from `Runbook::Statement`. This statement will be initialized with all arguments passed to the corresponding keyword in the DSL. Remember to also add a corresponding method to runs and views so your new statement can be interpretted in each context.
|
789
|
+
|
790
|
+
```ruby
|
791
|
+
module Runbook::Statements
|
792
|
+
class Diagram < Runbook::Statement
|
793
|
+
attr_reader :alt_text, :url
|
794
|
+
|
795
|
+
def initialize(alt_text, url)
|
796
|
+
@alt_text = alt_text
|
797
|
+
@url = url
|
798
|
+
end
|
799
|
+
end
|
800
|
+
end
|
801
|
+
```
|
802
|
+
|
803
|
+
In the above example a keyword `diagram` will be automatically added to the step dsl and its arguments will be used to initialize the Diagram object.
|
804
|
+
|
805
|
+
### Adding Run and View Functionality
|
806
|
+
|
807
|
+
You can add handlers for new statements and entities to your runs and views by prepending the modules with the new desired functionality.
|
808
|
+
|
809
|
+
```ruby
|
810
|
+
module MyRunbook::Extensions
|
811
|
+
module Diagram
|
812
|
+
def self.runbook__entities__diagram(object, output, metadata)
|
813
|
+
output << "![#{object.alt_text}](#{object.url})"
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
Runbook::Views::Markdown.prepend(Diagram)
|
818
|
+
end
|
819
|
+
```
|
820
|
+
|
821
|
+
If you are not modifying existing methods, you can simply re-open the module to add new methods.
|
822
|
+
|
823
|
+
### Augmenting Functionality With Hooks
|
824
|
+
|
825
|
+
You can add `before`, `after`, or `around` hooks to any statement or entity by defining a hook on a `Run` or `View`.
|
826
|
+
|
827
|
+
```ruby
|
828
|
+
Runbook::Runs::SSHKit.register_hook(
|
829
|
+
:notify_slack_of_step_run_time,
|
830
|
+
:around,
|
831
|
+
Runbook::Entities::Step
|
832
|
+
) do |object, metadata, block|
|
833
|
+
start = Time.now
|
834
|
+
block.call(object, metadata)
|
835
|
+
duration = Time.now - start
|
836
|
+
unless metadata[:noop]
|
837
|
+
message = "Step #{metadata[:position]}: #{object.title} took #{duration} seconds!"
|
838
|
+
notify_slack(message)
|
839
|
+
end
|
840
|
+
end
|
841
|
+
```
|
842
|
+
|
843
|
+
When registering a hook, you specify the name of the hook, the type, and the statement or entity to add the hook to. `before` and `after` hooks execute the block before and after executing the entity or statement, respectively. `around` hooks take a block which executes the specified entity or statement. When specifying the class that the hook applies to, you can have the hook apply to all entities by specifying `Runbook::Entity`, all statements by specifying `Runbook::Statement`, or all items by specifying `Object`. Additionally, you can specify any specific entity or statement you would like the hook to apply to.
|
844
|
+
|
845
|
+
When starting at a certain position in the runbook, hooks for any preceding sections and steps will be skipped. After hooks will be run for a parent when starting at a child entity of a parent.
|
846
|
+
|
847
|
+
### Adding New Run Behaviors
|
848
|
+
|
849
|
+
Every Entity and Statement gets access to a Toolbox in `metatada[:toolbox]`. This toolbox is used to provide methods with side effects (such as printing messages) when rendering and running your runbooks. Additional behaviors can be added to the toolbox by prepending `Runbook::Toolbox`.
|
850
|
+
|
851
|
+
```ruby
|
852
|
+
module MyRunbook::Extensions
|
853
|
+
module Logger
|
854
|
+
def initialize
|
855
|
+
super
|
856
|
+
log_file = ENV["LOG_FILE"] || "my_log_file.log"
|
857
|
+
@logger = Logger.new(log_file)
|
858
|
+
end
|
859
|
+
|
860
|
+
def log(msg)
|
861
|
+
@logger.info(msg)
|
862
|
+
end
|
863
|
+
end
|
864
|
+
|
865
|
+
Runbook::Toolbox.prepend(Logger)
|
866
|
+
end
|
867
|
+
```
|
868
|
+
|
869
|
+
Now you can access `log` in your handler code using `metadata[:toolbox].log("Come on ride the train, train")`.
|
870
|
+
|
871
|
+
```ruby
|
872
|
+
module MyRunbook::Extensions
|
873
|
+
module Logging
|
874
|
+
def self.runbook__entities__book(object, metadata)
|
875
|
+
super
|
876
|
+
metadata[:toolbox].log("Executing #{object.title}")
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
880
|
+
Runbook::Runs::SSHKit.prepend(Logging)
|
881
|
+
end
|
882
|
+
```
|
883
|
+
|
884
|
+
### Adding to Runbook's Run Metadata
|
885
|
+
|
886
|
+
You may want to add additional data to metadata at the time it is initialized so every node can have access to this data. You can add additional metadata to runs by prepending `Runbook::Runner`.
|
887
|
+
|
888
|
+
```ruby
|
889
|
+
module MyRunbook::Extensions
|
890
|
+
module RunbookNotesMetadata
|
891
|
+
def additional_metadata
|
892
|
+
super.merge({
|
893
|
+
notes: []
|
894
|
+
})
|
895
|
+
end
|
896
|
+
end
|
897
|
+
|
898
|
+
Runbook::Runner.prepend(RunbookNotesMetadata)
|
899
|
+
end
|
900
|
+
```
|
901
|
+
|
902
|
+
### Adding to Runbook's Configuration
|
903
|
+
|
904
|
+
You can add additional configuration to Runbook's configuration by prepending Runbook::Configuration.
|
905
|
+
|
906
|
+
```ruby
|
907
|
+
module MyRunbook::Extensions
|
908
|
+
module Configuration
|
909
|
+
attr_accessor :log_level
|
910
|
+
|
911
|
+
def initialize
|
912
|
+
super
|
913
|
+
self.log_level = :info
|
914
|
+
end
|
915
|
+
end
|
916
|
+
|
917
|
+
Runbook::Configuration.prepend(Configuration)
|
918
|
+
end
|
919
|
+
```
|
920
|
+
|
921
|
+
This will add a `log_level` attribute to Runbook's configuration with a default value of `:info`.
|
922
|
+
|
923
|
+
## Known Issues
|
924
|
+
|
925
|
+
### Command Quoting
|
926
|
+
|
927
|
+
Because ssh_config declarations such as `user`, `group`, `path`, `env`, and `umask` are implemented as wrappers around your provided commands, you must be aware that issues can arise if your commands contain characters such as single quotes that are not properly escaped.
|
928
|
+
|
929
|
+
As of SSHKit 1.16, declaring the above five ssh_config declarations will produce an ssh command similar to the following:
|
930
|
+
|
931
|
+
```
|
932
|
+
cd /home/root && umask 077 && ( export RAILS_ENV="development" ; sudo -u root RAILS_ENV="development" -- sh -c 'sg root -c \"/usr/bin/env echo I love cheese\"' )
|
933
|
+
```
|
934
|
+
|
935
|
+
One specific known issue due to improperly escaped characters is when providing a user declaration, any single quotes should be escaped with the following string: `'\\''`
|
936
|
+
|
937
|
+
```
|
938
|
+
command "echo '\\''I love cheese'\\''"
|
939
|
+
```
|
940
|
+
|
941
|
+
Alternatively, if you wish to avoid issues with SSHKit command wrapping, you can specify that your commands be executed in raw form, passed directly as written to the specified host.
|
942
|
+
|
943
|
+
### Specifying env values
|
944
|
+
|
945
|
+
When specifying the `env` for running commands, if you place curly braces `{}` around the env values, it is required to enclose the arguments in parenthesis `()`, otherwise the following syntax error will result:
|
946
|
+
|
947
|
+
```
|
948
|
+
syntax error, unexpected ':', expecting '}' (SyntaxError)
|
949
|
+
```
|
950
|
+
|
951
|
+
Env should be specified as:
|
952
|
+
|
953
|
+
```
|
954
|
+
env rails_env: :production
|
955
|
+
```
|
956
|
+
|
957
|
+
or
|
958
|
+
|
959
|
+
```
|
960
|
+
env ({rails_env: :production})
|
961
|
+
```
|
962
|
+
|
963
|
+
not as
|
964
|
+
|
965
|
+
```
|
966
|
+
env {rails_env: :production}
|
967
|
+
```
|
968
|
+
|
969
|
+
## Development
|
970
|
+
|
971
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
972
|
+
|
973
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
974
|
+
|
975
|
+
To execute runbook using this repo, run `bundle exec exe/runbook exec samples/layout_runbook.rb`.
|
976
|
+
|
977
|
+
To release a new version:
|
978
|
+
|
979
|
+
1. Update the version number in `version.rb`.
|
980
|
+
2. Update the changelog in `CHANGELOG.rb`.
|
981
|
+
3. Commit changes with commit messsage: "Bump runbook version to X.Y.Z"
|
982
|
+
4. Run `bundle exec rake release`, which will create a git tag for the version and push git commits and tags.
|
983
|
+
5. Push the `.gem` file in `pkg` to your gem repository
|
984
|
+
|
985
|
+
## Contributing
|
986
|
+
|
987
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/braintree/runbook. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
988
|
+
|
989
|
+
## Feature Requests
|
990
|
+
|
991
|
+
Any feature requests are always welcome and will be considered in accordance with time and need. Additionally, existing feature requests are tracked in TODO.md. If you choose to contribute, your contributions will be greatly appreciated.
|
992
|
+
|
993
|
+
## License
|
994
|
+
|
995
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
996
|
+
|
997
|
+
## Code of Conduct
|
998
|
+
|
999
|
+
Everyone interacting in the Runbook project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/braintree/runbook/blob/master/CODE_OF_CONDUCT.md).
|