Skip to content

Commit 05fed81

Browse files
committed
ovirt-img: support json output
Add json as an output option, to print the progress in jsonlines[1] format. A consistent machine readable format, that can be later parsed. $ ovirt-img download-disk -c engine --output json <disk_id> download.raw ... {"transferred": 249561088, "size": 261095424, elapsed: 1.234567, "description": "downloading image"} {"transferred": 256901120, "size": 261095424, elapsed: 1.345678, "description": "downloading image"} {"transferred": 261095424, "size": 261095424, elapsed: 1.456789, "description": "finalizing transfer"} ... [1] https://jsonlines.org/ Fixes: oVirt#154 Signed-off-by: Albert Esteve <aesteve@redhat.com>
1 parent c73bfc8 commit 05fed81

File tree

4 files changed

+129
-4
lines changed

4 files changed

+129
-4
lines changed

ovirt_imageio/client/_options.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def __repr__(self):
4646

4747

4848
log_level = Choices("log_level", ("debug", "info", "warning", "error"))
49-
output_format = Choices("output_format", ("human"))
49+
output_format = Choices("output_format", ("human", "json"))
5050

5151

5252
def bool_string(value):

ovirt_imageio/client/_ui.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-FileCopyrightText: Red Hat, Inc.
22
# SPDX-License-Identifier: GPL-2.0-or-later
33

4+
import json
45
import sys
56
import threading
67
import time
@@ -9,6 +10,7 @@
910

1011

1112
FORMAT_TEXT = 'human'
13+
FORMAT_JSON = 'json'
1214

1315
DEFAULT_WIDTH = 79
1416

@@ -30,19 +32,23 @@ def __init__(self, now, size=None, width=DEFAULT_WIDTH):
3032
# TODO: use current terminal width instead.
3133
self._width = width
3234

35+
@property
36+
def elapsed(self):
37+
return self._now() - self._start
38+
3339
def draw(self, value, transferred, phase=None, last=False):
3440
raise NotImplementedError
3541

3642

3743
class TextFormat(OutputFormat):
3844

3945
def draw(self, value, transferred, phase=None, last=False):
40-
elapsed = self._now() - self._start
4146
progress = f"{max(0, value):3d}%" if self.size else "----"
4247
done = util.humansize(transferred)
43-
rate = util.humansize((transferred / elapsed) if elapsed else 0)
48+
rate = util.humansize(
49+
(transferred / self.elapsed) if self.elapsed else 0)
4450
phase = f" | {phase}" if phase else ""
45-
line = f"[ {progress} ] {done}, {elapsed:.2f} s, {rate}/s{phase}"
51+
line = f"[ {progress} ] {done}, {self.elapsed:.2f} s, {rate}/s{phase}"
4652

4753
# Using "\r" moves the cursor to the first column, so the next progress
4854
# will overwrite this one. If this is the last progress, we use "\n" to
@@ -53,9 +59,21 @@ def draw(self, value, transferred, phase=None, last=False):
5359
return line.ljust(self._width, " ") + end
5460

5561

62+
class JsonFormat(OutputFormat):
63+
64+
def draw(self, value, transferred, phase=None, last=False):
65+
return json.dumps({
66+
'transferred': transferred,
67+
'size': self.size,
68+
'elapsed': self.elapsed,
69+
'description': phase or "",
70+
}) + '\n'
71+
72+
5673
FORMATTER = {
5774
None: TextFormat,
5875
FORMAT_TEXT: TextFormat,
76+
FORMAT_JSON: JsonFormat,
5977
}
6078

6179

test/client_options_test.py

+1
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ def test_transfer_options_disabled(config):
346346

347347
@pytest.mark.parametrize("format", [
348348
"human",
349+
"json",
349350
])
350351
def test_output_option(config, format):
351352
parser = _options.Parser()

test/client_ui_test.py

+106
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: GPL-2.0-or-later
33

44
import pytest
5+
import json
56

67
from ovirt_imageio import client
78
from ovirt_imageio._internal.units import MiB, GiB
@@ -84,6 +85,111 @@ def test_draw():
8485
assert f.last == line.ljust(79) + "\n"
8586

8687

88+
def test_json_format():
89+
fake_time = FakeTime()
90+
f = FakeFile()
91+
92+
# Size is unknown at this point.
93+
pb = client.ProgressBar(
94+
phase="setting up", output=f, format="json", now=fake_time)
95+
line_data = {
96+
"transferred": 0,
97+
"size": None,
98+
"elapsed": 0.0,
99+
"description": "setting up",
100+
}
101+
line = f'{json.dumps(line_data)}'
102+
assert f.last == line + "\n"
103+
104+
# Size was updated, but no bytes were transferred yet.
105+
fake_time.now += 0.1
106+
pb.size = 3 * GiB
107+
line_data = {
108+
"transferred": 0,
109+
"size": 3 * GiB,
110+
"elapsed": fake_time.now,
111+
"description": "setting up",
112+
}
113+
line = f'{json.dumps(line_data)}'
114+
assert f.last == line + "\n"
115+
116+
# Phase was updated.
117+
fake_time.now += 0.2
118+
pb.phase = "downloading image"
119+
line_data = {
120+
"transferred": 0,
121+
"size": 3 * GiB,
122+
"elapsed": fake_time.now,
123+
"description": "downloading image",
124+
}
125+
line = f'{json.dumps(line_data)}'
126+
assert f.last == line + "\n"
127+
128+
# Write some data...
129+
fake_time.now += 0.8
130+
pb.update(512 * MiB)
131+
line_data = {
132+
"transferred": 512 * MiB,
133+
"size": 3 * GiB,
134+
"elapsed": fake_time.now,
135+
"description": "downloading image",
136+
}
137+
line = f'{json.dumps(line_data)}'
138+
assert f.last == line + "\n"
139+
140+
# Write zeros (much faster)...
141+
fake_time.now += 0.2
142+
pb.update(2 * GiB)
143+
line_data = {
144+
"transferred": 2560 * MiB,
145+
"size": 3 * GiB,
146+
"elapsed": fake_time.now,
147+
"description": "downloading image",
148+
}
149+
line = f'{json.dumps(line_data)}'
150+
assert f.last == line + "\n"
151+
152+
# More data, slow down again...
153+
fake_time.now += 1.0
154+
pb.update(512 * MiB)
155+
line_data = {
156+
"transferred": 3 * GiB,
157+
"size": 3 * GiB,
158+
"elapsed": fake_time.now,
159+
"description": "downloading image",
160+
}
161+
line = f'{json.dumps(line_data)}'
162+
assert f.last == line + "\n"
163+
164+
# Cleaning up after download.
165+
pb.phase = "cleaning up"
166+
line_data = {
167+
"transferred": 3 * GiB,
168+
"size": 3 * GiB,
169+
"elapsed": fake_time.now,
170+
"description": "cleaning up",
171+
}
172+
line = f'{json.dumps(line_data)}'
173+
assert f.last == line + "\n"
174+
175+
# Cleaning can take few seconds, lowing the rate.
176+
fake_time.now += 3.0
177+
pb.phase = "download completed"
178+
line_data = {
179+
"transferred": 3 * GiB,
180+
"size": 3 * GiB,
181+
"elapsed": fake_time.now,
182+
"description": "download completed",
183+
}
184+
line = f'{json.dumps(line_data)}'
185+
assert f.last == line + "\n"
186+
187+
# Closing prints the final line with no comma.
188+
pb.close()
189+
line = f'{json.dumps(line_data)}'
190+
assert f.last == line + "\n"
191+
192+
87193
def test_with_size():
88194
fake_time = FakeTime()
89195
f = FakeFile()

0 commit comments

Comments
 (0)