]>
Commit | Line | Data |
---|---|---|
23102ed6 | 1 | From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 |
67af0fa4 WB |
2 | From: Wolfgang Bumiller <w.bumiller@proxmox.com> |
3 | Date: Wed, 2 Aug 2017 13:51:02 +0200 | |
53e83913 | 4 | Subject: [PATCH] PVE: backup: introduce vma archive format |
67af0fa4 | 5 | |
53e83913 | 6 | TODO: Move to a libvma block backend. |
b855dce7 | 7 | Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com> |
67af0fa4 WB |
8 | --- |
9 | MAINTAINERS | 6 + | |
10 | block/Makefile.objs | 3 + | |
b855dce7 TL |
11 | block/vma.c | 503 ++++++++++++++++++++++++++++++++++++++++ |
12 | blockdev.c | 536 +++++++++++++++++++++++++++++++++++++++++++ | |
53e83913 | 13 | configure | 29 +++ |
67af0fa4 | 14 | hmp-commands-info.hx | 13 ++ |
6838f038 | 15 | hmp-commands.hx | 31 +++ |
b855dce7 | 16 | hmp.c | 63 +++++ |
67af0fa4 | 17 | hmp.h | 3 + |
b855dce7 | 18 | qapi/block-core.json | 109 ++++++++- |
53e83913 WB |
19 | qapi/common.json | 13 ++ |
20 | qapi/misc.json | 13 -- | |
0775f12b | 21 | 12 files changed, 1308 insertions(+), 14 deletions(-) |
67af0fa4 WB |
22 | create mode 100644 block/vma.c |
23 | ||
24 | diff --git a/MAINTAINERS b/MAINTAINERS | |
b855dce7 | 25 | index 56139ac8ab..5588f5c91e 100644 |
67af0fa4 WB |
26 | --- a/MAINTAINERS |
27 | +++ b/MAINTAINERS | |
b855dce7 | 28 | @@ -2499,6 +2499,12 @@ L: qemu-block@nongnu.org |
67af0fa4 WB |
29 | S: Supported |
30 | F: block/vvfat.c | |
31 | ||
32 | +VMA | |
33 | +M: Wolfgang Bumiller <w.bumiller@proxmox.com>. | |
34 | +L: pve-devel@proxmox.com | |
35 | +S: Supported | |
36 | +F: block/vma.c | |
37 | + | |
38 | Image format fuzzer | |
39 | M: Stefan Hajnoczi <stefanha@redhat.com> | |
40 | L: qemu-block@nongnu.org | |
41 | diff --git a/block/Makefile.objs b/block/Makefile.objs | |
b855dce7 | 42 | index 03b5763bfa..00fa730d7b 100644 |
67af0fa4 WB |
43 | --- a/block/Makefile.objs |
44 | +++ b/block/Makefile.objs | |
b855dce7 | 45 | @@ -33,6 +33,7 @@ block-obj-$(CONFIG_RBD) += rbd.o |
67af0fa4 | 46 | block-obj-$(CONFIG_GLUSTERFS) += gluster.o |
6838f038 | 47 | block-obj-$(CONFIG_VXHS) += vxhs.o |
67af0fa4 WB |
48 | block-obj-$(CONFIG_LIBSSH2) += ssh.o |
49 | +block-obj-$(CONFIG_VMA) += vma.o | |
50 | block-obj-y += accounting.o dirty-bitmap.o | |
51 | block-obj-y += write-threshold.o | |
52 | block-obj-y += backup.o | |
b855dce7 | 53 | @@ -64,3 +65,5 @@ qcow.o-libs := -lz |
67af0fa4 | 54 | linux-aio.o-libs := -laio |
53e83913 WB |
55 | parallels.o-cflags := $(LIBXML2_CFLAGS) |
56 | parallels.o-libs := $(LIBXML2_LIBS) | |
67af0fa4 WB |
57 | +vma.o-cflags := $(VMA_CFLAGS) |
58 | +vma.o-libs := $(VMA_LIBS) | |
59 | diff --git a/block/vma.c b/block/vma.c | |
60 | new file mode 100644 | |
0775f12b | 61 | index 0000000000..b911b198dc |
67af0fa4 WB |
62 | --- /dev/null |
63 | +++ b/block/vma.c | |
0775f12b | 64 | @@ -0,0 +1,503 @@ |
67af0fa4 WB |
65 | +/* |
66 | + * VMA archive backend for QEMU, container object | |
67 | + * | |
68 | + * Copyright (C) 2017 Proxmox Server Solutions GmbH | |
69 | + * | |
70 | + * This work is licensed under the terms of the GNU GPL, version 2 or later. | |
71 | + * See the COPYING file in the top-level directory. | |
72 | + * | |
73 | + */ | |
74 | +#include <vma/vma.h> | |
75 | + | |
76 | +#include "qemu/osdep.h" | |
77 | +#include "qemu/uuid.h" | |
0775f12b | 78 | +#include "qemu/option.h" |
67af0fa4 WB |
79 | +#include "qemu-common.h" |
80 | +#include "qapi/error.h" | |
81 | +#include "qapi/qmp/qerror.h" | |
82 | +#include "qapi/qmp/qstring.h" | |
0775f12b | 83 | +#include "qapi/qmp/qdict.h" |
67af0fa4 WB |
84 | +#include "qom/object.h" |
85 | +#include "qom/object_interfaces.h" | |
86 | +#include "block/block_int.h" | |
87 | + | |
88 | +/* exported interface */ | |
89 | +void vma_object_add_config_file(Object *obj, const char *name, | |
90 | + const char *contents, size_t len, | |
91 | + Error **errp); | |
92 | + | |
93 | +#define TYPE_VMA_OBJECT "vma" | |
94 | +#define VMA_OBJECT(obj) \ | |
95 | + OBJECT_CHECK(VMAObjectState, (obj), TYPE_VMA_OBJECT) | |
96 | +#define VMA_OBJECT_GET_CLASS(obj) \ | |
97 | + OBJECT_GET_CLASS(VMAObjectClass, (obj), TYPE_VMA_OBJECT) | |
98 | + | |
99 | +typedef struct VMAObjectClass { | |
100 | + ObjectClass parent_class; | |
101 | +} VMAObjectClass; | |
102 | + | |
103 | +typedef struct VMAObjectState { | |
104 | + Object parent; | |
105 | + | |
106 | + char *filename; | |
107 | + | |
108 | + QemuUUID uuid; | |
109 | + bool blocked; | |
110 | + VMAWriter *vma; | |
111 | + QemuMutex mutex; | |
112 | +} VMAObjectState; | |
113 | + | |
114 | +static VMAObjectState *vma_by_id(const char *name) | |
115 | +{ | |
116 | + Object *container; | |
117 | + Object *obj; | |
118 | + | |
119 | + container = object_get_objects_root(); | |
120 | + obj = object_resolve_path_component(container, name); | |
121 | + | |
122 | + return VMA_OBJECT(obj); | |
123 | +} | |
124 | + | |
125 | +static void vma_object_class_complete(UserCreatable *uc, Error **errp) | |
126 | +{ | |
127 | + int rc; | |
128 | + VMAObjectState *vo = VMA_OBJECT(uc); | |
129 | + VMAObjectClass *voc = VMA_OBJECT_GET_CLASS(uc); | |
130 | + (void)!vo; | |
131 | + (void)!voc; | |
132 | + | |
133 | + if (!vo->filename) { | |
134 | + error_setg(errp, "Parameter 'filename' is required"); | |
135 | + return; | |
136 | + } | |
137 | + | |
0775f12b WB |
138 | + vo->vma = VMAWriter_fopen(vo->filename); |
139 | + if (!vo->vma) { | |
140 | + error_setg_errno(errp, errno, "failed to create VMA archive"); | |
67af0fa4 WB |
141 | + return; |
142 | + } | |
143 | + | |
144 | + rc = VMAWriter_set_uuid(vo->vma, vo->uuid.data, sizeof(vo->uuid.data)); | |
145 | + if (rc < 0) { | |
146 | + error_setg_errno(errp, -rc, "failed to set UUID of VMA archive"); | |
147 | + return; | |
148 | + } | |
149 | + | |
150 | + qemu_mutex_init(&vo->mutex); | |
151 | +} | |
152 | + | |
0775f12b | 153 | +static bool vma_object_can_be_deleted(UserCreatable *uc) |
67af0fa4 WB |
154 | +{ |
155 | + //VMAObjectState *vo = VMA_OBJECT(uc); | |
156 | + //if (!vo->vma) { | |
157 | + // return true; | |
158 | + //} | |
159 | + //return false; | |
160 | + return true; | |
161 | +} | |
162 | + | |
163 | +static void vma_object_class_init(ObjectClass *oc, void *data) | |
164 | +{ | |
165 | + UserCreatableClass *ucc = USER_CREATABLE_CLASS(oc); | |
166 | + | |
167 | + ucc->can_be_deleted = vma_object_can_be_deleted; | |
168 | + ucc->complete = vma_object_class_complete; | |
169 | +} | |
170 | + | |
171 | +static char *vma_object_get_filename(Object *obj, Error **errp) | |
172 | +{ | |
173 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
174 | + | |
175 | + return g_strdup(vo->filename); | |
176 | +} | |
177 | + | |
178 | +static void vma_object_set_filename(Object *obj, const char *str, Error **errp) | |
179 | +{ | |
180 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
181 | + | |
182 | + if (vo->vma) { | |
183 | + error_setg(errp, "filename cannot be changed after creation"); | |
184 | + return; | |
185 | + } | |
186 | + | |
187 | + g_free(vo->filename); | |
188 | + vo->filename = g_strdup(str); | |
189 | +} | |
190 | + | |
191 | +static char *vma_object_get_uuid(Object *obj, Error **errp) | |
192 | +{ | |
193 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
194 | + | |
195 | + return qemu_uuid_unparse_strdup(&vo->uuid); | |
196 | +} | |
197 | + | |
198 | +static void vma_object_set_uuid(Object *obj, const char *str, Error **errp) | |
199 | +{ | |
200 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
201 | + | |
202 | + if (vo->vma) { | |
203 | + error_setg(errp, "uuid cannot be changed after creation"); | |
204 | + return; | |
205 | + } | |
206 | + | |
207 | + qemu_uuid_parse(str, &vo->uuid); | |
208 | +} | |
209 | + | |
210 | +static bool vma_object_get_blocked(Object *obj, Error **errp) | |
211 | +{ | |
212 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
213 | + | |
214 | + return vo->blocked; | |
215 | +} | |
216 | + | |
217 | +static void vma_object_set_blocked(Object *obj, bool blocked, Error **errp) | |
218 | +{ | |
219 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
220 | + | |
221 | + (void)errp; | |
222 | + | |
223 | + vo->blocked = blocked; | |
224 | +} | |
225 | + | |
226 | +void vma_object_add_config_file(Object *obj, const char *name, | |
227 | + const char *contents, size_t len, | |
228 | + Error **errp) | |
229 | +{ | |
230 | + int rc; | |
231 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
232 | + | |
233 | + if (!vo || !vo->vma) { | |
234 | + error_setg(errp, "not a valid vma object to add config files to"); | |
235 | + return; | |
236 | + } | |
237 | + | |
238 | + rc = VMAWriter_addConfigFile(vo->vma, name, contents, len); | |
239 | + if (rc < 0) { | |
240 | + error_setg_errno(errp, -rc, "failed to add config file to VMA"); | |
241 | + return; | |
242 | + } | |
243 | +} | |
244 | + | |
245 | +static void vma_object_init(Object *obj) | |
246 | +{ | |
247 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
248 | + (void)!vo; | |
249 | + | |
250 | + object_property_add_str(obj, "filename", | |
251 | + vma_object_get_filename, vma_object_set_filename, | |
252 | + NULL); | |
253 | + object_property_add_str(obj, "uuid", | |
254 | + vma_object_get_uuid, vma_object_set_uuid, | |
255 | + NULL); | |
256 | + object_property_add_bool(obj, "blocked", | |
257 | + vma_object_get_blocked, vma_object_set_blocked, | |
258 | + NULL); | |
259 | +} | |
260 | + | |
261 | +static void vma_object_finalize(Object *obj) | |
262 | +{ | |
263 | + VMAObjectState *vo = VMA_OBJECT(obj); | |
264 | + VMAObjectClass *voc = VMA_OBJECT_GET_CLASS(obj); | |
265 | + (void)!voc; | |
266 | + | |
267 | + qemu_mutex_destroy(&vo->mutex); | |
268 | + | |
269 | + VMAWriter_destroy(vo->vma, true); | |
270 | + g_free(vo->filename); | |
271 | +} | |
272 | + | |
273 | +static const TypeInfo vma_object_info = { | |
274 | + .name = TYPE_VMA_OBJECT, | |
275 | + .parent = TYPE_OBJECT, | |
276 | + .class_size = sizeof(VMAObjectClass), | |
277 | + .class_init = vma_object_class_init, | |
278 | + .instance_size = sizeof(VMAObjectState), | |
279 | + .instance_init = vma_object_init, | |
280 | + .instance_finalize = vma_object_finalize, | |
281 | + .interfaces = (InterfaceInfo[]) { | |
282 | + { TYPE_USER_CREATABLE }, | |
283 | + { } | |
284 | + } | |
285 | +}; | |
286 | + | |
287 | +static void register_types(void) | |
288 | +{ | |
289 | + type_register_static(&vma_object_info); | |
290 | +} | |
291 | + | |
292 | +type_init(register_types); | |
293 | + | |
294 | +typedef struct { | |
295 | + VMAObjectState *vma_obj; | |
296 | + char *name; | |
297 | + size_t device_id; | |
298 | + uint64_t byte_size; | |
299 | +} BDRVVMAState; | |
300 | + | |
301 | +static void qemu_vma_parse_filename(const char *filename, QDict *options, | |
302 | + Error **errp) | |
303 | +{ | |
304 | + char *sep; | |
305 | + | |
0775f12b WB |
306 | + if (strncmp(filename, "vma:", sizeof("vma:")-1) == 0) { |
307 | + filename += sizeof("vma:")-1; | |
308 | + } | |
309 | + | |
67af0fa4 WB |
310 | + sep = strchr(filename, '/'); |
311 | + if (!sep || sep == filename) { | |
0775f12b | 312 | + error_setg(errp, "VMA file should be <vma-obj>/<name>/<size>"); |
67af0fa4 WB |
313 | + return; |
314 | + } | |
315 | + | |
0775f12b | 316 | + qdict_put(options, "vma", qstring_from_substr(filename, 0, sep-filename)); |
67af0fa4 WB |
317 | + |
318 | + while (*sep && *sep == '/') | |
319 | + ++sep; | |
320 | + if (!*sep) { | |
321 | + error_setg(errp, "missing device name\n"); | |
322 | + return; | |
323 | + } | |
324 | + | |
0775f12b WB |
325 | + filename = sep; |
326 | + sep = strchr(filename, '/'); | |
327 | + if (!sep || sep == filename) { | |
328 | + error_setg(errp, "VMA file should be <vma-obj>/<name>/<size>"); | |
329 | + return; | |
330 | + } | |
331 | + | |
332 | + qdict_put(options, "name", qstring_from_substr(filename, 0, sep-filename)); | |
333 | + | |
334 | + while (*sep && *sep == '/') | |
335 | + ++sep; | |
336 | + if (!*sep) { | |
337 | + error_setg(errp, "missing device size\n"); | |
338 | + return; | |
339 | + } | |
340 | + | |
341 | + filename = sep; | |
342 | + qdict_put_str(options, "size", filename); | |
67af0fa4 WB |
343 | +} |
344 | + | |
345 | +static QemuOptsList runtime_opts = { | |
346 | + .name = "vma-drive", | |
347 | + .head = QTAILQ_HEAD_INITIALIZER(runtime_opts.head), | |
348 | + .desc = { | |
349 | + { | |
350 | + .name = "vma", | |
351 | + .type = QEMU_OPT_STRING, | |
352 | + .help = "VMA Object name", | |
353 | + }, | |
354 | + { | |
355 | + .name = "name", | |
356 | + .type = QEMU_OPT_STRING, | |
357 | + .help = "VMA device name", | |
358 | + }, | |
359 | + { | |
360 | + .name = BLOCK_OPT_SIZE, | |
361 | + .type = QEMU_OPT_SIZE, | |
362 | + .help = "Virtual disk size" | |
363 | + }, | |
364 | + { /* end of list */ } | |
365 | + }, | |
366 | +}; | |
367 | +static int qemu_vma_open(BlockDriverState *bs, QDict *options, int flags, | |
368 | + Error **errp) | |
369 | +{ | |
370 | + Error *local_err = NULL; | |
371 | + BDRVVMAState *s = bs->opaque; | |
372 | + QemuOpts *opts; | |
373 | + const char *vma_id, *device_name; | |
374 | + ssize_t dev_id; | |
375 | + int64_t bytes = 0; | |
376 | + int ret; | |
377 | + | |
378 | + opts = qemu_opts_create(&runtime_opts, NULL, 0, &error_abort); | |
379 | + qemu_opts_absorb_qdict(opts, options, &local_err); | |
380 | + if (local_err) { | |
381 | + error_propagate(errp, local_err); | |
382 | + ret = -EINVAL; | |
383 | + goto failed_opts; | |
384 | + } | |
385 | + | |
386 | + bytes = ROUND_UP(qemu_opt_get_size_del(opts, BLOCK_OPT_SIZE, 0), | |
387 | + BDRV_SECTOR_SIZE); | |
388 | + | |
389 | + vma_id = qemu_opt_get(opts, "vma"); | |
0775f12b WB |
390 | + if (!vma_id) { |
391 | + ret = -EINVAL; | |
392 | + error_setg(errp, "missing 'vma' property"); | |
393 | + goto failed_opts; | |
394 | + } | |
395 | + | |
67af0fa4 | 396 | + device_name = qemu_opt_get(opts, "name"); |
0775f12b WB |
397 | + if (!device_name) { |
398 | + ret = -EINVAL; | |
399 | + error_setg(errp, "missing 'name' property"); | |
400 | + goto failed_opts; | |
401 | + } | |
67af0fa4 WB |
402 | + |
403 | + VMAObjectState *vma = vma_by_id(vma_id); | |
404 | + if (!vma) { | |
405 | + ret = -EINVAL; | |
406 | + error_setg(errp, "no such VMA object: %s", vma_id); | |
407 | + goto failed_opts; | |
408 | + } | |
409 | + | |
410 | + dev_id = VMAWriter_findDevice(vma->vma, device_name); | |
411 | + if (dev_id >= 0) { | |
412 | + error_setg(errp, "drive already exists in VMA object"); | |
413 | + ret = -EIO; | |
414 | + goto failed_opts; | |
415 | + } | |
416 | + | |
417 | + dev_id = VMAWriter_addDevice(vma->vma, device_name, (uint64_t)bytes); | |
418 | + if (dev_id < 0) { | |
419 | + error_setg_errno(errp, -dev_id, "failed to add VMA device"); | |
420 | + ret = -EIO; | |
421 | + goto failed_opts; | |
422 | + } | |
423 | + | |
424 | + object_ref(OBJECT(vma)); | |
425 | + s->vma_obj = vma; | |
426 | + s->name = g_strdup(device_name); | |
427 | + s->device_id = (size_t)dev_id; | |
428 | + s->byte_size = bytes; | |
429 | + | |
430 | + ret = 0; | |
431 | + | |
432 | +failed_opts: | |
433 | + qemu_opts_del(opts); | |
434 | + return ret; | |
435 | +} | |
436 | + | |
437 | +static void qemu_vma_close(BlockDriverState *bs) | |
438 | +{ | |
439 | + BDRVVMAState *s = bs->opaque; | |
440 | + | |
441 | + (void)VMAWriter_finishDevice(s->vma_obj->vma, s->device_id); | |
442 | + object_unref(OBJECT(s->vma_obj)); | |
443 | + | |
444 | + g_free(s->name); | |
445 | +} | |
446 | + | |
447 | +static int64_t qemu_vma_getlength(BlockDriverState *bs) | |
448 | +{ | |
449 | + BDRVVMAState *s = bs->opaque; | |
450 | + | |
451 | + return s->byte_size; | |
452 | +} | |
453 | + | |
454 | +static coroutine_fn int qemu_vma_co_writev(BlockDriverState *bs, | |
455 | + int64_t sector_num, | |
456 | + int nb_sectors, | |
0775f12b WB |
457 | + QEMUIOVector *qiov, |
458 | + int flags) | |
67af0fa4 WB |
459 | +{ |
460 | + size_t i; | |
461 | + ssize_t rc; | |
462 | + BDRVVMAState *s = bs->opaque; | |
463 | + VMAObjectState *vo = s->vma_obj; | |
464 | + off_t offset = sector_num * BDRV_SECTOR_SIZE; | |
0775f12b WB |
465 | + /* flags can be only values we set in supported_write_flags */ |
466 | + assert(flags == 0); | |
67af0fa4 WB |
467 | + |
468 | + qemu_mutex_lock(&vo->mutex); | |
469 | + if (vo->blocked) { | |
470 | + return -EPERM; | |
471 | + } | |
472 | + for (i = 0; i != qiov->niov; ++i) { | |
473 | + const struct iovec *v = &qiov->iov[i]; | |
474 | + size_t blocks = v->iov_len / VMA_BLOCK_SIZE; | |
475 | + if (blocks * VMA_BLOCK_SIZE != v->iov_len) { | |
476 | + return -EIO; | |
477 | + } | |
478 | + rc = VMAWriter_writeBlocks(vo->vma, s->device_id, | |
479 | + v->iov_base, blocks, offset); | |
480 | + if (errno) { | |
481 | + return -errno; | |
482 | + } | |
483 | + if (rc != blocks) { | |
484 | + return -EIO; | |
485 | + } | |
486 | + offset += v->iov_len; | |
487 | + } | |
488 | + qemu_mutex_unlock(&vo->mutex); | |
489 | + return 0; | |
490 | +} | |
491 | + | |
492 | +static int qemu_vma_get_info(BlockDriverState *bs, BlockDriverInfo *bdi) | |
493 | +{ | |
494 | + bdi->cluster_size = VMA_CLUSTER_SIZE; | |
495 | + bdi->unallocated_blocks_are_zero = true; | |
67af0fa4 WB |
496 | + return 0; |
497 | +} | |
498 | + | |
0775f12b WB |
499 | +static int qemu_vma_check_perm(BlockDriverState *bs, |
500 | + uint64_t perm, | |
501 | + uint64_t shared, | |
502 | + Error **errp) | |
503 | +{ | |
504 | + /* Nothing to do. */ | |
505 | + return 0; | |
506 | +} | |
507 | + | |
508 | +static void qemu_vma_set_perm(BlockDriverState *bs, | |
509 | + uint64_t perm, | |
510 | + uint64_t shared) | |
511 | +{ | |
512 | + /* Nothing to do. */ | |
513 | +} | |
514 | + | |
515 | +static void qemu_vma_abort_perm_update(BlockDriverState *bs) | |
516 | +{ | |
517 | + /* Nothing to do. */ | |
518 | +} | |
519 | + | |
520 | +static void qemu_vma_refresh_limits(BlockDriverState *bs, Error **errp) | |
521 | +{ | |
522 | + bs->bl.request_alignment = BDRV_SECTOR_SIZE; /* No sub-sector I/O */ | |
523 | +} | |
524 | +static void qemu_vma_child_perm(BlockDriverState *bs, BdrvChild *c, | |
525 | + const BdrvChildRole *role, | |
526 | + BlockReopenQueue *reopen_queue, | |
527 | + uint64_t perm, uint64_t shared, | |
528 | + uint64_t *nperm, uint64_t *nshared) | |
529 | +{ | |
530 | + *nperm = BLK_PERM_ALL; | |
531 | + *nshared = BLK_PERM_ALL; | |
532 | +} | |
533 | + | |
67af0fa4 WB |
534 | +static BlockDriver bdrv_vma_drive = { |
535 | + .format_name = "vma-drive", | |
0775f12b | 536 | + .protocol_name = "vma", |
67af0fa4 WB |
537 | + .instance_size = sizeof(BDRVVMAState), |
538 | + | |
539 | +#if 0 | |
540 | + .bdrv_create = qemu_vma_create, | |
541 | + .create_opts = &qemu_vma_create_opts, | |
542 | +#endif | |
543 | + | |
544 | + .bdrv_parse_filename = qemu_vma_parse_filename, | |
545 | + .bdrv_file_open = qemu_vma_open, | |
546 | + | |
547 | + .bdrv_close = qemu_vma_close, | |
548 | + .bdrv_has_zero_init = bdrv_has_zero_init_1, | |
549 | + .bdrv_getlength = qemu_vma_getlength, | |
550 | + .bdrv_get_info = qemu_vma_get_info, | |
551 | + | |
0775f12b | 552 | + //.bdrv_co_preadv = qemu_vma_co_preadv, |
67af0fa4 | 553 | + .bdrv_co_writev = qemu_vma_co_writev, |
0775f12b WB |
554 | + |
555 | + .bdrv_refresh_limits = qemu_vma_refresh_limits, | |
556 | + .bdrv_check_perm = qemu_vma_check_perm, | |
557 | + .bdrv_set_perm = qemu_vma_set_perm, | |
558 | + .bdrv_abort_perm_update = qemu_vma_abort_perm_update, | |
559 | + .bdrv_child_perm = qemu_vma_child_perm, | |
67af0fa4 WB |
560 | +}; |
561 | + | |
562 | +static void bdrv_vma_init(void) | |
563 | +{ | |
564 | + bdrv_register(&bdrv_vma_drive); | |
565 | +} | |
566 | + | |
567 | +block_init(bdrv_vma_init); | |
568 | diff --git a/blockdev.c b/blockdev.c | |
b8d43c59 | 569 | index a3542db57a..5d524b9a74 100644 |
67af0fa4 WB |
570 | --- a/blockdev.c |
571 | +++ b/blockdev.c | |
53e83913 | 572 | @@ -31,11 +31,13 @@ |
67af0fa4 WB |
573 | */ |
574 | ||
575 | #include "qemu/osdep.h" | |
576 | +#include "qemu/uuid.h" | |
577 | #include "sysemu/block-backend.h" | |
578 | #include "sysemu/blockdev.h" | |
579 | #include "hw/block/block.h" | |
580 | #include "block/blockjob.h" | |
53e83913 | 581 | #include "block/qdict.h" |
67af0fa4 WB |
582 | +#include "block/blockjob_int.h" |
583 | #include "block/throttle-groups.h" | |
584 | #include "monitor/monitor.h" | |
585 | #include "qemu/error-report.h" | |
53e83913 WB |
586 | @@ -44,6 +46,7 @@ |
587 | #include "qapi/qapi-commands-block.h" | |
588 | #include "qapi/qapi-commands-transaction.h" | |
589 | #include "qapi/qapi-visit-block-core.h" | |
590 | +#include "qapi/qapi-types-misc.h" | |
591 | #include "qapi/qmp/qdict.h" | |
592 | #include "qapi/qmp/qnum.h" | |
593 | #include "qapi/qmp/qstring.h" | |
b8d43c59 | 594 | @@ -3148,6 +3151,539 @@ out: |
67af0fa4 WB |
595 | aio_context_release(aio_context); |
596 | } | |
597 | ||
598 | +/* PVE backup related function */ | |
599 | + | |
600 | +static struct PVEBackupState { | |
601 | + Error *error; | |
602 | + bool cancel; | |
603 | + QemuUUID uuid; | |
604 | + char uuid_str[37]; | |
605 | + int64_t speed; | |
606 | + time_t start_time; | |
607 | + time_t end_time; | |
608 | + char *backup_file; | |
609 | + Object *vmaobj; | |
610 | + GList *di_list; | |
611 | + size_t next_job; | |
612 | + size_t total; | |
613 | + size_t transferred; | |
614 | + size_t zero_bytes; | |
6838f038 WB |
615 | + QemuMutex backup_mutex; |
616 | + bool backup_mutex_initialized; | |
67af0fa4 WB |
617 | +} backup_state; |
618 | + | |
619 | +typedef struct PVEBackupDevInfo { | |
620 | + BlockDriverState *bs; | |
621 | + size_t size; | |
622 | + uint8_t dev_id; | |
623 | + bool completed; | |
624 | + char targetfile[PATH_MAX]; | |
625 | + BlockDriverState *target; | |
626 | +} PVEBackupDevInfo; | |
627 | + | |
628 | +static void pvebackup_run_next_job(void); | |
629 | + | |
630 | +static void pvebackup_cleanup(void) | |
631 | +{ | |
6838f038 WB |
632 | + qemu_mutex_lock(&backup_state.backup_mutex); |
633 | + // Avoid race between block jobs and backup-cancel command: | |
634 | + if (!backup_state.vmaw) { | |
635 | + qemu_mutex_unlock(&backup_state.backup_mutex); | |
636 | + return; | |
637 | + } | |
638 | + | |
67af0fa4 WB |
639 | + backup_state.end_time = time(NULL); |
640 | + | |
641 | + if (backup_state.vmaobj) { | |
642 | + object_unparent(backup_state.vmaobj); | |
643 | + backup_state.vmaobj = NULL; | |
644 | + } | |
645 | + | |
6838f038 WB |
646 | + g_list_free(backup_state.di_list); |
647 | + backup_state.di_list = NULL; | |
648 | + qemu_mutex_unlock(&backup_state.backup_mutex); | |
67af0fa4 WB |
649 | +} |
650 | + | |
651 | +static void pvebackup_complete_cb(void *opaque, int ret) | |
652 | +{ | |
6838f038 WB |
653 | + // This always runs in the main loop |
654 | + | |
67af0fa4 WB |
655 | + PVEBackupDevInfo *di = opaque; |
656 | + | |
657 | + di->completed = true; | |
658 | + | |
659 | + if (ret < 0 && !backup_state.error) { | |
660 | + error_setg(&backup_state.error, "job failed with err %d - %s", | |
661 | + ret, strerror(-ret)); | |
662 | + } | |
663 | + | |
664 | + di->bs = NULL; | |
665 | + di->target = NULL; | |
666 | + | |
667 | + if (backup_state.vmaobj) { | |
668 | + object_unparent(backup_state.vmaobj); | |
669 | + backup_state.vmaobj = NULL; | |
670 | + } | |
671 | + | |
6838f038 WB |
672 | + // remove self from job queue |
673 | + qemu_mutex_lock(&backup_state.backup_mutex); | |
674 | + backup_state.di_list = g_list_remove(backup_state.di_list, di); | |
675 | + g_free(di); | |
676 | + qemu_mutex_unlock(&backup_state.backup_mutex); | |
677 | + | |
67af0fa4 WB |
678 | + if (!backup_state.cancel) { |
679 | + pvebackup_run_next_job(); | |
680 | + } | |
681 | +} | |
682 | + | |
683 | +static void pvebackup_cancel(void *opaque) | |
684 | +{ | |
685 | + backup_state.cancel = true; | |
6838f038 WB |
686 | + qemu_mutex_lock(&backup_state.backup_mutex); |
687 | + // Avoid race between block jobs and backup-cancel command: | |
688 | + if (!backup_state.vmaw) { | |
689 | + qemu_mutex_unlock(&backup_state.backup_mutex); | |
690 | + return; | |
691 | + } | |
67af0fa4 WB |
692 | + |
693 | + if (!backup_state.error) { | |
694 | + error_setg(&backup_state.error, "backup cancelled"); | |
695 | + } | |
696 | + | |
697 | + if (backup_state.vmaobj) { | |
698 | + Error *err; | |
699 | + /* make sure vma writer does not block anymore */ | |
700 | + if (!object_set_props(backup_state.vmaobj, &err, "blocked", "yes", NULL)) { | |
701 | + if (err) { | |
702 | + error_report_err(err); | |
703 | + } | |
704 | + } | |
705 | + } | |
706 | + | |
707 | + GList *l = backup_state.di_list; | |
708 | + while (l) { | |
709 | + PVEBackupDevInfo *di = (PVEBackupDevInfo *)l->data; | |
710 | + l = g_list_next(l); | |
711 | + if (!di->completed && di->bs) { | |
712 | + BlockJob *job = di->bs->job; | |
713 | + if (job) { | |
6838f038 WB |
714 | + AioContext *aio_context = blk_get_aio_context(job->blk); |
715 | + aio_context_acquire(aio_context); | |
67af0fa4 | 716 | + if (!di->completed) { |
53e83913 | 717 | + job_cancel(&job->job, false); |
67af0fa4 | 718 | + } |
6838f038 | 719 | + aio_context_release(aio_context); |
67af0fa4 WB |
720 | + } |
721 | + } | |
722 | + } | |
723 | + | |
6838f038 | 724 | + qemu_mutex_unlock(&backup_state.backup_mutex); |
67af0fa4 WB |
725 | + pvebackup_cleanup(); |
726 | +} | |
727 | + | |
728 | +void qmp_backup_cancel(Error **errp) | |
729 | +{ | |
6838f038 WB |
730 | + if (!backup_state.backup_mutex_initialized) |
731 | + return; | |
67af0fa4 WB |
732 | + Coroutine *co = qemu_coroutine_create(pvebackup_cancel, NULL); |
733 | + qemu_coroutine_enter(co); | |
734 | + | |
735 | + while (backup_state.vmaobj) { | |
736 | + /* FIXME: Find something better for this */ | |
737 | + aio_poll(qemu_get_aio_context(), true); | |
738 | + } | |
739 | +} | |
740 | + | |
741 | +void vma_object_add_config_file(Object *obj, const char *name, | |
742 | + const char *contents, size_t len, | |
743 | + Error **errp); | |
744 | +static int config_to_vma(const char *file, BackupFormat format, | |
745 | + Object *vmaobj, | |
746 | + const char *backup_dir, | |
747 | + Error **errp) | |
748 | +{ | |
6838f038 WB |
749 | + char *cdata = NULL; |
750 | + gsize clen = 0; | |
751 | + GError *err = NULL; | |
752 | + if (!g_file_get_contents(file, &cdata, &clen, &err)) { | |
753 | + error_setg(errp, "unable to read file '%s'", file); | |
754 | + return 1; | |
755 | + } | |
67af0fa4 | 756 | + |
6838f038 | 757 | + char *basename = g_path_get_basename(file); |
67af0fa4 | 758 | + |
6838f038 WB |
759 | + if (format == BACKUP_FORMAT_VMA) { |
760 | + vma_object_add_config_file(vmaobj, basename, cdata, clen, errp); | |
761 | + } else if (format == BACKUP_FORMAT_DIR) { | |
762 | + char config_path[PATH_MAX]; | |
763 | + snprintf(config_path, PATH_MAX, "%s/%s", backup_dir, basename); | |
764 | + if (!g_file_set_contents(config_path, cdata, clen, &err)) { | |
765 | + error_setg(errp, "unable to write config file '%s'", config_path); | |
766 | + g_free(cdata); | |
767 | + g_free(basename); | |
768 | + return 1; | |
769 | + } | |
770 | + } | |
67af0fa4 | 771 | + |
6838f038 WB |
772 | + g_free(basename); |
773 | + g_free(cdata); | |
774 | + return 0; | |
67af0fa4 WB |
775 | +} |
776 | + | |
777 | +static void pvebackup_run_next_job(void) | |
778 | +{ | |
6838f038 WB |
779 | + qemu_mutex_lock(&backup_state.backup_mutex); |
780 | + | |
67af0fa4 WB |
781 | + GList *next = g_list_nth(backup_state.di_list, backup_state.next_job); |
782 | + while (next) { | |
783 | + PVEBackupDevInfo *di = (PVEBackupDevInfo *)next->data; | |
784 | + backup_state.next_job++; | |
785 | + if (!di->completed && di->bs && di->bs->job) { | |
786 | + BlockJob *job = di->bs->job; | |
6838f038 WB |
787 | + AioContext *aio_context = blk_get_aio_context(job->blk); |
788 | + aio_context_acquire(aio_context); | |
789 | + qemu_mutex_unlock(&backup_state.backup_mutex); | |
790 | + if (backup_state.error || backup_state.cancel) { | |
53e83913 | 791 | + job_cancel_sync(job); |
67af0fa4 | 792 | + } else { |
53e83913 | 793 | + job_resume(job); |
67af0fa4 | 794 | + } |
6838f038 | 795 | + aio_context_release(aio_context); |
67af0fa4 WB |
796 | + return; |
797 | + } | |
798 | + next = g_list_next(next); | |
799 | + } | |
6838f038 WB |
800 | + qemu_mutex_unlock(&backup_state.backup_mutex); |
801 | + | |
802 | + // no more jobs, run the cleanup | |
67af0fa4 WB |
803 | + pvebackup_cleanup(); |
804 | +} | |
805 | + | |
806 | +UuidInfo *qmp_backup(const char *backup_file, bool has_format, | |
807 | + BackupFormat format, | |
808 | + bool has_config_file, const char *config_file, | |
809 | + bool has_firewall_file, const char *firewall_file, | |
810 | + bool has_devlist, const char *devlist, | |
811 | + bool has_speed, int64_t speed, Error **errp) | |
812 | +{ | |
813 | + BlockBackend *blk; | |
814 | + BlockDriverState *bs = NULL; | |
815 | + const char *backup_dir = NULL; | |
816 | + Error *local_err = NULL; | |
817 | + QemuUUID uuid; | |
818 | + gchar **devs = NULL; | |
819 | + GList *di_list = NULL; | |
820 | + GList *l; | |
821 | + UuidInfo *uuid_info; | |
822 | + BlockJob *job; | |
823 | + | |
6838f038 WB |
824 | + if (!backup_state.backup_mutex_initialized) { |
825 | + qemu_mutex_init(&backup_state.backup_mutex); | |
826 | + backup_state.backup_mutex_initialized = true; | |
827 | + } | |
828 | + | |
67af0fa4 WB |
829 | + if (backup_state.di_list || backup_state.vmaobj) { |
830 | + error_set(errp, ERROR_CLASS_GENERIC_ERROR, | |
831 | + "previous backup not finished"); | |
832 | + return NULL; | |
833 | + } | |
834 | + | |
835 | + /* Todo: try to auto-detect format based on file name */ | |
836 | + format = has_format ? format : BACKUP_FORMAT_VMA; | |
837 | + | |
838 | + if (has_devlist) { | |
839 | + devs = g_strsplit_set(devlist, ",;:", -1); | |
840 | + | |
841 | + gchar **d = devs; | |
842 | + while (d && *d) { | |
843 | + blk = blk_by_name(*d); | |
844 | + if (blk) { | |
845 | + bs = blk_bs(blk); | |
846 | + if (bdrv_is_read_only(bs)) { | |
847 | + error_setg(errp, "Node '%s' is read only", *d); | |
848 | + goto err; | |
849 | + } | |
850 | + if (!bdrv_is_inserted(bs)) { | |
851 | + error_setg(errp, QERR_DEVICE_HAS_NO_MEDIUM, *d); | |
852 | + goto err; | |
853 | + } | |
854 | + PVEBackupDevInfo *di = g_new0(PVEBackupDevInfo, 1); | |
855 | + di->bs = bs; | |
856 | + di_list = g_list_append(di_list, di); | |
857 | + } else { | |
858 | + error_set(errp, ERROR_CLASS_DEVICE_NOT_FOUND, | |
859 | + "Device '%s' not found", *d); | |
860 | + goto err; | |
861 | + } | |
862 | + d++; | |
863 | + } | |
864 | + | |
865 | + } else { | |
866 | + BdrvNextIterator it; | |
867 | + | |
868 | + bs = NULL; | |
869 | + for (bs = bdrv_first(&it); bs; bs = bdrv_next(&it)) { | |
870 | + if (!bdrv_is_inserted(bs) || bdrv_is_read_only(bs)) { | |
871 | + continue; | |
872 | + } | |
873 | + | |
874 | + PVEBackupDevInfo *di = g_new0(PVEBackupDevInfo, 1); | |
875 | + di->bs = bs; | |
876 | + di_list = g_list_append(di_list, di); | |
877 | + } | |
878 | + } | |
879 | + | |
880 | + if (!di_list) { | |
881 | + error_set(errp, ERROR_CLASS_GENERIC_ERROR, "empty device list"); | |
882 | + goto err; | |
883 | + } | |
884 | + | |
885 | + size_t total = 0; | |
886 | + | |
887 | + l = di_list; | |
888 | + while (l) { | |
889 | + PVEBackupDevInfo *di = (PVEBackupDevInfo *)l->data; | |
890 | + l = g_list_next(l); | |
891 | + if (bdrv_op_is_blocked(di->bs, BLOCK_OP_TYPE_BACKUP_SOURCE, errp)) { | |
892 | + goto err; | |
893 | + } | |
894 | + | |
895 | + ssize_t size = bdrv_getlength(di->bs); | |
896 | + if (size < 0) { | |
897 | + error_setg_errno(errp, -di->size, "bdrv_getlength failed"); | |
898 | + goto err; | |
899 | + } | |
900 | + di->size = size; | |
901 | + total += size; | |
902 | + } | |
903 | + | |
904 | + qemu_uuid_generate(&uuid); | |
905 | + | |
906 | + if (format == BACKUP_FORMAT_VMA) { | |
907 | + char uuidstr[UUID_FMT_LEN+1]; | |
908 | + qemu_uuid_unparse(&uuid, uuidstr); | |
909 | + uuidstr[UUID_FMT_LEN] = 0; | |
910 | + backup_state.vmaobj = | |
911 | + object_new_with_props("vma", object_get_objects_root(), | |
912 | + "vma-backup-obj", &local_err, | |
913 | + "filename", backup_file, | |
914 | + "uuid", uuidstr, | |
915 | + NULL); | |
916 | + if (!backup_state.vmaobj) { | |
917 | + if (local_err) { | |
918 | + error_propagate(errp, local_err); | |
919 | + } | |
920 | + goto err; | |
921 | + } | |
922 | + | |
923 | + l = di_list; | |
924 | + while (l) { | |
925 | + QDict *options = qdict_new(); | |
926 | + | |
927 | + PVEBackupDevInfo *di = (PVEBackupDevInfo *)l->data; | |
928 | + l = g_list_next(l); | |
929 | + | |
930 | + const char *devname = bdrv_get_device_name(di->bs); | |
931 | + snprintf(di->targetfile, PATH_MAX, "vma-backup-obj/%s.raw", devname); | |
932 | + | |
933 | + qdict_put(options, "driver", qstring_from_str("vma-drive")); | |
934 | + qdict_put(options, "size", qint_from_int(di->size)); | |
935 | + di->target = bdrv_open(di->targetfile, NULL, options, BDRV_O_RDWR, &local_err); | |
936 | + if (!di->target) { | |
937 | + error_propagate(errp, local_err); | |
938 | + goto err; | |
939 | + } | |
940 | + } | |
941 | + } else if (format == BACKUP_FORMAT_DIR) { | |
942 | + if (mkdir(backup_file, 0640) != 0) { | |
943 | + error_setg_errno(errp, errno, "can't create directory '%s'\n", | |
944 | + backup_file); | |
945 | + goto err; | |
946 | + } | |
947 | + backup_dir = backup_file; | |
948 | + | |
949 | + l = di_list; | |
950 | + while (l) { | |
951 | + PVEBackupDevInfo *di = (PVEBackupDevInfo *)l->data; | |
952 | + l = g_list_next(l); | |
953 | + | |
954 | + const char *devname = bdrv_get_device_name(di->bs); | |
955 | + snprintf(di->targetfile, PATH_MAX, "%s/%s.raw", backup_dir, devname); | |
956 | + | |
957 | + int flags = BDRV_O_RDWR; | |
958 | + bdrv_img_create(di->targetfile, "raw", NULL, NULL, NULL, | |
6838f038 | 959 | + di->size, flags, false, &local_err); |
67af0fa4 WB |
960 | + if (local_err) { |
961 | + error_propagate(errp, local_err); | |
962 | + goto err; | |
963 | + } | |
964 | + | |
965 | + di->target = bdrv_open(di->targetfile, NULL, NULL, flags, &local_err); | |
966 | + if (!di->target) { | |
967 | + error_propagate(errp, local_err); | |
968 | + goto err; | |
969 | + } | |
970 | + } | |
971 | + } else { | |
6838f038 WB |
972 | + error_set(errp, ERROR_CLASS_GENERIC_ERROR, "unknown backup format"); |
973 | + goto err; | |
67af0fa4 WB |
974 | + } |
975 | + | |
976 | + /* add configuration file to archive */ | |
977 | + if (has_config_file) { | |
6838f038 WB |
978 | + if(config_to_vma(config_file, format, backup_state.vmaobj, backup_dir, errp) != 0) { |
979 | + goto err; | |
980 | + } | |
67af0fa4 WB |
981 | + } |
982 | + | |
983 | + /* add firewall file to archive */ | |
984 | + if (has_firewall_file) { | |
6838f038 WB |
985 | + if(config_to_vma(firewall_file, format, backup_state.vmaobj, backup_dir, errp) != 0) { |
986 | + goto err; | |
987 | + } | |
67af0fa4 WB |
988 | + } |
989 | + /* initialize global backup_state now */ | |
990 | + | |
991 | + backup_state.cancel = false; | |
992 | + | |
993 | + if (backup_state.error) { | |
994 | + error_free(backup_state.error); | |
995 | + backup_state.error = NULL; | |
996 | + } | |
997 | + | |
998 | + backup_state.speed = (has_speed && speed > 0) ? speed : 0; | |
999 | + | |
1000 | + backup_state.start_time = time(NULL); | |
1001 | + backup_state.end_time = 0; | |
1002 | + | |
1003 | + if (backup_state.backup_file) { | |
1004 | + g_free(backup_state.backup_file); | |
1005 | + } | |
1006 | + backup_state.backup_file = g_strdup(backup_file); | |
1007 | + | |
1008 | + memcpy(&backup_state.uuid, &uuid, sizeof(uuid)); | |
1009 | + qemu_uuid_unparse(&uuid, backup_state.uuid_str); | |
1010 | + | |
6838f038 | 1011 | + qemu_mutex_lock(&backup_state.backup_mutex); |
67af0fa4 WB |
1012 | + backup_state.di_list = di_list; |
1013 | + backup_state.next_job = 0; | |
1014 | + | |
1015 | + backup_state.total = total; | |
1016 | + backup_state.transferred = 0; | |
1017 | + backup_state.zero_bytes = 0; | |
1018 | + | |
1019 | + /* start all jobs (paused state) */ | |
1020 | + l = di_list; | |
1021 | + while (l) { | |
1022 | + PVEBackupDevInfo *di = (PVEBackupDevInfo *)l->data; | |
1023 | + l = g_list_next(l); | |
1024 | + | |
1025 | + job = backup_job_create(NULL, di->bs, di->target, speed, MIRROR_SYNC_MODE_FULL, NULL, | |
1026 | + false, BLOCKDEV_ON_ERROR_REPORT, BLOCKDEV_ON_ERROR_REPORT, | |
53e83913 | 1027 | + JOB_DEFAULT, |
67af0fa4 WB |
1028 | + pvebackup_complete_cb, di, 2, NULL, &local_err); |
1029 | + if (di->target) { | |
1030 | + bdrv_unref(di->target); | |
1031 | + di->target = NULL; | |
1032 | + } | |
1033 | + if (!job || local_err != NULL) { | |
1034 | + error_setg(&backup_state.error, "backup_job_create failed"); | |
1035 | + pvebackup_cancel(NULL); | |
1036 | + } else { | |
53e83913 | 1037 | + job_start(&job->job); |
67af0fa4 WB |
1038 | + } |
1039 | + } | |
1040 | + | |
6838f038 WB |
1041 | + qemu_mutex_unlock(&backup_state.backup_mutex); |
1042 | + | |
67af0fa4 WB |
1043 | + if (!backup_state.error) { |
1044 | + pvebackup_run_next_job(); // run one job | |
1045 | + } | |
1046 | + | |
1047 | + uuid_info = g_malloc0(sizeof(*uuid_info)); | |
1048 | + uuid_info->UUID = g_strdup(backup_state.uuid_str); | |
1049 | + | |
1050 | + return uuid_info; | |
1051 | + | |
1052 | +err: | |
1053 | + | |
1054 | + l = di_list; | |
1055 | + while (l) { | |
1056 | + PVEBackupDevInfo *di = (PVEBackupDevInfo *)l->data; | |
1057 | + l = g_list_next(l); | |
1058 | + | |
1059 | + if (di->target) { | |
1060 | + bdrv_unref(di->target); | |
1061 | + } | |
1062 | + | |
1063 | + if (di->targetfile[0]) { | |
1064 | + unlink(di->targetfile); | |
1065 | + } | |
1066 | + g_free(di); | |
1067 | + } | |
1068 | + g_list_free(di_list); | |
1069 | + | |
1070 | + if (devs) { | |
1071 | + g_strfreev(devs); | |
1072 | + } | |
1073 | + | |
1074 | + if (backup_state.vmaobj) { | |
1075 | + object_unparent(backup_state.vmaobj); | |
1076 | + backup_state.vmaobj = NULL; | |
1077 | + } | |
1078 | + | |
1079 | + if (backup_dir) { | |
1080 | + rmdir(backup_dir); | |
1081 | + } | |
1082 | + | |
1083 | + return NULL; | |
1084 | +} | |
1085 | + | |
1086 | +BackupStatus *qmp_query_backup(Error **errp) | |
1087 | +{ | |
1088 | + BackupStatus *info = g_malloc0(sizeof(*info)); | |
1089 | + | |
1090 | + if (!backup_state.start_time) { | |
1091 | + /* not started, return {} */ | |
1092 | + return info; | |
1093 | + } | |
1094 | + | |
1095 | + info->has_status = true; | |
1096 | + info->has_start_time = true; | |
1097 | + info->start_time = backup_state.start_time; | |
1098 | + | |
1099 | + if (backup_state.backup_file) { | |
1100 | + info->has_backup_file = true; | |
1101 | + info->backup_file = g_strdup(backup_state.backup_file); | |
1102 | + } | |
1103 | + | |
1104 | + info->has_uuid = true; | |
1105 | + info->uuid = g_strdup(backup_state.uuid_str); | |
1106 | + | |
1107 | + if (backup_state.end_time) { | |
1108 | + if (backup_state.error) { | |
1109 | + info->status = g_strdup("error"); | |
1110 | + info->has_errmsg = true; | |
1111 | + info->errmsg = g_strdup(error_get_pretty(backup_state.error)); | |
1112 | + } else { | |
1113 | + info->status = g_strdup("done"); | |
1114 | + } | |
1115 | + info->has_end_time = true; | |
1116 | + info->end_time = backup_state.end_time; | |
1117 | + } else { | |
1118 | + info->status = g_strdup("active"); | |
1119 | + } | |
1120 | + | |
1121 | + info->has_total = true; | |
1122 | + info->total = backup_state.total; | |
1123 | + info->has_zero_bytes = true; | |
1124 | + info->zero_bytes = backup_state.zero_bytes; | |
1125 | + info->has_transferred = true; | |
1126 | + info->transferred = backup_state.transferred; | |
1127 | + | |
1128 | + return info; | |
1129 | +} | |
1130 | + | |
1131 | void qmp_block_stream(bool has_job_id, const char *job_id, const char *device, | |
1132 | bool has_base, const char *base, | |
1133 | bool has_base_node, const char *base_node, | |
1134 | diff --git a/configure b/configure | |
b855dce7 | 1135 | index 1c563a7027..d164677950 100755 |
67af0fa4 WB |
1136 | --- a/configure |
1137 | +++ b/configure | |
b855dce7 | 1138 | @@ -491,6 +491,7 @@ docker="no" |
53e83913 | 1139 | debug_mutex="no" |
b855dce7 TL |
1140 | libpmem="" |
1141 | default_devices="yes" | |
67af0fa4 WB |
1142 | +vma="" |
1143 | ||
53e83913 WB |
1144 | # cross compilers defaults, can be overridden with --cross-cc-ARCH |
1145 | cross_cc_aarch64="aarch64-linux-gnu-gcc" | |
b855dce7 | 1146 | @@ -1518,6 +1519,10 @@ for opt do |
67af0fa4 | 1147 | ;; |
b855dce7 | 1148 | --disable-libpmem) libpmem=no |
67af0fa4 | 1149 | ;; |
53e83913 | 1150 | + --enable-vma) vma=yes |
67af0fa4 | 1151 | + ;; |
53e83913 | 1152 | + --disable-vma) vma=no |
67af0fa4 WB |
1153 | + ;; |
1154 | *) | |
1155 | echo "ERROR: unknown option $opt" | |
1156 | echo "Try '$0 --help' for more information" | |
b855dce7 | 1157 | @@ -1818,6 +1823,7 @@ disabled with --disable-FEATURE, default is enabled if available: |
6838f038 | 1158 | capstone capstone disassembler support |
53e83913 | 1159 | debug-mutex mutex debugging support |
b855dce7 | 1160 | libpmem libpmem support |
67af0fa4 WB |
1161 | + vma VMA archive backend |
1162 | ||
1163 | NOTE: The object files are built at the place where configure is launched | |
1164 | EOF | |
b855dce7 TL |
1165 | @@ -4378,6 +4384,22 @@ EOF |
1166 | fi | |
67af0fa4 WB |
1167 | fi |
1168 | ||
b855dce7 | 1169 | +########################################## |
67af0fa4 WB |
1170 | +# vma probe |
1171 | +if test "$vma" != "no" ; then | |
1172 | + if $pkg_config --exact-version=0.1.0 vma; then | |
1173 | + vma="yes" | |
1174 | + vma_cflags=$($pkg_config --cflags vma) | |
1175 | + vma_libs=$($pkg_config --libs vma) | |
1176 | + else | |
1177 | + if test "$vma" = "yes" ; then | |
1178 | + feature_not_found "VMA Archive backend support" \ | |
1179 | + "Install libvma devel" | |
1180 | + fi | |
1181 | + vma="no" | |
1182 | + fi | |
1183 | +fi | |
1184 | + | |
b855dce7 | 1185 | ########################################## |
67af0fa4 WB |
1186 | # signalfd probe |
1187 | signalfd="no" | |
b855dce7 TL |
1188 | @@ -6438,6 +6460,7 @@ echo "docker $docker" |
1189 | echo "libpmem support $libpmem" | |
1190 | echo "libudev $libudev" | |
1191 | echo "default devices $default_devices" | |
67af0fa4 WB |
1192 | +echo "VMA support $vma" |
1193 | ||
b855dce7 TL |
1194 | if test "$supported_cpu" = "no"; then |
1195 | echo | |
1196 | @@ -6931,6 +6954,12 @@ if test "$usb_redir" = "yes" ; then | |
53e83913 | 1197 | echo "USB_REDIR_LIBS=$usb_redir_libs" >> $config_host_mak |
67af0fa4 WB |
1198 | fi |
1199 | ||
1200 | +if test "$vma" = "yes" ; then | |
1201 | + echo "CONFIG_VMA=y" >> $config_host_mak | |
1202 | + echo "VMA_CFLAGS=$vma_cflags" >> $config_host_mak | |
1203 | + echo "VMA_LIBS=$vma_libs" >> $config_host_mak | |
1204 | +fi | |
1205 | + | |
53e83913 WB |
1206 | if test "$opengl" = "yes" ; then |
1207 | echo "CONFIG_OPENGL=y" >> $config_host_mak | |
1208 | echo "OPENGL_LIBS=$opengl_libs" >> $config_host_mak | |
67af0fa4 | 1209 | diff --git a/hmp-commands-info.hx b/hmp-commands-info.hx |
b855dce7 | 1210 | index 444bd8e43d..21106bcbe6 100644 |
67af0fa4 WB |
1211 | --- a/hmp-commands-info.hx |
1212 | +++ b/hmp-commands-info.hx | |
b855dce7 TL |
1213 | @@ -536,6 +536,19 @@ STEXI |
1214 | @item info cpustats | |
1215 | @findex info cpustats | |
67af0fa4 | 1216 | Show CPU statistics. |
b855dce7 TL |
1217 | +ETEXI |
1218 | + | |
67af0fa4 WB |
1219 | + { |
1220 | + .name = "backup", | |
1221 | + .args_type = "", | |
1222 | + .params = "", | |
1223 | + .help = "show backup status", | |
1224 | + .cmd = hmp_info_backup, | |
1225 | + }, | |
1226 | + | |
1227 | +STEXI | |
1228 | +@item info backup | |
1229 | +show backup status | |
b855dce7 TL |
1230 | ETEXI |
1231 | ||
67af0fa4 | 1232 | #if defined(CONFIG_SLIRP) |
67af0fa4 | 1233 | diff --git a/hmp-commands.hx b/hmp-commands.hx |
b855dce7 | 1234 | index 284e97973a..d723552bee 100644 |
67af0fa4 WB |
1235 | --- a/hmp-commands.hx |
1236 | +++ b/hmp-commands.hx | |
b855dce7 TL |
1237 | @@ -105,6 +105,37 @@ STEXI |
1238 | @item block_stream | |
1239 | @findex block_stream | |
67af0fa4 | 1240 | Copy data from a backing file into a block device. |
b855dce7 TL |
1241 | +ETEXI |
1242 | + | |
67af0fa4 WB |
1243 | + { |
1244 | + .name = "backup", | |
1245 | + .args_type = "directory:-d,backupfile:s,speed:o?,devlist:s?", | |
1246 | + .params = "[-d] backupfile [speed [devlist]]", | |
1247 | + .help = "create a VM Backup." | |
1248 | + "\n\t\t\t Use -d to dump data into a directory instead" | |
1249 | + "\n\t\t\t of using VMA format.", | |
1250 | + .cmd = hmp_backup, | |
1251 | + }, | |
1252 | + | |
1253 | +STEXI | |
1254 | +@item backup | |
1255 | +@findex backup | |
1256 | +Create a VM backup. | |
1257 | +ETEXI | |
1258 | + | |
1259 | + { | |
1260 | + .name = "backup_cancel", | |
1261 | + .args_type = "", | |
1262 | + .params = "", | |
1263 | + .help = "cancel the current VM backup", | |
1264 | + .cmd = hmp_backup_cancel, | |
1265 | + }, | |
1266 | + | |
1267 | +STEXI | |
1268 | +@item backup_cancel | |
1269 | +@findex backup_cancel | |
1270 | +Cancel the current VM backup. | |
1271 | + | |
b855dce7 TL |
1272 | ETEXI |
1273 | ||
67af0fa4 | 1274 | { |
67af0fa4 | 1275 | diff --git a/hmp.c b/hmp.c |
b855dce7 | 1276 | index 16243bba50..113671ad2a 100644 |
67af0fa4 WB |
1277 | --- a/hmp.c |
1278 | +++ b/hmp.c | |
b855dce7 | 1279 | @@ -167,6 +167,44 @@ void hmp_info_mice(Monitor *mon, const QDict *qdict) |
67af0fa4 WB |
1280 | qapi_free_MouseInfoList(mice_list); |
1281 | } | |
1282 | ||
1283 | +void hmp_info_backup(Monitor *mon, const QDict *qdict) | |
1284 | +{ | |
1285 | + BackupStatus *info; | |
1286 | + | |
1287 | + info = qmp_query_backup(NULL); | |
1288 | + if (info->has_status) { | |
1289 | + if (info->has_errmsg) { | |
1290 | + monitor_printf(mon, "Backup status: %s - %s\n", | |
1291 | + info->status, info->errmsg); | |
1292 | + } else { | |
1293 | + monitor_printf(mon, "Backup status: %s\n", info->status); | |
1294 | + } | |
1295 | + } | |
1296 | + | |
1297 | + if (info->has_backup_file) { | |
1298 | + monitor_printf(mon, "Start time: %s", ctime(&info->start_time)); | |
1299 | + if (info->end_time) { | |
1300 | + monitor_printf(mon, "End time: %s", ctime(&info->end_time)); | |
1301 | + } | |
1302 | + | |
1303 | + int per = (info->has_total && info->total && | |
1304 | + info->has_transferred && info->transferred) ? | |
1305 | + (info->transferred * 100)/info->total : 0; | |
1306 | + int zero_per = (info->has_total && info->total && | |
1307 | + info->has_zero_bytes && info->zero_bytes) ? | |
1308 | + (info->zero_bytes * 100)/info->total : 0; | |
1309 | + monitor_printf(mon, "Backup file: %s\n", info->backup_file); | |
1310 | + monitor_printf(mon, "Backup uuid: %s\n", info->uuid); | |
1311 | + monitor_printf(mon, "Total size: %zd\n", info->total); | |
1312 | + monitor_printf(mon, "Transferred bytes: %zd (%d%%)\n", | |
1313 | + info->transferred, per); | |
1314 | + monitor_printf(mon, "Zero bytes: %zd (%d%%)\n", | |
1315 | + info->zero_bytes, zero_per); | |
1316 | + } | |
1317 | + | |
1318 | + qapi_free_BackupStatus(info); | |
1319 | +} | |
1320 | + | |
b855dce7 | 1321 | static char *SocketAddress_to_str(SocketAddress *addr) |
67af0fa4 | 1322 | { |
b855dce7 TL |
1323 | switch (addr->type) { |
1324 | @@ -2059,6 +2097,31 @@ void hmp_block_stream(Monitor *mon, const QDict *qdict) | |
67af0fa4 WB |
1325 | hmp_handle_error(mon, &error); |
1326 | } | |
1327 | ||
1328 | +void hmp_backup_cancel(Monitor *mon, const QDict *qdict) | |
1329 | +{ | |
1330 | + Error *error = NULL; | |
1331 | + | |
1332 | + qmp_backup_cancel(&error); | |
1333 | + | |
1334 | + hmp_handle_error(mon, &error); | |
1335 | +} | |
1336 | + | |
1337 | +void hmp_backup(Monitor *mon, const QDict *qdict) | |
1338 | +{ | |
1339 | + Error *error = NULL; | |
1340 | + | |
1341 | + int dir = qdict_get_try_bool(qdict, "directory", 0); | |
1342 | + const char *backup_file = qdict_get_str(qdict, "backupfile"); | |
1343 | + const char *devlist = qdict_get_try_str(qdict, "devlist"); | |
1344 | + int64_t speed = qdict_get_try_int(qdict, "speed", 0); | |
1345 | + | |
1346 | + qmp_backup(backup_file, true, dir ? BACKUP_FORMAT_DIR : BACKUP_FORMAT_VMA, | |
1347 | + false, NULL, false, NULL, !!devlist, | |
1348 | + devlist, qdict_haskey(qdict, "speed"), speed, &error); | |
1349 | + | |
1350 | + hmp_handle_error(mon, &error); | |
1351 | +} | |
1352 | + | |
1353 | void hmp_block_job_set_speed(Monitor *mon, const QDict *qdict) | |
1354 | { | |
1355 | Error *error = NULL; | |
1356 | diff --git a/hmp.h b/hmp.h | |
b855dce7 | 1357 | index bcb90c478f..043b74edee 100644 |
67af0fa4 WB |
1358 | --- a/hmp.h |
1359 | +++ b/hmp.h | |
53e83913 | 1360 | @@ -29,6 +29,7 @@ void hmp_info_migrate(Monitor *mon, const QDict *qdict); |
67af0fa4 WB |
1361 | void hmp_info_migrate_capabilities(Monitor *mon, const QDict *qdict); |
1362 | void hmp_info_migrate_parameters(Monitor *mon, const QDict *qdict); | |
1363 | void hmp_info_migrate_cache_size(Monitor *mon, const QDict *qdict); | |
1364 | +void hmp_info_backup(Monitor *mon, const QDict *qdict); | |
1365 | void hmp_info_cpus(Monitor *mon, const QDict *qdict); | |
1366 | void hmp_info_block(Monitor *mon, const QDict *qdict); | |
1367 | void hmp_info_blockstats(Monitor *mon, const QDict *qdict); | |
b855dce7 | 1368 | @@ -89,6 +90,8 @@ void hmp_eject(Monitor *mon, const QDict *qdict); |
67af0fa4 WB |
1369 | void hmp_change(Monitor *mon, const QDict *qdict); |
1370 | void hmp_block_set_io_throttle(Monitor *mon, const QDict *qdict); | |
1371 | void hmp_block_stream(Monitor *mon, const QDict *qdict); | |
1372 | +void hmp_backup(Monitor *mon, const QDict *qdict); | |
1373 | +void hmp_backup_cancel(Monitor *mon, const QDict *qdict); | |
1374 | void hmp_block_job_set_speed(Monitor *mon, const QDict *qdict); | |
1375 | void hmp_block_job_cancel(Monitor *mon, const QDict *qdict); | |
1376 | void hmp_block_job_pause(Monitor *mon, const QDict *qdict); | |
6838f038 | 1377 | diff --git a/qapi/block-core.json b/qapi/block-core.json |
b855dce7 | 1378 | index 7ccbfff9d0..6f97460806 100644 |
6838f038 WB |
1379 | --- a/qapi/block-core.json |
1380 | +++ b/qapi/block-core.json | |
b855dce7 TL |
1381 | @@ -796,6 +796,97 @@ |
1382 | { 'command': 'query-block', 'returns': ['BlockInfo'] } | |
6838f038 | 1383 | |
67af0fa4 | 1384 | |
b855dce7 | 1385 | +## |
67af0fa4 WB |
1386 | +# @BackupStatus: |
1387 | +# | |
1388 | +# Detailed backup status. | |
1389 | +# | |
1390 | +# @status: string describing the current backup status. | |
1391 | +# This can be 'active', 'done', 'error'. If this field is not | |
1392 | +# returned, no backup process has been initiated | |
1393 | +# | |
1394 | +# @errmsg: error message (only returned if status is 'error') | |
1395 | +# | |
1396 | +# @total: total amount of bytes involved in the backup process | |
1397 | +# | |
1398 | +# @transferred: amount of bytes already backed up. | |
1399 | +# | |
1400 | +# @zero-bytes: amount of 'zero' bytes detected. | |
1401 | +# | |
1402 | +# @start-time: time (epoch) when backup job started. | |
1403 | +# | |
1404 | +# @end-time: time (epoch) when backup job finished. | |
1405 | +# | |
1406 | +# @backup-file: backup file name | |
1407 | +# | |
1408 | +# @uuid: uuid for this backup job | |
1409 | +# | |
1410 | +## | |
1411 | +{ 'struct': 'BackupStatus', | |
1412 | + 'data': {'*status': 'str', '*errmsg': 'str', '*total': 'int', | |
1413 | + '*transferred': 'int', '*zero-bytes': 'int', | |
1414 | + '*start-time': 'int', '*end-time': 'int', | |
1415 | + '*backup-file': 'str', '*uuid': 'str' } } | |
1416 | + | |
1417 | +## | |
1418 | +# @BackupFormat: | |
1419 | +# | |
1420 | +# An enumeration of supported backup formats. | |
1421 | +# | |
1422 | +# @vma: Proxmox vma backup format | |
1423 | +## | |
1424 | +{ 'enum': 'BackupFormat', | |
1425 | + 'data': [ 'vma', 'dir' ] } | |
1426 | + | |
1427 | +## | |
1428 | +# @backup: | |
1429 | +# | |
1430 | +# Starts a VM backup. | |
1431 | +# | |
1432 | +# @backup-file: the backup file name | |
1433 | +# | |
1434 | +# @format: format of the backup file | |
1435 | +# | |
1436 | +# @config-file: a configuration file to include into | |
1437 | +# the backup archive. | |
1438 | +# | |
1439 | +# @speed: the maximum speed, in bytes per second | |
1440 | +# | |
1441 | +# @devlist: list of block device names (separated by ',', ';' | |
1442 | +# or ':'). By default the backup includes all writable block devices. | |
1443 | +# | |
1444 | +# Returns: the uuid of the backup job | |
1445 | +# | |
1446 | +## | |
1447 | +{ 'command': 'backup', 'data': { 'backup-file': 'str', | |
1448 | + '*format': 'BackupFormat', | |
1449 | + '*config-file': 'str', | |
1450 | + '*firewall-file': 'str', | |
1451 | + '*devlist': 'str', '*speed': 'int' }, | |
1452 | + 'returns': 'UuidInfo' } | |
1453 | + | |
1454 | +## | |
1455 | +# @query-backup: | |
1456 | +# | |
1457 | +# Returns information about current/last backup task. | |
1458 | +# | |
1459 | +# Returns: @BackupStatus | |
1460 | +# | |
1461 | +## | |
1462 | +{ 'command': 'query-backup', 'returns': 'BackupStatus' } | |
1463 | + | |
1464 | +## | |
1465 | +# @backup-cancel: | |
1466 | +# | |
1467 | +# Cancel the current executing backup process. | |
1468 | +# | |
1469 | +# Returns: nothing on success | |
1470 | +# | |
1471 | +# Notes: This command succeeds even if there is no backup process running. | |
1472 | +# | |
1473 | +## | |
1474 | +{ 'command': 'backup-cancel' } | |
1475 | + | |
b855dce7 | 1476 | ## |
6838f038 | 1477 | # @BlockDeviceTimedStats: |
67af0fa4 | 1478 | # |
b855dce7 TL |
1479 | @@ -2819,7 +2910,7 @@ |
1480 | 'qcow2', 'qed', 'quorum', 'raw', 'rbd', | |
1481 | { 'name': 'replication', 'if': 'defined(CONFIG_REPLICATION)' }, | |
1482 | 'sheepdog', | |
53e83913 WB |
1483 | - 'ssh', 'throttle', 'vdi', 'vhdx', 'vmdk', 'vpc', 'vvfat', 'vxhs' ] } |
1484 | + 'ssh', 'throttle', 'vdi', 'vhdx', 'vma-drive', 'vmdk', 'vpc', 'vvfat', 'vxhs' ] } | |
67af0fa4 WB |
1485 | |
1486 | ## | |
1487 | # @BlockdevOptionsFile: | |
b855dce7 TL |
1488 | @@ -3836,6 +3927,21 @@ |
1489 | 'server': 'InetSocketAddressBase', | |
6838f038 | 1490 | '*tls-creds': 'str' } } |
67af0fa4 | 1491 | |
b855dce7 | 1492 | +## |
67af0fa4 WB |
1493 | +# @BlockdevOptionsVMADrive: |
1494 | +# | |
1495 | +# Driver specific block device options for VMA Drives | |
1496 | +# | |
1497 | +# @filename: vma-drive path | |
1498 | +# | |
1499 | +# @size: drive size in bytes | |
1500 | +# | |
1501 | +# Since: 2.9 | |
1502 | +## | |
1503 | +{ 'struct': 'BlockdevOptionsVMADrive', | |
1504 | + 'data': { 'filename': 'str', | |
1505 | + 'size': 'int' } } | |
1506 | + | |
b855dce7 | 1507 | ## |
6838f038 | 1508 | # @BlockdevOptionsThrottle: |
67af0fa4 | 1509 | # |
b855dce7 | 1510 | @@ -3931,6 +4037,7 @@ |
6838f038 WB |
1511 | 'throttle': 'BlockdevOptionsThrottle', |
1512 | 'vdi': 'BlockdevOptionsGenericFormat', | |
67af0fa4 | 1513 | 'vhdx': 'BlockdevOptionsGenericFormat', |
6838f038 | 1514 | + 'vma-drive': 'BlockdevOptionsVMADrive', |
67af0fa4 WB |
1515 | 'vmdk': 'BlockdevOptionsGenericCOWFormat', |
1516 | 'vpc': 'BlockdevOptionsGenericFormat', | |
6838f038 | 1517 | 'vvfat': 'BlockdevOptionsVVFAT', |
53e83913 | 1518 | diff --git a/qapi/common.json b/qapi/common.json |
b855dce7 | 1519 | index 99d313ef3b..bae0650c51 100644 |
53e83913 WB |
1520 | --- a/qapi/common.json |
1521 | +++ b/qapi/common.json | |
b855dce7 TL |
1522 | @@ -193,3 +193,16 @@ |
1523 | 'ppc64', 'riscv32', 'riscv64', 's390x', 'sh4', | |
53e83913 WB |
1524 | 'sh4eb', 'sparc', 'sparc64', 'tricore', 'unicore32', |
1525 | 'x86_64', 'xtensa', 'xtensaeb' ] } | |
1526 | + | |
1527 | +## | |
1528 | +# @UuidInfo: | |
1529 | +# | |
1530 | +# Guest UUID information (Universally Unique Identifier). | |
1531 | +# | |
1532 | +# @UUID: the UUID of the guest | |
1533 | +# | |
1534 | +# Since: 0.14.0 | |
1535 | +# | |
1536 | +# Notes: If no UUID was specified for the guest, a null UUID is returned. | |
1537 | +## | |
1538 | +{ 'struct': 'UuidInfo', 'data': {'UUID': 'str'} } | |
1539 | diff --git a/qapi/misc.json b/qapi/misc.json | |
b855dce7 | 1540 | index 49dfda0b28..376f26002a 100644 |
53e83913 WB |
1541 | --- a/qapi/misc.json |
1542 | +++ b/qapi/misc.json | |
b855dce7 TL |
1543 | @@ -274,19 +274,6 @@ |
1544 | ## | |
53e83913 WB |
1545 | { 'command': 'query-kvm', 'returns': 'KvmInfo' } |
1546 | ||
b855dce7 | 1547 | -## |
53e83913 WB |
1548 | -# @UuidInfo: |
1549 | -# | |
1550 | -# Guest UUID information (Universally Unique Identifier). | |
1551 | -# | |
1552 | -# @UUID: the UUID of the guest | |
1553 | -# | |
1554 | -# Since: 0.14.0 | |
1555 | -# | |
1556 | -# Notes: If no UUID was specified for the guest, a null UUID is returned. | |
1557 | -## | |
1558 | -{ 'struct': 'UuidInfo', 'data': {'UUID': 'str'} } | |
1559 | - | |
b855dce7 | 1560 | ## |
53e83913 WB |
1561 | # @query-uuid: |
1562 | # |