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
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).