lib/engineyard/serverside_runner.rb
require 'escape'
require 'net/ssh'
require 'engineyard-serverside-adapter'
module EY
class ServersideRunner
def initialize(options)
@verbose = options[:verbose] || !!ENV['DEBUG']
@hostname = options[:bridge]
env = options[:environment]
@adapter = load_adapter(@hostname, options[:app], env, @verbose, options[:serverside_version])
@username = env.username
@hierarchy_name = env.hierarchy_name
@command = nil
end
def deploy(&block)
@command = @adapter.deploy(&block)
self
end
def rollback(&block)
@command = @adapter.rollback(&block)
self
end
def restart(&block)
@command = @adapter.restart(&block)
self
end
def put_up_maintenance_page(&block)
@command = @adapter.enable_maintenance(&block)
self
end
def take_down_maintenance_page(&block)
@command = @adapter.disable_maintenance(&block)
self
end
def call(out, err)
raise "No command!" unless @command
@command.call do |cmd|
run cmd, out, err
end
end
private
def load_adapter(bridge, app, environment, verbose, serverside_version)
EY::Serverside::Adapter.new("/usr/local/ey_resin/ruby/bin") do |args|
args.serverside_version = serverside_version
args.app = app.name
args.git = app.repository_uri
args.instances = instances_data(environment.deploy_to_instances, bridge)
args.stack = environment.app_server_stack_name
args.framework_env = environment.framework_env
args.environment_name = environment.name
args.account_name = app.account.name
args.verbose = verbose
end
end
# If we tell engineyard-serverside to use 'localhost', it'll run
# commands on the instance directly (#system). If we give it the
# instance's actual hostname, it'll SSH to itself.
#
# Using 'localhost' instead of its EC2 hostname speeds up
# deploys on solos and single-app-server clusters significantly.
def instances_data(instances, bridge)
instances.map do |i|
{
hostname: i.hostname == bridge ? 'localhost' : i.hostname,
roles: [i.role],
name: i.name,
}
end
end
def run(remote_command, out, err)
cmd = Escape.shell_command(['bash', '-lc', remote_command])
if cmd.respond_to?(:encoding) && cmd.respond_to?(:force_encoding)
out << "Encoding: #{cmd.encoding.name}" if @verbose
cmd.force_encoding('binary')
out << " => #{cmd.encoding.name}; __ENCODING__: #{__ENCODING__.name}; LANG: #{ENV['LANG']}; LC_CTYPE: #{ENV['LC_CTYPE']}\n" if @verbose
end
out << "Running command on #{@username}@#{@hostname}.\n"
out << cmd << "\n" if @verbose || ENV['PRINT_CMD']
if ENV["NO_SSH"]
out << "NO_SSH is set. No output.\n"
true
else
begin
ssh(cmd, @hostname, @username, out, err)
rescue Net::SSH::AuthenticationFailed
raise EY::Error, <<-ERROR
Authentication Failed. Things to fix:
1. Add your SSH key to your local SSH agent with `ssh-add path/to/key`.
2. Add your SSH key to #{@hierarchy_name} on Engine Yard Cloud and apply the changes.
(https://support.cloud.engineyard.com/entries/20996846-set-up-ssh-keys)
ERROR
end
end
end
def net_ssh_options
level = :fatal # default in Net::SSH
if debug = ENV["DEBUG"]
level = :info
if %w[debug info warn error fatal].include?(debug.downcase)
level = debug.downcase.to_sym
end
end
{paranoid: false, verbose: level, keepalive: true, keepalive_interval: 60}
end
def ssh(cmd, hostname, username, out, err)
exit_code = 1
Net::SSH.start(hostname, username, net_ssh_options) do |net_ssh|
net_ssh.open_channel do |channel|
channel.exec cmd do |_, success|
unless success
err << "Remote command execution failed"
return false
end
channel.on_data do |_, data|
out << data
end
channel.on_extended_data do |_, _, data|
err << data
end
channel.on_request("exit-status") do |_, data|
exit_code = data.read_long
end
channel.on_request("exit-signal") do |_, data|
exit_code = 255
end
# sending eof declares no more data coming from this end (close stdin)
channel.eof!
end
end
net_ssh.loop
end
exit_code.zero?
end
end
end