Files
ubicloud/rhizome/host/lib/storage_volume.rb
Hadi Moshayedi dba2add1eb Enable setting IO rate limits in Rhizome's storage volume
This is done by calling SPDK's `bdev_set_qos_limit`. Rate limits for a
storage volume are optional and won't be set if none are provided.
2024-11-20 13:26:27 -08:00

332 lines
9.2 KiB
Ruby

# frozen_string_literal: true
require_relative "../../common/lib/util"
require "fileutils"
require "json"
require "openssl"
require "base64"
require_relative "boot_image"
require_relative "vm_path"
require_relative "spdk_path"
require_relative "spdk_rpc"
require_relative "spdk_setup"
require_relative "storage_key_encryption"
require_relative "storage_path"
class StorageVolume
attr_reader :image_path, :read_only
def initialize(vm_name, params)
@vm_name = vm_name
@disk_index = params["disk_index"]
@device_id = params["device_id"]
@encrypted = params["encrypted"]
@disk_size_gib = params["size_gib"]
@use_bdev_ubi = params["use_bdev_ubi"] || false
@skip_sync = params["skip_sync"] || false
@image_path = BootImage.new(params["image"], params["image_version"]).image_path if params["image"]
@device = params["storage_device"] || DEFAULT_STORAGE_DEVICE
@spdk_version = params["spdk_version"]
@read_only = params["read_only"] || false
@max_ios_per_sec = params["max_ios_per_sec"]
@max_read_mbytes_per_sec = params["max_read_mbytes_per_sec"]
@max_write_mbytes_per_sec = params["max_write_mbytes_per_sec"]
end
def vp
@vp ||= VmPath.new(@vm_name)
end
def rpc_client
@rpc_client ||= SpdkRpc.new(SpdkPath.rpc_sock(@spdk_version))
end
def prep(key_wrapping_secrets)
# Device path is intended to be created by system admin, so fail loudly if
# it doesn't exist
fail "Storage device directory doesn't exist: #{sp.device_path}" if !File.exist?(sp.device_path)
FileUtils.mkdir_p storage_dir
encryption_key = setup_data_encryption_key(key_wrapping_secrets) if @encrypted
if @image_path.nil?
fail "bdev_ubi requires a base image" if @use_bdev_ubi
create_empty_disk_file
return
end
verify_imaged_disk_size
if @use_bdev_ubi
create_ubi_writespace(encryption_key)
elsif @encrypted
create_empty_disk_file
encrypted_image_copy(encryption_key, @image_path)
else
unencrypted_image_copy
end
end
def start(key_wrapping_secrets)
encryption_key = read_data_encryption_key(key_wrapping_secrets) if @encrypted
retries = 0
begin
setup_spdk_bdev(encryption_key)
set_qos_limits
setup_spdk_vhost
rescue SpdkExists
# If some of SPDK artifacts exist, purge and retry. But retry only once
# to prevent potential retry loops.
if retries == 0
retries += 1
purge_spdk_artifacts
retry
end
raise
end
end
def purge_spdk_artifacts
vhost_controller = SpdkPath.vhost_controller(@vm_name, @disk_index)
rpc_client.vhost_delete_controller(vhost_controller)
non_ubi_bdev = @use_bdev_ubi ? "#{@device_id}_base" : @device_id
if @use_bdev_ubi
rpc_client.bdev_ubi_delete(@device_id)
end
if @encrypted
rpc_client.bdev_crypto_delete(non_ubi_bdev)
rpc_client.bdev_aio_delete("#{@device_id}_aio")
rpc_client.accel_crypto_key_destroy("#{@device_id}_key")
else
rpc_client.bdev_aio_delete(non_ubi_bdev)
end
rm_if_exists(SpdkPath.vhost_sock(vhost_controller))
end
def setup_data_encryption_key(key_wrapping_secrets)
data_encryption_key = OpenSSL::Cipher.new("aes-256-xts").random_key.unpack1("H*")
result = {
cipher: "AES_XTS",
key: data_encryption_key[..63],
key2: data_encryption_key[64..]
}
key_file = data_encryption_key_path
# save encrypted key
sek = StorageKeyEncryption.new(key_wrapping_secrets)
sek.write_encrypted_dek(key_file, result)
FileUtils.chown @vm_name, @vm_name, key_file
FileUtils.chmod "u=rw,g=,o=", key_file
sync_parent_dir(key_file)
result
end
def read_data_encryption_key(key_wrapping_secrets)
sek = StorageKeyEncryption.new(key_wrapping_secrets)
sek.read_encrypted_dek(data_encryption_key_path)
end
def unencrypted_image_copy
q_image_path = @image_path.shellescape
q_disk_file = disk_file.shellescape
r "cp --reflink=auto #{q_image_path} #{q_disk_file}"
r "truncate -s #{@disk_size_gib}G #{q_disk_file}"
set_disk_file_permissions
end
def verify_imaged_disk_size
size = File.size(@image_path)
fail "Image size greater than requested disk size" unless size <= @disk_size_gib * 2**30
end
def encrypted_image_copy(encryption_key, input_file, block_size: 2097152, count: nil)
# Note that spdk_dd doesn't interact with the main spdk process. It is a
# tool which starts the spdk infra as a separate process, creates bdevs
# from config, does the copy, and exits. Since it is a separate process
# for each image, although bdev names are same, they don't conflict.
# Goal is to copy the image into disk_file, which will be registered
# in the main spdk daemon after this function returns.
bdev_conf = [{
method: "bdev_aio_create",
params: {
name: "aio0",
block_size: 512,
filename: disk_file,
readonly: false
}
},
{
method: "bdev_crypto_create",
params: {
base_bdev_name: "aio0",
name: "crypt0",
key_name: "super_key"
}
}]
accel_conf = [
{
method: "accel_crypto_key_create",
params: {
name: "super_key",
cipher: encryption_key[:cipher],
key: encryption_key[:key],
key2: encryption_key[:key2]
}
}
]
spdk_config_json = {
subsystems: [
{
subsystem: "accel",
config: accel_conf
},
{
subsystem: "bdev",
config: bdev_conf
}
]
}.to_json
# spdk_dd uses the same spdk app infra, so it will bind to an rpc socket,
# which we won't use. But its path shouldn't conflict with other VM setups,
# so it doesn't error out in concurrent VM creations.
rpc_socket = "/var/tmp/spdk_dd.sock.#{@vm_name}"
count_param = count.nil? ? "" : "--count #{count}"
r("#{SpdkPath.bin(@spdk_version, "spdk_dd")} --config /dev/stdin " \
"--disable-cpumask-locks " \
"--rpc-socket #{rpc_socket.shellescape} " \
"--if #{input_file.shellescape} " \
"--ob crypt0 " \
"--bs=#{block_size} #{count_param}", stdin: spdk_config_json)
end
def create_ubi_writespace(encryption_key)
create_empty_disk_file(disk_size_mib: @disk_size_gib * 1024 + 16)
if @encrypted
# just clear the metadata section, i.e. first 8MB
encrypted_image_copy(encryption_key, "/dev/zero", block_size: 2097152, count: 4)
end
end
def create_empty_disk_file(disk_size_mib: @disk_size_gib * 1024)
FileUtils.touch(disk_file)
File.truncate(disk_file, disk_size_mib * 1024 * 1024)
set_disk_file_permissions
end
def set_disk_file_permissions
FileUtils.chown @vm_name, @vm_name, disk_file
# don't allow others to read user's disk
FileUtils.chmod "u=rw,g=r,o=", disk_file
# allow spdk to access the image
r "setfacl -m u:spdk:rw #{disk_file.shellescape}"
end
def setup_spdk_bdev(encryption_key)
non_ubi_bdev = @use_bdev_ubi ? "#{@device_id}_base" : @device_id
if encryption_key
key_name = "#{@device_id}_key"
aio_bdev = "#{@device_id}_aio"
rpc_client.accel_crypto_key_create(
key_name,
encryption_key[:cipher],
encryption_key[:key],
encryption_key[:key2]
)
rpc_client.bdev_aio_create(aio_bdev, disk_file, 512)
rpc_client.bdev_crypto_create(non_ubi_bdev, aio_bdev, key_name)
else
rpc_client.bdev_aio_create(non_ubi_bdev, disk_file, 512)
end
if @use_bdev_ubi
rpc_client.bdev_ubi_create(@device_id, non_ubi_bdev, @image_path, @skip_sync)
end
end
def set_qos_limits
return unless @max_ios_per_sec || @max_read_mbytes_per_sec || @max_write_mbytes_per_sec
rpc_client.bdev_set_qos_limit(
@device_id,
rw_ios_per_sec: @max_ios_per_sec,
r_mbytes_per_sec: @max_read_mbytes_per_sec,
w_mbytes_per_sec: @max_write_mbytes_per_sec
)
end
def setup_spdk_vhost
vhost_controller = SpdkPath.vhost_controller(@vm_name, @disk_index)
spdk_vhost_sock = SpdkPath.vhost_sock(vhost_controller)
rpc_client.vhost_create_blk_controller(vhost_controller, @device_id)
# don't allow others to access the vhost socket
FileUtils.chmod "u=rw,g=r,o=", spdk_vhost_sock
# allow vm user to access the vhost socket
r "setfacl -m u:#{@vm_name}:rw #{spdk_vhost_sock.shellescape}"
# create a symlink to the socket in the per vm storage dir
rm_if_exists(vhost_sock)
FileUtils.ln_s spdk_vhost_sock, vhost_sock
# Change ownership of the symlink. FileUtils.chown uses File.lchown for
# symlinks and doesn't follow links. We don't use File.lchown directly
# because it expects numeric uid & gid, which is less convenient.
FileUtils.chown @vm_name, @vm_name, vhost_sock
vhost_sock
end
def spdk_service
@spdk_service ||= SpdkSetup.new(@spdk_version).spdk_service
end
def sp
@sp ||= StoragePath.new(@vm_name, @device, @disk_index)
end
def storage_root
@storage_root ||= sp.storage_root
end
def storage_dir
@storage_dir ||= sp.storage_dir
end
def disk_file
@disk_file ||= sp.disk_file
end
def data_encryption_key_path
@dek_path ||= sp.data_encryption_key
end
def vhost_sock
@vhost_sock ||= sp.vhost_sock
end
end