lib/fog/compute/google/models/server.rb
require "fog/compute/models/server"
module Fog
module Compute
class Google
class Server < Fog::Compute::Server
identity :name
# @return [Boolean]
attribute :can_ip_forward, :aliases => "canIpForward"
# @return [String]
attribute :cpu_platform, :aliases => "cpuPlatform"
# @return [String]
attribute :creation_timestamp, :aliases => "creationTimestamp"
# @return [Boolean]
attribute :deletion_protection, :aliases => "deletionProtection"
# @return [String]
attribute :description
# New disks may include :initialize_params before save.
#
# @example Minimal disks pre-creation:
# [
# {
# :initialize_params => {
# :source_image => "projects/debian-cloud/global/images/family/debian-11"
# }
# }
# ]
#
# @example disks post-creation:
# [
# {
# :auto_delete => false,
# :boot => true,
# :device_name => "persistent-disk-0",
# :index => 0,
# :interface => "SCSI",
# :kind => "compute#attachedDisk",
# :licenses => ["https://www.googleapis.com/compute/v1/..."],
# :mode => "READ_WRITE",
# :source => "https://www.googleapis.com/compute/v1/.../mydisk",
# :type => "PERSISTENT"
# }
# ]
# @return [Array<Hash>]
attribute :disks
# @example Enable the display device
# {
# :enable_display => true
# }
# @return [Hash<String,Boolean>]
attribute :display_device, :aliases => "displayDevice"
# @example Guest accelerators
# [
# {
# :accelerator_count => 1,
# :accelerator_type => "...my/accelerator/type"
# }
# ]
# @return [Array<Hash>]
attribute :guest_accelerators, :aliases => "guestAccelerators"
# @return [Fixnum]
attribute :id
# @return [String]
attribute :kind
# @return [String]
attribute :label_fingerprint, :aliases => "labelFingerprint"
# @return [Hash<String,String>]
attribute :labels
# @return [String]
attribute :machine_type, :aliases => "machineType"
# If set initially before save, the expected format
# is the API format as shown below.
#
# If you want to pass in a Hash, see {#set_metadata}.
# If you want to access the metadata items as a Hash, see
# {#metadata_as_h}.
#
# @example Metadata in API format
#
# {
# :fingerprint => "...",
# :items => [
# { :key => "foo", :value => "bar" },
# ]
# }
# @return [Hash]
attribute :metadata
# @return [String]
attribute :min_cpu_platform, :aliases => "minCpuPlatform"
# @example Network interfaces
# [
# {
# :kind => "compute#networkInterface",
# :name => "nic0",
# :network => "https://www.googleapis.com/compute/v1/.../my-network/"
# :network_ip => "0.0.0.0",
# :subnetwork => "https://www.googleapis.com/compute/v1/.../my-subnetwork"
# }
# ],
# @return [Array<Hash>]
attribute :network_interfaces, :aliases => "networkInterfaces"
# @example Scheduling object
# {
# :automatic_restart => true,
# :on_host_maintenance => "MIGRATE",
# :preemptible=>false
# }
# @return [Hash]
attribute :scheduling
# @return [String]
attribute :self_link, :aliases => "selfLink"
# @example Service accounts in API format
# [
# {
# :email => "my-service-account@developer.gserviceaccount.com",
# :scopes => [],
# }
# ]
# @return [Array<Hash>]
attribute :service_accounts, :aliases => "serviceAccounts"
# @return [Boolean]
attribute :start_restricted, :aliases => "startRestricted"
# @return [String]
attribute :status, :aliases => "status"
# @return [String]
attribute :status_message, :aliases => "statusMessage"
# @example Tags in API format
# @return [Hash]
attribute :tags
# @return [String]
attribute :zone, :aliases => :zone_name
GCE_SCOPE_ALIASES = {
"default" => %w(
https://www.googleapis.com/auth/cloud.useraccounts.readonly
https://www.googleapis.com/auth/devstorage.read_only
https://www.googleapis.com/auth/logging.write
https://www.googleapis.com/auth/monitoring.write
https://www.googleapis.com/auth/pubsub
https://www.googleapis.com/auth/service.management.readonly
https://www.googleapis.com/auth/servicecontrol
https://www.googleapis.com/auth/trace.append
),
"bigquery" => ["https://www.googleapis.com/auth/bigquery"],
"cloud-platform" => ["https://www.googleapis.com/auth/cloud-platform"],
"compute-ro" => ["https://www.googleapis.com/auth/compute.readonly"],
"compute-rw" => ["https://www.googleapis.com/auth/compute"],
"datastore" => ["https://www.googleapis.com/auth/datastore"],
"logging-write" => ["https://www.googleapis.com/auth/logging.write"],
"monitoring" => ["https://www.googleapis.com/auth/monitoring"],
"monitoring-write" => ["https://www.googleapis.com/auth/monitoring.write"],
"service-control" => ["https://www.googleapis.com/auth/servicecontrol"],
"service-management" => ["https://www.googleapis.com/auth/service.management.readonly"],
"sql" => ["https://www.googleapis.com/auth/sqlservice"],
"sql-admin" => ["https://www.googleapis.com/auth/sqlservice.admin"],
"storage-full" => ["https://www.googleapis.com/auth/devstorage.full_control"],
"storage-ro" => ["https://www.googleapis.com/auth/devstorage.read_only"],
"storage-rw" => ["https://www.googleapis.com/auth/devstorage.read_write"],
"taskqueue" => ["https://www.googleapis.com/auth/taskqueue"],
"useraccounts-ro" => ["https://www.googleapis.com/auth/cloud.useraccounts.readonly"],
"useraccounts-rw" => ["https://www.googleapis.com/auth/cloud.useraccounts"],
"userinfo-email" => ["https://www.googleapis.com/auth/userinfo.email"]
}.freeze
# Return the source image of the server's boot disk
#
# @return [String] image self link
def image_name
boot_disk = disks.first
unless boot_disk.is_a?(Disk)
source = boot_disk[:source]
match = source.match(%r{/zones/(.*)/disks/(.*)$})
boot_disk = service.disks.get(match[2], match[1])
end
boot_disk.source_image.nil? ? nil : boot_disk.source_image
end
# Destroy a server.
#
# @param async [TrueClass] execute the command asynchronously
# @return [Fog::Compute::Google::Operation]
def destroy(async = true)
requires :name, :zone
data = service.delete_server(name, zone_name)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
operation
end
# Helper method that returns first public ip address needed for
# Fog::Compute::Server.ssh default behavior.
#
# @return [String]
def public_ip_address
public_ip_addresses.first
end
# Helper method that returns all of server's public ip addresses.
#
# @return [Array]
def public_ip_addresses
addresses = []
if network_interfaces.respond_to? :flat_map
addresses = network_interfaces.flat_map do |nic|
if nic[:access_configs].respond_to? :each
nic[:access_configs].select { |config| config[:name] == "External NAT" }
.map { |config| config[:nat_ip] }
else
[]
end
end
end
addresses
end
# Helper method that returns the first private ip address of the
# instance.
#
# @return [String]
def private_ip_address
private_ip_addresses.first
end
# Helper method that returns all of server's private ip addresses.
#
# @return [Array]
def private_ip_addresses
addresses = []
if network_interfaces.respond_to? :map
addresses = network_interfaces.map { |nic| nic[:network_ip] }
end
addresses
end
# Helper method that returns all of server's ip addresses,
# both private and public.
#
# @return [Array]
def addresses
private_ip_addresses + public_ip_addresses
end
# Attach a disk to a running server
#
# @param disk [Object, String] disk object or a self-link
# @param async [TrueClass] execute the api call asynchronously
# @param options [Hash]
# @return [Object]
def attach_disk(disk, async = true, attached_disk_options = {})
requires :identity, :zone
if disk.is_a? Disk
disk_obj = disk.get_attached_disk
elsif disk.is_a? String
disk_obj = service.disks.attached_disk_obj(disk, **attached_disk_options)
end
data = service.attach_disk(identity, zone_name, disk_obj)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
reload
end
# Detach disk from a running instance
#
# @param device_name [Object]
# @param async [TrueClass]
# @returns [Fog::Compute::Google::Server] server object
def detach_disk(device_name, async = true)
requires :identity, :zone
data = service.detach_disk(identity, zone, device_name)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
reload
end
# Returns metadata items as a Hash.
#
# @return [Hash<String, String>] items
def metadata_as_h
if metadata.nil? || metadata[:items].nil? || metadata[:items].empty?
return {}
end
Hash[metadata[:items].map { |item| [item[:key], item[:value]] }]
end
def reboot(async = true)
requires :identity, :zone
data = service.reset_server(identity, zone_name)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
operation
end
def start(async = true)
requires :identity, :zone
data = service.start_server(identity, zone_name)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
operation
end
def stop(async = true, discard_local_ssd=false)
requires :identity, :zone
data = service.stop_server(identity, zone_name, discard_local_ssd)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
operation
end
def serial_port_output(port: 1)
requires :identity, :zone
service.get_server_serial_port_output(identity, zone_name, :port => port).to_h[:contents]
end
def set_disk_auto_delete(auto_delete, device_name = nil, async = true)
requires :identity, :zone
if device_name.nil? && disks.count > 1
raise ArgumentError.new("Device name is required if multiple disks are attached")
end
device_name ||= disks.first[:device_name]
data = service.set_server_disk_auto_delete(
identity, zone_name, auto_delete, device_name
)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
reload
end
def set_scheduling(async = true,
on_host_maintenance: nil,
automatic_restart: nil,
preemptible: nil)
requires :identity, :zone
data = service.set_server_scheduling(
identity, zone_name,
:on_host_maintenance => on_host_maintenance,
:automatic_restart => automatic_restart,
:preemptible => preemptible
)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
reload
end
# Set an instance metadata
#
# @param [Bool] async Perform the operation asyncronously
# @param [Hash] new_metadata A new metadata object
# Format: {'foo' => 'bar', 'baz'=>'foo'}
#
# @returns [Fog::Compute::Google::Server] server object
def set_metadata(new_metadata = {}, async = true)
requires :identity, :zone
unless new_metadata.is_a?(Hash)
raise Fog::Errors::Error.new("Instance metadata should be a hash")
end
# If metadata is presented in {'foo' => 'bar', 'baz'=>'foo'}
new_metadata_items = new_metadata.each.map { |k, v| { :key => k.to_s, :value => v.to_s } }
data = service.set_server_metadata(
identity, zone_name, metadata[:fingerprint], new_metadata_items
)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
reload
end
def set_machine_type(new_machine_type, async = true)
requires :identity, :zone
raise Fog::Errors::Error.new("Instance must be stopped to change machine type") unless stopped?
data = service.set_server_machine_type(
identity, zone_name, new_machine_type
)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
reload
end
def set_tags(new_tags = [], async = true)
requires :identity, :zone
data = service.set_server_tags(
identity, zone_name, tags[:fingerprint], new_tags
)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
reload
end
# Check if instance is provisioning. On staging vs. provisioning difference:
# https://cloud.google.com/compute/docs/instances/checking-instance-status
#
# @return [TrueClass or FalseClass]
def provisioning?
status == "PROVISIONING"
end
# Check if instance is staging. On staging vs. provisioning difference:
# https://cloud.google.com/compute/docs/instances/checking-instance-status
#
# @return [TrueClass or FalseClass]
def staging?
status == "STAGING"
end
# Check if instance is stopped.
#
# @return [TrueClass or FalseClass]
def stopped?
status == "TERMINATED"
end
# Check if instance is ready.
#
# @return [TrueClass or FalseClass]
def ready?
status == "RUNNING"
end
def zone_name
zone.nil? ? nil : zone.split("/")[-1]
end
def add_ssh_key(username, key, async = true)
metadata = generate_ssh_key_metadata(username, key)
data = service.set_server_metadata(
identity, zone_name, metadata[:fingerprint], metadata[:items]
)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? } unless async
reload
end
def reload
data = service.get_server(name, zone_name).to_h
merge_attributes(data)
end
def map_scopes(scopes)
return [] if scopes.nil?
scopes.flat_map do |scope|
if GCE_SCOPE_ALIASES.key? scope
# Expand scope alias to list of related scopes
GCE_SCOPE_ALIASES[scope]
else
[scope_url(scope)]
end
end
end
def save(username: nil, public_key: nil)
requires :name
requires :machine_type
requires :disks
requires :zone
generate_ssh_key_metadata(self.username, self.public_key) if self.public_key
# XXX HACK This is a relic of 1.0 change that for some reason added those arguments
# to `save` method. This is left in place to keep things backwards-compatible
# TODO(2.0): Remove arguments from save
generate_ssh_key_metadata(username, public_key) if public_key
options = attributes.reject { |_, v| v.nil? }
if service_accounts && service_accounts[0]
service_accounts[0][:scopes] = map_scopes(service_accounts[0][:scopes])
options[:service_accounts] = service_accounts
end
if attributes[:external_ip]
if options[:network_interfaces].nil? || options[:network_interfaces].empty?
options[:network_interfaces] = [
{
:network => "global/networks/#{GOOGLE_COMPUTE_DEFAULT_NETWORK}"
}
]
end
# Add external IP as default access config if given
options[:network_interfaces][0][:access_configs] = [
{
:name => "External NAT",
:type => "ONE_TO_ONE_NAT",
:nat_ip => attributes[:external_ip]
}
]
end
if attributes[:network_ip]
options[:network_interfaces][0][:network_ip] = attributes[:network_ip]
end
data = service.insert_server(name, zone_name, options)
operation = Fog::Compute::Google::Operations
.new(:service => service)
.get(data.name, data.zone)
operation.wait_for { ready? }
reload
end
def generate_ssh_key_metadata(username, key)
if metadata.nil?
self.metadata = Hash.new
end
metadata[:items] = [] if metadata[:items].nil?
metadata_map = Hash[metadata[:items].map { |item| [item[:key], item[:value]] }]
ssh_keys = metadata_map["ssh-keys"] || metadata_map["sshKeys"] || ""
ssh_keys += "\n" unless ssh_keys.empty?
ssh_keys += "#{username}:#{ensure_key_comment(key, username)}"
metadata_map["ssh-keys"] = ssh_keys
metadata[:items] = metadata_to_item_list(metadata_map)
metadata
end
def ensure_key_comment(key, default_comment = "fog-user")
parts = key.strip.split
parts << default_comment if parts.size < 3
parts.join(" ")
end
def reset_windows_password(user)
service.reset_windows_password(:server => self, :user => user)
end
private
def metadata_to_item_list(metadata)
metadata.map { |k, v| { :key => k, :value => v } }
end
def scope_url(scope)
if scope.start_with?("https://")
scope
else
"https://www.googleapis.com/auth/#{scope}"
end
end
end
end
end
end