diff --git a/doc/rbd/rbd-live-migration.rst b/doc/rbd/rbd-live-migration.rst index c81670fd5ea..e8b48c5c9f0 100644 --- a/doc/rbd/rbd-live-migration.rst +++ b/doc/rbd/rbd-live-migration.rst @@ -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", diff --git a/qa/workunits/rbd/cli_migration.sh b/qa/workunits/rbd/cli_migration.sh index cf665940c45..42ef5655e96 100755 --- a/qa/workunits/rbd/cli_migration.sh +++ b/qa/workunits/rbd/cli_migration.sh @@ -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 <complete(r); + return; + } + + auto header = *reinterpret_cast(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(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::max() - (1ULL << shift)) || + m_l1_size > (std::numeric_limits::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(this, l2_bits); + m_cluster_cache = std::make_unique(this); + + read_l1_table(on_finish); } template