Files
ubicloud/rhizome/host/spec/spdk_rpc_spec.rb
Hadi Moshayedi 1e1a274d97 Enable multiple SPDK installations
This is done by:
* Adding version information to SPDK files on the host. This includes:
  * Package installation path: `/opt/spdk` => `/opt/spdk-$version`
  * Hugetables path: `/home/spdk/hugetables` =>
  * `/home/spdk/hugetables.$version`
  * Hugetables mount: `home-spdk-hugetable` =>
  * `home-spdk-hugetables.$version`
  * Service file: `spdk.service` => `spdk-$version.service`
  * RPC socket: `/home/spdk/spdk.sock` => `/home/spdk/spdk-$version.sock`
* Keeping information about the installation about the installation in
* the SpdkInstallation table.
* Sending the SPDK version to be used with each storage volume data.

Since we have legacy servers which don't have the above version
information, the code has assumed `LEGACY_SPDK_VERSION` as SPDK version
of those installations, which maps the above information to old names
and paths. This can be removed after transitioning all servers.

Currently we allow at maximum 2 installations per host, which should be
enough when upgrading to a new host.

== REPL interactions ==
Specifying an SPDK version when setting up a host:

```
> Prog::Vm::HostNexus.assemble(new_host_ip, ... other params ...,
  spdk_version: "v23.09")
```

Installing a second SPDK installation on an existing host:

```
> Prog::Storage::SetupSpdk.setup("v23.09-ubi-0.1", start_service: true,
  allocation_weight: 10)
```

After this there will be two SPDK installations on the host, v23.09
which has weight 100, and the v23.09-ubi-0.1 which has weight 10. So
100/110 of new VMs will use the v23.09, and 10/110 of new VMs will use
v23.09-ubi-0.1.
2023-11-22 11:39:28 -08:00

260 lines
8.5 KiB
Ruby

# frozen_string_literal: true
require_relative "../lib/spdk_rpc"
RSpec.describe SpdkRpc do
subject(:sr) {
described_class.new("/path/to/spdk.sock", 5, 100)
}
describe "#bdev_aio_create" do
it "can create an aio bdev" do
expect(sr).to receive(:call).with("bdev_aio_create", {
name: "name",
filename: "filename",
block_size: 512,
readonly: false
})
sr.bdev_aio_create("name", "filename", 512)
end
end
describe "#bdev_aio_delete" do
it "can delete an aio bdev" do
expect(sr).to receive(:call).with("bdev_aio_delete", {
name: "name"
})
sr.bdev_aio_delete("name")
end
it "ignores exception if bdev doesn't exist and if_exists=true" do
expect(sr).to receive(:call).with("bdev_aio_delete", {
name: "name"
}).and_raise SpdkRpcError.build("No such device", -19)
sr.bdev_aio_delete("name")
end
it "raises exception if bdev doesn't exist and if_exists=false" do
expect(sr).to receive(:call).with("bdev_aio_delete", {
name: "name"
}).and_raise SpdkRpcError.build("No such device", -19)
expect { sr.bdev_aio_delete("name", false) }.to raise_error SpdkNotFound
end
end
describe "#bdev_crypto_create" do
it "can create an crypto bdev" do
expect(sr).to receive(:call).with("bdev_crypto_create", {
name: "name",
base_bdev_name: "base",
key_name: "key"
})
sr.bdev_crypto_create("name", "base", "key")
end
end
describe "#bdev_crypto_delete" do
it "can delete an crypto bdev" do
expect(sr).to receive(:call).with("bdev_crypto_delete", {
name: "name"
})
sr.bdev_crypto_delete("name")
end
it "ignores exception if bdev doesn't exist and if_exists=true" do
expect(sr).to receive(:call).with("bdev_crypto_delete", {
name: "name"
}).and_raise SpdkRpcError.build("No such device", -19)
sr.bdev_crypto_delete("name")
end
it "raises exception if bdev doesn't exist and if_exists=false" do
expect(sr).to receive(:call).with("bdev_crypto_delete", {
name: "name"
}).and_raise SpdkRpcError.build("No such device", -19)
expect { sr.bdev_crypto_delete("name", false) }.to raise_error SpdkNotFound
end
end
describe "#vhost_create_blk_controller" do
it "can create a vhost block controller" do
expect(sr).to receive(:call).with("vhost_create_blk_controller", {
ctrlr: "name",
dev_name: "bdev"
})
sr.vhost_create_blk_controller("name", "bdev")
end
it "raises SpdkExists if device already exists" do
expect(sr).to receive(:call).with("vhost_create_blk_controller", {
ctrlr: "name",
dev_name: "bdev"
}).and_raise SpdkRpcError.build("File exists", -32602)
expect { sr.vhost_create_blk_controller("name", "bdev") }.to raise_error SpdkExists
end
it "raises SpdkNotFound for other errors" do
expect(sr).to receive(:call).with("vhost_create_blk_controller", {
ctrlr: "name",
dev_name: "bdev"
}).and_raise SpdkRpcError.build("No such device", -32602)
expect { sr.vhost_create_blk_controller("name", "bdev") }.to raise_error SpdkNotFound
end
end
describe "#vhost_delete_controller" do
it "can delete an vhost controller" do
expect(sr).to receive(:call).with("vhost_delete_controller", {
ctrlr: "name"
})
sr.vhost_delete_controller("name")
end
it "ignores exception if controller doesn't exist and if_exists=true" do
expect(sr).to receive(:call).with("vhost_delete_controller", {
ctrlr: "name"
}).and_raise SpdkRpcError.build("No such device", -32602)
sr.vhost_delete_controller("name")
end
it "raises exception if controller doesn't exist and if_exists=false" do
expect(sr).to receive(:call).with("vhost_delete_controller", {
ctrlr: "name"
}).and_raise SpdkRpcError.build("No such device", -32602)
expect { sr.vhost_delete_controller("name", false) }.to raise_error SpdkNotFound
end
end
describe "#accel_crypto_key_create" do
it "can create a crypto key" do
expect(sr).to receive(:call).with("accel_crypto_key_create", {
name: "name",
cipher: "cipher",
key: "key",
key2: "key2"
})
sr.accel_crypto_key_create("name", "cipher", "key", "key2")
end
it "raises SpdkExists if key exists" do
expect(sr).to receive(:call).with("accel_crypto_key_create", {
name: "name",
cipher: "cipher",
key: "key",
key2: "key2"
}).and_raise SpdkRpcError.build("failed to create DEK, rc -17", -32602)
expect {
sr.accel_crypto_key_create("name", "cipher", "key", "key2")
}.to raise_error SpdkExists
end
it "raises SpdkRpcError for other errors" do
expect(sr).to receive(:call).with("accel_crypto_key_create", {
name: "name",
cipher: "cipher",
key: "key",
key2: "key2"
}).and_raise SpdkRpcError.build("failed to create DEK, rc -22", -32602)
expect {
sr.accel_crypto_key_create("name", "cipher", "key", "key2")
}.to raise_error SpdkRpcError, "failed to create DEK, rc -22"
end
end
describe "#accel_crypto_key_destroy" do
it "can delete an crypto key" do
expect(sr).to receive(:call).with("accel_crypto_key_destroy", {
key_name: "name"
})
sr.accel_crypto_key_destroy("name")
end
it "ignores exception if crypto key doesn't exist and if_exists=true" do
expect(sr).to receive(:call).with("accel_crypto_key_destroy", {
key_name: "name"
}).and_raise SpdkRpcError.build("No key object found", -32602)
sr.accel_crypto_key_destroy("name")
end
it "raises exception if crypto key doesn't exist and if_exists=false" do
expect(sr).to receive(:call).with("accel_crypto_key_destroy", {
key_name: "name"
}).and_raise SpdkRpcError.build("No key object found", -32602)
expect { sr.accel_crypto_key_destroy("name", false) }.to raise_error SpdkNotFound
end
end
describe "#call" do
let(:unix_socket) { instance_double(UNIXSocket) }
before do
allow(UNIXSocket).to receive(:new).and_return(unix_socket)
end
it "can call a method" do
params = {"x" => "y"}
expect(unix_socket).to receive(:write_nonblock)
expect(sr).to receive(:read_response).with(unix_socket).and_return('{"result": "response"}')
expect(unix_socket).to receive(:close)
expect(sr.call("url", params)).to eq("response")
end
it "raises an exception if response is an error" do
params = {"x" => "y"}
response = {
error: {
message: "an error happened",
code: -5
}
}.to_json
expect(unix_socket).to receive(:write_nonblock)
expect(sr).to receive(:read_response).with(unix_socket).and_return(response)
expect { sr.call("url", params) }.to raise_error SpdkRpcError, "an error happened"
end
end
describe "#read_response" do
let(:unix_socket) { instance_double(UNIXSocket) }
it "can read a valid response" do
response = {a: "b", c: 1}.to_json
expect(IO).to receive(:select).and_return(1)
expect(unix_socket).to receive(:read_nonblock).and_return(response)
expect(sr.read_response(unix_socket)).to eq(response)
end
it "throws a timeout exception if select returns nil" do
expect(IO).to receive(:select).and_return(nil)
expect(unix_socket).to receive(:close)
expect { sr.read_response(unix_socket) }.to raise_error RuntimeError, "The request timed out after 5 seconds."
end
it "throws an exception if response exceeds the limit" do
expect(IO).to receive(:select).and_return(1)
expect(unix_socket).to receive(:read_nonblock).and_return("a" * 200)
expect { sr.read_response(unix_socket) }.to raise_error RuntimeError, "Response size limit exceeded."
end
it "can read a multi-part valid response" do
response = {a: "b", c: 1}.to_json
expect(IO).to receive(:select).and_return(1, 1)
expect(unix_socket).to receive(:read_nonblock).and_invoke(
->(_) { response[..5] },
->(_) { raise IO::EAGAINWaitReadable },
->(_) { response[6..] }
)
expect(sr.read_response(unix_socket)).to eq(response)
end
end
describe "#valid_json?" do
it "returns true for a valid json" do
expect(sr.valid_json?({"a" => 1}.to_json)).to be(true)
end
it "returnf alse for an incomplete json" do
expect(sr.valid_json?({"a" => 1}.to_json[..5])).to be(false)
end
end
end