class Chef::Knife::Cloud::GoogleService

Constants

IMAGE_ALIAS_MAP
PUBLIC_PROJECTS
SCOPE_ALIAS_MAP

Attributes

max_page_size[R]
max_pages[R]
project[R]
refresh_rate[R]
wait_time[R]
zone[R]

Public Class Methods

new(options = {}) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 101
def initialize(options = {})
  @project       = options[:project]
  @zone          = options[:zone]
  @wait_time     = options[:wait_time]
  @refresh_rate  = options[:refresh_rate]
  @max_pages     = options[:max_pages]
  @max_page_size = options[:max_page_size]
end

Public Instance Methods

adding_local_ssd(options) click to toggle source

To create a instance with an attached local SSD

# File lib/chef/knife/cloud/google_service.rb, line 345
def adding_local_ssd(options)
  disk, params = common_operation(options, "local-ssd")
  disk.type = "SCRATCH"
  disk.interface = options[:interface]

  disk.initialize_params = params
  disk
end
authorization() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 123
def authorization
  @authorization ||= Google::Auth.get_application_default(
    [
      "https://www.googleapis.com/auth/cloud-platform",
      "https://www.googleapis.com/auth/compute",
    ]
  )
end
available_projects() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 648
def available_projects
  [project] | PUBLIC_PROJECTS
end
boot_disk_name_for(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 422
def boot_disk_name_for(options)
  options[:boot_disk_name].nil? ? options[:name] : options[:boot_disk_name]
end
boot_disk_source_image(image, image_project) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 366
def boot_disk_source_image(image, image_project)
  @boot_disk_source ||= image_search_for(image, image_project)
end
boot_disk_type_for(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 362
def boot_disk_type_for(options)
  options[:boot_disk_ssd] ? "pd-ssd" : "pd-standard"
end
check_api_call() { || ... } click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 247
def check_api_call
  yield
rescue Google::Apis::ClientError
  false
else
  true
end
common_operation(options, disk_type) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 354
def common_operation(options, disk_type)
  disk = Google::Apis::ComputeV1::AttachedDisk.new
  params = Google::Apis::ComputeV1::AttachedDiskInitializeParams.new
  disk.auto_delete = options[:boot_disk_autodelete]
  params.disk_type = disk_type_url_for(disk_type)
  [disk, params]
end
connection() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 110
def connection
  return @connection unless @connection.nil?

  @connection = Google::Apis::ComputeV1::ComputeService.new
  @connection.authorization = authorization
  @connection.client_options = Google::Apis::ClientOptions.new.tap do |opts|
    opts.application_name    = "knife-google"
    opts.application_version = Knife::Google::VERSION
  end

  @connection
end
create_disk(name, size, type, source_image = nil) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 168
def create_disk(name, size, type, source_image = nil)
  disk = Google::Apis::ComputeV1::Disk.new
  disk.name    = name
  disk.size_gb = size
  disk.type    = disk_type_url_for(type)

  ui.msg("Creating a #{size} GB disk named #{name}...")

  wait_for_operation(connection.insert_disk(project, zone, disk, source_image: source_image))

  ui.msg("Waiting for disk to be ready...")

  wait_for_status("READY") { connection.get_disk(project, zone, name) }

  ui.msg("Disk created successfully.")
end
create_server(options = {}) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 132
def create_server(options = {})
  validate_server_create_options!(options)

  ui.msg("Creating instance...")

  instance_object = instance_object_for(options)
  wait_for_operation(connection.insert_instance(project, zone, instance_object))
  wait_for_status("RUNNING") { get_server(options[:name]) }

  ui.msg("Instance created!")

  get_server(options[:name])
end
delete_disk(name) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 185
def delete_disk(name)
  begin
    connection.get_disk(project, zone, name)
  rescue Google::Apis::ClientError
    ui.warn("Unable to locate disk #{name} in project #{project}, zone #{zone}")
    return
  end

  ui.confirm("Do you really want to delete disk #{name}")

  ui.msg("Deleting disk #{name}...")
  wait_for_operation(connection.delete_disk(project, zone, name))
  ui.msg("Disk #{name} deleted successfully.")
end
delete_server(instance_name) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 146
def delete_server(instance_name)
  begin
    instance = get_server(instance_name)
  rescue Google::Apis::ClientError
    ui.warn("Unable to locate instance #{instance_name} in project #{project}, zone #{zone}")
    return
  end

  server_summary(instance)
  ui.confirm("Do you really want to delete this instance")

  ui.msg("Deleting instance #{instance_name}...")

  wait_for_operation(connection.delete_instance(project, zone, instance_name))

  ui.msg("Instance #{instance_name} deleted successfully.")
end
disk_type_url_for(type) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 559
def disk_type_url_for(type)
  "zones/#{zone}/diskTypes/#{type}"
end
get_server(instance_name) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 164
def get_server(instance_name)
  connection.get_instance(project, zone, instance_name)
end
image_alias_url(image_alias) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 407
def image_alias_url(image_alias)
  return unless IMAGE_ALIAS_MAP.key?(image_alias)

  image_project = IMAGE_ALIAS_MAP[image_alias][:project]
  image_prefix  = IMAGE_ALIAS_MAP[image_alias][:prefix]

  latest_image = connection.list_images(image_project).items
    .select { |image| image.name.start_with?(image_prefix) }
    .max_by(&:name)

  return if latest_image.nil?

  latest_image.self_link
end
image_exist?(image_project, image_name) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 273
def image_exist?(image_project, image_name)
  check_api_call { connection.get_image(image_project, image_name) }
end
image_search_for(image, image_project) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 370
def image_search_for(image, image_project)
  # if the user provided an image_project, assume they want it, no questions asked
  unless image_project.nil?
    ui.msg("Searching project #{image_project} for image #{image}")
    return image_url_for(image_project, image)
  end

  # no image project has been provided. Check to see if the image is a known alias.
  alias_url = image_alias_url(image)
  unless alias_url.nil?
    ui.msg("image #{image} is a known alias - using image URL: #{alias_url}")
    return alias_url
  end

  # Doesn't match an alias. Let's check the user's project for the image.
  url = image_url_for(project, image)
  unless url.nil?
    ui.msg("Located image #{image} in project #{project} - using image URL: #{url}")
    return url
  end

  # Image not found in user's project. Is there a public project this image might exist in?
  public_project = public_project_for_image(image)
  if public_project
    ui.msg("Searching public image project #{public_project} for image #{image}")
    return image_url_for(public_project, image)
  end

  # No image in user's project or public project, so it doesn't exist.
  ui.error("Image search failed for image #{image} - no suitable image located")
  nil
end
image_url_for(image_project, image_name) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 403
def image_url_for(image_project, image_name)
  return "projects/#{image_project}/global/images/#{image_name}" if image_exist?(image_project, image_name)
end
instance_access_configs_for(public_ip) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 454
def instance_access_configs_for(public_ip)
  return [] if public_ip.nil? || public_ip.match(/none/i)

  access_config = Google::Apis::ComputeV1::AccessConfig.new
  access_config.name = "External NAT"
  access_config.type = "ONE_TO_ONE_NAT"
  access_config.nat_ip = public_ip if valid_ip_address?(public_ip)

  Array(access_config)
end
instance_boot_disk_for(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 333
def instance_boot_disk_for(options)
  disk, params = common_operation(options, boot_disk_type_for(options))
  disk.boot           = true
  params.disk_name    = boot_disk_name_for(options)
  params.disk_size_gb = options[:boot_disk_size]
  params.source_image = boot_disk_source_image(options[:image], options[:image_project])

  disk.initialize_params = params
  disk
end
instance_disks_for(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 315
def instance_disks_for(options)
  disks = []
  disks << instance_boot_disk_for(options)
  options[:number_of_local_ssd].to_i.times { disks << adding_local_ssd(options) } if options[:local_ssd]
  options[:additional_disks].each do |disk_name|
    begin
      disk = connection.get_disk(project, zone, disk_name)
    rescue Google::Apis::ClientError => e
      ui.error("Unable to attach disk #{disk_name} to the instance: #{e.message}")
      raise
    end

    disks << Google::Apis::ComputeV1::AttachedDisk.new.tap { |x| x.source = disk.self_link }
  end

  disks
end
instance_metadata_for(metadata) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 430
def instance_metadata_for(metadata)
  return if metadata.nil? || metadata.empty?

  metadata_obj = Google::Apis::ComputeV1::Metadata.new
  metadata_obj.items = metadata.each_with_object([]) do |(k, v), memo|
    metadata_item       = Google::Apis::ComputeV1::Metadata::Item.new
    metadata_item.key   = k
    metadata_item.value = v

    memo << metadata_item
  end

  metadata_obj
end
instance_network_interfaces_for(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 445
def instance_network_interfaces_for(options)
  interface = Google::Apis::ComputeV1::NetworkInterface.new
  interface.network = network_url_for(options[:network])
  interface.subnetwork = subnet_url_for(options[:subnet]) if options[:subnet]
  interface.access_configs = instance_access_configs_for(options[:public_ip])

  Array(interface)
end
instance_object_for(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 300
def instance_object_for(options)
  inst_obj                    = Google::Apis::ComputeV1::Instance.new
  inst_obj.name               = options[:name]
  inst_obj.can_ip_forward     = options[:can_ip_forward]
  inst_obj.disks              = instance_disks_for(options)
  inst_obj.machine_type       = machine_type_url_for(options[:machine_type])
  inst_obj.metadata           = instance_metadata_for(options[:metadata])
  inst_obj.network_interfaces = instance_network_interfaces_for(options)
  inst_obj.scheduling         = instance_scheduling_for(options)
  inst_obj.service_accounts   = instance_service_accounts_for(options) unless instance_service_accounts_for(options).nil?
  inst_obj.tags               = instance_tags_for(options[:tags])

  inst_obj
end
instance_scheduling_for(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 473
def instance_scheduling_for(options)
  scheduling = Google::Apis::ComputeV1::Scheduling.new
  scheduling.automatic_restart   = options[:auto_restart].to_s
  scheduling.on_host_maintenance = migrate_setting_for(options[:auto_migrate])
  scheduling.preemptible         = options[:preemptible].to_s

  scheduling
end
instance_service_accounts_for(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 486
def instance_service_accounts_for(options)
  return if options[:service_account_scopes].nil? || options[:service_account_scopes].empty?

  service_account = Google::Apis::ComputeV1::ServiceAccount.new
  service_account.email  = options[:service_account_name]
  service_account.scopes = options[:service_account_scopes].map { |scope| service_account_scope_url(scope) }

  Array(service_account)
end
instance_tags_for(tags) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 506
def instance_tags_for(tags)
  return if tags.nil? || tags.empty?

  tag_obj = Google::Apis::ComputeV1::Tags.new
  tag_obj.items = tags

  tag_obj
end
list_disks() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 226
def list_disks
  paginated_results(:list_disks, :items, project, zone) || []
end
list_images() click to toggle source

Retrieves the list of custom images and public images. Custom images are images you create that belong to your project.

# File lib/chef/knife/cloud/google_service.rb, line 222
def list_images
  available_projects.map { |project| paginated_results(:list_images, :items, project) || [] }.flatten
end
list_project_quotas() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 234
def list_project_quotas
  connection.get_project(project).quotas || []
end
list_regions() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 230
def list_regions
  paginated_results(:list_regions, :items, project) || []
end
list_servers() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 200
def list_servers
  instances = paginated_results(:list_instances, :items, project, zone)
  return [] if instances.nil?

  instances.each_with_object([]) do |instance, memo|
    memo << OpenStruct.new(
      name:         instance.name,
      status:       instance.status,
      machine_type: machine_type_for(instance),
      network:      network_for(instance),
      private_ip:   private_ip_for(instance),
      public_ip:    public_ip_for(instance)
    )
  end
end
list_zones() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 216
def list_zones
  paginated_results(:list_zones, :items, project) || []
end
machine_type_for(instance) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 521
def machine_type_for(instance)
  instance.machine_type.split("/").last
end
machine_type_url_for(machine_type) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 426
def machine_type_url_for(machine_type)
  "zones/#{zone}/machineTypes/#{machine_type}"
end
migrate_setting_for(auto_migrate) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 482
def migrate_setting_for(auto_migrate)
  auto_migrate ? "MIGRATE" : "TERMINATE"
end
network_for(instance) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 515
def network_for(instance)
  instance.network_interfaces.first.network.split("/").last
rescue NoMethodError
  "unknown"
end
network_url_for(network) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 465
def network_url_for(network)
  "projects/#{project}/global/networks/#{network}"
end
operation_errors(operation_name) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 641
def operation_errors(operation_name)
  operation = zone_operation(operation_name)
  return [] if operation.error.nil?

  operation.error.errors
end
paginated_results(api_method, items_method, *args) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 563
def paginated_results(api_method, items_method, *args)
  items      = []
  next_token = nil
  loop_num   = 0

  loop do
    loop_num += 1

    response       = connection.send(api_method.to_sym, *args, max_results: max_page_size, page_token: next_token)
    response_items = response.send(items_method.to_sym)

    break if response_items.nil?

    items += response_items

    next_token = response.next_page_token
    break if next_token.nil?

    if loop_num >= max_pages
      ui.warn("Max pages (#{max_pages}) reached, but more results exist - truncating results...")
      break
    end
  end

  items
end
public_project_for_image(image) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 536
def public_project_for_image(image)
  case image
  when /centos/
    "centos-cloud"
  when /container-vm/
    "google-containers"
  when /coreos/
    "coreos-cloud"
  when /debian/
    "debian-cloud"
  when /opensuse-cloud/
    "opensuse-cloud"
  when /rhel/
    "rhel-cloud"
  when /sles/
    "suse-cloud"
  when /ubuntu/
    "ubuntu-os-cloud"
  when /windows/
    "windows-cloud"
  end
end
region() click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 296
def region
  @region ||= connection.get_zone(project, zone).region.split("/").last
end
server_summary(server, _columns_with_info = nil) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 525
def server_summary(server, _columns_with_info = nil)
  msg_pair("Instance Name", server.name)
  msg_pair("Status", server.status)
  msg_pair("Machine Type", machine_type_for(server))
  msg_pair("Project", project)
  msg_pair("Zone", zone)
  msg_pair("Network", network_for(server))
  msg_pair("Private IP", private_ip_for(server))
  msg_pair("Public IP", public_ip_for(server))
end
service_account_scope_url(scope) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 496
def service_account_scope_url(scope)
  return scope if scope.start_with?("https://www.googleapis.com/auth/")

  "https://www.googleapis.com/auth/#{translate_scope_alias(scope)}"
end
subnet_url_for(subnet) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 469
def subnet_url_for(subnet)
  "projects/#{project}/regions/#{region}/subnetworks/#{subnet}"
end
translate_scope_alias(scope_alias) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 502
def translate_scope_alias(scope_alias)
  SCOPE_ALIAS_MAP.fetch(scope_alias, scope_alias)
end
valid_ip_address?(ip_address) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 288
def valid_ip_address?(ip_address)
  IPAddr.new(ip_address)
rescue IPAddr::InvalidAddressError
  false
else
  true
end
valid_machine_type?(machine_type) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 255
def valid_machine_type?(machine_type)
  return false if machine_type.nil?

  check_api_call { connection.get_machine_type(project, zone, machine_type) }
end
valid_network?(network) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 261
def valid_network?(network)
  return false if network.nil?

  check_api_call { connection.get_network(project, network) }
end
valid_public_ip_setting?(public_ip) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 277
def valid_public_ip_setting?(public_ip)
  case
  when public_ip.nil? || public_ip.match(/(ephemeral|none)/i)
    true
  when valid_ip_address?(public_ip)
    true
  else
    false
  end
end
valid_subnet?(subnet) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 267
def valid_subnet?(subnet)
  return false if subnet.nil?

  check_api_call { connection.get_subnetwork(project, region, subnet) }
end
validate_server_create_options!(options) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 238
def validate_server_create_options!(options)
  raise "Invalid machine type: #{options[:machine_type]}" unless valid_machine_type?(options[:machine_type])
  raise "Invalid network: #{options[:network]}" unless valid_network?(options[:network])
  raise "Invalid subnet: #{options[:subnet]}" if options[:subnet] && !valid_subnet?(options[:subnet])
  raise "Invalid Public IP setting: #{options[:public_ip]}" unless valid_public_ip_setting?(options[:public_ip])
  raise "Invalid image: #{options[:image]} - check your image name, or set an image project if needed" if boot_disk_source_image(options[:image], options[:image_project]).nil?
  raise "Maximum number of local SSDs for an instance should be 8, while #{options[:number_of_local_ssd]} is requested." if options[:number_of_local_ssd].to_i > 8
end
wait_for_operation(operation) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 622
def wait_for_operation(operation)
  operation_name = operation.name

  wait_for_status("DONE") { zone_operation(operation_name) }

  errors = operation_errors(operation_name)
  return if errors.empty?

  errors.each do |error|
    ui.error("#{ui.color(error.code, :bold)}: #{error.message}")
  end

  raise "Operation #{operation_name} failed."
end
wait_for_status(requested_status) { || ... } click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 590
def wait_for_status(requested_status)
  last_status = ""

  begin
    Timeout.timeout(wait_time) do
      loop do
        item = yield
        current_status = item.status

        if current_status == requested_status
          print "\n"
          break
        end

        if last_status == current_status
          print "."
        else
          last_status = current_status
          print "\n"
          print "Current status: #{current_status}."
        end

        sleep refresh_rate
      end
    end
  rescue Timeout::Error
    ui.msg("")
    ui.error("Request did not complete in #{wait_time} seconds. Check the Google Cloud Console for more info.")
    exit 1
  end
end
zone_operation(operation_name) click to toggle source
# File lib/chef/knife/cloud/google_service.rb, line 637
def zone_operation(operation_name)
  connection.get_zone_operation(project, zone, operation_name)
end