cult 0.1.4.pre → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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