Salticid
Salticidae is the family of jumping spiders: small, nimble, and quite intelligent within a limited domain.
Salticid runs commands on other computers via SSH, with just enough programmability and structure to run small-scale (~10-20 nodes in parallel) deployments. It relies on horrifyingly evil ruby metaprogramming to provide a small DSL which looks a bit like a shell.
gem install salticid
# example.rb
host 'my_machine.com' do
user 'my_username'
task :hello do
exec! 'ls -la /', echo: true
end
end
salticid -l example.rb -h my_machine hello
Salticid will ssh to my_machine (you'll want your SSH agent to have your
credentials cached) and run the hello
task, which lists the root directory
and echoes it to the console. If you don't have a remote node available, you
could always use host 'localhost'
. Hit q to quit the ncurses interface when
you're satisfied.
We don't have to use fully qualified paths. Salticid hosts have a state machine which tracks the current directory:
host 'my_machine.com' do
user 'my_username'
task :hello do
cd '/'
cd 'tmp'
log "Now in directory", cwd
exec! 'ls -la /', echo: true
end
end
You can scroll using the arrow keys, or pgup/pgdn. If you don't like q, you can always leave via Control+C, too. Sometimes salticid doesn't shut down actively running processes properly: ^C will make sure they quit.
Some commands are built into lib/salticid/host.rb. Any ruby code you write inside a task is instance_eval'd inside a Host object, so you have access to any methods there. Any unrecognized methods will be intrepreted as shell commands (or possibly as roles or tasks; we'll get to that later). So we can replace exec! with:
host 'my_machine.com' do
user 'my_username'
task :hello do
ls '-la', '/', echo: true
end
end
Salticid will escape any strings you pass this way. cat 'foo bar'
in ruby
will turn into the shell command cat "foo bar"
. exec!
doesn't escape its
string: exec! 'cat foo bar'
turns into the shell command cat foo bar
. You
can always use escape("some string")
if you need to do your own escaping.
Salticid functions return whatever the process printed to stdout, so it's easy to get information from the remote system, massage it in Ruby, and use it in your program:
host 'my_machine.com' do
user 'my_username'
task :hello do
my_username = whoami
log "I am #{my_username.inspect}"
end
end
The bar at the top of the interface turns green when the task completes successfully, or red if it fails. Salticid checks the exit status of every command, and throws an exception if it's non-zero:
host 'my_machine.com' do
user 'my_username'
task :hello do
cat '/frobnitz'
end
end
Salticid logs the command which failed, the exit status, and the processes' stderr and stdout. Now you can use Ruby's exception handling techniques to write more complex tasks:
host 'my_machine.com' do
user 'my_username'
task :hello do
x = begin
cat '/frobnitz'
rescue
:purple
end
log "Got #{x}"
end
end
Salticid is asynchronous, and you can register handlers to process stderr and stdout interactively, or echo it to the interface as lines arrive:
task :tail do
tail '-F', '/var/log/syslog', echo: true
end
This one you have to kill with ^C.
You can redirect to a file with the :to
argument, and send stdin to a process using :stdin
:
echo :stdin 'hallo', to: '/tmp/salutations'
Become a different user for the scope of a block:
sudo do
shutdown '-h', :now
end
sudo :postgres do
createdb :omghai2u
end
Upload and download files (you probably want to check the source: these do some helpful but non-obvious things)
# Remote path, local path:
download '/etc/passwd', 'omghax.txt'
__DIR__
is the directory the current file is in, so it's easy to keep scripts
and their related data files together. /
combines path fragments. Pretty much
anywhere in Salticid, you can treat symbols and strings interchangeably.
echo File.read(__DIR__/:riak/'app.config'),
to: '/opt/riak/rel/riak/etc/app.config'
Often, you want to replace a file while preserving its ownership and mode:
sudo_upload __DIR__/'apache.conf', '/etc/apache2/apache.conf'
Higher-level structure
You can group related tasks together into a role, and invoke tasks from each other to bundle together dependencies:
role :riak do
task :start do
sudo do
cd '/opt/riak/rel/riak'
exec! 'bash -c "ulimit -n 10000 && bin/riak start"', echo: true
end
end
task :restart do
sudo do
cd '/opt/riak/rel/riak'
exec! 'bin/riak start', echo: true
end
end
task :stop do
sudo do
cd '/opt/riak/rel/riak'
exec! 'bin/riak stop', echo: true
end
end
task :ping do
sudo do
exec! '/opt/riak/rel/riak/bin/riak ping', echo: true
end
end
task :deploy do
sudo do
riak.stop
echo File.read(__DIR__/:riak/'app.config'), to: '/opt/riak/rel/riak/etc/app.config'
echo File.read(__DIR__/:riak/'vm.args').gsub('%%NODE%%', name), to: '/opt/riak/rel/riak/etc/vm.args'
end
riak.start
end
end
You can assign hosts to roles individually:
host 'my-host' do
role :riak
role :postgres
end
And hosts can be bound together into groups:
group :texas do
host :n1
host :n2
host :n3
host :n4
each_host do
user :deploy
# You can assign instance variables to hosts to keep track of state.
# @password is used by sudo and friends:
@password = "sup"
# We can add roles and tasks here too:
role :riak
role :postgres
end
end
Use salticid -s salticid
to show all the groups, roles, hosts, and top-level
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:
# Runs the deploy task in the riak role, on every host which has the riak role.
salticid riak.deploy
Salticid parallelizes across hosts. Use the left and right arrow keys, or tab, to switch between hosts in the ncurses interface.
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
(or .salticidrc in the current working directory); they'll be evaluated on the top-level salticid context:
load ENV['HOME'] + '/my-deploy/salticid/*.rb'
load
can be used to load files in any salticid context. It obeys shell glob
expansion.
An example
Take a look at Jepsen's, old salticid config: https://github.com/aphyr/jepsen/blob/old/salticid/main.rb
Caveats
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.
Salticid's ncurses interface is a little flaky/slow. It doesn't scroll line-wise very well.
Salticid is not a cloud deployment system, though you could dynamically create hosts to make it into one. As presented here, it's designed for fixed sets of nodes. We used it at Showyou and Vodpod to manage ~30 physical nodes in two datacenters.
Salticid has no central control of deployment. There is no server or locking. There is nothing to install. All you need is SSH. Anyone with credentials can use it, which makes it ideal for scenarios when you don't have control over the entire infrastructure but still need to automate some tasks. We had a small team and kept our config in a git repo, and added a ruby check to verify the repo was up to date before running any commands. All is anarchy.
This is not a config management tool. It has no notion of convergence or scheduled checks, and can't tell you when things are out of sync with a target. On the other hand, it runs at interactive latencies and tells you what went wrong immediately, so you may find keeping systems up to date is easier with Salticid. I strongly recommend writing idempotent tasks so you can just re-run them whenever you make a change or want to confirm everything is in order.
apt-get -y
is your friend. :)
Advanced
Salticid can tunnel its connections through an intermediate SSH gateway node. I like to give each user a distinct user account on the gateway and a special key for the gateway, and share a single deploy account/keypair for everything behind the gateway; makes it fast and easy to revoke a pubkey for a specific user if their laptop is stolen, without having to touch every machine.
gw 'gw.mycorp.com' do
user ENV['USER']
end
[1,2,3,4,5].each do |i|
host "db#{i}" do
gw 'gw.mycorp.com'
user :deploy
role :ubuntu
role :riak
end
end
You can break out of Salticid at any point, which is useful if you don't want the full interface:
task :console do
Salticid::Interface.shutdown false
if tunnel
tunnel.open(name, 22) do |port|
system "ssh #{user}@localhost -p #{port}"
end
else
system "ssh #{user}@#{name}"
end
end
Now salticid -h my.host.com console
will drop you into an SSH console on that
node.
History
Salticid is a deployment tool I wrote for Vodpod and Showyou in a couple of weekends. Its only design goals were:
- Magic
- More Magic
It violates every principle of good software development possible. It is clunky, slow, buggy, and dangerous.
It also worked surprisingly well. YMMV. d=('_~)=b