lib/inception/inception_server.rb
require "fog"
module Inception
class InceptionServer
DEFAULT_SERVER_NAME = "inception"
DEFAULT_DISK_SIZE = 16
DEFAULT_SECURITY_GROUPS = ["ssh"]
attr_reader :attributes
# @provider_client [Inception::Providers::FogProvider] - interact with IaaS
# @attributes [ReadWriteSettings]
#
# Required @attributes:
# {
# "name" => "inception",
# "ip_address" => "54.214.15.178",
# "key_pair" => {
# "name" => "inception",
# "private_key" => "private_key",
# "public_key" => "public_key"
# }
# }
#
# Including optional @attributes and default values:
# {
# "name" => "inception",
# "ip_address" => "54.214.15.178",
# "security_groups" => ["ssh"],
# "flavor" => "m3.medium",
# "key_pair" => {
# "name" => "inception",
# "private_key" => "private_key",
# "public_key" => "public_key"
# }
# }
def initialize(provider_client, attributes, ssh_dir)
@provider_client = provider_client
@ssh_dir = ssh_dir
@attributes = attributes.is_a?(Hash) ? ReadWriteSettings.new(attributes) : attributes
raise "@attributes must be ReadWriteSettings (or Hash)" unless @attributes.is_a?(ReadWriteSettings)
end
# Create the underlying server, with key pair & security groups, unless it is already created
#
# The @attributes hash is updated with a `provisioned` key during/after creation.
# When saved as YAML it might look like:
# inception:
# provisioned:
# image_id: ami-123456
# server_id: i-e7f005d2
# security_groups:
# - ssh
# - mosh
# username: ubuntu
# disk_device: /dev/sdi
# host: ec2-54-214-15-178.us-west-2.compute.amazonaws.com
# validated: true
# converged: true
def create
validate_attributes_for_bootstrap
ensure_required_security_groups
create_missing_default_security_groups
bootstrap_vm
attach_persistent_disk
end
# Delete the server, volume and release the IP address
def delete_all
delete_server
delete_volume
delete_key_pair
release_ip_address
end
def delete_server
@fog_server = nil # force reload of fog_server model
if fog_server
print "Deleting server... "
fog_server.destroy
wait_for_termination(fog_server) unless Fog.mocking?
puts "done."
else
puts "Server already destroyed"
end
provisioned.delete("host")
provisioned.delete("server_id")
provisioned.delete("username")
end
def delete_volume
volume_id = provisioned.exists?("disk_device.volume_id")
if volume_id && (volume = fog_compute.volumes.get(volume_id)) && volume.ready?
print "Deleting volume... "
volume.destroy
wait_for_termination(volume, "deleting")
puts ""
else
puts "Volume already destroyed"
end
provisioned.delete("disk_device")
end
def delete_key_pair
key_pair_name = attributes.exists?("key_pair.name")
if key_pair_name && key_pair = fog_compute.key_pairs.get(key_pair_name)
puts "Deleting key pair '#{key_pair_name}'"
key_pair.destroy
else
puts "Keypair already destroyed"
end
attributes.delete("key_pair")
end
def release_ip_address
public_ip = provisioned.exists?("ip_address")
if public_ip && ip_address = fog_compute.addresses.get(public_ip)
puts "Releasing IP address #{public_ip}"
ip_address.destroy
else
puts "IP address already released"
end
provisioned.delete("ip_address")
end
def security_groups
@attributes.security_groups
end
def server_name
@attributes["name"] ||= DEFAULT_SERVER_NAME
@attributes.name
end
def key_name
@attributes.key_pair.name
end
def private_key_path
@private_key_path ||= File.join(@ssh_dir, key_name)
end
def public_key
@attributes.exists?("key_pair.public_key")
end
# Flavor/instance type of the server to be provisioned
# TODO: DEFAULT_FLAVOR should become IaaS/provider specific
def flavor
@attributes["flavor"] ||= @provider_client.default_flavor
end
# Size of attached persistent disk for the inception server
def disk_size
@attributes["disk_size"] ||= DEFAULT_DISK_SIZE
end
def ip_address
provisioned.ip_address
end
def initial_user
@attributes["initial_user"] || "ubuntu"
end
def image_id
@attributes["image_id"]
end
# The progresive/final attributes of the provisioned Inception server &
# persistent disk.
def provisioned
@attributes["provisioned"] = {} unless @attributes["provisioned"]
@attributes.provisioned
end
# Because @attributes["provisioned"] is not the same as @attributes.provisioned
# we need a helper to export the complete nested attributes.
def export_attributes
attrs = attributes.to_nested_hash
attrs["provisioned"] = provisioned.to_nested_hash
attrs
end
def disk_devices
provisioned["disk_device"] ||= default_disk_device
end
def external_disk_device
disk_devices["external"]
end
def default_disk_device
@provider_client.default_disk_device(fog_server)
end
def user_host
"#{provisioned.username}@#{provisioned.host}"
end
def fog_server
@fog_server ||= begin
if server_id = provisioned["server_id"]
fog_compute.servers.get(server_id)
end
end
end
def fog_compute
@provider_client.fog_compute
end
protected
# set_resource_name(fog_server, "inception")
# set_resource_name(volume, "inception-root")
# set_resource_name(volume, "inception-store")
def set_resource_name(resource, name)
@provider_client.set_resource_name(resource, name)
end
def fog_attributes
@provider_client.fog_attributes(self)
end
def validate_attributes_for_bootstrap
missing_attributes = []
missing_attributes << "provisioned.ip_address" unless @attributes.exists?("provisioned.ip_address")
missing_attributes << "key_pair.private_key" unless @attributes.exists?("key_pair.private_key")
if missing_attributes.size > 0
raise "Missing InceptionServer attributes: #{missing_attributes.join(', ')}"
end
end
# ssh group must be first (bootstrap method looks for port 22 in first group)
def ensure_required_security_groups
if @attributes["security_groups"] && @attributes["security_groups"].is_a?(Array)
unless @attributes["security_groups"].include?("ssh")
@attributes["security_groups"] = ["ssh", *@attributes["security_groups"]]
end
else
@attributes["security_groups"] = ["ssh"]
end
end
def create_missing_default_security_groups
# provider method only creates group if missing
@provider_client.create_security_group("ssh", "ssh", {ssh: 22})
end
def bootstrap_vm
unless fog_server
print "Booting #{flavor} inception server... "
@fog_server = @provider_client.bootstrap(fog_attributes)
provisioned["server_id"] = fog_server.id
provisioned["host"] = fog_server.public_ip_address
provisioned["username"] = fog_attributes[:username]
puts provisioned.server_id
end
set_resource_name(fog_server, server_name)
end
def attach_persistent_disk
unless Fog.mocking?
print "Confirming ssh access to server... "
Fog.wait_for(60) { fog_server.sshable?(ssh_options) }
puts "done"
end
unless volume = @provider_client.find_server_device(fog_server, external_disk_device)
print "Provisioning #{disk_size}Gb persistent disk for inception server... "
volume = @provider_client.create_and_attach_volume("Inception Disk", disk_size, fog_server, external_disk_device)
disk_devices["volume_id"] = volume.id
puts disk_devices.volume_id
end
set_resource_name(volume, server_name)
end
def ssh_options
{
keys: [private_key_path]
}
end
# Poll a fog model until it terminates; print . each second
def wait_for_termination(fog_model, state_to_wait_for="terminated")
fog_model.wait_for do
print "."
state == state_to_wait_for
end
end
protected
# TODO emit events rather than writing directly to STDOUT
def say(*args)
puts(*args)
end
end
end