ubicloud/helpers/vm.rb
Jeremy Evans 45a0f65e46 Support creating a VM using a registered SSH public key in the web UI
Only show registered SSH public key options if the project has at least
one registered. If the project has registered an SSH public key, do not
make the SSH public key textarea a required input.  If a registered SSH
public key is selected, hide the SSH public key textarea using a
pure-CSS approach.
2025-10-04 01:36:33 +09:00

210 lines
8.3 KiB
Ruby

# frozen_string_literal: true
class Clover
def authorized_vm(perm: "Vm:view", location_id: nil)
authorized_object(association: :vms, key: "vm_id", perm:, location_id:)
end
def vm_list_dataset
dataset_authorize(@project.vms_dataset, "Vm:view")
end
def vm_list_api_response(dataset)
dataset = dataset.where(location_id: @location.id) if @location
paginated_result(dataset, Serializers::Vm)
end
def vm_post(name)
project = @project
authorize("Vm:create", project)
fail Validation::ValidationFailed.new({billing_info: "Project doesn't have valid billing information"}) unless project.has_valid_payment_method?
if api?
public_key = typecast_params.nonempty_str!("public_key")
if !public_key.include?(" ") && (ssh_public_key = project.ssh_public_keys_dataset.first(name: public_key))
public_key = ssh_public_key.public_key
end
else
public_key = if (spk_id = typecast_params.ubid_uuid("ssh_public_key")) && (ssh_public_key = project.ssh_public_keys_dataset.first(id: spk_id))
ssh_public_key.public_key
else
typecast_params.nonempty_str!("public_key")
end
end
assemble_params = typecast_params.convert!(symbolize: true) do |tp|
tp.nonempty_str(["size", "unix_user", "boot_image", "private_subnet_id", "gpu"])
tp.pos_int("storage_size")
tp.bool("enable_ip4")
end
assemble_params.compact!
# Generally parameter validation is handled in progs while creating resources.
# Since Vm::Nexus both handles VM creation requests from user and also Postgres
# service, moved the boot_image validation here to not allow users to pass
# postgres image as boot image while creating a VM.
if assemble_params[:boot_image]
Validation.validate_boot_image(assemble_params[:boot_image])
end
# Same as above, moved the size validation here to not allow users to
# pass gpu instance while creating a VM.
if assemble_params[:size]
parsed_size = Validation.validate_vm_size(assemble_params[:size], "x64", only_visible: true)
end
if assemble_params[:storage_size]
storage_size = Validation.validate_vm_storage_size(assemble_params[:size] || Prog::Vm::Nexus::DEFAULT_SIZE, "x64", assemble_params[:storage_size])
assemble_params[:storage_volumes] = [{size_gib: storage_size, encrypted: true}]
assemble_params.delete(:storage_size)
end
if assemble_params[:gpu]
gpu_count, gpu_device = Validation.validate_vm_gpu(assemble_params[:gpu], @location.name, project, parsed_size)
assemble_params[:gpu_count] = gpu_count
assemble_params[:gpu_device] = gpu_device
assemble_params.delete(:gpu)
end
if (ps_id = assemble_params[:private_subnet_id])
if web? && ps_id.start_with?("new-")
assemble_params[:private_subnet_id] = nil
ps_name = typecast_params.nonempty_str!("new_private_subnet_name")
unless ps_name.match(Validation::ALLOWED_NAME_PATTERN)
fail Validation::ValidationFailed.new({new_private_subnet_name: "Name must only contain lowercase letters, numbers, and hyphens and have max length 63."})
end
assemble_params[:new_private_subnet_name] = ps_name
elsif (ps = authorized_private_subnet(location_id: @location.id))
assemble_params[:private_subnet_id] = ps.id
else
fail Validation::ValidationFailed.new({private_subnet_id: "Private subnet with the given id \"#{ps_id}\" is not found in the location \"#{@location.display_name}\""})
end
end
requested_vm_vcpu_count = parsed_size.nil? ? 2 : parsed_size.vcpus
Validation.validate_vcpu_quota(project, "VmVCpu", requested_vm_vcpu_count)
vm = nil
DB.transaction do
vm = Prog::Vm::Nexus.assemble(
public_key,
project.id,
name:,
location_id: @location.id,
**assemble_params
).subject
audit_log(vm, "create")
end
if api?
Serializers::Vm.serialize(vm, {detailed: true})
else
flash["notice"] = "'#{name}' will be ready in a few minutes"
request.redirect vm
end
end
def generate_vm_options
options = OptionTreeGenerator.new
@show_gpu = typecast_params.bool("show_gpu")
@show_gpu = false unless @project.get_ff_gpu_vm
# @show_gpu:
# true: Only show options valid for GPU configurations
# false: Do not show GPU options
# nil: Show GPU options, but also show options not valid for GPU configurations
if @show_gpu != false
available_gpus = DB[:pci_device]
.join(:vm_host, id: :vm_host_id)
.join(:location, id: :location_id)
.where(device_class: ["0300", "0302"], vm_id: nil, visible: true)
.group_and_count(:vm_host_id, :name, :device)
.from_self
.select_group { [name.as(:location_name), device] }
.select_append { max(:count).as(:max_count) }
.all.filter { !!BillingRate.from_resource_properties("Gpu", it[:device], it[:location_name]) }
gpu_counts = [1, 2, 4, 8]
gpu_options = available_gpus.map { it[:device] }.uniq.flat_map { |x| gpu_counts.map { |i| "#{i}:#{x}" } }
gpu_availability = available_gpus.each_with_object({}) do |entry, hash|
hash[entry[:location_name]] ||= {}
hash[entry[:location_name]][entry[:device]] = entry[:max_count]
end
gpu_locations = gpu_availability.keys
if @show_gpu
if gpu_locations.empty? && web?
flash["error"] = "Unfortunately, no virtual machines with GPUs are currently available."
request.redirect @project, "/vm/create"
end
location_family_check = lambda do |location, family|
!gpu_locations.include?(location.name) || family == "burstable"
end
end
end
options.add_option(name: "name")
options.add_option(name: "location", values: Option.locations(feature_flags: @project.feature_flags)) do |location|
!@show_gpu || gpu_locations.include?(location.name)
end
subnets = dataset_authorize(@project.private_subnets_dataset, "PrivateSubnet:view").map {
{
location_id: it.location_id,
value: it.ubid,
display_name: it.name
}
}
Option.locations(feature_flags: @project.feature_flags).each do |location|
subnets << {
location_id: location.id,
value: "new-#{location.ubid}",
display_name: "New Private Subnet"
}
end
options.add_option(name: "private_subnet_id", values: subnets, parent: "location") do |location, private_subnet|
private_subnet[:location_id] == location.id
end
options.add_option(name: "enable_ip4", values: ["1"], parent: "location")
options.add_option(name: "family", values: Option.families.map(&:name), parent: "location") do |location, family|
next false if location_family_check&.call(location, family)
!!BillingRate.from_resource_properties("VmVCpu", family, location.name)
end
options.add_option(name: "size", values: Option::VmSizes.select(&:visible).map(&:display_name), parent: "family") do |location, family, size|
vm_size = Option::VmSizes.find { it.display_name == size && it.arch == "x64" }
vm_size.family == family
end
options.add_option(name: "storage_size", values: ["10", "20", "40", "80", "160", "320", "600", "640", "1200", "2400"], parent: "size") do |location, family, size, storage_size|
vm_size = Option::VmSizes.find { it.display_name == size && it.arch == "x64" }
vm_size.storage_size_options.include?(storage_size.to_i)
end
if @show_gpu != false
base_gpu_options = @show_gpu ? [] : ["0:"]
options.add_option(name: "gpu", values: base_gpu_options + gpu_options, parent: "family") do |location, family, gpu|
gpu_count, device = gpu.split(":", 2)
gpu_count = gpu_count.to_i
device_availability = gpu_availability.dig(location.name, device)
next true if gpu_count == 0
family == "standard" &&
!!BillingRate.from_resource_properties("Gpu", device, location.name) &&
device_availability &&
device_availability >= gpu_count
end
end
options.add_option(name: "boot_image", values: Option::BootImages.map(&:name))
options.add_option(name: "unix_user")
options.add_option(name: "ssh_public_key", values: @project.ssh_public_keys)
options.add_option(name: "public_key")
options.serialize
end
end