Files
ubicloud/helpers/vm.rb
Jeremy Evans b5beb4f5ea Split VM creation page into GPU/non-GPU if get_ff_gpu_vm feature flag is set
This can potentially make it easier to select a GPU instance.

With the feature flag set, instead of one button link to the
create page, there are two:

* The "Create Virtual Machine" link does not show any GPU options

* The "Create GPU Virtual Machine" only shows valid options for a
  GPU configuration

If users have a direct link to the old page, it works the same as
before, showing both GPU and non GPU options.

For this to work, you need to check for available GPUs earlier,
and use it to filter the families and locations.
2025-06-20 06:00:14 +09:00

191 lines
7.1 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.id)
fail Validation::ValidationFailed.new({billing_info: "Project doesn't have valid billing information"}) unless project.has_valid_payment_method?
public_key = typecast_params.nonempty_str!("public_key")
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 assemble_params[:private_subnet_id]
unless (ps = authorized_private_subnet(location_id: @location.id))
fail Validation::ValidationFailed.new({private_subnet_id: "Private subnet with the given id \"#{assemble_params[:private_subnet_id]}\" is not found in the location \"#{@location.display_name}\""})
end
end
assemble_params[:private_subnet_id] = ps&.id
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 "#{project.path}#{vm.path}"
end
end
def generate_vm_options
options = OptionTreeGenerator.new
@show_gpu = if flash["old"]
case flash["old"]["show_gpu"]
when "true"
true
when "false"
false
end
else
typecast_params.bool("show_gpu")
end
@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)
.group_and_count(:vm_host_id, :name, :device)
.from_self
.select_group { [name.as(:location_name), device] }
.select_append { max(:count).as(:max_count) }
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.path}/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
}
}
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: "public_key")
options.serialize
end
end