Skip to content

Commit c647a60

Browse files
nirsaesteve-rh
authored andcommitted
options: Report image format and virtual size
Report image virtual size in OPTIONS so clients can get the image size without a possibly slow extent call. Report the format since OPTIONS can report the virtual size only when the backend provides raw format. This is used when using the http backend to report OPTIONS to the client. Reporting virtual size is easy with the nbd and memory backends since they always use raw format. When using file backend and qcow2 image, we don't have access to the image virtual size, and this size is not helpful to the user uploading or downloading data. Currently we don't know about the image format since engine does not report it in the ticket. The http backend reports the info from the remote server, so it depends on the backend used by the remote server, and on having new server reporting the format and size. To keep code and the API simple, we report virtual size only when using the nbd and memory backends. When engine will report the image format for the file backend, we can also report the size for raw images access via the file backend. Change-Id: I89118301c98dc2d11c25a4d1e7ef83df26336f01 Related: #67 Bug-Url: https://bugzilla.redhat.com/1924945 Signed-off-by: Nir Soffer <nsoffer@redhat.com>
1 parent 8ff7393 commit c647a60

7 files changed

Lines changed: 163 additions & 18 deletions

File tree

docs/images.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ When using multiple writers, each writer should modify a distinct byte
8484
range. If two writers modify the same byte range concurrently they will
8585
overwrite each other data.
8686

87+
### transfer_format
88+
89+
The transfer data format. Always available when using the `nbd` and
90+
`memory` backends, not available when using the `file` backend. When
91+
using the `http` backend `transfer_format` is available only if the
92+
remote server reports it.
93+
94+
Since 2.4.6.
95+
96+
### virtual_size
97+
98+
The underlying image virtual size. Available only when the backend is
99+
using `raw` transfer format. Always available when using the `nbd` and
100+
`memory` backends, not available when using the `file` backend. When
101+
using the `http` backend the `virtual_size` is available only if the
102+
remote server reports it.
103+
104+
Since 2.4.6.
105+
87106
### Errors
88107

89108
Specific errors for OPTIONS request:
@@ -112,9 +131,9 @@ Response:
112131
Content-Length: 122
113132

114133
{"unix_socket": "\u0000/org/ovirt/imageio", "features": ["extents", "zero", "flush"],
115-
"max_readers": 8, "max_writers": 8}
134+
"max_readers": 8, "max_writers": 8, "transfer_format": "raw", "virtual_size": 53687091200}
116135

117-
Get options for ticket-id with read-write access using nbd backend:
136+
Get options for ticket-id with read-write access using `nbd` backend:
118137

119138
$ curl -k -X OPTIONS https://server:54322/images/{ticket-id} | jq
120139
{
@@ -125,11 +144,13 @@ Get options for ticket-id with read-write access using nbd backend:
125144
"flush"
126145
],
127146
"max_readers": 8,
128-
"max_writers": 8
147+
"max_writers": 8,
148+
"transfer_format": "raw",
149+
"virtual_size": 53687091200
129150
}
130151

131-
The nbd backend is used when specifying the "raw" transfer format when
132-
creating an image transfer in oVirt API.
152+
The nbd backend is used when creating an image transfer with
153+
format="raw" in oVirt API.
133154

134155
Get options for ticket-id with read-only access using file backend:
135156

@@ -143,6 +164,9 @@ Get options for ticket-id with read-only access using file backend:
143164
"max_writers": 1
144165
}
145166

167+
Note that `virtual_size` and `transfer_format` are not reported, and this
168+
backend does not support multiple writers.
169+
146170
Get all available options for the special `*` ticket:
147171

148172
$ curl -sk -X OPTIONS 'https://server:54322/images/*' | jq

ovirt_imageio/_internal/backends/file.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ def __init__(self, fio, sparse=False, max_connections=8):
7474
def max_readers(self):
7575
return self._max_connections
7676

77+
@property
78+
def format(self):
79+
"""
80+
Currently we don't have access to the underlying disk format.
81+
"""
82+
return None
83+
7784
# io.FileIO interface
7885

7986
def readinto(self, buf):

ovirt_imageio/_internal/backends/http.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ def __init__(self, url, cafile=None, secure=True, connect_timeout=10,
6363
self._connect_timeout = connect_timeout
6464
self._read_timeout = read_timeout
6565
self._position = 0
66-
self._size = None
6766
self._extents = {}
6867

6968
# Initlized during connection.
@@ -74,6 +73,8 @@ def __init__(self, url, cafile=None, secure=True, connect_timeout=10,
7473
self._can_flush = False
7574
self._max_readers = 1
7675
self._max_writers = 1
76+
self._format = None
77+
self._size = None
7778

7879
if connect:
7980
self._connect()
@@ -104,8 +105,10 @@ def clone(self):
104105
backend._can_flush = self._can_flush
105106
backend._max_readers = self._max_readers
106107
backend._max_writers = self._max_writers
108+
backend._format = self._format
107109

108-
# Copy size and extents to save expensive EXTENTS calls.
110+
# Copy extents and size to save expensive EXTENTS calls with old
111+
# imageio servers.
109112
backend._size = self._size
110113
for ctx in list(self._extents):
111114
backend._extents[ctx] = self._extents[ctx].copy()
@@ -133,6 +136,10 @@ def _connect(self):
133136
# max_writers does not support multiple writers.
134137
self._max_writers = options.get("max_writers", 1)
135138

139+
# Newer server reports also transfer_format and virtual_size.
140+
self._format = options.get("transfer_format")
141+
self._size = options.get("virtual_size")
142+
136143
self._optimize_connection(options.get("unix_socket"))
137144
except Exception:
138145
self._con.close()
@@ -155,6 +162,14 @@ def max_readers(self):
155162
def max_writers(self):
156163
return self._max_writers
157164

165+
@property
166+
def format(self):
167+
"""
168+
Will be None for old server (imageio < 2.4.6) or when server is using
169+
file backend.
170+
"""
171+
return self._format
172+
158173
# Preferred interface.
159174

160175
def read_from(self, reader, length, buf):
@@ -325,10 +340,17 @@ def seek(self, n, how=os.SEEK_SET):
325340
return self._position
326341

327342
def size(self):
328-
# We have 2 bad options:
329-
# - Get last extent, may be slow, and may not be neded otherwise.
330-
# - Emulate HEAD request, logging tracebacks in the remote server.
331-
# Getting extents is more polite, so lets use it if we can.
343+
"""
344+
Return backend size in bytes.
345+
346+
With newer server (imageio >= 2.4.6) reporting the virtual size the size
347+
is intialized in connect().
348+
349+
Otherwise we have 2 bad options:
350+
- Get the last extent, may be slow, and may not be neded otherwise.
351+
- Emulate HEAD request, logging tracebacks in the remote server.
352+
Getting extents is more polite, so lets use it if we can.
353+
"""
332354
if self._size is None:
333355
if self._can_extents:
334356
last = list(self.extents())[-1]

ovirt_imageio/_internal/backends/memory.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def max_writers(self):
7272
# support more than one writer concurrently.
7373
return 1
7474

75+
@property
76+
def format(self):
77+
return "raw"
78+
7579
# io.BaseIO interface
7680

7781
def readinto(self, buf):

ovirt_imageio/_internal/backends/nbd.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ def max_readers(self):
9595
def max_writers(self):
9696
return self._max_connections
9797

98+
@property
99+
def format(self):
100+
return "raw"
101+
98102
# Backend interface
99103

100104
def readinto(self, buf):

ovirt_imageio/_internal/handlers/images.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,5 +268,11 @@ def options(self, req, resp, ticket_id):
268268
options["max_readers"] = ctx.backend.max_readers
269269
options["max_writers"] = ctx.backend.max_writers
270270

271+
# Optional backend options.
272+
if ctx.backend.format is not None:
273+
options["transfer_format"] = ctx.backend.format
274+
if ctx.backend.format == "raw":
275+
options["virtual_size"] = ctx.backend.size()
276+
271277
resp.headers["allow"] = ",".join(allow)
272278
resp.send_json(options)

test/handlers/images_test.py

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
import pytest
1515

1616
from ovirt_imageio._internal import config
17+
from ovirt_imageio._internal import qemu_img
1718
from ovirt_imageio._internal import server
18-
from ovirt_imageio._internal.units import KiB, MiB
19+
from ovirt_imageio._internal.units import KiB, MiB, GiB
1920

2021
from .. import testutil
2122
from .. import http
@@ -705,7 +706,7 @@ def test_options_all(srv, client):
705706
assert "max_writers" not in options
706707

707708

708-
def test_options_read_write(srv, client, tmpdir):
709+
def test_options_file_read_write(srv, client, tmpdir):
709710
size = 128 * KiB
710711
image = testutil.create_tempfile(tmpdir, "image", size=size)
711712
ticket = testutil.create_ticket(
@@ -717,11 +718,15 @@ def test_options_read_write(srv, client, tmpdir):
717718
assert set(res.getheader("allow").split(',')) == allows
718719
options = json.loads(res.read())
719720
assert set(options["features"]) == ALL_FEATURES
721+
722+
# File backend specific options.
720723
assert options["max_readers"] == srv.config.daemon.max_connections
721-
assert options["max_writers"] == 1 # Using file backend.
724+
assert options["max_writers"] == 1
725+
assert "transfer_format" not in options
726+
assert "virtual_size" not in options
722727

723728

724-
def test_options_read(srv, client, tmpdir):
729+
def test_options_file_read(srv, client, tmpdir):
725730
size = 128 * KiB
726731
image = testutil.create_tempfile(tmpdir, "image", size=size)
727732
ticket = testutil.create_ticket(
@@ -733,11 +738,15 @@ def test_options_read(srv, client, tmpdir):
733738
assert set(res.getheader("allow").split(',')) == allows
734739
options = json.loads(res.read())
735740
assert set(options["features"]) == BASE_FEATURES
741+
742+
# File backend specific options.
736743
assert options["max_readers"] == srv.config.daemon.max_connections
737-
assert options["max_writers"] == 1 # Using file backend.
744+
assert options["max_writers"] == 1
745+
assert "transfer_format" not in options
746+
assert "virtual_size" not in options
738747

739748

740-
def test_options_write(srv, client, tmpdir):
749+
def test_options_file_write(srv, client, tmpdir):
741750
size = 128 * KiB
742751
image = testutil.create_tempfile(tmpdir, "image", size=size)
743752
ticket = testutil.create_ticket(
@@ -750,8 +759,77 @@ def test_options_write(srv, client, tmpdir):
750759
assert set(res.getheader("allow").split(',')) == allows
751760
options = json.loads(res.read())
752761
assert set(options["features"]) == ALL_FEATURES
762+
763+
# File backend specific options.
764+
assert options["max_readers"] == srv.config.daemon.max_connections
765+
assert options["max_writers"] == 1
766+
assert "transfer_format" not in options
767+
assert "virtual_size" not in options
768+
769+
770+
@pytest.mark.parametrize("fmt", ["raw", "qcow2"])
771+
def test_options_nbd_read(srv, client, tmpdir, nbd_server, fmt):
772+
# Create disk.
773+
size = GiB
774+
disk = str(tmpdir.join(f"disk.{fmt}"))
775+
qemu_img.create(disk, fmt, size=size)
776+
777+
# Start nbd server exporting the disk.
778+
nbd_server.image = disk
779+
nbd_server.fmt = fmt
780+
nbd_server.read_only = True
781+
nbd_server.start()
782+
783+
# Add ticket using nbd server url.
784+
ticket = testutil.create_ticket(
785+
url=nbd_server.sock.url(), size=size, ops=["read"])
786+
srv.auth.add(ticket)
787+
788+
# Get OPTIONS.
789+
res = client.options("/images/" + ticket["uuid"])
790+
allows = {"OPTIONS", "GET"}
791+
assert res.status == 200
792+
assert set(res.getheader("allow").split(',')) == allows
793+
options = json.loads(res.read())
794+
assert set(options["features"]) == BASE_FEATURES
795+
796+
# NBD backend specific options.
797+
assert options["max_readers"] == srv.config.daemon.max_connections
798+
assert options["max_writers"] == srv.config.daemon.max_connections
799+
assert options["transfer_format"] == "raw"
800+
assert options["virtual_size"] == size
801+
802+
803+
@pytest.mark.parametrize("fmt", ["raw", "qcow2"])
804+
def test_options_nbd_write(srv, client, tmpdir, nbd_server, fmt):
805+
# Create disk.
806+
size = GiB
807+
disk = str(tmpdir.join(f"disk.{fmt}"))
808+
qemu_img.create(disk, fmt, size=size)
809+
810+
# Start nbd server exporting the disk.
811+
nbd_server.image = disk
812+
nbd_server.fmt = fmt
813+
nbd_server.start()
814+
815+
# Add ticket using nbd server url.
816+
ticket = testutil.create_ticket(
817+
url=nbd_server.sock.url(), size=size, ops=["write"])
818+
srv.auth.add(ticket)
819+
820+
# Get OPTIONS.
821+
res = client.options("/images/" + ticket["uuid"])
822+
allows = {"OPTIONS", "GET", "PUT", "PATCH"}
823+
assert res.status == 200
824+
assert set(res.getheader("allow").split(',')) == allows
825+
options = json.loads(res.read())
826+
assert set(options["features"]) == ALL_FEATURES
827+
828+
# NBD backend specific options.
753829
assert options["max_readers"] == srv.config.daemon.max_connections
754-
assert options["max_writers"] == 1 # Using file backend.
830+
assert options["max_writers"] == srv.config.daemon.max_connections
831+
assert options["transfer_format"] == "raw"
832+
assert options["virtual_size"] == size
755833

756834

757835
def test_options_extends_ticket(srv, client, tmpdir, fake_time):

0 commit comments

Comments
 (0)