blimpy/lib/blimpy/box.rb

284 lines
6.3 KiB
Ruby

require 'rubygems'
require 'yaml'
require 'blimpy/helpers/state'
require 'blimpy/livery'
require 'blimpy/keys'
require 'blimpy/securitygroups'
require 'blimpy/boxes'
module Blimpy
class Box
include Blimpy::Helpers::State
attr_reader :allowed_regions, :region
attr_accessor :image_id, :flavor, :group, :ports
attr_accessor :dns, :internal_dns
attr_accessor :name, :tags, :fleet_id, :username, :ssh_port, :livery
attr_accessor :provision_on_start
def self.from_instance_id(an_id, data)
return if data[:type].nil?
name = data[:type].to_sym
return unless Blimpy::Boxes.const_defined? name
klass = Blimpy::Boxes.const_get(name)
server = klass.fog_server_for_instance(an_id, data)
return if server.nil?
box = klass.new(server)
box.with_data(an_id, data)
box
end
def initialize(server=nil)
@provision_on_start = true
@livery = nil
@group = nil
@name = 'Unnamed Box'
@tags = {}
@ports = []
@server = server
@fleet_id = 0
@ssh_connected = false
@exec_commands = true
end
def region=(newRegion)
unless (@allowed_regions.nil?) || (@allowed_regions.include?(newRegion))
raise InvalidRegionError
end
@region = newRegion
end
def online!
write_state_file
end
def validate!
raise NotImplementedError, '#validate! should be defined in a subclass of Blimpy::Box'
end
def prestart
end
def start
ensure_state_folder
prestart
@server = create_host
poststart
write_state_file
end
def poststart
end
def bootstrap
@exec_commands = false
if @livery.nil?
return
end
if @livery.respond_to? :new
@livery = @livery.new
end
wait_for_sshd
bootstrap_livery
end
# This is just here to make things more consistent from an API perspective
def provision
bootstrap
end
def stop
unless @server.nil?
@server.stop
end
end
def resume
unless @server.nil?
@server.start
end
end
def predestroy
end
def destroy
unless @server.nil?
predestroy
@server.destroy
postdestroy
File.unlink(state_file)
end
end
def postdestroy
end
def type
# We only really care about the class name as part of the Blimpy::Boxes
# module
self.class.to_s.split('::').last
end
def serializable_attributes
[:type, :name, :region, :dns, :internal_dns, :flavor, :tags]
end
def immutable_attributes
[:type]
end
def write_state_file
data = {}
serializable_attributes.each do |attr|
data[attr] = self.send(attr)
end
File.open(state_file, 'w') do |f|
f.write(data.to_yaml)
end
end
def state_file
if @server.nil?
raise Exception, "I can't make a state file without a @server!"
end
File.join(state_folder, "#{@server.id}.blimp")
end
def wait_for_state(until_state, &block)
if @server.nil?
return
end
@server.wait_for do
block.call
state == until_state
end
end
def with_data(ship_id, data)
data.each do |key, value|
next if immutable_attributes.include? key.to_sym
self.send("#{key}=", value)
end
end
def dns
@dns ||= begin
if @server.nil?
'no name'
else
@server.dns_name
end
end
end
def internal_dns
@internal_dns ||= begin
if @server.nil?
'no name'
else
@server.private_dns_name
end
end
end
def run_command(*args)
if @exec_commands
::Kernel.exec(*args)
else
::Kernel.system(*args)
end
end
def ssh_commands(*args)
['ssh', '-o', 'PasswordAuthentication=no',
'-o', 'StrictHostKeyChecking=no',
'-p', (ssh_port||22).to_s,
'-l', username, dns, *args]
end
def ssh_into(*args)
run_command(*ssh_commands(*args))
end
def scp_file(filename, directory='', *args)
filename = File.expand_path(filename)
run_command('scp', '-o', 'StrictHostKeyChecking=no',
filename, "#{username}@#{dns}:#{directory}", *args)
end
def scp_files(directory, files)
filename = File.expand_path(filename)
run_command(*['scp', '-o', 'StrictHostKeyChecking=no']+files+["#{username}@#{dns}:#{directory}"])
end
def bootstrap_livery
if @livery.kind_of? Symbol
raise Blimpy::InvalidLiveryException, 'Symbol liveries are unsupported!'
end
@livery.setup_on(self)
@livery.preflight(self)
@livery.flight(self)
@livery.postflight(self)
end
def wait_for_sshd
return if @ssh_connected
start = Time.now.to_i
use_exec = @exec_commands
# Even if we are supposed to use #exec here, we wait to disable it until
# after sshd(8) comes online
@exec_commands = false
$stdout.sync = true
need_nl = false
until @ssh_connected
# Run the `true` command and exit
@ssh_connected = ssh_into('-q', 'true')
# if SSH is killed, don't repeat
if $?.signaled?
if $?.termsig==2
# if Ctrl+C, report what we were doing
puts "Failed to connect. To try it yourself:\n#{ssh_commands('-v','true').join(' ')}"
end
raise Exception, "ssh was killed: #{$?}"
end
unless @ssh_connected
if !need_nl
p = ssh_port.nil? ? "" : ":#{ssh_port}"
print ">> Connecting #{username}@#{name}#{p}"
end
if (Time.now.to_i - start) < 60
print '.'
need_nl = true
sleep 1
end
end
end
puts if need_nl
@exec_commands = use_exec
end
def fog
raise NotImplementedError, '#fog should be implemented by cloud-specific subclasses'
end
private
def create_host
raise NotImplementedError, '#create_host should be implemented by a cloud-specific subclass'
end
end
end