Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A toolkit for deploying code and assets to servers in a repeatable, testable, reliable way.

License

NotificationsYou must be signed in to change notification settings

capistrano/sshkit

Repository files navigation

SSHKit Logo

SSHKit is a toolkit for running commands in a structured way on one ormore servers.

Gem VersionBuild Status

Example

  • Connect to 2 servers
  • Execute commands asdeploy user withRAILS_ENV=production
  • Execute commands in serial (default is:parallel)
require'sshkit'require'sshkit/dsl'includeSSHKit::DSLon["1.example.com","2.example.com"],in::sequencedo |host|puts"Now executing on#{host}"within"/opt/sites/example.com"doas:deploydowithRAILS_ENV:'production'doexecute:rake,"assets:precompile"execute:rails,"runner","S3::Sync.notify"endendendend

Many other examples are inEXAMPLES.md.

Basic usage

Theon() method is used to specify the backends on which you'd like to run the commands.You can pass one or more hosts as parameters; this runs commands via SSH. Alternatively you canpass:local to run commands locally. By default SSKit will run the commands on all hosts inparallel.

Running commands

All backends support theexecute(*args),test(*args) &capture(*args) methodsfor executing a command. You can call any of these methods in the context of anon()block.

Note: In SSHKit, the first parameter of theexecute /test /capture methodshas a special significance. If the first parameter isn't a Symbol,SSHKit assumes that you want to execute the raw command and theas /within /with methods,SSHKit.config.umask andthe comand maphave no effect.

Typically, you would pass a Symbol for the command name and it's args as follows:

on'1.example.com'doiftest("[ -f somefile.txt ]")execute(:cp,'somefile.txt','somewhere_else.txt')endls_output=capture(:ls,'-l')end

By default thecapture methods strips whitespace. If you need to preserve whitespaceyou can pass thestrip: false option:capture(:ls, '-l', strip: false)

Transferring files

All backends also support theupload! anddownload! methods for transferring files.For the remote backend, the file is transferred with scp by default, but sftp is alsosupported. SeeEXAMPLES.md for details.

on'1.example.com'doupload!'some_local_file.txt','/home/some_user/somewhere'download!'/home/some_user/some_remote_file.txt','somewhere_local',log_percent:25end

Users, working directories, environment variables and umask

When running commands, you can tell SSHKit to set up the context for thosecommands using the following methods:

as(user:'un',group:'grp'){execute('cmd')}# Executes sudo -u un -- sh -c 'sg grp cmd'within('/somedir'){execute('cmd')}# Executes cd /somedir && cmdwith(env_var:'value'){execute('cmd')}# Executes ENV_VAR=value cmdSSHKit.config.umask='077'# All commands are executed with umask 077 && cmd

Theas() /within() /with() are nestable in any order, repeatable, and stackable.

When used inside a block in this way,as() andwithin() will guardthe block they are given with a check.

In the case ofwithin(), an error-raising check will be made that the directoryexists; foras() a simple call tosudo -u <user> -- sh -c <command>' wrapped in a check forsuccess, raising an error if unsuccessful.

The directory check is implemented like this:

if test ! -d <directory>; then echo "Directory doesn't exist" 2>&1; false; fi

And the user switching test is implemented like this:

if ! sudo -u <user> whoami > /dev/null; then echo "Can't switch user" 2>&1; false; fi

According to the defaults, any command that exits with a status other than 0raises an error (this can be changed). The body of the message is whatever waswritten tostdout by the process. The1>&2 redirects the standard outputof echo to the standard error channel, so that it's available as the body ofthe raised error.

Helpers such asrunner() andrake() which expand toexecute(:rails, "runner", ...) andexecute(:rake, ...) are convenience helpers for Ruby, and Rails based apps.

Verbosity / Silence

  • raise verbosity of a command:execute "echo DEAD", verbosity: :ERROR
  • hide a command from output:execute "echo HIDDEN", verbosity: :DEBUG

Parallel

Notice on theon() call thein: :sequence option, the following will dowhat you might expect:

on(in::parallel){ ...}on(in::sequence,wait:5){ ...}on(in::groups,limit:2,wait:5){ ...}

The default is to runin: :parallel which has no limit. If you have 400 servers,this might be a problem and you might better look at changing that to run ingroups, orsequence.

Groups were designed in this case to relieve problems (mass Git checkouts)where you rely on a contested resource that you don't want to DDOS by hittingit too hard.

Sequential runs were intended to be used for rolling restarts, amongst othersimilar use-cases.

The default runner can be set with theSSHKit.config.default_runner option. Forexample:

SSHKit.config.default_runner=:parallelSSHKit.config.default_runner=:sequenceSSHKit.config.default_runner=:groupsSSHKit.config.default_runner=MyRunner# A custom runner

If more control over the default runner is needed, theSSHKit.config.default_runner_configcan be set.

# Set the runner and then the config for the runnerSSHKit.config.default_runner=:sequenceSSHKit.config.default_runner_config={wait:5}# Or just set everything onceSSHKit.config.default_runner_config={in::sequence,wait:5}

Synchronisation

Theon() block is the unit of synchronisation, oneon() block will waitfor all servers to complete before it returns.

For example:

all_servers=%w{one.example.comtwo.example.comthree.example.com}site_dir='/opt/sites/example.com'# Let's simulate a backup task, assuming that some servers take longer# then others to completeonall_serversdo |host|withinsite_dirdoexecute:tar,'-czf',"backup-#{host.hostname}.tar.gz",'current'# Will run: "/usr/bin/env tar -czf backup-one.example.com.tar.gz current"endend# Now we can do something with those backups, safe in the knowledge that# they will all exist (all tar commands exited with a success status, or# that we will have raised an exception if one of them failed.onall_serversdo |host|withinsite_dirdobackup_filename="backup-#{host.hostname}.tar.gz"target_filename="backups/#{Time.now.utc.iso8601}/#{host.hostname}.tar.gz"putscapture(:s3cmd,'put',backup_filename,target_filename)endend

The Command Map

It's often a problem that programmatic SSH sessions don't have the same environmentvariables as interactive sessions.

A problem often arises when calling out to executables expected to be onthe$PATH. Under conditions without dotfiles or other environmentalconfiguration,$PATH may not be set as expected, and thus executables are not found where expected.

To try and solve this there is thewith() helper which takes a hash of variables and makes themavailable to the environment.

withpath:'/usr/local/bin/rbenv/shims:$PATH'doexecute:ruby,'--version'end

Will execute:

( PATH=/usr/local/bin/rbenv/shims:$PATH /usr/bin/env ruby --version )

By contrast, the following won't modify the command at all:

withpath:'/usr/local/bin/rbenv/shims:$PATH'doexecute'ruby --version'end

Will execute, without mapping the environmental variables, or querying the command map:

ruby --version

(This behaviour is sometimes considered confusing, but it has mostly to do with shell escaping: in the case of whitespace in your command, or newlines, we have no way of reliably composing a correct shell command from the input given.)

Often more preferable is to use thecommand map.

Thecommand map is used by default when instantiating aCommand object

Thecommand map exists on the configuration object, and in principle isquite simple, it's aHash structure with a default key factory blockspecified, for example:

putsSSHKit.config.command_map[:ruby]# => /usr/bin/env ruby

To make clear the environment is being deferred to, the/usr/bin/env prefix is applied to all commands.Although this is what happens anyway when one would simply attempt to executeruby, making itexplicit hopefully leads people to explore the documentation.

One can override the hash map for individual commands:

SSHKit.config.command_map[:rake]="/usr/local/rbenv/shims/rake"putsSSHKit.config.command_map[:rake]# => /usr/local/rbenv/shims/rake

Another opportunity is to add command prefixes:

SSHKit.config.command_map.prefix[:rake].push("bundle exec")putsSSHKit.config.command_map[:rake]# => bundle exec rakeSSHKit.config.command_map.prefix[:rake].unshift("/usr/local/rbenv/bin exec")putsSSHKit.config.command_map[:rake]# => /usr/local/rbenv/bin exec bundle exec rake

One can also override the command map completely, this may not be wise, but itwould be possible, for example:

SSHKit.config.command_map=Hash.newdo |hash,command|hash[command]="/usr/local/rbenv/shims/#{command}"end

This would effectively make it impossible to call any commands which didn'tprovide an executable in that directory, but in some cases that might bedesirable.

Note: All keys should be symbolised, as theCommand object will symbolize it'sfirst argument before attempting to find it in thecommand map.

Interactive commands

(Added in version 1.8.0)

By default, commands against remote servers are run in anon-login, non-interactive ssh session.This is by design, to try and isolate the environment and make sure that things work as expected,regardless of any changes that might happen on the server side. This means that,although the server may have prompted you, and be waiting for it,you cannot send data to the server by typing into your terminal window.Wherever possible, you should call commands in a way that doesn't require interaction(eg by specifying all options as command arguments).

However in some cases, you may want to programmatically drive interaction with a commandand this can be achieved by specifying an:interaction_handler option when youexecute,capture ortest a command.

It is not necessary, or desirable to enableNetssh.config.pty to use theinteraction_handler option.Only enableNetssh.config.pty if the command you are calling won't work without a pty.

Aninteraction_handler is an object which responds toon_data(command, stream_name, data, channel).Theinteraction_handler'son_data method will be called each timestdout orstderr data is available fromthe server. Data can be sent back to the server using thechannel parameter. This allows scripting of commandinteraction by responding tostdout orstderr lines with any input required.

For example, an interaction handler to change the password of your linux user using thepasswd command could look like this:

classPasswdInteractionHandlerdefon_data(command,stream_name,data,channel)putsdatacasedatawhen'(current) UNIX password: 'channel.send_data("old_pw\n")when'Enter new UNIX password: ','Retype new UNIX password: 'channel.send_data("new_pw\n")when'passwd: password updated successfully'elseraise"Unexpected stderr#{stderr}"endendend# ...execute(:passwd,interaction_handler:PasswdInteractionHandler.new)

Using theSSHKit::MappingInteractionHandler

Often, you want to map directly from a short output string returned by the server (either stdout or stderr)to a corresponding input string (as in the case above). For this case you can specifytheinteraction_handler option as a hash. This is used to create aSSHKit::MappingInteractionHandler whichprovides similar functionality to the linuxexpect library:

execute(:passwd,interaction_handler:{'(current) UNIX password: '=>"old_pw\n",/(Enter|Retype) new UNIX password: /=>"new_pw\n"})

Note: the key to the hash keys are matched against the server outputdata using the case equals=== method.This means that regexes and any objects which define=== can be used as hash keys.

Hash keys are matched in order, which allows for default wildcard matches:

execute(:my_command,interaction_handler:{"some specific line\n"=>"specific input\n",/.*/=>"default input\n"})

You can also pass a Proc object to map the output line from the server:

execute(:passwd,interaction_handler:lambda{ |server_data|caseserver_datawhen'(current) UNIX password: '"old_pw\n"when/(Enter|Retype) new UNIX password: /"new_pw\n"end})

MappingInteractionHandlers are stateless, so you can assign one to a constant and reuse it:

ENTER_PASSWORD=SSHKit::MappingInteractionHandler.new("Please Enter Password\n"=>"some_password\n")execute(:first_command,interaction_handler:ENTER_PASSWORD)execute(:second_command,interaction_handler:ENTER_PASSWORD)

Exploratory logging

By default, theMappingInteractionHandler does not log, in case the server output or input contains sensitiveinformation. However, if you pass a secondlog_level parameter to the constructor, theMappingInteractionHandlerwill log information about what output is being returned by the server, and what input is being sentin response. This can be helpful if you don't know exactly what the server is sending back (whitespace, newlines etc).

# Start with this and run your scriptexecute(:unfamiliar_command,interaction_handler:MappingInteractionHandler.new({},:info))# INFO log => Unable to find interaction handler mapping for stdout:#             "Please type your input:\r\n" so no response was sent"# Add missing mapping:execute(:unfamiliar_command,interaction_handler:MappingInteractionHandler.new({"Please type your input:\r\n"=>"Some input\n"},:info))

Thedata parameter

Thedata parameter ofon_data(command, stream_name, data, channel) is a string containing the latest datadelivered from the backend.

When using theNetssh backend for commands where a small amount of data is returned (eg prompting for sudo passwords),on_data will normally be called once per line anddata will be terminated by a newline. For commands withlarger amounts of output,data is delivered as it arrives from the underlying network stack, which depends onnetwork conditions, buffer sizes, etc. In this case, you may need to implement a more complexinteraction_handlerto concatenatedata from multiple calls toon_data before matching the required output.

When using theLocal backend,on_data is always called once per line.

Thechannel parameter

When using theNetssh backend, thechannel parameter ofon_data(command, stream_name, data, channel) is aNet::SSH Channel.When using theLocal backend, it is aruby IO object.If you need to support both sorts of backends with the same interaction handler,you need to call methods on the appropriate API depending on the channel type.One approach is to detect the presence of the API methods you need -egchannel.respond_to?(:send_data) # Net::SSH channel andchannel.respond_to?(:write) # IO.See theSSHKit::MappingInteractionHandler for an example of this.

Output Handling

Example Output

By default, the output format is set to:pretty:

SSHKit.config.use_format:pretty

However, if you prefer non colored text you can use the:simpletext formatter. If you want minimal output,there is also a:dot formatter which will simply output red or green dots based on the success or failure of commands.There is also a:blackhole formatter which does not output anything.

By default, formatters log to$stdout, but they can be constructed with any object which implements<<for example anyIO subclass,String,Logger etc:

# Output to a String:output=String.newSSHKit.config.output=SSHKit::Formatter::Pretty.new(output)# Do something with output# Or output to a file:SSHKit.config.output=SSHKit::Formatter::SimpleText.new(File.open('log/deploy.log','wb'))

Output & Log Redaction

If necessary,redact can be used on a section of yourexecute arguments to hide it from both STDOUT and the capistrano.log. It supports the majority of data types.

# Example from capistrano-postgresql gemexecute(:psql,fetch(:pg_system_db),'-c',%Q{"CREATE USER\\"#{fetch(:pg_username)}\\" PASSWORD},redact("'#{fetch(:pg_password)}'"),%Q{;"})

Once wrapped, sshkit logging will replace the actual pg_password with a [REDACTED] value. The created database user will have the value fromfetch(:pg_password).

# STDOUT00:00 postgresql:create_database_user      01 sudo -i -u postgres psql -d postgres -c "CREATE USER \"db_admin_user\" PASSWORD [REDACTED] ;"      01 CREATE ROLE    ✔ 01 user@localhost 0.099s# capistrano.logINFO [59dbd2ba] Running /usr/bin/env sudo -i -u postgres psql -d postgres -c "CREATE USER \"db_admin_user\" PASSWORD [REDACTED] ;" as user@localhostDEBUG [59dbd2ba] Command: ( export PATH="$HOME/.gem/ruby/2.5.0/bin:$PATH" ; /usr/bin/env sudo -i -u postgres psql -d postgres -c "CREATE USER \"db_admin_user\" PASSWORD [REDACTED] ;" )DEBUG [529b623c] CREATE ROLE

Certain commands will require that no spaces exist between a string and what you want hidden. Because SSHKIT will include a whitespace between each argument ofexecute, this can be dealt with by wrapping both in redact:

# lib/capistrano/tasks/systemd.rakeexecute:sudo,:echo,redact("CONTENT_WEB_TOOLS_PASS='#{ENV['CONTENT_WEB_TOOLS_PASS']}'"),">> /etc/systemd/system/#{fetch(:application)}_sidekiq.service.d/EnvironmentFile",'"'

Output Colors

By default, SSHKit will color the output using ANSI color escape sequencesif the output you are using is associated with a terminal device (tty).This means that you should see colors if you are writing output to the terminal (the default),but you shouldn't see ANSI color escape sequences if you are writing to a file.

Colors are supported for thePretty andDot formatters, but for historical reasonstheSimpleText formatter never shows colors.

If you want to force SSHKit to show colors, you can set theSSHKIT_COLOR environment variable:

ENV['SSHKIT_COLOR']='TRUE'

Custom formatters

Want custom output formatting? Here's what you have to do:

  1. Write a new formatter class in theSSHKit::Formatter module. Your class should subclassSSHKit::Formatter::Abstract to inherit conveniences and common behavior. For a basic an example, check out thePretty formatter.
  2. Set the output format as described above. E.g. if your new formatter is calledFooBar:
SSHKit.config.use_format:foobar

All formatters that extend fromSSHKit::Formatter::Abstract accept an options Hash as a constructor argument. You can pass options to your formatter like this:

SSHKit.config.use_format:foobar,:my_option=>"value"

You can then access these options using theoptions accessor within your formatter code.

For a much more full-featured formatter example that makes use of options, check out theAirbrussh repository.

Output Verbosity

By default calls tocapture() andtest() are not logged, they are usedso frequently by backend tasks to check environmental settings that itproduces a large amount of noise. They are tagged with a verbosity option ontheCommand instances ofLogger::DEBUG. The default configuration foroutput verbosity is available to override withSSHKit.config.output_verbosity=,and defaults toLogger::INFO.Another way to is to provide a hash containing{verbosity: Logger::INFO} asa last parameter for the method call.

At present theLogger::WARN,ERROR andFATAL are not used.

Deprecation warnings

Deprecation warnings are logged directly tostderr by default. This behaviourcan be changed by setting theSSHKit.config.deprecation_output option:

# Disable deprecation warningsSSHKit.config.deprecation_output=nil# Log deprecation warnings to a fileSSHKit.config.deprecation_output=File.open('log/deprecation_warnings.log','wb')

Connection Pooling

SSHKit uses a simple connection pool (enabled by default) to reduce thecost of negotiating a new SSH connection for everyon() block. Depending onusage and network conditions, this can add up to a significant time savings.In one test, a basiccap deploy ran 15-20 seconds faster thanks to theconnection pooling added in recent versions of SSHKit.

To prevent connections from "going stale", an existing pooled connection willbe replaced with a new connection if it hasn't been used for more than 30seconds. This timeout can be changed as follows:

SSHKit::Backend::Netssh.pool.idle_timeout=60# seconds

If you suspect the connection pooling is causing problems, you can disable thepooling behaviour entirely by setting the idle_timeout to zero:

SSHKit::Backend::Netssh.pool.idle_timeout=0# disabled

Tunneling and other related SSH themes

In order to do special gymnastics with SSH, tunneling, aliasing, complex options, etc with SSHKit it is possible to usethe underlying Net::SSH API however in many cases it is preferred to use the system SSH configuration file at~/.ssh/config. This allows you to have personal configuration tied to your machine that does not have to be committed with the repository. If this is not suitable (everyone on the team needs a proxy command, or some special aliasing) a file in the same format can be placed in the project directory at~/yourproject/.ssh/config, this will be merged with the system settings in~/.ssh/config, and with any configuration specified inSSHKit::Backend::Netssh.config.ssh_options.

These system level files are the preferred way of setting up tunneling and proxies because the system implementations of these things are faster and better than the Ruby implementations you would get if you were to configure them through Net::SSH. In cases where it's not possible (Windows?), it should be possible to make use of the Net::SSH APIs to setup tunnels and proxy commands before deferring control to Capistrano/SSHKit..

Proxying

To connect to the target host via a jump/bastion host, use aNet::SSH::Proxy::Jump

host=SSHKit::Host.new(hostname:'target.host.com',ssh_options:{proxy:Net::SSH::Proxy::Jump.new("proxy.bar.com")})on[host]doexecute:echo,'1'end

SSHKit Related Blog Posts

Embedded Capistrano with SSHKit

About

A toolkit for deploying code and assets to servers in a repeatable, testable, reliable way.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp