ublk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UBLK
4
+ Params = Data.define(
5
+ :size, :logical_block_size, :physical_block_size, :max_io_bytes,
6
+ :read_only, :rotational, :discard
7
+ ) do
8
+ def initialize(size:, logical_block_size: 512, physical_block_size: 4096,
9
+ max_io_bytes: 512 * 1024, read_only: false, rotational: false,
10
+ discard: true)
11
+ [logical_block_size, physical_block_size].each do |block_size|
12
+ valid = block_size.is_a?(Integer) && block_size.between?(512, 4096) && (block_size & (block_size - 1)).zero?
13
+ raise ArgumentError, "block sizes must be powers of two between 512 and 4096" unless valid
14
+ end
15
+ raise ArgumentError, "physical block size must not be smaller than logical block size" if physical_block_size < logical_block_size
16
+ raise ArgumentError, "size must be aligned to the logical block size" unless (size % logical_block_size).zero?
17
+ raise ArgumentError, "max_io_bytes must be a positive multiple of 512" unless max_io_bytes.is_a?(Integer) && max_io_bytes.positive? && (max_io_bytes % 512).zero?
18
+ raise ArgumentError, "max_io_bytes must not exceed 32 MiB" if max_io_bytes > 32 * 1024 * 1024
19
+
20
+ super
21
+ end
22
+
23
+ def self.from_target(target, max_io_bytes: 512 * 1024)
24
+ new(
25
+ size: target.size,
26
+ logical_block_size: target.logical_block_size,
27
+ physical_block_size: target.physical_block_size,
28
+ max_io_bytes:,
29
+ read_only: target.read_only?,
30
+ rotational: target.rotational?,
31
+ discard: target.method(:discard).owner != Target || target.method(:write_zeroes).owner != Target
32
+ )
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UBLK
4
+ class Target
5
+ attr_reader :size, :queues, :depth
6
+
7
+ def initialize(size:, queues: 1, depth: 128)
8
+ raise ArgumentError, "size must be a positive multiple of 512" unless size.is_a?(Integer) && size.positive? && (size % 512).zero?
9
+ raise ArgumentError, "queues must be between 1 and 4096" unless queues.is_a?(Integer) && queues.between?(1, 4096)
10
+ raise ArgumentError, "depth must be between 1 and 4096" unless depth.is_a?(Integer) && depth.between?(1, 4096)
11
+
12
+ @size = size
13
+ @queues = queues
14
+ @depth = depth
15
+ end
16
+
17
+ def read(_offset, _length)
18
+ raise NotImplementedError, "targets must implement #read"
19
+ end
20
+
21
+ def write(_offset, _data)
22
+ raise Errno::EROFS if read_only?
23
+
24
+ raise NotImplementedError, "targets must implement #write"
25
+ end
26
+
27
+ def flush = 0
28
+
29
+ def discard(_offset, _length)
30
+ raise Errno::EOPNOTSUPP
31
+ end
32
+
33
+ def write_zeroes(_offset, _length)
34
+ raise Errno::EOPNOTSUPP
35
+ end
36
+
37
+ def logical_block_size = 512
38
+ def physical_block_size = 4096
39
+ def read_only? = false
40
+ def rotational? = false
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UBLK
4
+ VERSION = "1.0.0"
5
+ end
data/lib/ublk.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ublk/version"
4
+ require_relative "ublk/target"
5
+ require_relative "ublk/device_info"
6
+ require_relative "ublk/params"
7
+
8
+ module UBLK
9
+ class Error < StandardError; end
10
+ class UnsupportedError < Error; end
11
+ class DeviceError < Error; end
12
+
13
+ begin
14
+ require "ublk/ublk"
15
+ rescue LoadError => error
16
+ @native_load_error = error
17
+ end
18
+
19
+ class << self
20
+ attr_reader :native_load_error
21
+
22
+ def supported?
23
+ !!(defined?(Native) && Native.supported?)
24
+ end
25
+
26
+ def ensure_supported!
27
+ return if supported?
28
+
29
+ detail = native_load_error&.message || "/dev/ublk-control is unavailable"
30
+ raise UnsupportedError, "ublk requires Linux 6.6+, ublk_drv, and liburing (#{detail})"
31
+ end
32
+ end
33
+ end
34
+
35
+ require_relative "ublk/control"
36
+ require_relative "ublk/device"
data/sig/ublk.rbs ADDED
@@ -0,0 +1,96 @@
1
+ module UBLK
2
+ VERSION: String
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class UnsupportedError < Error
8
+ end
9
+
10
+ class DeviceError < Error
11
+ end
12
+
13
+ def self.supported?: () -> bool
14
+ def self.ensure_supported!: () -> nil
15
+ def self.native_load_error: () -> LoadError?
16
+
17
+ class Target
18
+ attr_reader size: Integer
19
+ attr_reader queues: Integer
20
+ attr_reader depth: Integer
21
+
22
+ def initialize: (size: Integer, ?queues: Integer, ?depth: Integer) -> void
23
+ def read: (Integer offset, Integer length) -> String
24
+ def write: (Integer offset, String data) -> Integer
25
+ def flush: () -> Integer
26
+ def discard: (Integer offset, Integer length) -> Integer
27
+ def write_zeroes: (Integer offset, Integer length) -> Integer
28
+ def logical_block_size: () -> Integer
29
+ def physical_block_size: () -> Integer
30
+ def read_only?: () -> bool
31
+ def rotational?: () -> bool
32
+ end
33
+
34
+ class DeviceInfo
35
+ attr_reader id: Integer
36
+ attr_reader queues: Integer
37
+ attr_reader depth: Integer
38
+ attr_reader state: Integer
39
+ attr_reader max_io_bytes: Integer
40
+ attr_reader pid: Integer
41
+ attr_reader flags: Integer
42
+ attr_reader owner_uid: Integer
43
+ attr_reader owner_gid: Integer
44
+
45
+ def path: () -> String
46
+ def char_path: () -> String
47
+ def self.from_native: (Array[Integer] values) -> DeviceInfo
48
+ end
49
+
50
+ class Params
51
+ attr_reader size: Integer
52
+ attr_reader logical_block_size: Integer
53
+ attr_reader physical_block_size: Integer
54
+ attr_reader max_io_bytes: Integer
55
+ attr_reader read_only: bool
56
+ attr_reader rotational: bool
57
+ attr_reader discard: bool
58
+
59
+ def initialize: (size: Integer, ?logical_block_size: Integer, ?physical_block_size: Integer, ?max_io_bytes: Integer, ?read_only: bool, ?rotational: bool, ?discard: bool) -> void
60
+ def self.from_target: (Target target, ?max_io_bytes: Integer) -> Params
61
+ end
62
+
63
+ class Control
64
+ SYSFS: String
65
+
66
+ def initialize: () -> void
67
+ def add_dev: (?id: Integer?, ?queues: Integer, ?depth: Integer, ?max_io_bytes: Integer, ?recovery: bool) -> DeviceInfo
68
+ def set_params: (Integer id, Params params) -> Params
69
+ def start_dev: (Integer id, ?pid: Integer) -> bool
70
+ def stop_dev: (Integer id) -> bool
71
+ def del_dev: (Integer id) -> bool
72
+ def start_user_recovery: (Integer id) -> bool
73
+ def end_user_recovery: (Integer id, ?pid: Integer) -> bool
74
+ def get_dev_info: (Integer id) -> DeviceInfo
75
+ def list: () -> Array[DeviceInfo]
76
+ def close: () -> nil
77
+ end
78
+
79
+ class Device
80
+ attr_reader target: Target
81
+ attr_reader info: DeviceInfo
82
+ attr_reader params: Params
83
+
84
+ def self.create: (Target target, ?id: Integer?, ?queues: Integer, ?depth: Integer, ?max_io_bytes: Integer, ?recovery: bool, ?mlock: bool) -> Device
85
+ def self.recover: (Target target, id: Integer, ?mlock: bool) -> Device
86
+ def self.list: () -> Array[DeviceInfo]
87
+ def self.delete_all!: () -> void
88
+ def id: () -> Integer
89
+ def path: () -> String
90
+ def char_path: () -> String
91
+ def start: (?threads: :auto | Integer) -> Device
92
+ def stop: () -> Device
93
+ def delete: () -> Device
94
+ def run: (?threads: :auto | Integer) -> Device
95
+ end
96
+ end
data/tools/bench.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require_relative "../examples/loop"
5
+
6
+ abort "set UBLK_BENCH=1 inside a privileged Linux VM" unless ENV["UBLK_BENCH"] == "1"
7
+
8
+ device = nil
9
+ backing = nil
10
+ at_exit do
11
+ device&.delete
12
+ backing&.close!
13
+ end
14
+ duration = Integer(ENV.fetch("UBLK_BENCH_SECONDS", "60"), 10)
15
+ path = ENV["UBLK_BENCH_DEVICE"]
16
+ if path
17
+ abort "UBLK_BENCH_DEVICE must be a block device" unless File.blockdev?(path)
18
+ else
19
+ GC.stress = true if ENV["UBLK_BENCH_GC_STRESS"] == "1"
20
+ backing = Tempfile.new("ublk-bench")
21
+ backing.truncate(256 * 1024 * 1024)
22
+ device = UBLK::Device.create(LoopTarget.new(backing.path), depth: 128, mlock: false)
23
+ device.start
24
+ path = device.path
25
+ end
26
+
27
+ %w[randread randwrite read].each do |workload|
28
+ success = system(
29
+ "fio", "--name=#{workload}", "--filename=#{path}", "--direct=1", "--rw=#{workload}",
30
+ "--ioengine=io_uring", "--bs=4k", "--iodepth=32", "--runtime=#{duration}", "--time_based=1",
31
+ "--output-format=json"
32
+ )
33
+ abort "fio #{workload} failed" unless success
34
+ end
@@ -0,0 +1,19 @@
1
+ #include <stdio.h>
2
+ #include "../ext/ublk/compat.h"
3
+
4
+ #define DUMP(value) printf("%s=%llu\n", #value, (unsigned long long)(value))
5
+
6
+ int main(void)
7
+ {
8
+ DUMP(UBLK_F_USER_COPY);
9
+ DUMP(UBLK_F_CMD_IOCTL_ENCODE);
10
+ DUMP(UBLK_F_USER_RECOVERY);
11
+ DUMP(UBLK_MAX_QUEUE_DEPTH);
12
+ DUMP(UBLKSRV_IO_BUF_OFFSET);
13
+ DUMP(UBLK_IO_OP_READ);
14
+ DUMP(UBLK_IO_OP_WRITE);
15
+ DUMP(UBLK_IO_OP_FLUSH);
16
+ DUMP(UBLK_IO_OP_DISCARD);
17
+ DUMP(UBLK_IO_OP_WRITE_ZEROES);
18
+ return 0;
19
+ }
data/tools/vm/Makefile ADDED
@@ -0,0 +1,5 @@
1
+ KVER ?= v6.12
2
+
3
+ .PHONY: test
4
+ test:
5
+ ./run.sh $(KVER)
@@ -0,0 +1,4 @@
1
+ CONFIG_BLK_DEV_UBLK=y
2
+ CONFIG_BLK_DEV_LOOP=y
3
+ CONFIG_EXT4_FS=y
4
+ CONFIG_IO_URING=y
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ modprobe ublk_drv
5
+ export UBLK_SYSTEM_TEST=1
6
+ exec bundle exec rake test:system
data/tools/vm/run.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ kernel=${1:-v6.12}
5
+ exec vng -r "$kernel" --user root -- tools/vm/guest-test.sh
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ublk
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A native Ruby binding for Linux ublk using io_uring and UBLK_F_USER_COPY.
13
+ email:
14
+ - t.yudai92@gmail.com
15
+ executables: []
16
+ extensions:
17
+ - ext/ublk/extconf.rb
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE.txt
21
+ - README.md
22
+ - Rakefile
23
+ - examples/encrypted.rb
24
+ - examples/fault_injection.rb
25
+ - examples/http_disk.rb
26
+ - examples/loop.rb
27
+ - examples/ramdisk.rb
28
+ - examples/tracing.rb
29
+ - ext/ublk/compat.h
30
+ - ext/ublk/constants.c
31
+ - ext/ublk/control.c
32
+ - ext/ublk/extconf.rb
33
+ - ext/ublk/internal.h
34
+ - ext/ublk/server.c
35
+ - ext/ublk/ublk.c
36
+ - lib/ublk.rb
37
+ - lib/ublk/control.rb
38
+ - lib/ublk/device.rb
39
+ - lib/ublk/device_info.rb
40
+ - lib/ublk/params.rb
41
+ - lib/ublk/target.rb
42
+ - lib/ublk/version.rb
43
+ - sig/ublk.rbs
44
+ - tools/bench.rb
45
+ - tools/dump_constants.c
46
+ - tools/vm/Makefile
47
+ - tools/vm/config-fragment
48
+ - tools/vm/guest-test.sh
49
+ - tools/vm/run.sh
50
+ homepage: https://github.com/ydah/ublk
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ allowed_push_host: https://rubygems.org
55
+ homepage_uri: https://github.com/ydah/ublk
56
+ source_code_uri: https://github.com/ydah/ublk
57
+ rubygems_mfa_required: 'true'
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 3.2.0
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 4.0.19
73
+ specification_version: 4
74
+ summary: Write Linux ublk block-device targets in Ruby
75
+ test_files: []