librbd/migration: basic QCOW2 format handler

The initial implementation does not support backing files, compression,
snapshots, (deprecated) encryption, external data files, nor L2 subcluster
allocation. The former three features will be added in a future commit.

Signed-off-by: Jason Dillaman <dillaman@redhat.com>
This commit is contained in:
Jason Dillaman 2021-01-12 15:35:49 -05:00
parent 83aa1a9e73
commit bd8db295ea
3 changed files with 193 additions and 11 deletions

View File

@ -178,10 +178,12 @@ it utilizes native Ceph operations. For example, to import from the image
}
The ``qcow`` format can be used to describe a QCOW (QEMU copy-on-write) block
device. Only the original QCOW (v1) format is currently supported, but support
for QCOW2 will be added in the near future. The ``qcow`` format data can be
linked to any supported stream source described below. For example,
its base ``source-spec`` JSON is encoded as follows::
device. Both the QCOW (v1) and QCOW2 formats are currently supported with the
exception of advanced features such as compression, encryption, backing
files, and external data files. Support for these missing features may be added
in a future release. The ``qcow`` format data can be linked to any supported
stream source described below. For example, its base ``source-spec`` JSON is
encoded as follows::
{
"type": "qcow",

View File

@ -191,6 +191,47 @@ EOF
remove_image "${dest_image}"
}
test_import_qcow2_format() {
local base_image=$1
local dest_image=$2
qemu-img convert -f raw -O qcow2 rbd:rbd/${base_image} ${TEMPDIR}/${base_image}.qcow2
qemu-img info -f qcow2 ${TEMPDIR}/${base_image}.qcow2
cat > ${TEMPDIR}/spec.json <<EOF
{
"type": "qcow",
"stream": {
"type": "file",
"file_path": "${TEMPDIR}/${base_image}.qcow2"
}
}
EOF
cat ${TEMPDIR}/spec.json
rbd migration prepare --import-only \
--source-spec-path ${TEMPDIR}/spec.json ${dest_image}
compare_images "${base_image}" "${dest_image}"
rbd migration abort ${dest_image}
rbd migration prepare --import-only \
--source-spec-path ${TEMPDIR}/spec.json ${dest_image}
compare_images "${base_image}" "${dest_image}"
rbd migration execute ${dest_image}
compare_images "${base_image}" "${dest_image}"
rbd migration commit ${dest_image}
compare_images "${base_image}" "${dest_image}"
remove_image "${dest_image}"
}
test_import_raw_format() {
local base_image=$1
local dest_image=$2
@ -279,6 +320,7 @@ export_base_image ${IMAGE1}
test_import_native_format ${IMAGE1} ${IMAGE2}
test_import_qcow_format ${IMAGE1} ${IMAGE2}
test_import_qcow2_format ${IMAGE1} ${IMAGE2}
test_import_raw_format ${IMAGE1} ${IMAGE2}
echo OK

View File

@ -317,9 +317,16 @@ private:
// table not in cache -- will restart once its loaded
return;
} else {
*request.cluster_offset = be64toh(l2_table[l2_index]);
ldout(cct, 20) << "image_offset=" << request.image_offset << ", "
<< "cluster_offset=" << *request.cluster_offset << dendl;
*request.cluster_offset = be64toh(l2_table[l2_index]) &
qcow_format->m_cluster_mask;
if (*request.cluster_offset == QCOW_OFLAG_ZERO) {
ldout(cct, 20) << "image_offset=" << request.image_offset << ", "
<< "cluster_offset=zeroed" << dendl;
} else {
ldout(cct, 20) << "image_offset=" << request.image_offset << ", "
<< "cluster_offset=" << *request.cluster_offset
<< dendl;
}
}
}
@ -538,6 +545,10 @@ private:
if (cluster_extent.cluster_offset == 0) {
// QCOW header is at offset 0, implies cluster DNE
log_ctx->complete(-ENOENT);
} else if (cluster_extent.cluster_offset == QCOW_OFLAG_ZERO) {
// explicitly zeroed section
read_ctx->bl.append_zero(cluster_extent.cluster_length);
log_ctx->complete(0);
} else {
// request the (sub)cluster from the cluster cache
qcow_format->m_cluster_cache->get_cluster(
@ -628,9 +639,14 @@ private:
if (r == -ENOENT) {
r = 0;
} else if (r >= 0 && cluster_extent.cluster_offset != 0) {
auto state = io::SPARSE_EXTENT_STATE_DATA;
if (cluster_extent.cluster_offset == QCOW_OFLAG_ZERO) {
state = io::SPARSE_EXTENT_STATE_ZEROED;
}
sparse_extents->insert(
cluster_extent.image_offset, cluster_extent.cluster_length,
{io::SPARSE_EXTENT_STATE_DATA, cluster_extent.cluster_length});
{state, cluster_extent.cluster_length});
}
on_finish->complete(r);
@ -729,7 +745,7 @@ void QCOWFormat<I>::handle_probe(int r, Context* on_finish) {
if (header_probe.version == 1) {
read_v1_header(on_finish);
return;
} else if (header_probe.version == 2) {
} else if (header_probe.version >= 2 && header_probe.version <= 3) {
read_v2_header(on_finish);
return;
} else {
@ -853,8 +869,130 @@ void QCOWFormat<I>::handle_read_v2_header(int r, Context* on_finish) {
auto cct = m_image_ctx->cct;
ldout(cct, 10) << "r=" << r << dendl;
// TODO add support for QCOW2
on_finish->complete(-ENOTSUP);
if (r < 0) {
lderr(cct) << "failed to read QCOW2 header: " << cpp_strerror(r) << dendl;
on_finish->complete(r);
return;
}
auto header = *reinterpret_cast<QCowHeader*>(m_bl.c_str());
// byte-swap important fields
header.magic = be32toh(header.magic);
header.version = be32toh(header.version);
header.backing_file_offset = be64toh(header.backing_file_offset);
header.backing_file_size = be32toh(header.backing_file_size);
header.cluster_bits = be32toh(header.cluster_bits);
header.size = be64toh(header.size);
header.crypt_method = be32toh(header.crypt_method);
header.l1_size = be32toh(header.l1_size);
header.l1_table_offset = be64toh(header.l1_table_offset);
header.nb_snapshots = be32toh(header.nb_snapshots);
header.snapshots_offset = be64toh(header.snapshots_offset);
if (header.version == 2) {
// valid only for version >= 3
header.incompatible_features = 0;
header.compatible_features = 0;
header.autoclear_features = 0;
header.header_length = 72;
header.compression_type = 0;
} else {
header.incompatible_features = be64toh(header.incompatible_features);
header.compatible_features = be64toh(header.compatible_features);
header.autoclear_features = be64toh(header.autoclear_features);
header.header_length = be32toh(header.header_length);
}
if (header.magic != QCOW_MAGIC || header.version < 2 || header.version > 3) {
// honestly shouldn't happen since we've already validated it
lderr(cct) << "header is not QCOW2" << dendl;
on_finish->complete(-EINVAL);
return;
}
if (header.cluster_bits < QCOW_MIN_CLUSTER_BITS ||
header.cluster_bits > QCOW_MAX_CLUSTER_BITS) {
lderr(cct) << "invalid cluster bits: " << header.cluster_bits << dendl;
on_finish->complete(-EINVAL);
return;
}
if (header.crypt_method != QCOW_CRYPT_NONE) {
lderr(cct) << "invalid or unsupported encryption method" << dendl;
on_finish->complete(-EINVAL);
return;
}
m_size = header.size;
if (p2roundup(m_size, static_cast<uint64_t>(512)) != m_size) {
lderr(cct) << "image size is not a multiple of block size" << dendl;
on_finish->complete(-EINVAL);
return;
}
if (header.header_length <= offsetof(QCowHeader, compression_type)) {
header.compression_type = 0;
}
if ((header.compression_type != 0) ||
((header.incompatible_features & QCOW2_INCOMPAT_COMPRESSION) != 0)) {
lderr(cct) << "invalid or unsupported compression type" << dendl;
on_finish->complete(-EINVAL);
return;
}
if ((header.incompatible_features & QCOW2_INCOMPAT_DATA_FILE) != 0) {
lderr(cct) << "external data file feature not supported" << dendl;
on_finish->complete(-ENOTSUP);
}
if ((header.incompatible_features & QCOW2_INCOMPAT_EXTL2) != 0) {
lderr(cct) << "extended L2 table feature not supported" << dendl;
on_finish->complete(-ENOTSUP);
return;
}
header.incompatible_features &= ~QCOW2_INCOMPAT_MASK;
if (header.incompatible_features != 0) {
lderr(cct) << "unknown incompatible feature enabled" << dendl;
on_finish->complete(-EINVAL);
return;
}
m_backing_file_offset = header.backing_file_offset;
m_backing_file_size = header.backing_file_size;
m_cluster_bits = header.cluster_bits;
m_cluster_size = 1UL << header.cluster_bits;
m_cluster_offset_mask = (1ULL << (63 - header.cluster_bits)) - 1;
m_cluster_mask = ~(QCOW_OFLAG_COMPRESSED | QCOW_OFLAG_COPIED);
// L2 table is fixed a (1) cluster block to hold 8-byte (3 bit) offsets
uint32_t l2_bits = m_cluster_bits - 3;
uint32_t shift = m_cluster_bits + l2_bits;
m_l1_size = (m_size + (1LL << shift) - 1) >> shift;
m_l1_table_offset = header.l1_table_offset;
if (m_size > (std::numeric_limits<uint64_t>::max() - (1ULL << shift)) ||
m_l1_size > (std::numeric_limits<int32_t>::max() / sizeof(uint64_t))) {
lderr(cct) << "image size too big: " << m_size << dendl;
on_finish->complete(-EINVAL);
return;
} else if (m_l1_size > header.l1_size) {
lderr(cct) << "invalid L1 table size in header (" << header.l1_size
<< " < " << m_l1_size << ")" << dendl;
on_finish->complete(-EINVAL);
return;
}
ldout(cct, 15) << "size=" << m_size << ", "
<< "cluster_bits=" << m_cluster_bits << dendl;
// allocate memory for L1 table and L2 + cluster caches
m_l2_table_cache = std::make_unique<L2TableCache>(this, l2_bits);
m_cluster_cache = std::make_unique<ClusterCache>(this);
read_l1_table(on_finish);
}
template <typename I>