salticid 0.9.4 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.markdown +357 -0
  2. data/bin/salticid +100 -0
  3. data/lib/salticid/version.rb +1 -1
  4. metadata +5 -19
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  class Salticid
2
- VERSION = "0.9.4"
2
+ VERSION = "0.9.5"
3
3
  end
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
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-21 00:00:00.000000000 Z
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: