cult 0.1.3.pre → 0.1.4.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +53 -47
  3. data/cult.gemspec +6 -5
  4. data/exe/cult +19 -4
  5. data/lib/cult/cli/common.rb +1 -2
  6. data/lib/cult/cli/console_cmd.rb +2 -2
  7. data/lib/cult/cli/cri_extensions.rb +66 -13
  8. data/lib/cult/cli/init_cmd.rb +6 -7
  9. data/lib/cult/cli/node_cmd.rb +233 -67
  10. data/lib/cult/cli/provider_cmd.rb +16 -13
  11. data/lib/cult/cli/role_cmd.rb +25 -26
  12. data/lib/cult/cli/task_cmd.rb +13 -13
  13. data/lib/cult/commander.rb +53 -17
  14. data/lib/cult/commander_sync.rb +29 -0
  15. data/lib/cult/definition.rb +21 -49
  16. data/lib/cult/driver.rb +1 -1
  17. data/lib/cult/drivers/common.rb +12 -11
  18. data/lib/cult/drivers/digital_ocean_driver.rb +2 -2
  19. data/lib/cult/drivers/virtual_box_driver.rb +156 -0
  20. data/lib/cult/drivers/vultr_driver.rb +3 -3
  21. data/lib/cult/named_array.rb +103 -15
  22. data/lib/cult/node.rb +139 -12
  23. data/lib/cult/paramap.rb +209 -0
  24. data/lib/cult/project.rb +2 -17
  25. data/lib/cult/provider.rb +3 -1
  26. data/lib/cult/role.rb +12 -8
  27. data/lib/cult/task.rb +73 -45
  28. data/lib/cult/template.rb +3 -4
  29. data/lib/cult/transaction.rb +11 -5
  30. data/lib/cult/user_refinements.rb +1 -1
  31. data/lib/cult/version.rb +1 -1
  32. data/lib/cult.rb +32 -3
  33. data/skel/roles/{all → base}/role.json +0 -0
  34. data/skel/roles/{all/tasks/00000-do-something-cool → base/tasks/000-do-something-cool} +0 -0
  35. data/skel/roles/{all/tasks/sync → base/tasks/sync-host-map} +5 -5
  36. data/skel/roles/base/tasks/sync-leader-of +11 -0
  37. data/skel/roles/bootstrap/files/cult-motd +15 -3
  38. data/skel/roles/bootstrap/tasks/{00000-set-hostname → 000-set-hostname} +1 -1
  39. data/skel/roles/bootstrap/tasks/{00001-add-cult-user → 001-add-cult-user} +0 -6
  40. data/skel/roles/bootstrap/tasks/002-disable-root-user +7 -0
  41. data/skel/roles/bootstrap/tasks/{00002-install-cult-motd → 002-install-cult-motd} +1 -1
  42. metadata +29 -11
  43. data/lib/cult/cli/fleet_cmd.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 255215974c24d700b973b431c1a6759b0f44c753
4
- data.tar.gz: c8b95b6beedcd9da94bc634000917d7452bee86c
3
+ metadata.gz: 69eca6bb220e6610aabe66cfa4727881535bf8d0
4
+ data.tar.gz: 90b206c03fa2fb8b9f7c6f743b9edaaca0ce643e
5
5
  SHA512:
6
- metadata.gz: 0d3618e471c1e966cb1c77b2643880eb0a133cace9769ca61553b215793228d8a129b2fb76ba490ebcc40ccb90656f1f5063946b084e657771fbbfc8fe322e22
7
- data.tar.gz: ae2c2c7a96ebb863bffc3ca2a0bf32ece7cb35f455b0642b6a9a53b17bda38ac223f47537e00c7324d0aae5dd5f7ba2340a4c0d1b6ffff84b449208971cabd8d
6
+ metadata.gz: 43ca3f870a7cad5335fbaa4785e29973364cc0a3a0410586f33c0fd24f61b628123078d8d09f8cfd6565fba29f1cc550d59fac142048c463ec25f424295115e4
7
+ data.tar.gz: e1e79c67bdb9b968f6d19a1c62a4c198f6828d2449df4fd33c3d2dd3e25ca644a1e77b8e6ba853eb58cb4cd088499f1515c331a5a554a27995149c5f37dd9827
data/README.md CHANGED
@@ -3,7 +3,6 @@
3
3
 
4
4
 
5
5
  ## Introduction
6
-
7
6
  Cult is a tool to manage fleets of servers. It tries to work in an obvious
8
7
  way, and doesn't intend on you learning a bunch of new metaphors, languages,
9
8
  and terminology.
@@ -56,7 +55,6 @@ Hopefully.
56
55
 
57
56
 
58
57
  ## Installation
59
-
60
58
  Cult is written in Ruby and available as a gem, but it is an application, not a
61
59
  library. It's not written in, and has no relation to Rails. It depends on
62
60
  Ruby 2.3 or greater. If you've got Ruby installed, via any means, the following
@@ -72,13 +70,12 @@ use the driver.
72
70
  Cult requires nothing to be installed on each node, other than an operating SSH
73
71
  server and Bourne Shell. If you know you've got Bash on the other end, feel
74
72
  free to write your tasks in Bash. If you want to write tasks in Ruby, Python,
75
- Node or Perl, etc, one of your firsts Tasks in the `all` or `bootstrap` role
73
+ Node or Perl, etc, one of your firsts Tasks in the `base` or `bootstrap` role
76
74
  should be to `apt-get -y install {ruby,python,node}`. All subsequent tasks will
77
75
  have that interpreter available.
78
76
 
79
77
 
80
78
  ## General Theory
81
-
82
79
  I think a reasonable level of abstraction for cloud deployments is such:
83
80
 
84
81
  1. *Nodes*: Actual machines, virtual or otherwise, running somewhere.
@@ -87,11 +84,11 @@ I think a reasonable level of abstraction for cloud deployments is such:
87
84
  3. *Tasks*: Roles are made of tasks, which are basically scripts, called
88
85
  things like `install-postgres` or `configure-nginx`. They're written in
89
86
  your language of choice. When you being up a Node with a Role, all of the
90
- Role's tasks are executed to get it up and running.
87
+ Role's tasks are executed to get it up and running, and later to update
88
+ configuration.
91
89
  4. *Providers*: Your VPS provider, e.g., Linode, DigitalOcean, etc. These
92
90
  allow you to spawn and destroy nodes, and typically charge you a few cents
93
- per node per hour. I guess there's also *Drivers*, but a Provider is
94
- really just an instance of a *Driver* with an API key configured.
91
+ per node per hour.
95
92
 
96
93
  We hate adding anything on top of this because it increases complexity.
97
94
 
@@ -100,11 +97,11 @@ hatches like ERB templating on shell scripts and JSON files to do weird stuff.
100
97
 
101
98
 
102
99
  ### Drivers
103
-
104
100
  Cult provides a handful of Drivers which can talk to common VPS providers,
105
- initially DigitalOcean, Linode, and Vultr. A driver is typically 200-300 lines
106
- of Ruby code, and is pretty well isolated from the rest of Cult, so feel free
107
- to open a PR to add your provider of choice. You can get a current list with:
101
+ initially DigitalOcean, Linode, and Vultr, and a VirtualBox driver for local
102
+ development. A driver is typically 200-300 lines of Ruby code, and is pretty
103
+ well isolated from the rest of Cult, so feel free to open a PR to add your
104
+ provider of choice. You can get a current list with:
108
105
 
109
106
  $ cult provider drivers
110
107
 
@@ -119,61 +116,79 @@ default bootstrap role indeed does exactly this.)
119
116
 
120
117
 
121
118
  ### Nodes
122
-
123
119
  A node is a physical instance running somewhere. A node has a name, like
124
- 'web1', but also has a full description, that looks something like:
125
-
126
- web1@ubuntu-16-01.2gb:digitalocean.nyc1
127
-
128
- A node named `web1` is an idea, but hasn't spawned. A node named with its full
129
- description should represent a real server somewhere, that is (hopefully)
130
- running.
120
+ 'web1-hr7sjdh8'. You create a node with `cult node new -r some-role`.
121
+ Multiple roles may be specified. You can check on all nodes with
122
+ `cult node ping`.
131
123
 
132
124
 
133
125
  ### Roles
134
126
  A role is a collection of files (usually configuration files), Tasks (usually
135
- shell scripts), and a configuration (`role.json`). A role can include other
136
- roles via `includes:`.
137
-
138
- A Role's tasks are named like `000-a-descriptive-name`, because the only
139
- ordering Cult does is asciibetical. Tasks named numerically are considered
140
- build tasks. A Task named "sync" is built, shipped, and executed during
141
- `cult fleet sync`. Other files are ignored, so you can symlink or whatever
142
- between them (if you want some sort of meta-role or something.)
127
+ shell scripts), and a generated configuration (`role.json`). A role can include
128
+ other roles via `includes:`.
143
129
 
144
- Every task, file, and even `role.json` is pre-processed with ERB before it gets
145
- processed by Cult or shipped to a node. This lets you customize behavior based
146
- on the node, the role, the provider, the project, /dev/urandom, or anything
147
- else you'd like.
130
+ Tasks and Files, and role.json are processed with ERB when used to create a
131
+ node. This lets you customize behavior based on the node, the role, the
132
+ provider, the project, /dev/urandom, or anything else you'd like.
148
133
 
149
134
 
150
135
  #### Special Roles
151
136
  There are two Roles generated by default that you should think of as
152
137
  special-ish:
153
138
 
154
- 1. `all`: If a Role does not explicitly list an `includes` value, Cult will
155
- act as if it specified `includes: ['all']`. In practice, this means tasks
156
- in the `all` role are common to all nodes that haven't explicitly opted
157
- out of it. A task can opt-out of including `all` with an explicit
139
+ 1. `base`: If a Role does not explicitly list an `includes` value, Cult will
140
+ act as if it specified `includes: ['base']`. In practice, this means tasks
141
+ in the `base` role are common to all nodes that haven't explicitly opted
142
+ out of it. A task can opt-out of including `base` with an explicit
158
143
  `includes: []`.
159
144
  2. `bootstrap`: The generator creates this role to be the first one ran on a
160
145
  new node. Before it starts, we have a root account with an SSH key, when
161
146
  `bootstrap` finishes, we have a `cult` user on the node with `sudo` access
162
147
  who uses the same SSH key, and the root account is disabled. `bootstrap`
163
- opts-out of `all`. The generator also installs a MOTD banner so you'll
148
+ opts-out of `base`. The generator also installs a MOTD banner so you'll
164
149
  know Cult was enabled on the server, has a demo script that sets the
165
150
  hostname.
166
151
 
152
+ ### Tasks
153
+ Tasks are simple, but because they've been utilized in building real systems,
154
+ there's quite a few special ways to use them. A Role's tasks live in
155
+ `roles/name/tasks`, and its type is inferred by its filename.
156
+
157
+ #### Build Tasks
158
+ A Role's build tasks are named like `000-a-descriptive-name`, because the only
159
+ ordering Cult does is asciibetical. Build tasks are executed in order to build
160
+ a node out of a role.
167
161
 
168
- ## Usage
169
162
 
163
+ #### Sync Tasks
164
+ Sync tasks are meant to inform existing nodes of their state. They are
165
+ executed via `cult node sync`. They are named `sync-description`. By default,
166
+ sync tasks are in "pass 0", which work a lot like build tasks. If you have
167
+ the need to place a sync task into a separate, later pass, you can specify it
168
+ by naming the file something like `sync-P1-something`. Note that
169
+ `sync-something` and `sync-P0-something` mean the same thing. All sync tasks
170
+ of the same pass are executed in parallel. To guarantee an order, place a sync
171
+ task that must execute after another in a later pass.
172
+
173
+
174
+ ## Usage
170
175
  We're going to put together a complete usage guide, tutorial, and example repo
171
176
  once Cult has settled down a bit. It's still pre-1.0 software, and we still
172
177
  like breaking things to make it work better for us.
173
178
 
174
179
 
175
- ### Spooky Secrets
180
+ ### Local VirtualBox-driver Development
181
+ The 'virtual-box' driver sees your VMs as images, and clones one to start from.
182
+ The guest must have the VirtualBox Guest Extensions installed and the first
183
+ network adapter set up to "Bridged", so it acts like an actual machine.
184
+
185
+ Cult expects to be able to login as "root" with the password "password", and
186
+ immediately locks the root account after provisioning. On ubuntu, you'll have
187
+ to re-enabled the account with `sudo passwd root` and/or `sudo passwd -u root`.
176
188
 
189
+ Note that VirtualBox is a little weird, and intended for development only.
190
+
191
+ ### Spooky Secrets
177
192
  * `cult console` is built to be really nice to use. If you're not afraid of
178
193
  Ruby, the method names are chosen to read almost like pseudo-code. It
179
194
  supports IRB, Pry, and Ripl with command-line flags.
@@ -187,18 +202,11 @@ like breaking things to make it work better for us.
187
202
  `nodes[/^dev/].with(role: /httpd/).with(something: /else/)`
188
203
  * The NamedArray stuff even works on the command-line with String arguments,
189
204
  and will convert strings that start with '/' to Regexps to search by name.
190
- * Although Cult will only *generate* JSON, not having comments and other
191
- stuff is a pain. If you don't care about JSON-readability of your
192
- `node.json`s or `role.json`s, you can just rename it to `node.yaml` or
193
- `node.yml` and it'll get parsed with a YAML parser. Cult keeps transient
194
- state in separate files for this reason: so it doesn't overwrite your
195
- long-lived YAML replacements with JSON equivalents.
196
205
 
197
206
 
198
207
  ## Development
199
208
 
200
209
  ### History
201
-
202
210
  Cult was developed to basically avoid Puppet, Chef, and Ansible. I know there
203
211
  are some really large, successful deployments of all of these. I just think
204
212
  they just do too much for me to feel safe with when shit goes crazy.
@@ -233,7 +241,6 @@ you feel a bit antsy. Think of it more as jazz improv than an orchestra.
233
241
 
234
242
 
235
243
  ### Contributing
236
-
237
244
  We greatly appreciate bug reports, pull-requests, questions, and general
238
245
  commentary in the GitHub Issues. These are all *contributions*. However,
239
246
  before opening an issue demanding us to work on your feature that Cult *has to
@@ -253,7 +260,6 @@ the above items you away.
253
260
 
254
261
 
255
262
  ## License
256
-
257
263
  Cult is available as open source software under the terms of the
258
264
  [MIT License](http://opensource.org/licenses/MIT).
259
265
 
data/cult.gemspec CHANGED
@@ -27,12 +27,13 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.required_ruby_version = '~> 2.3'
30
+ spec.required_ruby_version = '>= 2.2'
31
31
 
32
- spec.add_dependency "cri", "~> 2.7"
33
- spec.add_dependency "net-ssh", "~> 3.2"
34
- spec.add_dependency "net-scp", "~> 1.2"
35
- spec.add_dependency "rainbow", "~> 2.1"
32
+ spec.add_dependency "cri", "~> 2.7"
33
+ spec.add_dependency "net-ssh", "~> 3.2"
34
+ spec.add_dependency "net-scp", "~> 1.2"
35
+ spec.add_dependency "rainbow", "~> 2.1"
36
+ spec.add_dependency "erubis", "~> 2.7.0"
36
37
 
37
38
  spec.add_development_dependency "bundler", "~> 1.12"
38
39
  spec.add_development_dependency "rake", "~> 10.0"
data/exe/cult CHANGED
@@ -37,6 +37,7 @@ cult = Cri::Command.define do
37
37
 
38
38
  flag :h, :help, 'Show this help' do |value, cmd|
39
39
  puts cmd.help
40
+ exit
40
41
  end
41
42
 
42
43
  flag :y, :yes, 'Answer "yes" to any questions' do
@@ -52,13 +53,27 @@ cult = Cri::Command.define do
52
53
  puts 'Copyright (C) 2016 Mike A. Owens, meter.md, and Contributors'
53
54
  end
54
55
 
55
- run(arguments: 0) do |opts, args, cmd|
56
- puts cmd.help
56
+ required :j, :jobs, 'Number of concurrent jobs. Defaults to max' do |value|
57
+ Cult.concurrency = case value
58
+ when /^(\d+)$/
59
+ $1.to_i
60
+ when 'max'
61
+ :max
62
+ else
63
+ fail Cult::CLI::CLIError, "--jobs must be a number or 'max'"
64
+ end
65
+ end
66
+
67
+ run(arguments: none) do |opts, args, cmd|
68
+ if opts.empty? && args.empty?
69
+ puts cmd.help
70
+ exit
71
+ end
57
72
  end
58
73
  end
59
74
 
60
- Cult::CLI.commands.each do |m|
61
- cult.add_command(m)
75
+ Cult::CLI.commands.each do |root_command|
76
+ cult.add_command(root_command)
62
77
  end
63
78
 
64
79
  if (env = ENV['CULT_PROJECT'])
@@ -193,8 +193,7 @@ module Cult
193
193
 
194
194
 
195
195
  # Takes a list of keys and returns an array of objects that correspond
196
- # to any of them. If required is true, each key must correspond to at
197
- # least one object.
196
+ # to any of them.
198
197
  def fetch_items(*keys, **kw)
199
198
  keys.flatten.map do |key|
200
199
  fetch_item(key, method: :all, **kw)
@@ -50,7 +50,7 @@ module Cult
50
50
  def console_cmd
51
51
  Cri::Command.define do
52
52
  name 'console'
53
- summary 'Launch an REPL with you project loaded'
53
+ summary 'Launch a REPL with the project loaded'
54
54
  description <<~EOD.format_description
55
55
  The Cult console loads your project, and starts a Ruby REPL. This can
56
56
  be useful for troubleshooting, or just poking around the project.
@@ -63,7 +63,7 @@ module Cult
63
63
  flag :p, :pry, 'Pry'
64
64
  flag nil, :reexec, 'Console has been exec\'d for a reload'
65
65
 
66
- run(arguments: 0) do |opts, args, cmd|
66
+ run(arguments: none) do |opts, args, cmd|
67
67
  context = ConsoleContext.new(Cult.project, ARGV)
68
68
 
69
69
  if opts[:reexec]
@@ -8,6 +8,42 @@ module Cult
8
8
  end
9
9
  end
10
10
 
11
+ # This allows further -- options to be passed as literals instead of
12
+ # being stripped.
13
+ # cult node ssh Something -- some-command -- something
14
+ module ArgumentArrayExtensions
15
+ attr_reader :explicit_tail
16
+
17
+ def initialize(raw_arguments)
18
+ @explicit_tail = []
19
+
20
+ super_super = Array.instance_method(:initialize).bind(self)
21
+ if (index = raw_arguments.index("--"))
22
+ @explicit_tail = raw_arguments[index + 1 .. -1]
23
+ processed = raw_arguments[0 ... index] + @explicit_tail
24
+ super_super.call(processed)
25
+ else
26
+ super_super.call(raw_arguments)
27
+ end
28
+ @raw_arguments = raw_arguments
29
+ end
30
+
31
+ ::Cri::ArgumentArray.prepend(self)
32
+ end
33
+
34
+ # This extension stops option processing at the first non-option bare-word.
35
+ # Without it, further arguments that look like options are treated as such.
36
+ # use-case:
37
+ # cult node ssh SomeNode ls -l
38
+ module OptionParserExtensions
39
+ def run
40
+ peek = @unprocessed_arguments_and_options[0]
41
+ @no_more_options = true if peek && peek[0] != '-'
42
+ super
43
+ end
44
+ ::Cri::OptionParser.prepend(self)
45
+ end
46
+
11
47
 
12
48
  module CommandExtensions
13
49
  def project_required?
@@ -27,42 +63,44 @@ module Cult
27
63
  def block
28
64
  lambda do |opts, args, cmd|
29
65
  if project_required? && Cult.project.nil?
30
- fail CLIError, "command '#{name}' requires a Cult project"
66
+ fail CLIError, "command '#{cmd.name}' requires a Cult project"
31
67
  end
32
68
 
33
- check_argument_spec!(args, argument_spec) if argument_spec
69
+ check_argument_spec!(args, argument_spec, cmd) if argument_spec
34
70
 
35
71
  super.call(opts, args, cmd)
36
72
  end
37
73
  end
38
74
 
39
-
40
- def check_argument_spec!(args, range)
75
+ def check_argument_spec!(args, range, cmd)
76
+ range = (0..range) if range == Float::INFINITY
41
77
  range = (range..range) if range.is_a?(Integer)
42
- if range.end == -1
43
- range = range.begin .. Float::INFINITY
44
- end
45
78
 
46
79
  unless range.cover?(args.size)
47
80
  msg = case
48
81
  when range.size == 1 && range.begin == 0
49
82
  "accepts no arguments"
50
83
  when range.size == 1 && range.begin == 1
51
- "accepts one argument"
84
+ "requires exactly one argument"
52
85
  when range.begin == range.end
53
- "accepts exactly #{range.begin} arguments"
86
+ "requires exactly #{range.begin} arguments"
54
87
  else
55
88
  if range.end == Float::INFINITY
56
- "requires #{range.begin}+ arguments"
89
+ words =%w(zero one two three)
90
+ if range.begin < words.size
91
+ "requires #{words[range.begin]} or more arguments"
92
+ else
93
+ "requires #{range.begin}+ arguments"
94
+ end
57
95
  else
58
96
  "accepts #{range} arguments"
59
97
  end
60
98
  end
61
- fail CLIError, "Command #{msg}"
99
+ fail CLIError, "command '#{cmd.name}' #{msg} (usage: #{cmd.usage})"
62
100
  end
63
101
  end
64
102
 
65
- Cri::Command.prepend(self)
103
+ ::Cri::Command.prepend(self)
66
104
  end
67
105
 
68
106
 
@@ -72,12 +110,27 @@ module Cult
72
110
  end
73
111
 
74
112
 
113
+ # Lets us say run(arguments: 1 .. unlimited) instead of
114
+ # run(arguments: 1 .. Float::INFINITY)
115
+ # or just outright:
116
+ # run(arguments: unlimited)
117
+ def unlimited
118
+ Float::INFINITY
119
+ end
120
+
121
+ # Lets us say run(arguments: none)
122
+ def none
123
+ 0
124
+ end
125
+
126
+ # This allows an explicit number of arguments to be passed to
127
+ # run, and halts with an error otherwise
75
128
  def run(arguments: nil, &block)
76
129
  @command.argument_spec = arguments if arguments
77
130
  super(&block)
78
131
  end
79
132
 
80
- Cri::CommandDSL.prepend(self)
133
+ ::Cri::CommandDSL.prepend(self)
81
134
  end
82
135
  end
83
136
  end
@@ -1,4 +1,5 @@
1
1
  require 'cult/drivers/load'
2
+ require 'json'
2
3
 
3
4
  module Cult
4
5
  module CLI
@@ -37,7 +38,7 @@ module Cult
37
38
  be accomplished by hand, so if you want to change anything later, it's
38
39
  not a big deal.
39
40
 
40
- The project generated sets up a pretty common configuration: an 'all'
41
+ The project generated sets up a pretty common configuration: a `base`
41
42
  role, a 'bootstrap' role, and a demo task that puts a colorful banner
42
43
  in each node's MOTD.
43
44
  EOD
@@ -87,15 +88,13 @@ module Cult
87
88
  FileUtils.mkdir_p(provider_dir)
88
89
 
89
90
 
90
- provider_file = File.join(provider_dir,
91
- project.dump_name("provider"))
92
- File.write(provider_file, project.dump_object(provider_conf))
91
+ provider_file = File.join(provider_dir, "provider.json")
92
+ File.write(provider_file, JSON.pretty_generate(provider_conf))
93
93
 
94
94
 
95
- defaults_file = File.join(provider_dir,
96
- project.dump_name("defaults"))
95
+ defaults_file = File.join(provider_dir, "defaults.json")
97
96
  defaults = Provider.generate_defaults(provider_conf)
98
- File.write(defaults_file, project.dump_object(defaults))
97
+ File.write(defaults_file, JSON.pretty_generate(defaults))
99
98
  end
100
99
 
101
100
  if opts[:git]