bosh_openstack_cpi/lib/cloud/openstack/cloud.rb
# Copyright (c) 2009-2013 VMware, Inc.
# Copyright (c) 2012 Piston Cloud Computing, Inc.
module Bosh::OpenStackCloud
##
# BOSH OpenStack CPI
class Cloud < Bosh::Cloud
include Helpers
BOSH_APP_DIR = '/var/vcap/bosh'
FIRST_DEVICE_NAME_LETTER = 'b'
attr_reader :openstack
attr_reader :registry
attr_reader :glance
attr_reader :volume
attr_accessor :logger
##
# Creates a new BOSH OpenStack CPI
#
# @param [Hash] options CPI options
# @option options [Hash] openstack OpenStack specific options
# @option options [Hash] agent agent options
# @option options [Hash] registry agent options
def initialize(options)
@options = options.dup
validate_options
initialize_registry
@logger = Bosh::Clouds::Config.logger
@agent_properties = @options['agent'] || {}
@openstack_properties = @options['openstack']
@default_key_name = @openstack_properties["default_key_name"]
@default_security_groups = @openstack_properties["default_security_groups"]
@state_timeout = @openstack_properties["state_timeout"]
@stemcell_public_visibility = @openstack_properties["stemcell_public_visibility"]
@wait_resource_poll_interval = @openstack_properties["wait_resource_poll_interval"]
@boot_from_volume = @openstack_properties["boot_from_volume"]
unless @openstack_properties['auth_url'].match(/\/tokens$/)
@openstack_properties['auth_url'] = @openstack_properties['auth_url'] + '/tokens'
end
openstack_params = {
:provider => 'OpenStack',
:openstack_auth_url => @openstack_properties['auth_url'],
:openstack_username => @openstack_properties['username'],
:openstack_api_key => @openstack_properties['api_key'],
:openstack_tenant => @openstack_properties['tenant'],
:openstack_region => @openstack_properties['region'],
:openstack_endpoint_type => @openstack_properties['endpoint_type'],
:connection_options => @openstack_properties['connection_options']
}
begin
@openstack = Fog::Compute.new(openstack_params)
rescue Exception => e
@logger.error(e)
cloud_error('Unable to connect to the OpenStack Compute API. Check task debug log for details.')
end
glance_params = {
:provider => 'OpenStack',
:openstack_auth_url => @openstack_properties['auth_url'],
:openstack_username => @openstack_properties['username'],
:openstack_api_key => @openstack_properties['api_key'],
:openstack_tenant => @openstack_properties['tenant'],
:openstack_region => @openstack_properties['region'],
:openstack_endpoint_type => @openstack_properties['endpoint_type'],
:connection_options => @openstack_properties['connection_options']
}
begin
@glance = Fog::Image.new(glance_params)
rescue Exception => e
@logger.error(e)
cloud_error('Unable to connect to the OpenStack Image Service API. Check task debug log for details.')
end
volume_params = {
:provider => "OpenStack",
:openstack_auth_url => @openstack_properties["auth_url"],
:openstack_username => @openstack_properties["username"],
:openstack_api_key => @openstack_properties["api_key"],
:openstack_tenant => @openstack_properties["tenant"],
:openstack_endpoint_type => @openstack_properties["endpoint_type"],
:connection_options => @openstack_properties['connection_options']
}
begin
@volume = Fog::Volume.new(volume_params)
rescue Exception => e
@logger.error(e)
cloud_error("Unable to connect to the OpenStack Volume API. Check task debug log for details.")
end
@metadata_lock = Mutex.new
end
##
# Creates a new OpenStack Image using stemcell image. It requires access
# to the OpenStack Glance service.
#
# @param [String] image_path Local filesystem path to a stemcell image
# @param [Hash] cloud_properties CPI-specific properties
# @option cloud_properties [String] name Stemcell name
# @option cloud_properties [String] version Stemcell version
# @option cloud_properties [String] infrastructure Stemcell infraestructure
# @option cloud_properties [String] disk_format Image disk format
# @option cloud_properties [String] container_format Image container format
# @option cloud_properties [optional, String] kernel_file Name of the
# kernel image file provided at the stemcell archive
# @option cloud_properties [optional, String] ramdisk_file Name of the
# ramdisk image file provided at the stemcell archive
# @return [String] OpenStack image UUID of the stemcell
def create_stemcell(image_path, cloud_properties)
with_thread_name("create_stemcell(#{image_path}...)") do
begin
Dir.mktmpdir do |tmp_dir|
@logger.info('Creating new image...')
image_params = {
:name => "BOSH-#{generate_unique_name}",
:disk_format => cloud_properties['disk_format'],
:container_format => cloud_properties['container_format'],
:is_public => @stemcell_public_visibility.nil? ? false : @stemcell_public_visibility,
}
image_properties = {}
vanilla_options = ['name', 'version', 'os_type', 'os_distro', 'architecture', 'auto_disk_config']
vanilla_options.reject{ |o| cloud_properties[o].nil? }.each do |key|
image_properties[key.to_sym] = cloud_properties[key]
end
image_params[:properties] = image_properties unless image_properties.empty?
# If image_location is set in cloud properties, then pass the copy-from parm. Then Glance will fetch it
# from the remote location on a background job and store it in its repository.
# Otherwise, unpack image to temp directory and upload to Glance the root image.
if cloud_properties['image_location']
@logger.info("Using remote image from `#{cloud_properties['image_location']}'...")
image_params[:copy_from] = cloud_properties['image_location']
else
@logger.info("Extracting stemcell file to `#{tmp_dir}'...")
unpack_image(tmp_dir, image_path)
image_params[:location] = File.join(tmp_dir, 'root.img')
end
# Upload image using Glance service
@logger.debug("Using image parms: `#{image_params.inspect}'")
image = with_openstack { @glance.images.create(image_params) }
@logger.info("Creating new image `#{image.id}'...")
wait_resource(image, :active)
image.id.to_s
end
rescue => e
@logger.error(e)
raise e
end
end
end
##
# Deletes a stemcell
#
# @param [String] stemcell_id OpenStack image UUID of the stemcell to be
# deleted
# @return [void]
def delete_stemcell(stemcell_id)
with_thread_name("delete_stemcell(#{stemcell_id})") do
@logger.info("Deleting stemcell `#{stemcell_id}'...")
image = with_openstack { @glance.images.find_by_id(stemcell_id) }
if image
with_openstack { image.destroy }
@logger.info("Stemcell `#{stemcell_id}' is now deleted")
else
@logger.info("Stemcell `#{stemcell_id}' not found. Skipping.")
end
end
end
##
# Creates an OpenStack server and waits until it's in running state
#
# @param [String] agent_id UUID for the agent that will be used later on by
# the director to locate and talk to the agent
# @param [String] stemcell_id OpenStack image UUID that will be used to
# power on new server
# @param [Hash] resource_pool cloud specific properties describing the
# resources needed for this VM
# @param [Hash] networks list of networks and their settings needed for
# this VM
# @param [optional, Array] disk_locality List of disks that might be
# attached to this server in the future, can be used as a placement
# hint (i.e. server will only be created if resource pool availability
# zone is the same as disk availability zone)
# @param [optional, Hash] environment Data to be merged into agent settings
# @return [String] OpenStack server UUID
def create_vm(agent_id, stemcell_id, resource_pool,
network_spec = nil, disk_locality = nil, environment = nil)
with_thread_name("create_vm(#{agent_id}, ...)") do
@logger.info('Creating new server...')
server_name = "vm-#{generate_unique_name}"
network_configurator = NetworkConfigurator.new(network_spec)
openstack_security_groups = with_openstack { @openstack.security_groups }.collect { |sg| sg.name }
security_groups = network_configurator.security_groups(@default_security_groups)
security_groups.each do |sg|
cloud_error("Security group `#{sg}' not found") unless openstack_security_groups.include?(sg)
end
@logger.debug("Using security groups: `#{security_groups.join(', ')}'")
nics = network_configurator.nics
@logger.debug("Using NICs: `#{nics.join(', ')}'")
image = with_openstack { @openstack.images.find { |i| i.id == stemcell_id } }
cloud_error("Image `#{stemcell_id}' not found") if image.nil?
@logger.debug("Using image: `#{stemcell_id}'")
flavor = with_openstack { @openstack.flavors.find { |f| f.name == resource_pool['instance_type'] } }
cloud_error("Flavor `#{resource_pool['instance_type']}' not found") if flavor.nil?
if flavor_has_ephemeral_disk?(flavor)
if flavor.ram
# Ephemeral disk size should be at least the double of the vm total memory size, as agent will need:
# - vm total memory size for swapon,
# - the rest for /vcar/vcap/data
min_ephemeral_size = (flavor.ram / 1024) * 2
if flavor.ephemeral < min_ephemeral_size
cloud_error("Flavor `#{resource_pool['instance_type']}' should have at least #{min_ephemeral_size}Gb " +
'of ephemeral disk')
end
end
end
@logger.debug("Using flavor: `#{resource_pool['instance_type']}'")
keyname = resource_pool['key_name'] || @default_key_name
keypair = with_openstack { @openstack.key_pairs.find { |k| k.name == keyname } }
cloud_error("Key-pair `#{keyname}' not found") if keypair.nil?
@logger.debug("Using key-pair: `#{keypair.name}' (#{keypair.fingerprint})")
if @boot_from_volume
boot_vol_size = flavor.disk * 1024
boot_vol_id = create_boot_disk(boot_vol_size, stemcell_id)
cloud_error("Failed to create boot volume.") if boot_vol_id.nil?
@logger.debug("Using boot volume: `#{boot_vol_id}'")
end
server_params = {
:name => server_name,
:image_ref => image.id,
:flavor_ref => flavor.id,
:key_name => keypair.name,
:security_groups => security_groups,
:nics => nics,
:user_data => Yajl::Encoder.encode(user_data(server_name, network_spec))
}
availability_zone = select_availability_zone(disk_locality, resource_pool['availability_zone'])
server_params[:availability_zone] = availability_zone if availability_zone
if @boot_from_volume
server_params[:block_device_mapping] = [{
:volume_size => "",
:volume_id => boot_vol_id,
:delete_on_termination => "1",
:device_name => "/dev/vda"
}]
else
server_params[:personality] = [{
"path" => "#{BOSH_APP_DIR}/user_data.json",
"contents" => Yajl::Encoder.encode(user_data(server_name, network_spec, keypair.public_key))
}]
end
@logger.debug("Using boot parms: `#{server_params.inspect}'")
server = with_openstack { @openstack.servers.create(server_params) }
@logger.info("Creating new server `#{server.id}'...")
begin
wait_resource(server, :active, :state)
rescue Bosh::Clouds::CloudError => e
@logger.warn("Failed to create server: #{e.message}")
with_openstack { server.destroy }
raise Bosh::Clouds::VMCreationFailed.new(true)
end
@logger.info("Configuring network for server `#{server.id}'...")
network_configurator.configure(@openstack, server)
@logger.info("Updating settings for server `#{server.id}'...")
settings = initial_agent_settings(server_name, agent_id, network_spec, environment,
flavor_has_ephemeral_disk?(flavor))
@registry.update_settings(server.name, settings)
server.id.to_s
end
end
##
# Terminates an OpenStack server and waits until it reports as terminated
#
# @param [String] server_id OpenStack server UUID
# @return [void]
def delete_vm(server_id)
with_thread_name("delete_vm(#{server_id})") do
@logger.info("Deleting server `#{server_id}'...")
server = with_openstack { @openstack.servers.get(server_id) }
if server
with_openstack { server.destroy }
wait_resource(server, [:terminated, :deleted], :state, true)
@logger.info("Deleting settings for server `#{server.id}'...")
@registry.delete_settings(server.name)
else
@logger.info("Server `#{server_id}' not found. Skipping.")
end
end
end
##
# Checks if an OpenStack server exists
#
# @param [String] server_id OpenStack server UUID
# @return [Boolean] True if the vm exists
def has_vm?(server_id)
with_thread_name("has_vm?(#{server_id})") do
server = with_openstack { @openstack.servers.get(server_id) }
!server.nil? && ![:terminated, :deleted].include?(server.state.downcase.to_sym)
end
end
##
# Reboots an OpenStack Server
#
# @param [String] server_id OpenStack server UUID
# @return [void]
def reboot_vm(server_id)
with_thread_name("reboot_vm(#{server_id})") do
server = with_openstack { @openstack.servers.get(server_id) }
cloud_error("Server `#{server_id}' not found") unless server
soft_reboot(server)
end
end
##
# Configures networking on existing OpenStack server
#
# @param [String] server_id OpenStack server UUID
# @param [Hash] network_spec Raw network spec passed by director
# @return [void]
# @raise [Bosh::Clouds:NotSupported] If there's a network change that requires the recreation of the VM
def configure_networks(server_id, network_spec)
with_thread_name("configure_networks(#{server_id}, ...)") do
@logger.info("Configuring `#{server_id}' to use the following " \
"network settings: #{network_spec.pretty_inspect}")
network_configurator = NetworkConfigurator.new(network_spec)
server = with_openstack { @openstack.servers.get(server_id) }
cloud_error("Server `#{server_id}' not found") unless server
compare_security_groups(server, network_configurator.security_groups(@default_security_groups))
compare_private_ip_addresses(server, network_configurator.private_ip)
network_configurator.configure(@openstack, server)
update_agent_settings(server) do |settings|
settings['networks'] = network_spec
end
end
end
##
# Creates a new OpenStack volume
#
# @param [Integer] size disk size in MiB
# @param [optional, String] server_id OpenStack server UUID of the VM that
# this disk will be attached to
# @return [String] OpenStack volume UUID
def create_disk(size, server_id = nil)
with_thread_name("create_disk(#{size}, #{server_id})") do
raise ArgumentError, 'Disk size needs to be an integer' unless size.kind_of?(Integer)
cloud_error('Minimum disk size is 1 GiB') if (size < 1024)
cloud_error('Maximum disk size is 1 TiB') if (size > 1024 * 1000)
volume_params = {
:name => "volume-#{generate_unique_name}",
:description => '',
:size => (size / 1024.0).ceil
}
if server_id
server = with_openstack { @openstack.servers.get(server_id) }
if server && server.availability_zone
volume_params[:availability_zone] = server.availability_zone
end
end
@logger.info('Creating new volume...')
volume = with_openstack { @openstack.volumes.create(volume_params) }
@logger.info("Creating new volume `#{volume.id}'...")
wait_resource(volume, :available)
volume.id.to_s
end
end
##
# Creates a new OpenStack boot volume
#
# @param [Integer] size disk size in MiB
# @param [String] stemcell_id OpenStack image UUID that will be used to
# populate the boot volume
# @return [String] OpenStack volume UUID
def create_boot_disk(size, stemcell_id)
with_thread_name("create_boot_disk(#{size}, #{stemcell_id})") do
raise ArgumentError, "Disk size needs to be an integer" unless size.kind_of?(Integer)
cloud_error("Minimum disk size is 1 GiB") if (size < 1024)
cloud_error("Maximum disk size is 1 TiB") if (size > 1024 * 1000)
volume_params = {
:display_name => "volume-#{generate_unique_name}",
:size => (size / 1024.0).ceil,
:imageRef => stemcell_id
}
@logger.info("Creating new boot volume...")
boot_volume = with_openstack { @volume.volumes.create(volume_params) }
@logger.info("Creating new boot volume `#{boot_volume.id}'...")
wait_resource(boot_volume, :available)
boot_volume.id.to_s
end
end
##
# Deletes an OpenStack volume
#
# @param [String] disk_id OpenStack volume UUID
# @return [void]
# @raise [Bosh::Clouds::CloudError] if disk is not in available state
def delete_disk(disk_id)
with_thread_name("delete_disk(#{disk_id})") do
@logger.info("Deleting volume `#{disk_id}'...")
volume = with_openstack { @openstack.volumes.get(disk_id) }
if volume
state = volume.status
if state.to_sym != :available
cloud_error("Cannot delete volume `#{disk_id}', state is #{state}")
end
with_openstack { volume.destroy }
wait_resource(volume, :deleted, :status, true)
else
@logger.info("Volume `#{disk_id}' not found. Skipping.")
end
end
end
##
# Attaches an OpenStack volume to an OpenStack server
#
# @param [String] server_id OpenStack server UUID
# @param [String] disk_id OpenStack volume UUID
# @return [void]
def attach_disk(server_id, disk_id)
with_thread_name("attach_disk(#{server_id}, #{disk_id})") do
server = with_openstack { @openstack.servers.get(server_id) }
cloud_error("Server `#{server_id}' not found") unless server
volume = with_openstack { @openstack.volumes.get(disk_id) }
cloud_error("Volume `#{disk_id}' not found") unless volume
device_name = attach_volume(server, volume)
update_agent_settings(server) do |settings|
settings['disks'] ||= {}
settings['disks']['persistent'] ||= {}
settings['disks']['persistent'][disk_id] = device_name
end
end
end
##
# Detaches an OpenStack volume from an OpenStack server
#
# @param [String] server_id OpenStack server UUID
# @param [String] disk_id OpenStack volume UUID
# @return [void]
def detach_disk(server_id, disk_id)
with_thread_name("detach_disk(#{server_id}, #{disk_id})") do
server = with_openstack { @openstack.servers.get(server_id) }
cloud_error("Server `#{server_id}' not found") unless server
volume = with_openstack { @openstack.volumes.get(disk_id) }
cloud_error("Volume `#{disk_id}' not found") unless volume
detach_volume(server, volume)
update_agent_settings(server) do |settings|
settings['disks'] ||= {}
settings['disks']['persistent'] ||= {}
settings['disks']['persistent'].delete(disk_id)
end
end
end
##
# Takes a snapshot of an OpenStack volume
#
# @param [String] disk_id OpenStack volume UUID
# @param [Hash] metadata Metadata key/value pairs to add to snapshot
# @return [String] OpenStack snapshot UUID
# @raise [Bosh::Clouds::CloudError] if volume is not found
def snapshot_disk(disk_id, metadata)
with_thread_name("snapshot_disk(#{disk_id})") do
volume = with_openstack { @openstack.volumes.get(disk_id) }
cloud_error("Volume `#{disk_id}' not found") unless volume
devices = []
volume.attachments.each { |attachment| devices << attachment['device'] unless attachment.empty? }
description = [:deployment, :job, :index].collect { |key| metadata[key] }
description << devices.first.split('/').last unless devices.empty?
snapshot_params = {
:name => "snapshot-#{generate_unique_name}",
:description => description.join('/'),
:volume_id => volume.id
}
@logger.info("Creating new snapshot for volume `#{disk_id}'...")
snapshot = @openstack.snapshots.new(snapshot_params)
with_openstack { snapshot.save(true) }
@logger.info("Creating new snapshot `#{snapshot.id}' for volume `#{disk_id}'...")
wait_resource(snapshot, :available)
snapshot.id.to_s
end
end
##
# Deletes an OpenStack volume snapshot
#
# @param [String] snapshot_id OpenStack snapshot UUID
# @return [void]
# @raise [Bosh::Clouds::CloudError] if snapshot is not in available state
def delete_snapshot(snapshot_id)
with_thread_name("delete_snapshot(#{snapshot_id})") do
@logger.info("Deleting snapshot `#{snapshot_id}'...")
snapshot = with_openstack { @openstack.snapshots.get(snapshot_id) }
if snapshot
state = snapshot.status
if state.to_sym != :available
cloud_error("Cannot delete snapshot `#{snapshot_id}', state is #{state}")
end
with_openstack { snapshot.destroy }
wait_resource(snapshot, :deleted, :status, true)
else
@logger.info("Snapshot `#{snapshot_id}' not found. Skipping.")
end
end
end
##
# Set metadata for an OpenStack server
#
# @param [String] server_id OpenStack server UUID
# @param [Hash] metadata Metadata key/value pairs
# @return [void]
def set_vm_metadata(server_id, metadata)
with_thread_name("set_vm_metadata(#{server_id}, ...)") do
with_openstack do
server = @openstack.servers.get(server_id)
cloud_error("Server `#{server_id}' not found") unless server
metadata.each do |name, value|
TagManager.tag(server, name, value)
end
end
end
end
##
# Selects the availability zone to use from a list of disk volumes,
# resource pool availability zone (if any) and the default availability
# zone.
#
# @param [Array] volumes OpenStack volume UUIDs to attach to the vm
# @param [String] resource_pool_az availability zone specified in
# the resource pool (may be nil)
# @return [String] availability zone to use or nil
# @note this is a private method that is public to make it easier to test
def select_availability_zone(volumes, resource_pool_az)
if volumes && !volumes.empty?
disks = volumes.map { |vid| with_openstack { @openstack.volumes.get(vid) } }
ensure_same_availability_zone(disks, resource_pool_az)
disks.first.availability_zone
else
resource_pool_az
end
end
##
# Ensure all supplied availability zones are the same
#
# @param [Array] disks OpenStack volumes
# @param [String] default availability zone specified in
# the resource pool (may be nil)
# @return [String] availability zone to use or nil
# @note this is a private method that is public to make it easier to test
def ensure_same_availability_zone(disks, default)
zones = disks.map { |disk| disk.availability_zone }
zones << default if default
zones.uniq!
cloud_error "can't use multiple availability zones: %s" %
zones.join(', ') unless zones.size == 1 || zones.empty?
end
private
##
# Generates an unique name
#
# @return [String] Unique name
def generate_unique_name
SecureRandom.uuid
end
##
# Prepare server user data
#
# @param [String] server_name server name
# @param [Hash] network_spec network specification
# @return [Hash] server user data
def user_data(server_name, network_spec, public_key = nil)
data = {}
data['registry'] = { 'endpoint' => @registry.endpoint }
data['server'] = { 'name' => server_name }
data['openssh'] = { 'public_key' => public_key } if public_key
with_dns(network_spec) do |servers|
data['dns'] = { 'nameserver' => servers }
end
data
end
##
# Extract dns server list from network spec and yield the the list
#
# @param [Hash] network_spec network specification for instance
# @yield [Array]
def with_dns(network_spec)
network_spec.each_value do |properties|
if properties.has_key?('dns') && !properties['dns'].nil?
yield properties['dns']
return
end
end
end
##
# Generates initial agent settings. These settings will be read by Bosh Agent from Bosh Registry on a target
# server. Disk conventions in Bosh Agent for OpenStack are:
# - system disk: /dev/sda
# - ephemeral disk: /dev/sdb
# - persistent disks: /dev/sdc through /dev/sdz
# As some kernels remap device names (from sd* to vd* or xvd*), Bosh Agent will lookup for the proper device name
#
# @param [String] server_name Name of the OpenStack server (will be picked
# up by agent to fetch registry settings)
# @param [String] agent_id Agent id (will be picked up by agent to
# assume its identity
# @param [Hash] network_spec Agent network spec
# @param [Hash] environment Environment settings
# @param [Boolean] has_ephemeral Has Ephemeral disk?
# @return [Hash] Agent settings
def initial_agent_settings(server_name, agent_id, network_spec, environment, has_ephemeral)
settings = {
'vm' => {
'name' => server_name
},
'agent_id' => agent_id,
'networks' => network_spec,
'disks' => {
'system' => '/dev/sda',
'persistent' => {}
}
}
settings['disks']['ephemeral'] = has_ephemeral ? '/dev/sdb' : nil
settings['env'] = environment if environment
settings.merge(@agent_properties)
end
##
# Updates the agent settings
#
# @param [Fog::Compute::OpenStack::Server] server OpenStack server
def update_agent_settings(server)
raise ArgumentError, 'Block is not provided' unless block_given?
@logger.info("Updating settings for server `#{server.id}'...")
settings = @registry.read_settings(server.name)
yield settings
@registry.update_settings(server.name, settings)
end
##
# Soft reboots an OpenStack server
#
# @param [Fog::Compute::OpenStack::Server] server OpenStack server
# @return [void]
def soft_reboot(server)
@logger.info("Soft rebooting server `#{server.id}'...")
with_openstack { server.reboot }
wait_resource(server, :active, :state)
end
##
# Hard reboots an OpenStack server
#
# @param [Fog::Compute::OpenStack::Server] server OpenStack server
# @return [void]
def hard_reboot(server)
@logger.info("Hard rebooting server `#{server.id}'...")
with_openstack { server.reboot(type = 'HARD') }
wait_resource(server, :active, :state)
end
##
# Attaches an OpenStack volume to an OpenStack server
#
# @param [Fog::Compute::OpenStack::Server] server OpenStack server
# @param [Fog::Compute::OpenStack::Volume] volume OpenStack volume
# @return [String] Device name
def attach_volume(server, volume)
@logger.info("Attaching volume `#{volume.id}' to server `#{server.id}'...")
volume_attachments = with_openstack { server.volume_attachments }
device = volume_attachments.find { |a| a['volumeId'] == volume.id }
if device.nil?
device_name = select_device_name(volume_attachments, first_device_name_letter(server))
cloud_error('Server has too many disks attached') if device_name.nil?
@logger.info("Attaching volume `#{volume.id}' to server `#{server.id}', device name is `#{device_name}'")
with_openstack { volume.attach(server.id, device_name) }
wait_resource(volume, :'in-use')
else
device_name = device['device']
@logger.info("Volume `#{volume.id}' is already attached to server `#{server.id}' in `#{device_name}'. Skipping.")
end
device_name
end
##
# Select the first available device name
#
# @param [Array] volume_attachments Volume attachments
# @param [String] first_device_name_letter First available letter for device names
# @return [String] First available device name or nil is none is available
def select_device_name(volume_attachments, first_device_name_letter)
(first_device_name_letter..'z').each do |char|
# Some kernels remap device names (from sd* to vd* or xvd*).
device_names = ["/dev/sd#{char}", "/dev/vd#{char}", "/dev/xvd#{char}"]
# Bosh Agent will lookup for the proper device name if we set it initially to sd*.
return "/dev/sd#{char}" if volume_attachments.select { |v| device_names.include?( v['device']) }.empty?
@logger.warn("`/dev/sd#{char}' is already taken")
end
nil
end
##
# Returns the first letter to be used on device names
#
# @param [Fog::Compute::OpenStack::Server] server OpenStack server
# @return [String] First available letter
def first_device_name_letter(server)
letter = "#{FIRST_DEVICE_NAME_LETTER}"
return letter if server.flavor.nil?
return letter unless server.flavor.has_key?('id')
flavor = with_openstack { @openstack.flavors.find { |f| f.id == server.flavor['id'] } }
return letter if flavor.nil?
letter.succ! if flavor_has_ephemeral_disk?(flavor)
letter.succ! if flavor_has_swap_disk?(flavor)
letter
end
##
# Detaches an OpenStack volume from an OpenStack server
#
# @param [Fog::Compute::OpenStack::Server] server OpenStack server
# @param [Fog::Compute::OpenStack::Volume] volume OpenStack volume
# @return [void]
def detach_volume(server, volume)
@logger.info("Detaching volume `#{volume.id}' from `#{server.id}'...")
volume_attachments = with_openstack { server.volume_attachments }
if volume_attachments.find { |a| a['volumeId'] == volume.id }
with_openstack { volume.detach(server.id, volume.id) }
wait_resource(volume, :available)
else
@logger.info("Disk `#{volume.id}' is not attached to server `#{server.id}'. Skipping.")
end
end
##
# Compares actual server security groups with those specified at the network spec
#
# @param [Fog::Compute::OpenStack::Server] server OpenStack server
# @param [Array] specified_sg_names Security groups specified at the network spec
# @return [void]
# @raise [Bosh::Clouds:NotSupported] If the security groups change, we need to recreate the VM as you can't
# change the security group of a running server, so we need to send the InstanceUpdater a request to do it for us
def compare_security_groups(server, specified_sg_names)
actual_sg_names = with_openstack { server.security_groups }.collect { |sg| sg.name }
unless actual_sg_names.sort == specified_sg_names.sort
raise Bosh::Clouds::NotSupported,
'security groups change requires VM recreation: %s to %s' %
[actual_sg_names.join(', '), specified_sg_names.join(', ')]
end
end
##
# Compares actual server private IP addresses with the IP address specified at the network spec
#
# @param [Fog::Compute::OpenStack::Server] server OpenStack server
# @param [String] specified_ip_address IP address specified at the network spec (if Manual network)
# @return [void]
# @raise [Bosh::Clouds:NotSupported] If the IP address change, we need to recreate the VM as you can't
# change the IP address of a running server, so we need to send the InstanceUpdater a request to do it for us
def compare_private_ip_addresses(server, specified_ip_address)
actual_ip_addresses = with_openstack { server.private_ip_addresses }
unless specified_ip_address.nil? || actual_ip_addresses.include?(specified_ip_address)
raise Bosh::Clouds::NotSupported,
'IP address change requires VM recreation: %s to %s' %
[actual_ip_addresses.join(', '), specified_ip_address]
end
end
##
# Checks if the OpenStack flavor has ephemeral disk
#
# @param [Fog::Compute::OpenStack::Flavor] OpenStack flavor
# @return [Boolean] true if flavor has ephemeral disk, false otherwise
def flavor_has_ephemeral_disk?(flavor)
flavor.ephemeral.nil? || flavor.ephemeral.to_i <= 0 ? false : true
end
##
# Checks if the OpenStack flavor has swap disk
#
# @param [Fog::Compute::OpenStack::Flavor] OpenStack flavor
# @return [Boolean] true if flavor has swap disk, false otherwise
def flavor_has_swap_disk?(flavor)
flavor.swap.nil? || flavor.swap.to_i <= 0 ? false : true
end
##
# Unpacks a stemcell archive
#
# @param [String] tmp_dir Temporary directory
# @param [String] image_path Local filesystem path to a stemcell image
# @return [void]
def unpack_image(tmp_dir, image_path)
result = Bosh::Exec.sh("tar -C #{tmp_dir} -xzf #{image_path} 2>&1", :on_error => :return)
if result.failed?
@logger.error("Extracting stemcell root image failed in dir #{tmp_dir}, " +
"tar returned #{result.exit_status}, output: #{result.output}")
cloud_error('Extracting stemcell root image failed. Check task debug log for details.')
end
root_image = File.join(tmp_dir, 'root.img')
unless File.exists?(root_image)
cloud_error('Root image is missing from stemcell archive')
end
end
##
# Checks if options passed to CPI are valid and can actually
# be used to create all required data structures etc.
#
# @return [void]
# @raise [ArgumentError] if options are not valid
def validate_options
unless @options['openstack'].is_a?(Hash) &&
@options.has_key?('openstack') &&
@options['openstack']['auth_url'] &&
@options['openstack']['username'] &&
@options['openstack']['api_key'] &&
@options['openstack']['tenant']
raise ArgumentError, 'Invalid OpenStack configuration parameters'
end
unless @options.has_key?('registry') &&
@options['registry'].is_a?(Hash) &&
@options['registry']['endpoint'] &&
@options['registry']['user'] &&
@options['registry']['password']
raise ArgumentError, 'Invalid registry configuration parameters'
end
end
def initialize_registry
registry_properties = @options.fetch('registry')
registry_endpoint = registry_properties.fetch('endpoint')
registry_user = registry_properties.fetch('user')
registry_password = registry_properties.fetch('password')
@registry = Bosh::Registry::Client.new(registry_endpoint,
registry_user,
registry_password)
end
end
end