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 +4 -4
- data/cult +1 -1
- data/cult.gemspec +8 -7
- data/doc/future.md +73 -0
- data/lib/cult/cli/common.rb +1 -1
- data/lib/cult/cli/console_cmd.rb +4 -7
- data/lib/cult/cli/node_cmd.rb +28 -18
- data/lib/cult/cli/role_cmd.rb +5 -6
- data/lib/cult/cli/table_extensions.rb +24 -0
- data/lib/cult/commander.rb +1 -1
- data/lib/cult/drivers/virtual_box_driver.rb +48 -32
- data/lib/cult/named_array.rb +1 -1
- data/lib/cult/node.rb +12 -0
- data/lib/cult/paramap.rb +28 -7
- data/lib/cult/project.rb +18 -1
- data/lib/cult/project_context.rb +8 -2
- data/lib/cult/role.rb +7 -1
- data/lib/cult/template.rb +19 -9
- data/lib/cult/transaction.rb +2 -1
- data/lib/cult/user_refinements.rb +4 -6
- data/lib/cult/version.rb +1 -1
- data/skel/roles/base/tasks/000-do-something-cool +14 -4
- data/skel/roles/base/tasks/sync-etc-hosts +36 -0
- data/skel/roles/base/tasks/sync-zone-leaders-motd +15 -0
- data/skel/roles/bootstrap/tasks/001-add-cult-user +6 -0
- data/skel/roles/bootstrap/tasks/002-install-cult-motd +1 -1
- metadata +37 -23
- data/doc/welcome.txt +0 -1
- data/skel/roles/base/tasks/sync-host-map +0 -24
- data/skel/roles/base/tasks/sync-leader-of +0 -11
- data/skel/roles/bootstrap/tasks/002-disable-root-user +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bae39f7261b8ac419611d39c3279b8d2c7fe54f
|
4
|
+
data.tar.gz: 67ff3a53fdb223ee7f69928b85d2aba336e8bb07
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 53363d45cdaac8dfe9508fd1704e7f4308a6ff174d15a9aea7ebdf12fd4f03b779683d02d820850499afb0b0e0fed19d9672ba54a0b4c17e5ff9aba2a1a16b55
|
7
|
+
data.tar.gz: 5ca3ee8783f987edd25ab13495f8b750234c677ddb3d2f42c913ee033d169bae521da2d1515228a4dcee33de2a02e70e6acf5c125adff17afbe6eacffbb8ece0
|
data/cult
CHANGED
@@ -1 +1 @@
|
|
1
|
-
exe/cult
|
1
|
+
./exe/cult
|
data/cult.gemspec
CHANGED
@@ -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.
|
30
|
+
spec.required_ruby_version = '>= 2.4'
|
31
31
|
|
32
|
-
spec.add_dependency "cri", "~> 2.
|
33
|
-
spec.add_dependency "net-ssh", "~>
|
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.
|
36
|
-
spec.add_dependency "
|
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.
|
39
|
-
spec.add_development_dependency "rake", "~>
|
39
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
40
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
40
41
|
end
|
data/doc/future.md
ADDED
@@ -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.
|
data/lib/cult/cli/common.rb
CHANGED
data/lib/cult/cli/console_cmd.rb
CHANGED
@@ -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:
|
89
|
+
Ripl.start(binding: context_binding)
|
93
90
|
|
94
91
|
elsif opts[:pry]
|
95
92
|
require 'pry'
|
96
|
-
|
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(
|
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
|
|
data/lib/cult/cli/node_cmd.rb
CHANGED
@@ -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
|
-
|
265
|
-
|
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
|
-
|
279
|
+
role.name
|
270
280
|
end
|
271
|
-
end.join('
|
281
|
+
end.join(' ')
|
272
282
|
|
273
|
-
|
274
|
-
|
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
|
-
|
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
|
-
|
357
|
-
|
358
|
-
|
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)
|
data/lib/cult/cli/role_cmd.rb
CHANGED
@@ -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.
|
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
|
-
|
136
|
-
|
137
|
-
|
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
|
data/lib/cult/commander.rb
CHANGED
@@ -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
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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!
|
data/lib/cult/named_array.rb
CHANGED
data/lib/cult/node.rb
CHANGED
@@ -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)
|
data/lib/cult/paramap.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/cult/project.rb
CHANGED
@@ -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
|
-
@
|
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
|
data/lib/cult/project_context.rb
CHANGED
@@ -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
|
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
|
data/lib/cult/role.rb
CHANGED
@@ -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
|
-
|
103
|
+
role_file ]
|
98
104
|
end
|
99
105
|
|
100
106
|
|
data/lib/cult/template.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
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
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
23
|
-
|
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
|
45
|
+
context.send(:_process, text, filename: filename)
|
36
46
|
end
|
37
47
|
|
38
48
|
end
|
data/lib/cult/transaction.rb
CHANGED
@@ -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.
|
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
|
data/lib/cult/version.rb
CHANGED
@@ -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
|
-
#
|
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
|
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.
|
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:
|
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.
|
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.
|
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: '
|
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: '
|
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:
|
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:
|
68
|
+
version: 2.2.2
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: erubi
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
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:
|
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.
|
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.
|
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: '
|
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: '
|
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/
|
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-
|
173
|
-
- skel/roles/base/tasks/sync-
|
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.
|
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:
|
212
|
+
version: '0'
|
199
213
|
requirements: []
|
200
214
|
rubyforge_project:
|
201
|
-
rubygems_version: 2.
|
215
|
+
rubygems_version: 2.6.12
|
202
216
|
signing_key:
|
203
217
|
specification_version: 4
|
204
218
|
summary: Fleet Management like its 1990
|
data/doc/welcome.txt
DELETED
@@ -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 %>
|