cult 0.1.4.pre → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69eca6bb220e6610aabe66cfa4727881535bf8d0
4
- data.tar.gz: 90b206c03fa2fb8b9f7c6f743b9edaaca0ce643e
3
+ metadata.gz: 3bae39f7261b8ac419611d39c3279b8d2c7fe54f
4
+ data.tar.gz: 67ff3a53fdb223ee7f69928b85d2aba336e8bb07
5
5
  SHA512:
6
- metadata.gz: 43ca3f870a7cad5335fbaa4785e29973364cc0a3a0410586f33c0fd24f61b628123078d8d09f8cfd6565fba29f1cc550d59fac142048c463ec25f424295115e4
7
- data.tar.gz: e1e79c67bdb9b968f6d19a1c62a4c198f6828d2449df4fd33c3d2dd3e25ca644a1e77b8e6ba853eb58cb4cd088499f1515c331a5a554a27995149c5f37dd9827
6
+ metadata.gz: 53363d45cdaac8dfe9508fd1704e7f4308a6ff174d15a9aea7ebdf12fd4f03b779683d02d820850499afb0b0e0fed19d9672ba54a0b4c17e5ff9aba2a1a16b55
7
+ data.tar.gz: 5ca3ee8783f987edd25ab13495f8b750234c677ddb3d2f42c913ee033d169bae521da2d1515228a4dcee33de2a02e70e6acf5c125adff17afbe6eacffbb8ece0
data/cult CHANGED
@@ -1 +1 @@
1
- exe/cult
1
+ ./exe/cult
@@ -27,14 +27,15 @@ 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.2'
30
+ spec.required_ruby_version = '>= 2.4'
31
31
 
32
- spec.add_dependency "cri", "~> 2.7"
33
- spec.add_dependency "net-ssh", "~> 3.2"
32
+ spec.add_dependency "cri", "~> 2.8"
33
+ spec.add_dependency "net-ssh", "~> 4.1"
34
34
  spec.add_dependency "net-scp", "~> 1.2"
35
- spec.add_dependency "rainbow", "~> 2.1"
36
- spec.add_dependency "erubis", "~> 2.7.0"
35
+ spec.add_dependency "rainbow", "~> 2.2.2"
36
+ spec.add_dependency "erubi", "~> 1.6.0"
37
+ spec.add_dependency "terminal-table", "~> 1.7.2"
37
38
 
38
- spec.add_development_dependency "bundler", "~> 1.12"
39
- spec.add_development_dependency "rake", "~> 10.0"
39
+ spec.add_development_dependency "bundler", "~> 1.14"
40
+ spec.add_development_dependency "rake", "~> 12.0"
40
41
  end
@@ -0,0 +1,73 @@
1
+ # Future Plans
2
+
3
+ Here's some stuff I'd like to add to Cult:
4
+
5
+ * `cult node disable /NODE+/` and `cult node enable /NODE+/` in a clean way.
6
+ This means tasks won't see disabled nodes, so a node can be taken out of
7
+ the set without destroying it. `fetch_item[s]` probably needs to see the
8
+ whole set (otherwise, how could you re-enable a disabled item?), but
9
+ things like `nodes.each` need to skip them. This should work:
10
+
11
+ ```bash
12
+ cult node disable /pgpool/
13
+ cult node sync
14
+ # Now all nodes connect directly to master
15
+ cult node enable /pgpool/
16
+ cult node sync
17
+ # Now we're back to be first setup
18
+ ```
19
+
20
+ * A node needs a way to finish a task by saying "It's all good, but I'm gonna
21
+ have to reboot, so do your SSH loop again". The use case is a fresh node
22
+ created from an image that has security updates available immediately.
23
+
24
+ * Partition sets. Lets say you have ten front-end servers ->
25
+ two load balancers like pgpool -> three backend servers. There needs to be
26
+ a way for a front-end task to ask which load balancer to use that'd
27
+ consistently put equal weight on each one. If a load balancer is added or
28
+ removed, it'd answer the question differently. I'm thinking something
29
+ like:
30
+
31
+ ```ruby
32
+ node.fair(role, :zone)
33
+ ```
34
+
35
+ Would return a node with that role, in that zone, and return a balanced
36
+ list depending on 'node'. For bonus points we could have weights on a
37
+ per-node basis that `fair` would take into account.
38
+
39
+ * Fully document `NamedArray` and its usage in both code and the command
40
+ line. It's one of my favorite things about Cult, but I add features and
41
+ syntax haphazardly that are awesome, but not fully explained.
42
+
43
+ * Leader promotion/avoidance. I should be able to do something like:
44
+ `cult node promote -r some-role some-node`, and that'll make some-node the
45
+ zone_leader? of some-role. I should be able to `cult node demote` the
46
+ node out of leader position, if it was previously promoted, OR if it were
47
+ naturally selected.
48
+
49
+ * Generalized events: Now `sync` is sort of baked-in. It should just be a
50
+ specific case of a generalized event-running system.
51
+
52
+ * More environment separation: There should be warnings/confirmations in
53
+ production, and be totally hard to fat-finger fuck up production. I type
54
+ `cult node rm //` dozens of times a day. That doesn't need to work in
55
+ production. This might go as far as "cult env <something>" checking out
56
+ a branch. I don't have a problem tying Cult to git as an integral part of
57
+ its operations. We can always allow outside contributions for SCM adapters
58
+ later.
59
+
60
+ * General code clean-up. Luckily, there aren't any huge architectural
61
+ problems, but older code written while the vision was still up in the air
62
+ isn't exactly stuff I'd put in my portfolio. Particularly, we need a way
63
+ to save and load nodes and roles without it being hard-coded into the CLI
64
+ commands. This would make stuff more usable from the console, too.
65
+
66
+ * Some way to set project-global settings. For example, in our instance,
67
+ you can search the project for our OpenSSL cipher string and find it
68
+ duplicated in 8-10 places. We should be able to set that somewhere
69
+ besides `base/role.json` and have a fast, convenient way to reference it
70
+ from tasks or the console.
71
+
72
+ * Adding roles: With properly-written tasks, we should be able to add a role
73
+ to an existing node. We shouldn't even attempt to remove one.
@@ -1,6 +1,6 @@
1
1
  require 'io/console'
2
2
  require 'shellwords'
3
-
3
+ require 'cult/cli/table_extensions'
4
4
  require 'cult/cli/cri_extensions'
5
5
 
6
6
  module Cult
@@ -40,10 +40,6 @@ module Cult
40
40
  def cult(*argv)
41
41
  system $0, *argv
42
42
  end
43
-
44
- def binding
45
- super
46
- end
47
43
  end
48
44
 
49
45
  module_function
@@ -84,23 +80,24 @@ module Cult
84
80
  end
85
81
 
86
82
  context.load_rc
83
+ context_binding = context.instance_eval { binding }
87
84
 
88
85
  if opts[:ripl]
89
86
  require 'ripl'
90
87
  ARGV.clear
91
88
  # Look, something reasonable:
92
- Ripl.start(binding: context.binding)
89
+ Ripl.start(binding: context_binding)
93
90
 
94
91
  elsif opts[:pry]
95
92
  require 'pry'
96
- context.binding.pry
93
+ context_binding.pry
97
94
  else
98
95
  # irb: This is ridiculous.
99
96
  require 'irb'
100
97
  ARGV.clear
101
98
  IRB.setup(nil)
102
99
 
103
- irb = IRB::Irb.new(IRB::WorkSpace.new(context.binding))
100
+ irb = IRB::Irb.new(IRB::WorkSpace.new(context_binding))
104
101
  IRB.conf[:MAIN_CONTEXT] = irb.context
105
102
  IRB.conf[:IRB_RC].call(irb.context) if IRB.conf[:IRB_RC]
106
103
 
@@ -1,6 +1,7 @@
1
1
  require 'securerandom'
2
2
  require 'fileutils'
3
3
  require 'json'
4
+ require 'terminal-table'
4
5
 
5
6
  module Cult
6
7
  module CLI
@@ -67,6 +68,11 @@ module Cult
67
68
  concurrent = interactive || nodes.size == 1 ? 1 : nil
68
69
 
69
70
  Cult.paramap(nodes, concurrent: concurrent) do |node|
71
+ # Through source control, etc, these sometimes end up with improper
72
+ # permissions. OpenSSH won't let us use it otherwise, and there's
73
+ # no option to disable the check.
74
+ File.chmod(0600, node.ssh_private_key_file)
75
+
70
76
  ssh_args = 'ssh', '-i', esc.(node.ssh_private_key_file),
71
77
  '-p', esc.(node.ssh_port.to_s),
72
78
  '-o', "UserKnownHostsFile=#{esc.(node.ssh_known_hosts_file)}",
@@ -242,7 +248,7 @@ module Cult
242
248
  node_ls = Cri::Command.define do
243
249
  name 'ls'
244
250
  summary 'List nodes'
245
- usage 'ls /NODE+/ ...'
251
+ usage 'ls /NODE*/ ...'
246
252
  description <<~EOD.format_description
247
253
  This command lists the nodes in the project.
248
254
  EOD
@@ -261,19 +267,24 @@ module Cult
261
267
  end
262
268
  end
263
269
 
264
- Cult.paramap(nodes) do |node|
265
- role_string = node.build_order.map do |role|
270
+ table = Terminal::Table.new(headings:
271
+ ['Node', 'Provider', 'Zone', 'Public IPv4', 'Private IPv4', 'Roles']
272
+ )
273
+
274
+ table.rows = Cult.paramap(nodes) do |node|
275
+ role_string = node.build_order.reject(&:node?).map do |role|
266
276
  if node.zone_leader?(role)
267
277
  Rainbow('*' + role.name).cyan
268
278
  else
269
- '=' + role.name
279
+ role.name
270
280
  end
271
- end.join(', ')
281
+ end.join(' ')
272
282
 
273
- puts "#{node.name}\t#{node.provider&.name}\t" +
274
- "#{node.zone}\t#{node.addr(:public)}\t#{node.addr(:private)}\t" +
275
- "#{role_string}"
283
+ [ node.name, node.provider&.name, node.zone,
284
+ node.addr(:public), node.addr(:private), role_string]
276
285
  end
286
+
287
+ puts table
277
288
  end
278
289
  end
279
290
  node.add_command(node_ls)
@@ -281,7 +292,7 @@ module Cult
281
292
 
282
293
  node_sync = Cri::Command.define do
283
294
  name 'sync'
284
- usage 'sync /NODE+/ ...'
295
+ usage 'sync /NODE*/ ...'
285
296
  summary 'Synchronize host information across fleet'
286
297
  description <<~EOD.format_description
287
298
  Computes, pre-processes, and executes "sync" tasks on every NODE,
@@ -340,7 +351,7 @@ module Cult
340
351
  node_ping = Cri::Command.define do
341
352
  name 'ping'
342
353
  summary 'Check the responsiveness of each node'
343
- usage 'ping /NODE+/'
354
+ usage 'ping /NODE*/'
344
355
 
345
356
  flag :d, :destroy, 'Destroy nodes that are not responding.'
346
357
 
@@ -351,16 +362,15 @@ module Cult
351
362
  run(arguments: unlimited) do |opts, args, cmd|
352
363
  nodes = args.empty? ? Cult.project.nodes
353
364
  : CLI.fetch_items(args, from: Node)
354
- Cult.paramap(nodes) do |node|
365
+
366
+ table = Terminal::Table.new(headings: ["Node", "Status"])
367
+ table.rows = Cult.paramap(nodes, quiet: true) do |node|
355
368
  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
369
+ status = c.ping
370
+ [ node.name, status ? Rainbow(status).green
371
+ : Rainbow("unreachable").red ]
363
372
  end
373
+ puts table
364
374
  end
365
375
  end
366
376
  node.add_command(node_ping)
@@ -80,8 +80,7 @@ module Cult
80
80
  from: Role).map(&:name)
81
81
  end
82
82
  FileUtils.mkdir_p(role.path)
83
- File.write(role.definition_file,
84
- JSON.pretty_generate(data))
83
+ File.write(role.role_file, JSON.pretty_generate(data))
85
84
 
86
85
  FileUtils.mkdir_p(File.join(role.path, "files"))
87
86
  File.write(File.join(role.path, "files", ".keep"), '')
@@ -132,16 +131,16 @@ module Cult
132
131
  roles = CLI.fetch_items(*args, from: Role)
133
132
  end
134
133
 
135
- roles.each do |r|
136
- fmt = "%-20s %s\n"
137
- printf fmt, r.name, r.build_order.map(&:name).join(', ')
134
+ table = Terminal::Table.new(headings: ['Role', 'Build Order'])
135
+ table.rows = roles.map do |r|
136
+ [r.name, r.build_order.map(&:name).join(', ')]
138
137
  end
138
+ puts table
139
139
 
140
140
  end
141
141
  end
142
142
  role.add_command(role_ls)
143
143
 
144
-
145
144
  role
146
145
  end
147
146
  end
@@ -0,0 +1,24 @@
1
+ require 'terminal-table'
2
+
3
+ # This extends Terminal::Table to do plain tab-separated columns if Rainbow
4
+ # is disabled, which roughly translates to isatty?
5
+
6
+ module Cult
7
+ module TableExtensions
8
+ def render_plain
9
+ rows.map do |row|
10
+ row.cells.map do |cell|
11
+ cell.value
12
+ end.join("\t")
13
+ end.join("\n")
14
+ end
15
+
16
+ def render
17
+ Rainbow.enabled ? super : render_plain
18
+ end
19
+
20
+ alias_method :to_s, :render
21
+
22
+ ::Terminal::Table.prepend(self)
23
+ end
24
+ end
@@ -132,7 +132,7 @@ module Cult
132
132
 
133
133
  def ping
134
134
  connect do |ssh|
135
- ssh.exec! "uptime"
135
+ ssh.exec!("uptime").chomp
136
136
  end
137
137
  rescue
138
138
  nil
@@ -9,8 +9,10 @@ module Cult
9
9
 
10
10
 
11
11
  def sizes_map
12
+ # Cores = GB Ram is a pretty good scaling factor, and
13
+ # closely maps what VPS providers are doing.
12
14
  [1, 2, 4, 6, 8, 10, 12, 16].map do |size|
13
- ["#{size}gb", size * 1024 * 1024]
15
+ [ "#{size}gb", { ram: size * 1024, cores: size } ]
14
16
  end.to_h
15
17
  end
16
18
  with_id_mapping :sizes_map
@@ -83,6 +85,8 @@ module Cult
83
85
 
84
86
 
85
87
  def guest_copy(name, src, dst)
88
+ # NOTE: Bug in current (Sep 2016) VBox has a fucked copyto, where
89
+ # setting target-directory to the full path is a workaround
86
90
  cmd = "VBoxManage guestcontrol #{esc(name)} " +
87
91
  "--username root --password password " +
88
92
  "copyto #{esc(src)} --target-directory #{esc(dst)}"
@@ -101,38 +105,50 @@ module Cult
101
105
 
102
106
 
103
107
  def provision!(name:, size:, zone:, image:, ssh_public_key:)
104
- system 'VBoxManage', 'clonevm',
105
- fetch_mapped(name: :image, from: images_map, key: image),
106
- '--name', name, '--register'
107
-
108
- system 'VBoxManage', 'modifyvm', name, '--groups', '/Cult'
109
- system 'VBoxManage', 'startvm', name, '--type', 'headless'
110
-
111
- public_ip = await_ip_address(name, 0, :ipv4)
112
- private_ip = public_ip
113
-
114
- await_ssh(public_ip)
115
-
116
- guest_command(name, "mkdir -m 0600 /root/.ssh")
117
- guest_copy(name, ssh_public_key, "/root/.ssh/authorized_keys")
118
- guest_command(name, "chmod 0644 /root/.ssh/authorized_keys")
119
- guest_command(name, "passwd -l root")
108
+ transaction do |xac|
109
+ # Todo: transaction
110
+ system 'VBoxManage', 'clonevm',
111
+ fetch_mapped(name: :image, from: images_map, key: image),
112
+ '--name', name, '--register'
113
+
114
+ xac.rollback do
115
+ destroy!(id: name, ssh_key_id: nil)
116
+ end
120
117
 
121
- return {
122
- name: name,
123
- size: size,
124
- zone: zone,
125
- image: image,
126
-
127
- id: name,
128
- created_at: Time.now.iso8601,
129
- host: public_ip,
130
- ipv4_public: public_ip,
131
- ipv4_private: private_ip,
132
- ipv6_public: nil,
133
- ipv6_private: nil,
134
- meta: {}
135
- }
118
+ system_spec = sizes_map[size]
119
+ system 'VBoxManage', 'modifyvm', name,
120
+ '--groups', '/Cult',
121
+ '--memory', system_spec[:ram].to_s,
122
+ '--cpus', system_spec[:cores].to_s
123
+
124
+ system 'VBoxManage', 'startvm', name, '--type', 'headless'
125
+
126
+ public_ip = await_ip_address(name, 0, :ipv4)
127
+ private_ip = public_ip
128
+
129
+ await_ssh(public_ip)
130
+
131
+ guest_command(name, "mkdir -m 0600 /root/.ssh")
132
+ guest_copy(name, ssh_public_key, "/root/.ssh/authorized_keys")
133
+ guest_command(name, "chmod 0644 /root/.ssh/authorized_keys")
134
+ guest_command(name, "passwd -l root")
135
+
136
+ return {
137
+ name: name,
138
+ size: size,
139
+ zone: zone,
140
+ image: image,
141
+
142
+ id: name,
143
+ created_at: Time.now.iso8601,
144
+ host: public_ip,
145
+ ipv4_public: public_ip,
146
+ ipv4_private: private_ip,
147
+ ipv6_public: nil,
148
+ ipv6_private: nil,
149
+ meta: {}
150
+ }
151
+ end
136
152
  end
137
153
 
138
154
  def self.setup!
@@ -197,7 +197,7 @@ module Cult
197
197
 
198
198
  # first matching item, or raises KeyError
199
199
  def fetch(key)
200
- first(key) or raise KeyError
200
+ first(key) or raise KeyError, "Not found: #{key.inspect}"
201
201
  end
202
202
 
203
203
 
@@ -53,6 +53,10 @@ module Cult
53
53
  delegate_to_definition :created_at
54
54
 
55
55
 
56
+ def node?
57
+ true
58
+ end
59
+
56
60
  def self.path(project)
57
61
  File.join(project.path, 'nodes')
58
62
  end
@@ -188,6 +192,14 @@ module Cult
188
192
  c.first
189
193
  end
190
194
 
195
+ def role_leader(role)
196
+ leader(role)
197
+ end
198
+
199
+ def role_leader?(role)
200
+ role_leader(role) == self
201
+ end
202
+
191
203
 
192
204
  def provider_leader(role = nil)
193
205
  leader(role, :provider)
@@ -18,11 +18,11 @@
18
18
  module Cult
19
19
  class Paramap
20
20
  class Job
21
- attr_reader :ident, :value, :block
21
+ attr_reader :ident, :value, :rebind, :block
22
22
  attr_reader :pid, :pipe
23
23
 
24
- def initialize(ident, value, block)
25
- @ident, @value, @block = ident, value, block
24
+ def initialize(ident, value, rebind: {}, &block)
25
+ @ident, @value, @rebind, @block = ident, value, rebind, block
26
26
 
27
27
  @pipe = IO.pipe
28
28
  @pid = fork do
@@ -37,7 +37,22 @@ module Cult
37
37
  @pipe[1].close
38
38
  end
39
39
 
40
+ def rebind_streams!
41
+ names = {
42
+ stdout: STDOUT,
43
+ stdin: STDIN,
44
+ stderr: STDERR,
45
+ nil => '/dev/null'
46
+ }
47
+ rebind.each do |k, v|
48
+ src, dst = names[k], names[v]
49
+ dst = File.open(dst, 'w+') if dst.is_a?(String)
50
+ src.reopen(dst)
51
+ end
52
+ end
53
+
40
54
  def prepare_forked_environment!
55
+ rebind_streams!
41
56
  # Stub out things that have caused a problem in the past.
42
57
  Kernel.send(:define_method, :exec) do |*a|
43
58
  fail "don't use Kernel\#exec inside of a paramap job"
@@ -92,6 +107,7 @@ module Cult
92
107
  end
93
108
 
94
109
  attr_reader :enum, :iter
110
+ attr_reader :rebind
95
111
  attr_reader :block
96
112
  attr_reader :job_queue
97
113
  attr_reader :exception_strategy
@@ -99,14 +115,16 @@ module Cult
99
115
  attr_reader :results
100
116
  attr_reader :concurrent
101
117
 
102
- def initialize(enum, concurrent: nil, exception_strategy:, &block)
118
+ def initialize(enum, rebind: {}, concurrent: nil, exception_strategy:, &block)
103
119
  @enum = enum
120
+ @rebind = rebind
104
121
  @iter = @enum.to_enum
105
122
  @concurrent = concurrent || max_concurrent
106
123
  @exception_strategy = exception_strategy
107
124
  @block = block
108
125
  @exceptions, @results = [], []
109
126
  @job_queue = []
127
+
110
128
  end
111
129
 
112
130
  def max_concurrent
@@ -144,7 +162,7 @@ module Cult
144
162
  end
145
163
 
146
164
  def add_job(value)
147
- job_queue.push(Job.new(new_job_index, value, block))
165
+ job_queue.push(Job.new(new_job_index, value, rebind: rebind, &block))
148
166
  end
149
167
 
150
168
  def job_by_pid(pid)
@@ -202,8 +220,11 @@ module Cult
202
220
  private_constant :Paramap
203
221
 
204
222
  module_function
205
- def paramap(enum, concurrent: nil, exception: :raise, &block)
206
- Paramap.new(enum, concurrent: concurrent,
223
+ def paramap(enum, quiet: false, concurrent: nil, exception: :raise, &block)
224
+ rebind = quiet ? { stdout: nil, stderr: nil } : {}
225
+ Paramap.new(enum,
226
+ rebind: rebind,
227
+ concurrent: concurrent,
207
228
  exception_strategy: exception, &block).run
208
229
  end
209
230
  end
@@ -83,9 +83,19 @@ module Cult
83
83
  end
84
84
  end
85
85
 
86
+ # We allow setting to a lookup value instead of an instance
86
87
  attr_writer :default_provider
87
88
  def default_provider
88
- @default_provider ||= providers[0]
89
+ @default_provider_instance ||= begin
90
+ case @default_provider
91
+ when Cult::Provider
92
+ @default_provider
93
+ when nil;
94
+ providers.first
95
+ else
96
+ providers[@default_provider]
97
+ end
98
+ end
89
99
  end
90
100
 
91
101
 
@@ -126,6 +136,13 @@ module Cult
126
136
  end
127
137
  end
128
138
 
139
+ def git_commit_id(short: false)
140
+ short = short ? "--short" : ''
141
+ cmd = "git -C #{Shellwords.escape(path)} rev-parse #{short} " +
142
+ "--verify HEAD"
143
+ %x(#{cmd}).chomp
144
+ end
145
+
129
146
 
130
147
  def env
131
148
  ENV['CULT_ENV'] || begin
@@ -1,6 +1,11 @@
1
1
  require 'forwardable'
2
+ require 'etc'
3
+ require 'socket'
2
4
 
3
5
  module Cult
6
+ # Project Context is a binding useful for "cult console" and templates. It
7
+ # makes it so "nodes" and "roles" return something useful.
8
+
4
9
  class ProjectContext
5
10
  extend Forwardable
6
11
  def_delegators :project, :methods, :respond_to?, :to_s, :inspect
@@ -9,9 +14,9 @@ module Cult
9
14
 
10
15
  def initialize(project, **extra)
11
16
  @project = project
12
-
13
17
  extra.each do |k, v|
14
- define_singleton_method(k) { v }
18
+ v.respond_to?(:call) ? define_singleton_method(k, &v)
19
+ : define_singleton_method(k) { v }
15
20
  end
16
21
  end
17
22
 
@@ -19,5 +24,6 @@ module Cult
19
24
  project.send(*args)
20
25
  end
21
26
 
27
+ public :binding
22
28
  end
23
29
  end
@@ -21,6 +21,9 @@ module Cult
21
21
  @path = path
22
22
  end
23
23
 
24
+ def node?
25
+ false
26
+ end
24
27
 
25
28
  def exist?
26
29
  Dir.exist?(path)
@@ -86,6 +89,9 @@ module Cult
86
89
  end
87
90
  alias_method :files, :artifacts
88
91
 
92
+ def role_file
93
+ File.join(path, "role.json")
94
+ end
89
95
 
90
96
  def definition
91
97
  @definition ||= Definition.new(self)
@@ -94,7 +100,7 @@ module Cult
94
100
 
95
101
  def definition_path
96
102
  [ File.join(path, "extra.json"),
97
- File.join(path, "role.json") ]
103
+ role_file ]
98
104
  end
99
105
 
100
106
 
@@ -1,4 +1,4 @@
1
- require 'erubis'
1
+ require 'erubi'
2
2
  require 'cult/user_refinements'
3
3
 
4
4
  module Cult
@@ -11,16 +11,26 @@ module Cult
11
11
  super(project, **kw)
12
12
  end
13
13
 
14
- def _process(input, filename: nil)
15
- Dir.chdir(@pwd || Dir.pwd) do
16
- erb = Erubis::Eruby.new(input)
17
- erb.filename = filename
18
- erb.result(binding)
14
+ def cultsrcid
15
+ loc = caller_locations(1, 1)[0]
16
+ path = loc.absolute_path
17
+ if path.start_with?(project.path)
18
+ path = project.name + "/" + path[project.path.size + 1 .. -1]
19
19
  end
20
+
21
+ user, host = Etc.getlogin, Socket.gethostname
22
+ vcs = "#{git_branch}@#{git_commit_id(short: true)}"
23
+ timestamp = Time.now.iso8601
24
+
25
+ "@cultsrcid: #{path}:#{loc.lineno} #{vcs} #{timestamp} #{user}@#{host}"
20
26
  end
21
27
 
22
- def binding
23
- super
28
+ private
29
+ def _process(input, filename: nil)
30
+ Dir.chdir(@pwd || Dir.pwd) do
31
+ erb = Erubi::Engine.new(input, filename: filename)
32
+ binding.eval(erb.src)
33
+ end
24
34
  end
25
35
  end
26
36
 
@@ -32,7 +42,7 @@ module Cult
32
42
 
33
43
 
34
44
  def process(text, filename: nil)
35
- context._process(text, filename: filename)
45
+ context.send(:_process, text, filename: filename)
36
46
  end
37
47
 
38
48
  end
@@ -28,7 +28,8 @@ module Cult
28
28
  begin
29
29
  yield
30
30
  rescue Exception => e
31
- $stderr.puts "Rolling back actions due to: #{e.inspect}"
31
+ $stderr.puts "Rolling back actions due to: #{e.inspect}\n" +
32
+ e.backtrace
32
33
  unwind
33
34
  raise
34
35
  end
@@ -1,4 +1,3 @@
1
- require 'json'
2
1
  require 'shellwords'
3
2
 
4
3
  # These are refinements we enable in a user-facing context, e.g., the console
@@ -9,16 +8,15 @@ module Cult
9
8
  # Alright! We found a use for refinements!
10
9
  module Util
11
10
  module_function
11
+
12
12
  def squote(s)
13
13
  "'" + s.gsub("'", "\\\\\'") + "'"
14
14
  end
15
15
 
16
-
17
16
  def dquote(s)
18
- s.to_json
17
+ '"' + s.gsub('"', '\"') + '"'
19
18
  end
20
19
 
21
-
22
20
  def slash(s)
23
21
  Shellwords.escape(s)
24
22
  end
@@ -40,6 +38,7 @@ module Cult
40
38
  def slash
41
39
  Util.slash(self)
42
40
  end
41
+ alias_method :e, :slash
43
42
  end
44
43
 
45
44
 
@@ -55,11 +54,10 @@ module Cult
55
54
  end
56
55
  alias_method :sq, :squote
57
56
 
58
-
59
-
60
57
  def slash(sep = ' ')
61
58
  map {|v| Util.slash(v) }.join(sep)
62
59
  end
60
+ alias_method :e, :slash
63
61
  end
64
62
  end
65
63
  end
@@ -1,3 +1,3 @@
1
1
  module Cult
2
- VERSION = '0.1.4.pre'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -9,14 +9,24 @@ set -e
9
9
 
10
10
  # The ERB helper has quite a few methods for shell-escaping, for example:
11
11
 
12
- # The "q" method quotes a string with double-quotes.
12
+ # "e" does shell slash-escaping, e.g., I\'m\ Awesome. Importantly, it will
13
+ # also escape $ with \$. It is the most general-purpose for shell scripts,
14
+ # and should be your go-to, even though its results are uglier.
15
+ # AKA: slash
16
+ echo <%= node.name.e %>
17
+
18
+ # The "q" method quotes a string with double-quotes. AKA "dq" and "dquote".
19
+ # Quote characters are escaped with a slash, e.g., \"
20
+ # AKA: dq, dquote
13
21
  echo <%= node.name.q %>
14
22
 
15
- # The "sq" method single-quotes the string.
23
+ # The "sq" method single-quotes the string. This usually never what you want
24
+ # in shell-script context, but comes in handy with config files. We just
25
+ # happen to know it'll work here. Single-quotes are escaped with a slash,
26
+ # which shells don't like.
27
+ # AKA: squote
16
28
  echo <%= node.name.sq %>
17
29
 
18
- # "slash" does shell slash-escaping, e.g., I\'m\ Awesome
19
- echo <%= node.name.slash %>
20
30
 
21
31
  # The same methods work on Array (over each item), and have an optional
22
32
  # separator argument, which defaults to ' '
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ # This file generates a map of all hosts on each node, in /etc/hosts. Because
5
+ # it's included in `base`, it'll be executed before your custom roles' sync
6
+ # tasks. That means you can parse the map instead of using custom Ruby
7
+ # templating, if that's not your thing.
8
+ #
9
+ # Keep in mind that this file is evaluated on the local machine, and its result
10
+ # is sent to the remote host.
11
+ #
12
+ # The output format is:
13
+ #
14
+ # 192.168.1.1 node-name # base *role1 role2 role3
15
+ #
16
+ # Where the * signifies that the node is the zone leader of that role.
17
+ #
18
+
19
+ CULTMAP="$HOME/cult/hosts"
20
+ mkdir -p $(basename "$CULTMAP")
21
+ sudo rm -f "$CULTMAP"
22
+
23
+ cat - <<HOSTS | tee "$CULTMAP"
24
+ # <%= cultsrcid %>
25
+ <% nodes.each do |n| %>
26
+ <%
27
+ role_string = n.build_order.map do |r|
28
+ (n.zone_leader?(r) ? '*' : '') + r.name
29
+ end.join(' ')
30
+ %>
31
+ <%= n.addr_from(node) %> <%= n.name %> # cult: <%= role_string %>
32
+ <% end %>
33
+ HOSTS
34
+
35
+ HOSTS=$(cat /etc/hosts | grep -v '# cult: '; cat "$CULTMAP")
36
+ echo "$HOSTS" | sudo tee /etc/hosts
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ ZONE_LEADERS="$HOME/cult/zone-leaders"
4
+
5
+ <%
6
+ leader_of = node.build_order.select do |role|
7
+ node.zone_leader?(role) && node != role
8
+ end.map(&:name)
9
+ %>
10
+
11
+ <% if leader_of.empty? %>
12
+ rm -f "$ZONE_LEADERS"
13
+ <% else %>
14
+ echo <%= leader_of.join(" ").e %> | tee "$ZONE_LEADERS"
15
+ <% end %>
@@ -13,3 +13,9 @@ chmod -R 0700 /home/cult/.ssh
13
13
  chmod -R 0600 /home/cult/.ssh/*
14
14
 
15
15
  echo 'cult ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/cult-nopasswd
16
+
17
+ # disable root account
18
+ passwd -l root
19
+ sed -i.bak -e 's/^PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
20
+
21
+ systemctl reload sshd
@@ -1,4 +1,4 @@
1
- #!/bin/bash
1
+ #!/usr/bin/env bash
2
2
  set -e
3
3
 
4
4
  if [ -d "/etc/update-motd.d" ]; then
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cult
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4.pre
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Owens
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-09-22 00:00:00.000000000 Z
11
+ date: 2017-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cri
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.7'
19
+ version: '2.8'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '2.7'
26
+ version: '2.8'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: net-ssh
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '3.2'
33
+ version: '4.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '3.2'
40
+ version: '4.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: net-scp
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -58,56 +58,70 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '2.1'
61
+ version: 2.2.2
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '2.1'
68
+ version: 2.2.2
69
69
  - !ruby/object:Gem::Dependency
70
- name: erubis
70
+ name: erubi
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 2.7.0
75
+ version: 1.6.0
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 2.7.0
82
+ version: 1.6.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: terminal-table
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.7.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.7.2
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: bundler
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: '1.12'
103
+ version: '1.14'
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
- version: '1.12'
110
+ version: '1.14'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: rake
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - "~>"
102
116
  - !ruby/object:Gem::Version
103
- version: '10.0'
117
+ version: '12.0'
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
107
121
  requirements:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
- version: '10.0'
124
+ version: '12.0'
111
125
  description:
112
126
  email:
113
127
  - mike@meter.md
@@ -123,7 +137,7 @@ files:
123
137
  - Rakefile
124
138
  - cult
125
139
  - cult.gemspec
126
- - doc/welcome.txt
140
+ - doc/future.md
127
141
  - exe/cult
128
142
  - lib/cult.rb
129
143
  - lib/cult/artifact.rb
@@ -136,6 +150,7 @@ files:
136
150
  - lib/cult/cli/node_cmd.rb
137
151
  - lib/cult/cli/provider_cmd.rb
138
152
  - lib/cult/cli/role_cmd.rb
153
+ - lib/cult/cli/table_extensions.rb
139
154
  - lib/cult/cli/task_cmd.rb
140
155
  - lib/cult/commander.rb
141
156
  - lib/cult/commander_sync.rb
@@ -169,13 +184,12 @@ files:
169
184
  - skel/providers/.keep
170
185
  - skel/roles/base/role.json
171
186
  - skel/roles/base/tasks/000-do-something-cool
172
- - skel/roles/base/tasks/sync-host-map
173
- - skel/roles/base/tasks/sync-leader-of
187
+ - skel/roles/base/tasks/sync-etc-hosts
188
+ - skel/roles/base/tasks/sync-zone-leaders-motd
174
189
  - skel/roles/bootstrap/files/cult-motd
175
190
  - skel/roles/bootstrap/role.json
176
191
  - skel/roles/bootstrap/tasks/000-set-hostname
177
192
  - skel/roles/bootstrap/tasks/001-add-cult-user
178
- - skel/roles/bootstrap/tasks/002-disable-root-user
179
193
  - skel/roles/bootstrap/tasks/002-install-cult-motd
180
194
  homepage: https://github.com/metermd/cult
181
195
  licenses:
@@ -190,15 +204,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
190
204
  requirements:
191
205
  - - ">="
192
206
  - !ruby/object:Gem::Version
193
- version: '2.2'
207
+ version: '2.4'
194
208
  required_rubygems_version: !ruby/object:Gem::Requirement
195
209
  requirements:
196
- - - ">"
210
+ - - ">="
197
211
  - !ruby/object:Gem::Version
198
- version: 1.3.1
212
+ version: '0'
199
213
  requirements: []
200
214
  rubyforge_project:
201
- rubygems_version: 2.5.1
215
+ rubygems_version: 2.6.12
202
216
  signing_key:
203
217
  specification_version: 4
204
218
  summary: Fleet Management like its 1990
@@ -1 +0,0 @@
1
- Welcome to cult.
@@ -1,24 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
-
4
- # This file generates a map of all hosts on each node, in an /etc/hosts like
5
- # format. Because it's included in `base`, it'll be executed before your
6
- # custom roles' sync task. That means you can parse the cultmap instead of
7
- # using custom Ruby templating, if that's not your thing.
8
- #
9
- # Keep in mind that this file is evaluated on the local machine, and its result
10
- # is sent to the remote host.
11
- #
12
- # The output format is:
13
- #
14
- # 192.168.1.1 node-name # base role1 role2 role3
15
- #
16
-
17
- CULTMAP="$HOME/cult/hosts"
18
- mkdir -p $(basename "$CULTMAP")
19
- sudo rm -f "$CULTMAP"
20
-
21
- <% nodes.each do |n| %>
22
- <% roles = n.build_order.map(&:name).join " " %>
23
- echo "<%= n.addr_from(node) %> <%= n.name %> # <%= roles %>" | sudo tee -a "$CULTMAP"
24
- <% end %>
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
- LEADER_FILE=~cult/cult/leader-of
4
-
5
- <% leader_of = roles.select {|r| node.zone_leader?(r) }.map(&:name) %>
6
-
7
- <% if leader_of.empty? %>
8
- rm -f "$LEADER_FILE"
9
- <% else %>
10
- echo <%= leader_of.join(" ").sq %> | tee "$LEADER_FILE"
11
- <% end %>
@@ -1,7 +0,0 @@
1
- #!/bin/sh
2
- set -e
3
-
4
- passwd -l root
5
- sed -i.bak -e 's/^PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
6
-
7
- systemctl reload sshd