268 lines
10 KiB
Ruby
268 lines
10 KiB
Ruby
include Opscode::Aws::Ec2
|
|
|
|
# Support whyrun
|
|
def whyrun_supported?
|
|
true
|
|
end
|
|
|
|
action :create do
|
|
raise "Cannot create a volume with a specific id (EC2 chooses volume ids)" if new_resource.volume_id
|
|
if new_resource.snapshot_id =~ /vol/
|
|
new_resource.snapshot_id(find_snapshot_id(new_resource.snapshot_id, new_resource.most_recent_snapshot))
|
|
end
|
|
|
|
nvid = volume_id_in_node_data
|
|
if nvid
|
|
# volume id is registered in the node data, so check that the volume in fact exists in EC2
|
|
vol = volume_by_id(nvid)
|
|
exists = vol && vol[:aws_status] != "deleting"
|
|
# TODO: determine whether this should be an error or just cause a new volume to be created. Currently erring on the side of failing loudly
|
|
raise "Volume with id #{nvid} is registered with the node but does not exist in EC2. To clear this error, remove the ['aws']['ebs_volume']['#{new_resource.name}']['volume_id'] entry from this node's data." unless exists
|
|
else
|
|
# Determine if there is a volume that meets the resource's specifications and is attached to the current
|
|
# instance in case a previous [:create, :attach] run created and attached a volume but for some reason was
|
|
# not registered in the node data (e.g. an exception is thrown after the attach_volume request was accepted
|
|
# by EC2, causing the node data to not be stored on the server)
|
|
if new_resource.device && (attached_volume = currently_attached_volume(instance_id, new_resource.device))
|
|
Chef::Log.debug("There is already a volume attached at device #{new_resource.device}")
|
|
compatible = volume_compatible_with_resource_definition?(attached_volume)
|
|
raise "Volume #{attached_volume[:aws_id]} attached at #{attached_volume[:aws_device]} but does not conform to this resource's specifications" unless compatible
|
|
Chef::Log.debug("The volume matches the resource's definition, so the volume is assumed to be already created")
|
|
converge_by("update the node data with volume id: #{attached_volume[:aws_id]}") do
|
|
node.set['aws']['ebs_volume'][new_resource.name]['volume_id'] = attached_volume[:aws_id]
|
|
node.save unless Chef::Config[:solo]
|
|
end
|
|
else
|
|
# If not, create volume and register its id in the node data
|
|
converge_by("create a volume with id=#{new_resource.snapshot_id} size=#{new_resource.size} availability_zone=#{new_resource.availability_zone} and update the node data with created volume's id") do
|
|
nvid = create_volume(new_resource.snapshot_id,
|
|
new_resource.size,
|
|
new_resource.availability_zone,
|
|
new_resource.timeout,
|
|
new_resource.volume_type,
|
|
new_resource.piops)
|
|
node.set['aws']['ebs_volume'][new_resource.name]['volume_id'] = nvid
|
|
node.save unless Chef::Config[:solo]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
action :attach do
|
|
# determine_volume returns a Hash, not a Mash, and the keys are
|
|
# symbols, not strings.
|
|
vol = determine_volume
|
|
|
|
if vol[:aws_status] == "in-use"
|
|
if vol[:aws_instance_id] != instance_id
|
|
raise "Volume with id #{vol[:aws_id]} exists but is attached to instance #{vol[:aws_instance_id]}"
|
|
else
|
|
Chef::Log.debug("Volume is already attached")
|
|
end
|
|
else
|
|
converge_by("attach the volume with aws_id=#{vol[:aws_id]} id=#{instance_id} device=#{new_resource.device} and update the node data with created volume's id") do
|
|
# attach the volume and register its id in the node data
|
|
attach_volume(vol[:aws_id], instance_id, new_resource.device, new_resource.timeout)
|
|
# always use a symbol here, it is a Hash
|
|
node.set['aws']['ebs_volume'][new_resource.name]['volume_id'] = vol[:aws_id]
|
|
node.save unless Chef::Config[:solo]
|
|
end
|
|
end
|
|
end
|
|
|
|
action :detach do
|
|
vol = determine_volume
|
|
converge_by("detach volume with id: #{vol[:aws_id]}") do
|
|
detach_volume(vol[:aws_id], new_resource.timeout)
|
|
end
|
|
end
|
|
|
|
action :snapshot do
|
|
vol = determine_volume
|
|
converge_by("would create a snapshot for volume: #{vol[:aws_id]}") do
|
|
snapshot = ec2.create_snapshot(vol[:aws_id],new_resource.description)
|
|
Chef::Log.info("Created snapshot of #{vol[:aws_id]} as #{snapshot[:aws_id]}")
|
|
end
|
|
end
|
|
|
|
action :prune do
|
|
vol = determine_volume
|
|
old_snapshots = Array.new
|
|
Chef::Log.info "Checking for old snapshots"
|
|
ec2.describe_snapshots.sort { |a,b| b[:aws_started_at] <=> a[:aws_started_at] }.each do |snapshot|
|
|
if snapshot[:aws_volume_id] == vol[:aws_id]
|
|
Chef::Log.info "Found old snapshot #{snapshot[:aws_id]} (#{snapshot[:aws_volume_id]}) #{snapshot[:aws_started_at]}"
|
|
old_snapshots << snapshot
|
|
end
|
|
end
|
|
if old_snapshots.length > new_resource.snapshots_to_keep
|
|
old_snapshots[new_resource.snapshots_to_keep, old_snapshots.length].each do |die|
|
|
converge_by("delete snapshot with id: #{die[:aws_id]}") do
|
|
Chef::Log.info "Deleting old snapshot #{die[:aws_id]}"
|
|
ec2.delete_snapshot(die[:aws_id])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def volume_id_in_node_data
|
|
begin
|
|
node['aws']['ebs_volume'][new_resource.name]['volume_id']
|
|
rescue NoMethodError => e
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Pulls the volume id from the volume_id attribute or the node data and verifies that the volume actually exists
|
|
def determine_volume
|
|
vol = currently_attached_volume(instance_id, new_resource.device)
|
|
vol_id = new_resource.volume_id || volume_id_in_node_data || ( vol ? vol[:aws_id] : nil )
|
|
raise "volume_id attribute not set and no volume id is set in the node data for this resource (which is populated by action :create) and no volume is attached at the device" unless vol_id
|
|
|
|
# check that volume exists
|
|
vol = volume_by_id(vol_id)
|
|
raise "No volume with id #{vol_id} exists" unless vol
|
|
|
|
vol
|
|
end
|
|
|
|
# Retrieves information for a volume
|
|
def volume_by_id(volume_id)
|
|
ec2.describe_volumes.find{|v| v[:aws_id] == volume_id}
|
|
end
|
|
|
|
# Returns the volume that's attached to the instance at the given device or nil if none matches
|
|
def currently_attached_volume(instance_id, device)
|
|
ec2.describe_volumes.find{|v| v[:aws_instance_id] == instance_id && v[:aws_device] == device}
|
|
end
|
|
|
|
# Returns true if the given volume meets the resource's attributes
|
|
def volume_compatible_with_resource_definition?(volume)
|
|
if new_resource.snapshot_id =~ /vol/
|
|
new_resource.snapshot_id(find_snapshot_id(new_resource.snapshot_id, new_resource.most_recent_snapshot))
|
|
end
|
|
(new_resource.size.nil? || new_resource.size == volume[:aws_size]) &&
|
|
(new_resource.availability_zone.nil? || new_resource.availability_zone == volume[:zone]) &&
|
|
(new_resource.snapshot_id.nil? || new_resource.snapshot_id == volume[:snapshot_id])
|
|
end
|
|
|
|
# Creates a volume according to specifications and blocks until done (or times out)
|
|
def create_volume(snapshot_id, size, availability_zone, timeout, volume_type, piops)
|
|
availability_zone ||= instance_availability_zone
|
|
|
|
# Sanity checks so we don't shoot ourselves.
|
|
raise "Invalid volume type: #{volume_type}" unless ['standard', 'io1'].include?(volume_type)
|
|
|
|
# PIOPs requested. Must specify an iops param and probably won't be "low".
|
|
if volume_type == 'io1'
|
|
raise 'IOPS value not specified.' unless piops >= 100
|
|
end
|
|
|
|
# Shouldn't see non-zero piops param without appropriate type.
|
|
if piops > 0
|
|
raise 'IOPS param without piops volume type.' unless volume_type == 'io1'
|
|
end
|
|
|
|
create_volume_opts = { :volume_type => volume_type }
|
|
# TODO: this may have to be casted to a string. rightaws vs aws doc discrepancy.
|
|
create_volume_opts[:iops] = piops if volume_type == 'io1'
|
|
|
|
nv = ec2.create_volume(snapshot_id, size, availability_zone, create_volume_opts)
|
|
Chef::Log.debug("Created new volume #{nv[:aws_id]}#{snapshot_id ? " based on #{snapshot_id}" : ""}")
|
|
|
|
# block until created
|
|
begin
|
|
Timeout::timeout(timeout) do
|
|
while true
|
|
vol = volume_by_id(nv[:aws_id])
|
|
if vol && vol[:aws_status] != "deleting"
|
|
if ["in-use", "available"].include?(vol[:aws_status])
|
|
Chef::Log.info("Volume #{nv[:aws_id]} is available")
|
|
break
|
|
else
|
|
Chef::Log.debug("Volume is #{vol[:aws_status]}")
|
|
end
|
|
sleep 3
|
|
else
|
|
raise "Volume #{nv[:aws_id]} no longer exists"
|
|
end
|
|
end
|
|
end
|
|
rescue Timeout::Error
|
|
raise "Timed out waiting for volume creation after #{timeout} seconds"
|
|
end
|
|
|
|
nv[:aws_id]
|
|
end
|
|
|
|
# Attaches the volume and blocks until done (or times out)
|
|
def attach_volume(volume_id, instance_id, device, timeout)
|
|
Chef::Log.debug("Attaching #{volume_id} as #{device}")
|
|
ec2.attach_volume(volume_id, instance_id, device)
|
|
|
|
# block until attached
|
|
begin
|
|
Timeout::timeout(timeout) do
|
|
while true
|
|
vol = volume_by_id(volume_id)
|
|
if vol && vol[:aws_status] != "deleting"
|
|
if vol[:aws_attachment_status] == "attached"
|
|
if vol[:aws_instance_id] == instance_id
|
|
Chef::Log.info("Volume #{volume_id} is attached to #{instance_id}")
|
|
break
|
|
else
|
|
raise "Volume is attached to instance #{vol[:aws_instance_id]} instead of #{instance_id}"
|
|
end
|
|
else
|
|
Chef::Log.debug("Volume is #{vol[:aws_status]}")
|
|
end
|
|
sleep 3
|
|
else
|
|
raise "Volume #{volume_id} no longer exists"
|
|
end
|
|
end
|
|
end
|
|
rescue Timeout::Error
|
|
raise "Timed out waiting for volume attachment after #{timeout} seconds"
|
|
end
|
|
end
|
|
|
|
# Detaches the volume and blocks until done (or times out)
|
|
def detach_volume(volume_id, timeout)
|
|
vol = volume_by_id(volume_id)
|
|
if vol[:aws_instance_id] != instance_id
|
|
Chef::Log.debug("EBS Volume #{volume_id} is not attached to this instance (attached to #{vol[:aws_instance_id]}). Skipping...")
|
|
return
|
|
end
|
|
Chef::Log.debug("Detaching #{volume_id}")
|
|
orig_instance_id = vol[:aws_instance_id]
|
|
ec2.detach_volume(volume_id)
|
|
|
|
# block until detached
|
|
begin
|
|
Timeout::timeout(timeout) do
|
|
while true
|
|
vol = volume_by_id(volume_id)
|
|
if vol && vol[:aws_status] != "deleting"
|
|
if vol[:aws_instance_id] != orig_instance_id
|
|
Chef::Log.info("Volume detached from #{orig_instance_id}")
|
|
break
|
|
else
|
|
Chef::Log.debug("Volume: #{vol.inspect}")
|
|
end
|
|
else
|
|
Chef::Log.debug("Volume #{volume_id} no longer exists")
|
|
break
|
|
end
|
|
sleep 3
|
|
end
|
|
end
|
|
rescue Timeout::Error
|
|
raise "Timed out waiting for volume detachment after #{timeout} seconds"
|
|
end
|
|
end
|
|
|
|
|