class Bosh::OpenStackCloud::Cloud
BOSH OpenStack CPI
Constants
- BOSH_APP_DIR
- CONNECT_RETRY_COUNT
- CONNECT_RETRY_DELAY
- FIRST_DEVICE_NAME_LETTER
- OPTION_KEYS
Attributes
Public Class Methods
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
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
# File lib/cloud/openstack/cloud.rb, line 102 def auth_url @openstack_properties['auth_url'] 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
# 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
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
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
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
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
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
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
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
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
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
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
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
# File lib/cloud/openstack/cloud.rb, line 683 def is_v3 @options['openstack']['auth_url'].match(/\/v3(?=\/|$)/) end
# 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
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
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 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
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
# 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
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
# 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
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
# 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 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
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
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
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
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
Generates an unique name
@return [String] Unique name
# File lib/cloud/openstack/cloud.rb, line 737 def generate_unique_name SecureRandom.uuid end
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
# 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
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
# 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
# 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
# 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
# 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 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
# 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 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
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
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
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
# 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
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
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