Weidong Shao 486c596323 Replace cookbooks for IceHouse release
Change-Id: Ibb6bdd3ed7795b86f773f9cb6ce1dfffb9b79faf
2014-08-07 08:36:02 +00:00

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