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
@@ -1,5 +1,6 @@
1
- require 'SecureRandom'
1
+ require 'securerandom'
2
2
  require 'fileutils'
3
+ require 'json'
3
4
 
4
5
  module Cult
5
6
  module CLI
@@ -16,65 +17,105 @@ module Cult
16
17
  conceptually description of a server.
17
18
  EOD
18
19
 
19
- run(arguments: 0) do |opts, args, cmd|
20
+ run(arguments: none) do |opts, args, cmd|
20
21
  puts cmd.help
22
+ exit
21
23
  end
22
24
  end
23
25
 
24
26
  node_ssh = Cri::Command.define do
25
27
  name 'ssh'
26
- usage 'ssh NODE'
27
- summary 'Starts an interactive SSH shell to NODE'
28
+ usage 'ssh /NODE+/ [command...]'
29
+ summary 'Starts an SSH shell to NODE'
30
+
31
+ flag :i, :interactive, "Force interactive mode"
32
+ flag :I, :'non-interactive', "Force non-interactive mode"
33
+
28
34
  description <<~EOD.format_description
35
+ With no additional arguments, initiates an interactive SSH connection
36
+ to a node, authenticated with the node's public key.
37
+
38
+ Additional arguments are passed to the 'ssh' command to allow for
39
+ scripting or running one-off commands on the node.
40
+
41
+ By default, cult assumes an interactive SSH session when no extra
42
+ SSH arguments are passed, and a non-interactive session otherwise.
43
+ You can force this behavior one way or the other with --interactive
44
+ or --non-interactive.
45
+
46
+ Cult will run SSH commands over all matching nodes in parallel if it
47
+ considers your command non-interactive.
29
48
  EOD
30
49
 
31
- run(arguments: 1) do |opts, args, cmd|
32
- node = CLI.fetch_item(args[0], from: Node)
33
- exec "ssh", '-i', node.ssh_private_key_file,
34
- "#{node.user}@#{node.host}"
50
+ esc = ->(s) { Shellwords.escape(s) }
51
+
52
+ run(arguments: 1 .. unlimited) do |opts, args, cmd|
53
+ if opts[:interactive] && opts[:'non-interactive']
54
+ fail CLIError, "can't specify --interactive and --non-interactive"
55
+ end
56
+
57
+ interactive = opts[:interactive] ||
58
+ (opts[:'non-interactive'] && false)
59
+
60
+ nodes = CLI.fetch_items(args[0], from: Node)
61
+
62
+ # With args, we'll assume it's a non-interactive session and run them
63
+ # in parallel, otherwise, we'll assume it's interactive and force
64
+ # them to run one at a time.
65
+ ssh_extra = args[1 .. -1]
66
+ interactive ||= ssh_extra.empty?
67
+ concurrent = interactive || nodes.size == 1 ? 1 : nil
68
+
69
+ Cult.paramap(nodes, concurrent: concurrent) do |node|
70
+ ssh_args = 'ssh', '-i', esc.(node.ssh_private_key_file),
71
+ '-p', esc.(node.ssh_port.to_s),
72
+ '-o', "UserKnownHostsFile=#{esc.(node.ssh_known_hosts_file)}",
73
+ esc.("#{node.user}@#{node.host}")
74
+ ssh_args += ssh_extra
75
+ # We used to use exec here, but with paramap, the forked process
76
+ # has to live to long enough to report it's return value.
77
+ system(*ssh_args)
78
+ exit if interactive
79
+ end
35
80
  end
36
81
  end
37
82
  node.add_command(node_ssh)
38
83
 
39
- node_create = Cri::Command.define do
40
- name 'create'
41
- aliases 'new'
42
- usage 'create [options] NAME...'
84
+ node_new = Cri::Command.define do
85
+ name 'new'
86
+ usage 'new -r /ROLE+/ [options] NAME0 NAME1 ...'
43
87
  summary 'Create a new node'
44
88
  description <<~EOD.format_description
45
89
  This command creates a new node specification and then creates it with
46
90
  your provider.
47
91
 
48
92
  The newly created node will have all the roles listed in --role. If
49
- none are specified, it'll have the role "all". If no name is
50
- provided, it will be named for its role(s).
93
+ none are specified, it'll have the role "base". If no name is
94
+ provided, it will be named after its role(s).
51
95
 
52
96
  If multiple names are provided, a new node is created for each name
53
- given.
97
+ given. The --count option is incompatible with multiple names given
98
+ on the command line.
54
99
 
55
100
  The --count option lets you create an arbitrary amount of new nodes.
56
- The nodes will be identical, except they'll be named with increasing
57
- sequential numbers, like:
101
+ The nodes will be identical, except they'll be named with arbitrary
102
+ random suffixes, like:
58
103
 
59
- > web-01, web-02
104
+ > web-fjfowhs7, web-48pqee6v
60
105
 
61
- And so forth. The --count option is incompatible with multiple names
62
- given on the command line. If --count is specified with one name, the
63
- name will become the prefix for all nodes created. If --count is
64
- specified with no names, the prefix will be generated from the role
65
- names used, as discussed above.
106
+ And so forth.
66
107
  EOD
67
108
 
68
- required :r, :role, 'Specify possibly multiple roles',
109
+ required :r, :role, 'Specify possibly multiple /ROLE+/',
69
110
  multiple: true
70
111
  required :n, :count, 'Generates <value> number of nodes'
71
112
 
72
- required :p, :provider, 'Provider'
113
+ required :p, :provider, 'Use /PROVIDER/ to create the node'
73
114
  required :Z, :zone, 'Provider zone'
74
115
  required :I, :image, 'Provider image'
75
116
  required :S, :size, 'Provider instance size'
76
117
 
77
- run(arguments: 0..-1) do |opts, args, cmd|
118
+ run(arguments: unlimited) do |opts, args, cmd|
78
119
  random_suffix = ->(basename) do
79
120
  begin
80
121
  suffix = CLI.unique_id
@@ -85,42 +126,32 @@ module Cult
85
126
  end
86
127
 
87
128
  generate_sequenced_names = ->(name, n) do
88
- result = []
89
- result.push(random_suffix.(name)) until result.size == n
90
- result
91
- end
92
-
93
- unless opts[:count].nil? || opts[:count].match(/^\d+$/)
94
- fail CLIError, "--count must be an integer"
129
+ (0...n).map do
130
+ random_suffix.(name)
131
+ end
95
132
  end
96
133
 
97
134
  names = args.dup
98
135
 
99
- roles = opts[:role] ? CLI.fetch_items(opts[:role], from: Role) : []
100
-
101
- if roles.empty?
102
- roles = CLI.fetch_items('all', from: Role)
103
- if names.empty?
104
- begin
105
- names.push CLI.fetch_item('node', from: Node, exist: false)
106
- rescue
107
- names.push random_suffix.('node')
108
- end
109
- end
136
+ unless opts[:count].nil? || opts[:count].match(/^\d+$/)
137
+ fail CLIError, "--count must be an integer"
110
138
  end
111
139
 
112
140
  if names.size > 1 && opts[:count]
113
141
  fail CLIError, "cannot specify both --count and more than one name"
114
142
  end
115
143
 
116
- if names.empty? && !roles.empty?
117
- names.push roles.map(&:name).sort.join('-')
118
- end
144
+ roles = CLI.fetch_items(opts[:role] || 'base', from: Role)
119
145
 
120
- if opts[:count]
121
- names = generate_sequenced_names.(names[0], opts[:count].to_i)
146
+ if names.empty?
147
+ names.push roles.map(&:name).join("-")
148
+ opts[:count] ||= 1
122
149
  end
123
150
 
151
+ names = opts[:count] ? generate_sequenced_names.(names[0],
152
+ opts[:count].to_i)
153
+ : names
154
+
124
155
  # Makes sure they're all new.
125
156
  names = names.map do |name|
126
157
  CLI.fetch_item(name, from: Node, exist: false)
@@ -140,36 +171,40 @@ module Cult
140
171
  [m, value]
141
172
  end.to_h
142
173
 
143
- nodes = names.map do |name|
174
+ Cult.paramap(names) do |name|
144
175
  data = {
145
176
  name: name,
146
177
  roles: roles.map(&:name)
147
178
  }
148
179
 
149
180
  Node.from_data!(Cult.project, data).tap do |node|
181
+ puts "Provisioning #{node.name}..."
150
182
  prov_data = provider.provision!(name: node.name,
151
183
  image: node_spec[:image],
152
184
  size: node_spec[:size],
153
185
  zone: node_spec[:zone],
154
186
  ssh_public_key: node.ssh_public_key_file)
155
187
  prov_data['provider'] = provider.name
156
- File.write(Cult.project.dump_name(node.state_path),
157
- Cult.project.dump_object(prov_data))
188
+ File.write(node.state_path, JSON.pretty_generate(prov_data))
158
189
 
159
190
  c = Commander.new(project: Cult.project, node: node)
191
+ puts "Bootstrapping #{node.name}..."
160
192
  c.bootstrap!
193
+
194
+ puts "Installing roles for #{node.name}..."
161
195
  c.install!(node)
196
+
197
+ puts "Node installed: #{node.name}"
162
198
  end
163
199
  end
164
200
 
165
201
  end
166
202
  end
167
- node.add_command node_create
203
+ node.add_command(node_new)
168
204
 
169
- node_destroy = Cri::Command.define do
170
- name 'destroy'
171
- aliases 'rm'
172
- usage 'destroy NODE'
205
+ node_rm = Cri::Command.define do
206
+ name 'rm'
207
+ usage 'rm /NODE+/ ...'
173
208
  summary 'Destroy nodes'
174
209
  description <<~EOD.format_description
175
210
  Destroys all nodes named NODE, or match the pattern described by
@@ -181,9 +216,10 @@ module Cult
181
216
  be prompted before each destroy.
182
217
  EOD
183
218
 
184
- run(arguments: 1..-1) do |opts, args, cmd|
219
+ run(arguments: 1 .. unlimited) do |opts, args, cmd|
185
220
  nodes = CLI.fetch_items(args, from: Node)
186
- nodes.each do |node|
221
+ concurrent = CLI.yes? ? :max : 1
222
+ Cult.paramap(nodes, concurrent: concurrent) do |node|
187
223
  if CLI.yes_no?("Destroy node `#{node}`?")
188
224
  puts "destroying #{node}"
189
225
  begin
@@ -197,15 +233,16 @@ module Cult
197
233
  fail unless node.path.match(/#{Regexp.escape(node.name)}/)
198
234
  FileUtils.rm_rf(node.path)
199
235
  end
236
+ nil
200
237
  end
201
238
  end
202
239
  end
203
- node.add_command(node_destroy)
240
+ node.add_command(node_rm)
204
241
 
205
- node_list = Cri::Command.define do
206
- name 'list'
207
- aliases 'ls'
242
+ node_ls = Cri::Command.define do
243
+ name 'ls'
208
244
  summary 'List nodes'
245
+ usage 'ls /NODE+/ ...'
209
246
  description <<~EOD.format_description
210
247
  This command lists the nodes in the project.
211
248
  EOD
@@ -213,9 +250,9 @@ module Cult
213
250
  required :r, :role, 'List only nodes which include <value>',
214
251
  multiple: true
215
252
 
216
- run(arguments: 0..1) do |opts, args, cmd|
253
+ run(arguments: unlimited) do |opts, args, cmd|
217
254
  nodes = args.empty? ? Cult.project.nodes
218
- : CLI.fetch_items(*args, from: Node)
255
+ : CLI.fetch_items(args, from: Node)
219
256
 
220
257
  if opts[:role]
221
258
  roles = CLI.fetch_items(opts[:role], from: Role)
@@ -224,12 +261,141 @@ module Cult
224
261
  end
225
262
  end
226
263
 
264
+ Cult.paramap(nodes) do |node|
265
+ role_string = node.build_order.map do |role|
266
+ if node.zone_leader?(role)
267
+ Rainbow('*' + role.name).cyan
268
+ else
269
+ '=' + role.name
270
+ end
271
+ end.join(', ')
272
+
273
+ puts "#{node.name}\t#{node.provider&.name}\t" +
274
+ "#{node.zone}\t#{node.addr(:public)}\t#{node.addr(:private)}\t" +
275
+ "#{role_string}"
276
+ end
277
+ end
278
+ end
279
+ node.add_command(node_ls)
280
+
281
+
282
+ node_sync = Cri::Command.define do
283
+ name 'sync'
284
+ usage 'sync /NODE+/ ...'
285
+ summary 'Synchronize host information across fleet'
286
+ description <<~EOD.format_description
287
+ Computes, pre-processes, and executes "sync" tasks on every NODE,
288
+ or all nodes if none are specified.
289
+
290
+ Sync tasks are tasks that begin with 'sync-'. They are meant to
291
+ process dynamic information about the fleet well after a node has
292
+ been created. Typically, you'll run `cult node sync` to let each
293
+ instance know about its new neighborhood after you add or remove
294
+ new nodes.
295
+
296
+ Sync tasks can optionally specify a "pass", with "sync-P0-..." or
297
+ "sync-P1-...". When `cult node sync` executes, it ensures that:
298
+
299
+ 1. On a given node, all tasks in the current pass are executed
300
+ sequentially, in role and asciibetical order.
301
+
302
+ 2. Across the fleet, nodes which have tasks to run in a given pass
303
+ are run concurently with each other.
304
+
305
+ 3. The entire fleet (or NODE selection) synchonizes between passes.
306
+ "Pass 0" has run on EVERY node (across any role boundaries)
307
+ before "Pass 1" is started on ANY node.
308
+
309
+ Sync tasks without a specified pass are implicitly in "Pass 0".
310
+
311
+ The sync can be restricted to a specified set of passes with the
312
+ --pass option. Note that this skips dependent passes.
313
+
314
+ The sync can be restricted to a specified set of CONCRETE role tasks
315
+ with the --role option. No dependencies are considered: Cult
316
+ calculates the tasks it would've ran, then removes all tasks not
317
+ belonging to a role given to --roles
318
+ EOD
319
+
320
+ required :R, :role, "Skip sync tasks not in /ROLE/. Can be specified " +
321
+ "more than once.",
322
+ multiple: true
323
+
324
+ required :P, :pass, "Only execute PASS. Can be specified more than " +
325
+ "once.",
326
+ multiple: true
327
+
328
+ run(arguments: unlimited) do |opts, args, cmd|
329
+ nodes = args.empty? ? Cult.project.nodes
330
+ : CLI.fetch_items(args, from: Node)
331
+ roles = opts[:role].nil? ? Cult.project.roles
332
+ : CLI.fetch_items(opts[:role], from: Role)
333
+ c = CommanderSync.new(project: Cult.project, nodes: nodes)
334
+ passes = opts[:pass] ? opts[:pass].map(&:to_i) : nil
335
+ c.sync!(roles: roles, passes: passes)
336
+ end
337
+ end
338
+ node.add_command(node_sync)
339
+
340
+ node_ping = Cri::Command.define do
341
+ name 'ping'
342
+ summary 'Check the responsiveness of each node'
343
+ usage 'ping /NODE+/'
344
+
345
+ flag :d, :destroy, 'Destroy nodes that are not responding.'
346
+
347
+ description <<~EOD.format_description
348
+ Connects to each node and reports health information.
349
+ EOD
350
+
351
+ run(arguments: unlimited) do |opts, args, cmd|
352
+ nodes = args.empty? ? Cult.project.nodes
353
+ : CLI.fetch_items(args, from: Node)
354
+ Cult.paramap(nodes) do |node|
355
+ c = Commander.new(project: Cult.project, node: node)
356
+ if (r = c.ping)
357
+ puts Rainbow(node.name).green + ": #{r}"
358
+ nil
359
+ else
360
+ puts Rainbow(node.name).red + ": Unreachable"
361
+ node
362
+ end
363
+ end
364
+ end
365
+ end
366
+ node.add_command(node_ping)
367
+
368
+ node_addr = Cri::Command.define do
369
+ name 'addr'
370
+ aliases 'ip'
371
+ summary 'print IP address of node'
372
+ usage 'addr [/NODE+/ ...]'
373
+ flag :p, :private, 'Print private address'
374
+ flag :'6', :ipv6, 'Print ipv6 address'
375
+ flag :'4', :ipv4, 'Print ipv4 address'
376
+ description <<~EOD.format_description
377
+ EOD
378
+
379
+ run(arguments: unlimited) do |opts, args, cmd|
380
+ prot = Cult.project.default_ip_protocol
381
+ if opts[:ipv4] && opts[:ipv6]
382
+ fail CLIError, "can't specify --ipv4 and --ipv6"
383
+ end
384
+
385
+ prot = :ipv6 if opts[:ipv6]
386
+ prot = :ipv4 if opts[:ipv4]
387
+
388
+ priv = opts[:private] ? :private: :public
389
+
390
+ nodes = args.empty? ? Cult.project.nodes
391
+ : CLI.fetch_items(args, from: Node)
227
392
  nodes.each do |node|
228
- puts "#{node.name}\t#{node.provider&.name}\t#{node.roles.map(&:name)}"
393
+ puts node.addr(priv, prot)
229
394
  end
230
395
  end
231
396
  end
232
- node.add_command node_list
397
+ node.add_command(node_addr)
398
+
233
399
 
234
400
  return node
235
401
  end
@@ -1,4 +1,5 @@
1
1
  require 'cult/drivers/load'
2
+ require 'json'
2
3
 
3
4
  module Cult
4
5
  module CLI
@@ -9,7 +10,7 @@ module Cult
9
10
  optional_project
10
11
  name 'provider'
11
12
  aliases 'providers'
12
- summary 'Provider Commands'
13
+ summary 'Provider commands'
13
14
  description <<~EOD.format_description
14
15
  A provider is a VPS service. Cult ships with drivers for quite a few
15
16
  services, (which can be listed with `cult provider drivers`).
@@ -30,15 +31,16 @@ module Cult
30
31
  a driver of the same name.
31
32
  EOD
32
33
 
33
- run(arguments: 0) do |opts, args, cmd|
34
+ run(arguments: none) do |opts, args, cmd|
34
35
  puts cmd.help
36
+ exit
35
37
  end
36
38
  end
37
39
 
38
40
 
39
- provider_list = Cri::Command.define do
40
- name 'list'
41
- aliases 'ls'
41
+ provider_ls = Cri::Command.define do
42
+ name 'ls'
43
+ usage 'ls [/PROVIDER+/ ...]'
42
44
  summary 'List Providers'
43
45
  description <<~EOD.format_description
44
46
  Lists Providers for this project. If --driver is specified, it only
@@ -46,7 +48,7 @@ module Cult
46
48
  EOD
47
49
  required :d, :driver, "Restrict list to providers using DRIVER"
48
50
 
49
- run(arguments: 0..1) do |opts, args, cmd|
51
+ run(arguments: 0 .. 1) do |opts, args, cmd|
50
52
  providers = Cult.project.providers
51
53
 
52
54
  # Filtering
@@ -65,7 +67,7 @@ module Cult
65
67
 
66
68
  end
67
69
  end
68
- provider.add_command(provider_list)
70
+ provider.add_command(provider_ls)
69
71
 
70
72
 
71
73
  provider_avail = Cri::Command.define do
@@ -77,7 +79,7 @@ module Cult
77
79
  gem dependencies.
78
80
  EOD
79
81
 
80
- run(arguments: 0) do |opts, args, cmd|
82
+ run(arguments: none) do |opts, args, cmd|
81
83
  Cult::Drivers.all.each do |p|
82
84
  printf "%-20s %-s\n", p.driver_name, p.required_gems
83
85
  end
@@ -86,10 +88,9 @@ module Cult
86
88
  provider.add_command(provider_avail)
87
89
 
88
90
 
89
- provider_create = Cri::Command.define do
90
- name 'create'
91
- aliases 'new'
92
- usage 'create NAME'
91
+ provider_new = Cri::Command.define do
92
+ name 'new'
93
+ usage 'new NAME'
93
94
  summary 'creates a new provider for your project'
94
95
  required :d, :driver, 'Specify driver, if different than NAME'
95
96
  description <<~EOD.format_description
@@ -111,10 +112,12 @@ module Cult
111
112
  driver = CLI.fetch_item(opts[:driver] || name, from: Driver)
112
113
  name = CLI.fetch_item(name, from: Provider, exist: false)
113
114
 
115
+ puts JSON.pretty_generate(driver.setup!)
116
+ fail "FIXME"
114
117
  puts [driver, name].inspect
115
118
  end
116
119
  end
117
- provider.add_command(provider_create)
120
+ provider.add_command(provider_new)
118
121
 
119
122
 
120
123
  provider
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'json'
2
3
 
3
4
  module Cult
4
5
  module CLI
@@ -36,28 +37,28 @@ module Cult
36
37
  `rat-site` and `tempo-site` roles and be happily serving both sites.
37
38
 
38
39
  By default, `cult init` generates two root roles that don't depend on
39
- anything else: `all` and `bootstrap`. The `bootstrap` role exists
40
+ anything else: `base` and `bootstrap`. The `bootstrap` role exists
40
41
  to get a node from a clean OS install to a configuration to be
41
- managed by the settings in `all'. Theoretically, if you're happy
42
+ managed by the settings in `base'. Theoretically, if you're happy
42
43
  doing all deploys as the root user, you don't need a `bootstrap` role
43
- at all: Delete it and set the `user` key in `all/role.json` to
44
+ at all: Delete it and set the `user` key in `base/role.json` to
44
45
  "root".
45
46
 
46
- The tasks in the `all` role are considered shared amongst all roles.
47
- However, the only thing special about the `all` role is that Cult
47
+ The tasks in the `base` role are considered shared amongst all roles.
48
+ However, the only thing special about the `base` role is that Cult
48
49
  assumes roles and nodes without an explicit `includes` setting belong
49
- to all.
50
+ to `base`.
50
51
  EOD
51
52
 
52
- run(arguments: 0) do |opts, args, cmd|
53
+ run(arguments: none) do |opts, args, cmd|
53
54
  puts cmd.help
55
+ exit
54
56
  end
55
57
  end
56
58
 
57
59
 
58
- role_create = Cri::Command.define do
59
- name 'create'
60
- aliases 'new'
60
+ role_new = Cri::Command.define do
61
+ name 'new'
61
62
  summary 'creates a new role'
62
63
  usage 'create [options] NAME'
63
64
  description <<~EOD.format_description
@@ -65,7 +66,7 @@ module Cult
65
66
  $CULT_PROJECT/roles/$NAME
66
67
  EOD
67
68
 
68
- required :r, :roles, 'this role depends on another role',
69
+ required :r, :roles, 'this role depends on another /ROLE+/ (multiple)',
69
70
  multiple: true
70
71
 
71
72
  run(arguments: 1) do |opts, args, cmd|
@@ -79,8 +80,8 @@ module Cult
79
80
  from: Role).map(&:name)
80
81
  end
81
82
  FileUtils.mkdir_p(role.path)
82
- File.write(Cult.project.dump_name(role.definition_file),
83
- Cult.project.dump_object(data))
83
+ File.write(role.definition_file,
84
+ JSON.pretty_generate(data))
84
85
 
85
86
  FileUtils.mkdir_p(File.join(role.path, "files"))
86
87
  File.write(File.join(role.path, "files", ".keep"), '')
@@ -89,19 +90,18 @@ module Cult
89
90
  File.write(File.join(role.path, "tasks", ".keep"), '')
90
91
  end
91
92
  end
92
- role.add_command role_create
93
+ role.add_command(role_new)
93
94
 
94
95
 
95
- role_destroy = Cri::Command.define do
96
- name 'destroy'
97
- aliases 'delete', 'rm'
98
- usage 'destroy ROLES...'
96
+ role_rm = Cri::Command.define do
97
+ name 'rm'
98
+ usage 'rm /ROLE+/ ...'
99
99
  summary 'Destroy role ROLE'
100
100
  description <<~EOD.format_description
101
101
  Destroys all roles specified.
102
102
  EOD
103
103
 
104
- run(arguments: 1..-1) do |opts, args, cmd|
104
+ run(arguments: 1 .. unlimited) do |opts, args, cmd|
105
105
  roles = args.map do |role_name|
106
106
  CLI.fetch_items(role_name, from: Role)
107
107
  end.flatten
@@ -114,20 +114,19 @@ module Cult
114
114
  end
115
115
  end
116
116
  end
117
- role.add_command(role_destroy)
117
+ role.add_command(role_rm)
118
118
 
119
119
 
120
- role_list = Cri::Command.define do
121
- name 'list'
122
- aliases 'ls'
123
- usage 'list [ROLES...]'
120
+ role_ls = Cri::Command.define do
121
+ name 'ls'
122
+ usage 'ls [/ROLE+/ ...]'
124
123
  summary 'List existing roles'
125
124
  description <<~EOD.format_description
126
125
  Lists roles in this project. By default, lists all roles. If one or
127
126
  more ROLES are specified, only lists those
128
127
  EOD
129
128
 
130
- run(arguments: 0..-1) do |opts, args, cmd|
129
+ run(arguments: unlimited) do |opts, args, cmd|
131
130
  roles = Cult.project.roles
132
131
  unless args.empty?
133
132
  roles = CLI.fetch_items(*args, from: Role)
@@ -140,7 +139,7 @@ module Cult
140
139
 
141
140
  end
142
141
  end
143
- role.add_command(role_list)
142
+ role.add_command(role_ls)
144
143
 
145
144
 
146
145
  role