salticid 0.9.4 → 0.9.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +357 -0
- data/bin/salticid +100 -0
- data/lib/salticid/version.rb +1 -1
- metadata +5 -19
data/README.markdown
CHANGED
@@ -1,6 +1,363 @@
|
|
1
1
|
Salticid
|
2
2
|
=====
|
3
3
|
|
4
|
+
<a href="http://www.flickr.com/photos/robertwhyte/8034414631/" title="Salticidae Maratus sp. by Robert Whyte www.arachne.org.au, on Flickr"><img src="http://farm9.staticflickr.com/8034/8034414631_1d7906ac5d_b.jpg" width="850" height="674" alt="Salticidae Maratus sp."></a>
|
5
|
+
|
6
|
+
Salticidae is the family of jumping spiders: small, nimble, and quite
|
7
|
+
intelligent within a limited domain.
|
8
|
+
|
9
|
+
<img src="http://aphyr.com/media/salticid.png" width="100%" alt="Salticid installing riak" />
|
10
|
+
|
11
|
+
Salticid runs commands on other computers via SSH, with just enough
|
12
|
+
programmability and structure to run small-scale (~10-20 nodes in parallel)
|
13
|
+
deployments. It relies on horrifyingly evil ruby metaprogramming to provide a
|
14
|
+
small DSL which looks a bit like a shell.
|
15
|
+
|
16
|
+
```sh
|
17
|
+
gem install salticid
|
18
|
+
```
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
# example.rb
|
22
|
+
host 'my_machine.com' do
|
23
|
+
user 'my_username'
|
24
|
+
|
25
|
+
task :hello do
|
26
|
+
exec! 'ls -la /', echo: true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
```sh
|
32
|
+
salticid -l example.rb -h my_machine hello
|
33
|
+
```
|
34
|
+
|
35
|
+
Salticid will ssh to my_machine (you'll want your SSH agent to have your
|
36
|
+
credentials cached) and run the `hello` task, which lists the root directory
|
37
|
+
and echoes it to the console. If you don't have a remote node available, you
|
38
|
+
could always use `host 'localhost'`. Hit q to quit the ncurses interface when
|
39
|
+
you're satisfied.
|
40
|
+
|
41
|
+
We don't have to use fully qualified paths. Salticid hosts have a state machine which tracks the current directory:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
host 'my_machine.com' do
|
45
|
+
user 'my_username'
|
46
|
+
|
47
|
+
task :hello do
|
48
|
+
cd '/'
|
49
|
+
cd 'tmp'
|
50
|
+
log "Now in directory", cwd
|
51
|
+
exec! 'ls -la /', echo: true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
You can scroll using the arrow keys, or pgup/pgdn. If you don't like q, you can
|
57
|
+
always leave via Control+C, too. Sometimes salticid doesn't shut down actively
|
58
|
+
running processes properly: ^C will make sure they quit.
|
59
|
+
|
60
|
+
Some commands are built into lib/salticid/host.rb. Any ruby code you write
|
61
|
+
inside a task is instance_eval'd inside a Host object, so you have access to
|
62
|
+
any methods there. Any unrecognized methods will be intrepreted as shell
|
63
|
+
commands (or possibly as roles or tasks; we'll get to that later). So we can
|
64
|
+
replace exec! with:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
host 'my_machine.com' do
|
68
|
+
user 'my_username'
|
69
|
+
|
70
|
+
task :hello do
|
71
|
+
ls '-la', '/', echo: true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
Salticid will escape any strings you pass this way. `cat 'foo bar'` in ruby
|
77
|
+
will turn into the shell command `cat "foo bar"`. `exec!` doesn't escape its
|
78
|
+
string: `exec! 'cat foo bar'` turns into the shell command `cat foo bar`. You
|
79
|
+
can always use `escape("some string")` if you need to do your own escaping.
|
80
|
+
|
81
|
+
Salticid functions return whatever the process printed to stdout, so it's easy
|
82
|
+
to get information from the remote system, massage it in Ruby, and use it in
|
83
|
+
your program:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
host 'my_machine.com' do
|
87
|
+
user 'my_username'
|
88
|
+
|
89
|
+
task :hello do
|
90
|
+
my_username = whoami
|
91
|
+
log "I am #{my_username.inspect}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
The bar at the top of the interface turns green when the task completes
|
97
|
+
successfully, or red if it fails. Salticid checks the exit status of every
|
98
|
+
command, and throws an exception if it's non-zero:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
host 'my_machine.com' do
|
102
|
+
user 'my_username'
|
103
|
+
|
104
|
+
task :hello do
|
105
|
+
cat '/frobnitz'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
Salticid logs the command which failed, the exit status, and the processes'
|
111
|
+
stderr and stdout. Now you can use Ruby's exception handling techniques to
|
112
|
+
write more complex tasks:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
host 'my_machine.com' do
|
116
|
+
user 'my_username'
|
117
|
+
|
118
|
+
task :hello do
|
119
|
+
x = begin
|
120
|
+
cat '/frobnitz'
|
121
|
+
rescue
|
122
|
+
:purple
|
123
|
+
end
|
124
|
+
log "Got #{x}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
Salticid is asynchronous, and you can register handlers to process stderr and
|
130
|
+
stdout interactively, or echo it to the interface as lines arrive:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
task :tail do
|
134
|
+
tail '-F', '/var/log/syslog', echo: true
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
This one you have to kill with ^C.
|
139
|
+
|
140
|
+
You can redirect to a file with the `:to` argument, and send stdin to a process using `:stdin`:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
echo :stdin 'hallo', to: '/tmp/salutations'
|
144
|
+
```
|
145
|
+
|
146
|
+
Become a different user for the scope of a block:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
sudo do
|
150
|
+
shutdown '-h', :now
|
151
|
+
end
|
152
|
+
|
153
|
+
sudo :postgres do
|
154
|
+
createdb :omghai2u
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
Upload and download files (you probably want to check the source: these do some
|
159
|
+
helpful but non-obvious things)
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
# Remote path, local path:
|
163
|
+
download '/etc/passwd', 'omghax.txt'
|
164
|
+
```
|
165
|
+
|
166
|
+
`__DIR__` is the directory the current file is in, so it's easy to keep scripts
|
167
|
+
and their related data files together. `/` combines path fragments. Pretty much
|
168
|
+
anywhere in Salticid, you can treat symbols and strings interchangeably.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
echo File.read(__DIR__/:riak/'app.config'),
|
172
|
+
to: '/opt/riak/rel/riak/etc/app.config'
|
173
|
+
```
|
174
|
+
|
175
|
+
Often, you want to replace a file while preserving its ownership and mode:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
sudo_upload __DIR__/'apache.conf', '/etc/apache2/apache.conf'
|
179
|
+
```
|
180
|
+
|
181
|
+
Higher-level structure
|
182
|
+
---
|
183
|
+
|
184
|
+
You can group related tasks together into a role, and invoke tasks from each
|
185
|
+
other to bundle together dependencies:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
role :riak do
|
189
|
+
task :start do
|
190
|
+
sudo do
|
191
|
+
cd '/opt/riak/rel/riak'
|
192
|
+
exec! 'bash -c "ulimit -n 10000 && bin/riak start"', echo: true
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
task :restart do
|
197
|
+
sudo do
|
198
|
+
cd '/opt/riak/rel/riak'
|
199
|
+
exec! 'bin/riak start', echo: true
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
task :stop do
|
204
|
+
sudo do
|
205
|
+
cd '/opt/riak/rel/riak'
|
206
|
+
exec! 'bin/riak stop', echo: true
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
task :ping do
|
211
|
+
sudo do
|
212
|
+
exec! '/opt/riak/rel/riak/bin/riak ping', echo: true
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
task :deploy do
|
217
|
+
sudo do
|
218
|
+
riak.stop
|
219
|
+
echo File.read(__DIR__/:riak/'app.config'), to: '/opt/riak/rel/riak/etc/app.config'
|
220
|
+
echo File.read(__DIR__/:riak/'vm.args').gsub('%%NODE%%', name), to: '/opt/riak/rel/riak/etc/vm.args'
|
221
|
+
end
|
222
|
+
riak.start
|
223
|
+
end
|
224
|
+
end
|
225
|
+
```
|
226
|
+
|
227
|
+
You can assign hosts to roles individually:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
host 'my-host' do
|
231
|
+
role :riak
|
232
|
+
role :postgres
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
And hosts can be bound together into groups:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
group :texas do
|
240
|
+
host :n1
|
241
|
+
host :n2
|
242
|
+
host :n3
|
243
|
+
host :n4
|
244
|
+
|
245
|
+
each_host do
|
246
|
+
user :deploy
|
247
|
+
|
248
|
+
# You can assign instance variables to hosts to keep track of state.
|
249
|
+
# @password is used by sudo and friends:
|
250
|
+
@password = "sup"
|
251
|
+
|
252
|
+
# We can add roles and tasks here too:
|
253
|
+
role :riak
|
254
|
+
role :postgres
|
255
|
+
end
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
259
|
+
Use `salticid -s salticid` to show all the groups, roles, hosts, and top-level
|
260
|
+
tasks defined. You can run a task only on hosts belonging to a role, group, or a specific host with -r, -g, and -h respectively; or on all appropriate hosts by default:
|
261
|
+
|
262
|
+
```sh
|
263
|
+
# Runs the deploy task in the riak role, on every host which has the riak role.
|
264
|
+
salticid riak.deploy
|
265
|
+
```
|
266
|
+
|
267
|
+
Salticid parallelizes across hosts. Use the left and right arrow keys, or tab,
|
268
|
+
to switch between hosts in the ncurses interface.
|
269
|
+
|
270
|
+
Typically you'll use salticid with a common set of files over and over again, instead of loading files explicitly with `-l`. You can put any commands to source in `~/.salticidrc`; they'll be evaluated on the top-level salticid context:
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
load ENV['HOME'] + '/my-deploy/salticid/*.rb'
|
274
|
+
```
|
275
|
+
|
276
|
+
`load` can be used to load files in any salticid context. It obeys shell glob
|
277
|
+
expansion.
|
278
|
+
|
279
|
+
An example
|
280
|
+
---
|
281
|
+
|
282
|
+
Take a look at Jepsen's salticid config:
|
283
|
+
https://github.com/aphyr/jepsen/blob/master/salticid/main.rb
|
284
|
+
|
285
|
+
Caveats
|
286
|
+
---
|
287
|
+
|
288
|
+
Salticid starts to get unresponsive or flaky above 10 or 20 nodes in parallel. There's no reason the interface and scheduler can't execute tasks in small chunks instead of all at once; I just never needed the feature so I didn't write it.
|
289
|
+
|
290
|
+
Salticid's ncurses interface is a little flaky/slow. It doesn't scroll
|
291
|
+
line-wise very well.
|
292
|
+
|
293
|
+
Salticid is *not* a cloud deployment system, though you could dynamically
|
294
|
+
create hosts to make it into one. As presented here, it's designed for fixed
|
295
|
+
sets of nodes. We used it at Showyou and Vodpod to manage ~30 physical nodes in
|
296
|
+
two datacenters.
|
297
|
+
|
298
|
+
Salticid has no central control of deployment. There is no server or locking.
|
299
|
+
There is nothing to install. All you need is SSH. Anyone with credentials can
|
300
|
+
use it, which makes it ideal for scenarios when you don't have control over the
|
301
|
+
entire infrastructure but still need to automate some tasks. We had a small
|
302
|
+
team and kept our config in a git repo, and added a ruby check to verify the
|
303
|
+
repo was up to date before running any commands. All is anarchy.
|
304
|
+
|
305
|
+
This is not a config management tool. It has no notion of convergence or
|
306
|
+
scheduled checks, and can't tell you when things are out of sync with a target.
|
307
|
+
On the other hand, it runs at interactive latencies and tells you what went
|
308
|
+
wrong immediately, so you may find keeping systems up to date is easier with
|
309
|
+
Salticid. I strongly recommend writing idempotent tasks so you can just re-run
|
310
|
+
them whenever you make a change or want to confirm everything is in order.
|
311
|
+
|
312
|
+
`apt-get -y` is your friend. :)
|
313
|
+
|
314
|
+
Advanced
|
315
|
+
---
|
316
|
+
|
317
|
+
Salticid can tunnel its connections through an intermediate SSH gateway node. I
|
318
|
+
like to give each user a distinct user account on the gateway and a special key
|
319
|
+
for the gateway, and share a single deploy account/keypair for everything
|
320
|
+
*behind* the gateway; makes it fast and easy to revoke a pubkey for a specific
|
321
|
+
user if their laptop is stolen, without having to touch every machine.
|
322
|
+
|
323
|
+
```
|
324
|
+
gw 'gw.mycorp.com' do
|
325
|
+
user ENV['USER']
|
326
|
+
end
|
327
|
+
|
328
|
+
[1,2,3,4,5].each do |i|
|
329
|
+
host "db#{i}" do
|
330
|
+
gw 'gw.mycorp.com'
|
331
|
+
user :deploy
|
332
|
+
role :ubuntu
|
333
|
+
role :riak
|
334
|
+
end
|
335
|
+
end
|
336
|
+
```
|
337
|
+
|
338
|
+
You can break out of Salticid at any point, which is useful if you don't want
|
339
|
+
the full interface:
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
task :console do
|
343
|
+
Hydra::Interface.shutdown false
|
344
|
+
|
345
|
+
if tunnel
|
346
|
+
tunnel.open(name, 22) do |port|
|
347
|
+
system "ssh #{user}@localhost -p #{port}"
|
348
|
+
end
|
349
|
+
else
|
350
|
+
system "ssh #{user}@#{name}"
|
351
|
+
end
|
352
|
+
end
|
353
|
+
```
|
354
|
+
|
355
|
+
Now `hydra -h my.host.com console` will drop you into an SSH console on that
|
356
|
+
node.
|
357
|
+
|
358
|
+
History
|
359
|
+
---
|
360
|
+
|
4
361
|
Salticid is a deployment tool I wrote for Vodpod and Showyou in a couple of weekends. Its only design goals were:
|
5
362
|
|
6
363
|
1. Magic
|
data/bin/salticid
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'trollop'
|
5
|
+
require "#{File.dirname(__FILE__)}/../lib/salticid"
|
6
|
+
require 'salticid/interface'
|
7
|
+
|
8
|
+
opts = Trollop::options do
|
9
|
+
opt :exec, 'A command line to execute.', :type => :string
|
10
|
+
opt :group, 'One or more groups to run task on', :type => :string, :multi => true
|
11
|
+
opt :host, 'One or more hosts to run task on', :type => :string, :multi => true
|
12
|
+
opt :load, 'Files to load', :default => [File.join(ENV["HOME"], '.salticidrc')], :multi => true
|
13
|
+
opt :role, 'One or more roles. Every host in the role will have the specified task run.', :type => :string, :multi => true
|
14
|
+
opt :show, 'Show roles, tasks, hosts, and groups. Try --show salticid or --show <host>', :type => :string
|
15
|
+
end
|
16
|
+
|
17
|
+
task = ARGV.first
|
18
|
+
|
19
|
+
STDOUT.sync = true
|
20
|
+
@h = Salticid.new
|
21
|
+
|
22
|
+
# Load files
|
23
|
+
opts[:load].each do |file|
|
24
|
+
@h.load file
|
25
|
+
end
|
26
|
+
|
27
|
+
# Map args to objects
|
28
|
+
opts[:groups] = opts[:group].map { |g| @h.group g }
|
29
|
+
opts[:roles] = opts[:role].map { |r| @h.role r }
|
30
|
+
opts[:hosts] = opts[:host].map { |h| @h.host h }
|
31
|
+
|
32
|
+
# Get hosts
|
33
|
+
hosts = opts[:groups].map { |g| g.hosts }
|
34
|
+
hosts << opts[:roles].map { |r| r.hosts }
|
35
|
+
hosts << opts[:hosts]
|
36
|
+
hosts.flatten!.uniq!
|
37
|
+
|
38
|
+
# If no hosts were given, we may infer them from the task.
|
39
|
+
if hosts.empty?
|
40
|
+
hosts |= @h.hosts_for(task) rescue []
|
41
|
+
end
|
42
|
+
|
43
|
+
# Show info
|
44
|
+
if opts[:show]
|
45
|
+
case opts[:show]
|
46
|
+
when 'salticid'
|
47
|
+
puts @h.to_string
|
48
|
+
else
|
49
|
+
begin
|
50
|
+
o = @h.send(opts[:show].to_sym)
|
51
|
+
rescue NoMethodError
|
52
|
+
puts "No such object: #{opts[:show]}"
|
53
|
+
end
|
54
|
+
|
55
|
+
if o.respond_to? :to_string
|
56
|
+
puts o.to_string
|
57
|
+
elsif o
|
58
|
+
p o
|
59
|
+
end
|
60
|
+
end
|
61
|
+
exit 0
|
62
|
+
end
|
63
|
+
|
64
|
+
Trollop::die 'Nothing to do' if task.nil? and opts[:exec].nil?
|
65
|
+
|
66
|
+
# Run commands on the hosts
|
67
|
+
Thread.abort_on_exception = true
|
68
|
+
interface = Salticid::Interface.new @h
|
69
|
+
begin
|
70
|
+
interface.main
|
71
|
+
threads = hosts.map do |host|
|
72
|
+
interface.add_tab host
|
73
|
+
Thread.new do
|
74
|
+
begin
|
75
|
+
host.log "Starting..."
|
76
|
+
# Run explicit execs
|
77
|
+
if opts[:exec]
|
78
|
+
host.exec! opts[:exec], :echo => true
|
79
|
+
end
|
80
|
+
|
81
|
+
# Run task
|
82
|
+
host.instance_eval(task, "command line") if task
|
83
|
+
|
84
|
+
host.log :finished, "Finished."
|
85
|
+
rescue => e
|
86
|
+
host.log :error, e.to_s
|
87
|
+
host.log :error, e.backtrace.join("\n")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Run the interface loop.
|
93
|
+
interface.join
|
94
|
+
|
95
|
+
# When the interface exits, wait for each thread to complete work.
|
96
|
+
threads.each { |t| t.join rescue nil }
|
97
|
+
rescue => e
|
98
|
+
interface.shutdown
|
99
|
+
raise e
|
100
|
+
end
|
data/lib/salticid/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: salticid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-04-
|
12
|
+
date: 2013-04-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: trollop
|
@@ -91,25 +91,10 @@ dependencies:
|
|
91
91
|
- - ! '>='
|
92
92
|
- !ruby/object:Gem::Version
|
93
93
|
version: '0'
|
94
|
-
- !ruby/object:Gem::Dependency
|
95
|
-
name: ncurses
|
96
|
-
requirement: !ruby/object:Gem::Requirement
|
97
|
-
none: false
|
98
|
-
requirements:
|
99
|
-
- - ~>
|
100
|
-
- !ruby/object:Gem::Version
|
101
|
-
version: 0.9.1
|
102
|
-
type: :runtime
|
103
|
-
prerelease: false
|
104
|
-
version_requirements: !ruby/object:Gem::Requirement
|
105
|
-
none: false
|
106
|
-
requirements:
|
107
|
-
- - ~>
|
108
|
-
- !ruby/object:Gem::Version
|
109
|
-
version: 0.9.1
|
110
94
|
description:
|
111
95
|
email: aphyr@aphyr.com
|
112
|
-
executables:
|
96
|
+
executables:
|
97
|
+
- salticid
|
113
98
|
extensions: []
|
114
99
|
extra_rdoc_files: []
|
115
100
|
files:
|
@@ -140,6 +125,7 @@ files:
|
|
140
125
|
- lib/snippets/symbol/to_proc.rb
|
141
126
|
- LICENSE
|
142
127
|
- README.markdown
|
128
|
+
- bin/salticid
|
143
129
|
homepage: https://github.com/aphyr/salticid
|
144
130
|
licenses: []
|
145
131
|
post_install_message:
|