ubicloud/helpers/vm.rb
Jeremy Evans a4bc4a36c4 Support creating private subnet when creating virtual machine
This adds a "New Private Subnet" option to the Private Subnet select
box. When chosen, the "Private Subnet Name" input displays. When
filled out correctly, it creates the virtual machine in the newly
created private subnet.

This uses a CSS approach instead of a Javascript approach for
handling the display of the "Private Subnet Name" input when the
"New Private Subnet" option is selected. This type of CSS is now
widely supported by browsers: https://caniuse.com/css-has
2025-09-12 01:09:24 +09:00

197 lines
7.8 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 (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: "public_key")
options.serialize
end
end