class Bosh::OpenStackCloud::Cloud

BOSH OpenStack CPI

Constants

BOSH_APP_DIR
CONNECT_RETRY_COUNT
CONNECT_RETRY_DELAY
FIRST_DEVICE_NAME_LETTER
OPTION_KEYS

Attributes

glance[R]
logger[RW]
openstack[R]
registry[R]
state_timeout[R]
volume[R]

Public Class Methods

new(options) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 31
def initialize(options)
  @options = normalize_options(options)

  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"]
  @boot_volume_cloud_properties = @openstack_properties["boot_volume_cloud_properties"] || {}
  @use_dhcp = @openstack_properties.fetch('use_dhcp', true)

  unless @openstack_properties['auth_url'].match(/\/tokens$/)
    if is_v3
      @openstack_properties['auth_url'] += '/auth/tokens'
    else
      @openstack_properties['auth_url'] += '/tokens'
    end
  end

  @openstack_properties['connection_options'] ||= {}

  @extra_connection_options = {'instrumentor' => Bosh::OpenStackCloud::ExconLoggingInstrumentor}

  @openstack_params = openstack_params

  connect_retry_errors = [Excon::Errors::GatewayTimeout, Excon::Errors::SocketError]

  @connect_retry_options = {
    sleep: CONNECT_RETRY_DELAY,
    tries: CONNECT_RETRY_COUNT,
    on: connect_retry_errors,
  }

  begin
    Bosh::Common.retryable(@connect_retry_options) do |tries, error|
      @logger.error("Failed #{tries} times, last failure due to: #{error.inspect}") unless error.nil?
      @openstack = Fog::Compute.new(@openstack_params)
    end
  rescue Excon::Errors::SocketError => e
    cloud_error(socket_error_msg + "#{e.message}")
  rescue Bosh::Common::RetryCountExceeded, Excon::Errors::ClientError, Excon::Errors::ServerError
    cloud_error('Unable to connect to the OpenStack Compute API. Check task debug log for details.')
  end

  @az_provider = Bosh::OpenStackCloud::AvailabilityZoneProvider.new(
    @openstack,
    @openstack_properties["ignore_server_availability_zone"])

  begin
    Bosh::Common.retryable(@connect_retry_options) do |tries, error|
      @logger.error("Failed #{tries} times, last failure due to: #{error.inspect}") unless error.nil?
      @glance = Fog::Image::OpenStack::V1.new(@openstack_params)
    end
  rescue Excon::Errors::SocketError => e
    cloud_error(socket_error_msg + "#{e.message}")
  rescue Bosh::Common::RetryCountExceeded, Excon::Errors::ClientError, Excon::Errors::ServerError
    cloud_error('Unable to connect to the OpenStack Image Service API. Check task debug log for details.')
  end

  @metadata_lock = Mutex.new
end

Public Instance Methods

attach_disk(server_id, disk_id) click to toggle source

Attaches an OpenStack volume to an OpenStack server

@param [String] server_id OpenStack server UUID @param [String] disk_id OpenStack volume UUID @return [void]

# File lib/cloud/openstack/cloud.rb, line 547
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
auth_url() click to toggle source
# File lib/cloud/openstack/cloud.rb, line 102
def auth_url
  @openstack_properties['auth_url']
end
configure_networks(server_id, network_spec) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 419
def configure_networks(server_id, network_spec)
  with_thread_name("configure_networks(#{server_id}, ...)") do
    raise Bosh::Clouds::NotSupported,
      'network configuration change requires VM recreation: %s' % [network_spec]

  end
end
create_boot_disk(size, stemcell_id, availability_zone = nil, boot_volume_cloud_properties = {}) click to toggle source

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

@param [optional, String] availability_zone to be passed to the volume API @param [optional, String] volume_type to be passed to the volume API @return [String] OpenStack volume UUID

# File lib/cloud/openstack/cloud.rb, line 476
def create_boot_disk(size, stemcell_id, availability_zone = nil, boot_volume_cloud_properties = {})
  volume_service_client = connect_to_volume_service
  with_thread_name("create_boot_disk(#{size}, #{stemcell_id}, #{availability_zone}, #{boot_volume_cloud_properties})") 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)

    volume_params = {
      :display_name => "volume-#{generate_unique_name}",
      :size => (size / 1024.0).ceil,
      :imageRef => stemcell_id
    }

    if availability_zone && @az_provider.constrain_to_server_availability_zone?
      volume_params[:availability_zone] = availability_zone
    end
    volume_params[:volume_type] = boot_volume_cloud_properties["type"] if boot_volume_cloud_properties["type"]

    @logger.info("Creating new boot volume...")
    boot_volume = with_openstack { volume_service_client.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
create_disk(size, cloud_properties, server_id = nil) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 434
def create_disk(size, cloud_properties, server_id = nil)
  volume_service_client = connect_to_volume_service
  with_thread_name("create_disk(#{size}, #{cloud_properties}, #{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)

    volume_params = {
      :display_name => "volume-#{generate_unique_name}",
      :display_description => '',
      :size => (size / 1024.0).ceil
    }

    if cloud_properties.has_key?('type')
      volume_params[:volume_type] = cloud_properties['type']
    end

    if server_id  && @az_provider.constrain_to_server_availability_zone?
      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...')
    new_volume = with_openstack { volume_service_client.volumes.create(volume_params) }

    @logger.info("Creating new volume `#{new_volume.id}'...")
    wait_resource(new_volume, :available)

    new_volume.id.to_s
  end
end
create_stemcell(image_path, cloud_properties) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 122
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',
                           'hw_vif_model', 'hypervisor_type', 'vmware_adaptertype', 'vmware_disktype',
                           'vmware_linked_clone', 'vmware_ostype']
        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
create_vm(agent_id, stemcell_id, resource_pool, network_spec = nil, disk_locality = nil, environment = nil) click to toggle source

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] network_spec 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

# File lib/cloud/openstack/cloud.rb, line 207
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)

    network_spec_security_groups = network_configurator.security_groups
    resource_pool_spec_security_groups = resource_pool_spec_security_groups(resource_pool)

    if network_spec_security_groups.size > 0 && resource_pool_spec_security_groups.size > 0
      cloud_error('Cannot define security groups in both network and resource pool.')
    end

    openstack_security_groups = with_openstack { @openstack.security_groups }.collect { |sg| sg.name }
    network_security_groups = network_configurator.security_groups(@default_security_groups)
    security_groups_to_be_used = resource_pool_spec_security_groups.size > 0 ? resource_pool_spec_security_groups : network_security_groups

    security_groups_to_be_used.each do |sg|
      cloud_error("Security group `#{sg}' not found") unless openstack_security_groups.include?(sg)
    end
    @logger.debug("Using security groups: `#{security_groups_to_be_used.join(', ')}'")

    nics = network_configurator.nics
    @logger.debug("Using NICs: `#{nics.join(', ')}'")

    image = nil
    begin
      Bosh::Common.retryable(@connect_retry_options) do |tries, error|
        @logger.error("Unable to connect to OpenStack API to find image: `#{stemcell_id} due to: #{error.inspect}") unless error.nil?
        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}'")
      end
    rescue Bosh::Common::RetryCountExceeded, Excon::Errors::SocketError => e
      cloud_error("Unable to connect to OpenStack API to find image: `#{stemcell_id}'")
    end

    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 /var/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
    validate_key_exists(keyname)

    use_config_drive = !!@openstack_properties.fetch("config_drive", nil)

    if resource_pool['scheduler_hints']
      @logger.debug("Using scheduler hints: `#{resource_pool['scheduler_hints']}'")
    end

    server_params = {
      :name => server_name,
      :image_ref => image.id,
      :flavor_ref => flavor.id,
      :key_name => keyname,
      :security_groups => security_groups_to_be_used,
      :os_scheduler_hints => resource_pool['scheduler_hints'],
      :nics => nics,
      :config_drive => use_config_drive,
      :user_data => Yajl::Encoder.encode(user_data(server_name, network_spec))
    }

    availability_zone = @az_provider.select(disk_locality, resource_pool['availability_zone'])
    server_params[:availability_zone] = availability_zone if availability_zone

    if @boot_from_volume
      boot_vol_size = flavor.disk * 1024

      boot_vol_id = create_boot_disk(boot_vol_size, stemcell_id, availability_zone, @boot_volume_cloud_properties)
      cloud_error("Failed to create boot volume.") if boot_vol_id.nil?
      @logger.debug("Using boot volume: `#{boot_vol_id}'")

      server_params[:block_device_mapping] = [{
                                               :volume_size => boot_vol_size,
                                               :volume_id => boot_vol_id,
                                               :delete_on_termination => "1",
                                               :device_name => "/dev/vda"
                                             }]
    end

    @logger.debug("Using boot parms: `#{server_params.inspect}'")
    begin
      server = with_openstack { @openstack.servers.create(server_params) }
    rescue Excon::Errors::Timeout => e
      @logger.debug(e.backtrace)
      cloud_error_message = "VM creation with name '#{server_params[:name]}' received a timeout. " +
                            "The VM might still have been created by OpenStack.\nOriginal message: "
      raise Bosh::Clouds::VMCreationFailed.new(false), cloud_error_message + e.message
    rescue Excon::Errors::NotFound, Fog::Compute::OpenStack::NotFound => e
      not_existing_net_ids = not_existing_net_ids(nics)
      if not_existing_net_ids.empty?
        raise e
      else
        @logger.debug(e.backtrace)
        cloud_error_message = "VM creation with name '#{server_params[:name]}' failed. Following network " +
        "IDs are not existing or not accessible from this project: '#{not_existing_net_ids.join(",")}'. " +
        "Make sure you do not use subnet IDs"
        raise Bosh::Clouds::VMCreationFailed.new(false), cloud_error_message
      end
    end

    @logger.info("Creating new server `#{server.id}'...")
    begin
      wait_resource(server, :active, :state)

      @logger.info("Configuring network for server `#{server.id}'...")
      network_configurator.configure(@openstack, server)
    rescue => e
      @logger.warn("Failed to create server: #{e.message}")
      destroy_server(server)
      raise Bosh::Clouds::VMCreationFailed.new(true), e.message
    end

    begin
      @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)
    rescue => e
      @logger.warn("Failed to register server: #{e.message}")
      destroy_server(server)
      raise Bosh::Clouds::VMCreationFailed.new(false), e.message
    end

    server.id.to_s
  end
end
delete_disk(disk_id) click to toggle source

Deletes an OpenStack volume

@param [String] disk_id OpenStack volume UUID @return [void] @raise [Bosh::Clouds::CloudError] if disk is not in available state

# File lib/cloud/openstack/cloud.rb, line 523
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
delete_snapshot(snapshot_id) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 632
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
delete_stemcell(stemcell_id) click to toggle source

Deletes a stemcell

@param [String] stemcell_id OpenStack image UUID of the stemcell to be

deleted

@return [void]

# File lib/cloud/openstack/cloud.rb, line 177
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
delete_vm(server_id) click to toggle source

Terminates an OpenStack server and waits until it reports as terminated

@param [String] server_id OpenStack server UUID @return [void]

# File lib/cloud/openstack/cloud.rb, line 370
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
detach_disk(server_id, disk_id) click to toggle source

Detaches an OpenStack volume from an OpenStack server

@param [String] server_id OpenStack server UUID @param [String] disk_id OpenStack volume UUID @return [void]

# File lib/cloud/openstack/cloud.rb, line 571
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) }
    if volume.nil?
      @logger.info("Disk `#{disk_id}' not found while trying to detach it from vm `#{server_id}'...")
    else
      detach_volume(server, volume)
    end

    update_agent_settings(server) do |settings|
      settings['disks'] ||= {}
      settings['disks']['persistent'] ||= {}
      settings['disks']['persistent'].delete(disk_id)
    end
  end
end
has_disk?(disk_id) click to toggle source

Check whether an OpenStack volume exists or not

@param [String] disk_id OpenStack volume UUID @return [bool] whether the specific disk is there or not

# File lib/cloud/openstack/cloud.rb, line 508
def has_disk?(disk_id)
  with_thread_name("has_disk?(#{disk_id})") do
    @logger.info("Check the presence of disk with id `#{disk_id}'...")
    volume = with_openstack { @openstack.volumes.get(disk_id) }

    !volume.nil?
  end
end
has_vm?(server_id) click to toggle source

Checks if an OpenStack server exists

@param [String] server_id OpenStack server UUID @return [Boolean] True if the vm exists

# File lib/cloud/openstack/cloud.rb, line 391
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
is_v3() click to toggle source
# File lib/cloud/openstack/cloud.rb, line 683
def is_v3
  @options['openstack']['auth_url'].match(/\/v3(?=\/|$)/)
end
not_existing_net_ids(nics) click to toggle source
# File lib/cloud/openstack/cloud.rb, line 349
def not_existing_net_ids(nics)
  result = []
  begin
    network = connect_to_network_service
    nics.each do |nic|
      if nic["net_id"]
        result << nic["net_id"] unless network.networks.get(nic["net_id"])
      end
    end
  rescue Bosh::Clouds::CloudError => e
    @logger.debug(e.backtrace)
  end
  result
end
reboot_vm(server_id) click to toggle source

Reboots an OpenStack Server

@param [String] server_id OpenStack server UUID @return [void]

# File lib/cloud/openstack/cloud.rb, line 403
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
select_availability_zone(volumes, resource_pool_az) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 679
def select_availability_zone(volumes, resource_pool_az)
  @az_provider.select(volumes, resource_pool_az)
end
set_vm_metadata(server_id, metadata) click to toggle source

Set metadata for an OpenStack server

@param [String] server_id OpenStack server UUID @param [Hash] metadata Metadata key/value pairs @return [void]

# File lib/cloud/openstack/cloud.rb, line 656
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
snapshot_disk(disk_id, metadata) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 598
def snapshot_disk(disk_id, metadata)
  with_thread_name("snapshot_disk(#{disk_id})") do
    metadata = Hash[metadata.map{|key,value| [key.to_s, value] }]
    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

Private Instance Methods

agent_network_spec(network_spec) click to toggle source
# File lib/cloud/openstack/cloud.rb, line 810
def agent_network_spec(network_spec)
  Hash[*network_spec.map do |name, settings|
    settings['use_dhcp'] = @use_dhcp
    [name, settings]
  end.flatten]
end
attach_volume(server, volume) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 858
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
connect_to_network_service() click to toggle source
# File lib/cloud/openstack/cloud.rb, line 707
def connect_to_network_service
  begin
    Bosh::Common.retryable(@connect_retry_options) do |tries, error|
      @logger.error("Failed #{tries} times, last failure due to: #{error.inspect}") unless error.nil?
      network ||= Fog::Network.new(@openstack_params)
    end
  rescue Bosh::Common::RetryCountExceeded, Excon::Errors::ClientError, Excon::Errors::ServerError => e
    cloud_error("Unable to connect to the OpenStack Network API: #{e.message}. Check task debug log for details.")
  end
end
connect_to_volume_service() click to toggle source

Creates a client for the OpenStack volume service, or return the existing connectuion

# File lib/cloud/openstack/cloud.rb, line 694
def connect_to_volume_service
  begin
    Bosh::Common.retryable(@connect_retry_options) do |tries, error|
      @logger.error("Failed #{tries} times, last failure due to: #{error.inspect}") unless error.nil?
      @volume ||= Fog::Volume::OpenStack::V1.new(@openstack_params)
    end
  rescue Bosh::Common::RetryCountExceeded, Excon::Errors::ClientError, Excon::Errors::ServerError => e
    cloud_error("Unable to connect to the OpenStack Volume API: #{e.message}. Check task debug log for details.")
  end

  @volume
end
delete_entries_with_nil_keys(options) click to toggle source
# File lib/cloud/openstack/cloud.rb, line 1067
def delete_entries_with_nil_keys(options)
  options.each do |key, value|
    if value == nil
      options.delete(key)
    elsif value.kind_of?(Hash)
      options[key] = delete_entries_with_nil_keys(value.dup)
    end
  end
  options
end
destroy_server(server) click to toggle source

Destroy server and wait until the server is really terminated/deleted

# File lib/cloud/openstack/cloud.rb, line 1079
def destroy_server(server)
  with_openstack { server.destroy }

  begin
    wait_resource(server, [:terminated, :deleted], :state, true)
  rescue Bosh::Clouds::CloudError => delete_server_error
    @logger.warn("Failed to destroy server: #{delete_server_error.inspect}\n#{delete_server_error.backtrace.join('\n')}")
  end
end
detach_volume(server, volume) click to toggle source

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]

# File lib/cloud/openstack/cloud.rb, line 920
def detach_volume(server, volume)
  @logger.info("Detaching volume `#{volume.id}' from `#{server.id}'...")
  volume_attachments = with_openstack { server.volume_attachments }
  attachment = volume_attachments.find { |a| a['volumeId'] == volume.id }
  if attachment
    with_openstack { volume.detach(server.id, attachment['id']) }
    wait_resource(volume, :available)
  else
    @logger.info("Disk `#{volume.id}' is not attached to server `#{server.id}'. Skipping.")
  end
end
first_device_name_letter(server) click to toggle source

Returns the first letter to be used on device names

@param [Fog::Compute::OpenStack::Server] server OpenStack server @return [String] First available letter

# File lib/cloud/openstack/cloud.rb, line 901
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.succ! if @openstack_properties['config_drive'] == 'disk'
  letter
end
flavor_has_ephemeral_disk?(flavor) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 937
def flavor_has_ephemeral_disk?(flavor)
  flavor.ephemeral && flavor.ephemeral.to_i > 0
end
flavor_has_swap_disk?(flavor) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 946
def flavor_has_swap_disk?(flavor)
  flavor.swap.nil? || flavor.swap.to_i <= 0 ? false : true
end
generate_unique_name() click to toggle source

Generates an unique name

@return [String] Unique name

# File lib/cloud/openstack/cloud.rb, line 737
def generate_unique_name
  SecureRandom.uuid
end
hard_reboot(server) click to toggle source

Hard reboots an OpenStack server

@param [Fog::Compute::OpenStack::Server] server OpenStack server @return [void]

# File lib/cloud/openstack/cloud.rb, line 846
def hard_reboot(server)
  @logger.info("Hard rebooting server `#{server.id}'...")
  with_openstack { server.reboot(type = 'HARD') }
  wait_resource(server, :active, :state)
end
hash_filter(hash) { |key| ... } click to toggle source
# File lib/cloud/openstack/cloud.rb, line 1059
def hash_filter(hash)
  copy = {}
  hash.each do |key, value|
    copy[key] = value if yield(key)
  end
  copy
end
initial_agent_settings(server_name, agent_id, network_spec, environment, has_ephemeral) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 792
def initial_agent_settings(server_name, agent_id, network_spec, environment, has_ephemeral)
  settings = {
    'vm' => {
      'name' => server_name
    },
    'agent_id' => agent_id,
    'networks' => agent_network_spec(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
initialize_registry() click to toggle source
# File lib/cloud/openstack/cloud.rb, line 1038
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
normalize_options(options) click to toggle source
# File lib/cloud/openstack/cloud.rb, line 1049
def normalize_options(options)
  unless options.kind_of?(Hash)
    raise ArgumentError, "Invalid OpenStack cloud properties: Hash expected, received #{options}"
  end
  # we only care about two top-level fields
  options = hash_filter(options.dup) { |key| OPTION_KEYS.include?(key) }
  # nil values should be treated the same as missing keys (makes validating optional fields easier)
  delete_entries_with_nil_keys(options)
end
openstack_params() click to toggle source
# File lib/cloud/openstack/cloud.rb, line 718
def 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_project_name => @openstack_properties['project'],
      :openstack_domain_name => @openstack_properties['domain'],
      :openstack_region => @openstack_properties['region'],
      :openstack_endpoint_type => @openstack_properties['endpoint_type'],
      :connection_options => @openstack_properties['connection_options'].merge(@extra_connection_options)
  }
end
resource_pool_spec_security_groups(resource_pool_spec) click to toggle source
# File lib/cloud/openstack/cloud.rb, line 1089
def resource_pool_spec_security_groups(resource_pool_spec)
  if resource_pool_spec && resource_pool_spec.has_key?("security_groups")
    unless resource_pool_spec["security_groups"].is_a?(Array)
      raise ArgumentError, "security groups must be an Array"
    end
    return resource_pool_spec["security_groups"]
  end

  []
end
select_device_name(volume_attachments, first_device_name_letter) click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 884
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
socket_error_msg() click to toggle source
# File lib/cloud/openstack/cloud.rb, line 1106
def socket_error_msg
  "Unable to connect to the OpenStack Keystone API #{@openstack_params[:openstack_auth_url]}\n"
end
soft_reboot(server) click to toggle source

Soft reboots an OpenStack server

@param [Fog::Compute::OpenStack::Server] server OpenStack server @return [void]

# File lib/cloud/openstack/cloud.rb, line 835
def soft_reboot(server)
  @logger.info("Soft rebooting server `#{server.id}'...")
  with_openstack { server.reboot }
  wait_resource(server, :active, :state)
end
unpack_image(tmp_dir, image_path) click to toggle source

Unpacks a stemcell archive

@param [String] tmp_dir Temporary directory @param [String] image_path Local filesystem path to a stemcell image @return [void]

# File lib/cloud/openstack/cloud.rb, line 956
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
update_agent_settings(server) { |settings| ... } click to toggle source

Updates the agent settings

@param [Fog::Compute::OpenStack::Server] server OpenStack server

# File lib/cloud/openstack/cloud.rb, line 821
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
user_data(server_name, network_spec, public_key = nil) click to toggle source

Prepare server user data

@param [String] server_name server name @param [Hash] network_spec network specification @return [Hash] server user data

# File lib/cloud/openstack/cloud.rb, line 747
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
  data['networks'] = agent_network_spec(network_spec)

  with_dns(network_spec) do |servers|
    data['dns'] = { 'nameserver' => servers }
  end

  data
end
validate_key_exists(keyname) click to toggle source
# File lib/cloud/openstack/cloud.rb, line 1100
def validate_key_exists(keyname)
  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})")
end
validate_options() click to toggle source

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

# File lib/cloud/openstack/cloud.rb, line 975
def validate_options
  if @options['openstack'] && is_v3
    schema = Membrane::SchemaParser.parse do
      {
        'openstack' => {
          'auth_url' => String,
          'username' => String,
          'api_key' => String,
          'project' => String,
          'domain' => String,
          optional('region') => String,
          optional('endpoint_type') => String,
          optional('state_timeout') => Numeric,
          optional('stemcell_public_visibility') => enum(String, bool),
          optional('connection_options') => Hash,
          optional('boot_from_volume') => bool,
          optional('default_key_name') => String,
          optional('default_security_groups') => [String],
          optional('wait_resource_poll_interval') => Integer,
          optional('config_drive') => enum('disk', 'cdrom'),
        },
        'registry' => {
          'endpoint' => String,
          'user' => String,
          'password' => String,
        },
        optional('agent') => Hash,
      }
    end
  else
    schema = Membrane::SchemaParser.parse do
      {
        'openstack' => {
          'auth_url' => String,
          'username' => String,
          'api_key' => String,
          'tenant' => String,
          optional('domain') => String,
          optional('region') => String,
          optional('endpoint_type') => String,
          optional('state_timeout') => Numeric,
          optional('stemcell_public_visibility') => enum(String, bool),
          optional('connection_options') => Hash,
          optional('boot_from_volume') => bool,
          optional('default_key_name') => String,
          optional('default_security_groups') => [String],
          optional('wait_resource_poll_interval') => Integer,
          optional('config_drive') => enum('disk', 'cdrom'),
        },
        'registry' => {
          'endpoint' => String,
          'user' => String,
          'password' => String,
        },
        optional('agent') => Hash,
      }
    end
  end
  schema.validate(@options)
rescue Membrane::SchemaValidationError => e
  raise ArgumentError, "Invalid OpenStack cloud properties: #{e.inspect}"
end
with_dns(network_spec) { |properties| ... } click to toggle source

Extract dns server list from network spec and yield the the list

@param [Hash] network_spec network specification for instance @yield [Array]

# File lib/cloud/openstack/cloud.rb, line 767
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