cephのpool dump (rados export) をparseする
What
Object storageであるCephはRADOS Gateway(rgw)経由でS3互換のAPIを提供しているが、S3を経由にせずpool dump自体parseできないかを試した。
rados export -p default.rgw.buckets.index rados_export.bin
# ↑これをparseしたい
TL;DR
C++の実装読んだらなんとかなった。結構面白いフォーマットだった。
プロセス
まずhexとにらめっこしてmagic headerに気づく。

cephをcloneして調べる。
const uint16_t shortmagic = 0xffce; //goes into stream as "ceff"
//endmagic goes into stream as "ceff ffec"
const mymagic_t endmagic = (0xecff << 16) | shortmagic;
headerとfooterのencodeを読み解く。
struct super_header {
static const uint32_t super_magic = (shortmagic << 16) | shortmagic;
// ver = 1, Initial version
// ver = 2, Add OSDSuperblock to pg_begin
static const uint32_t super_ver = 2;
static const uint32_t FIXED_LENGTH = 16;
uint32_t magic;
uint32_t version;
uint32_t header_size;
uint32_t footer_size;
super_header() : magic(0), version(0), header_size(0), footer_size(0) { }
void encode(bufferlist& bl) const {
using ceph::encode;
encode(magic, bl);
encode(version, bl);
encode(header_size, bl);
encode(footer_size, bl);
}
void decode(bufferlist::const_iterator& bl) {
using ceph::decode;
decode(magic, bl);
decode(version, bl);
decode(header_size, bl);
decode(footer_size, bl);
}
};
struct header {
sectiontype_t type;
mysize_t size;
header(sectiontype_t type, mysize_t size) :
type(type), size(size) { }
header(): type(0), size(0) { }
void encode(bufferlist& bl) const {
uint32_t debug_type = (type << 24) | (type << 16) | shortmagic;
ENCODE_START(1, 1, bl);
encode(debug_type, bl);
encode(size, bl);
ENCODE_FINISH(bl);
}
void decode(bufferlist::const_iterator& bl) {
uint32_t debug_type;
DECODE_START(1, bl);
decode(debug_type, bl);
type = debug_type >> 24;
decode(size, bl);
DECODE_FINISH(bl);
}
};
struct footer {
mymagic_t magic;
footer() : magic(endmagic) { }
void encode(bufferlist& bl) const {
ENCODE_START(1, 1, bl);
encode(magic, bl);
ENCODE_FINISH(bl);
}
void decode(bufferlist::const_iterator& bl) {
DECODE_START(1, bl);
decode(magic, bl);
DECODE_FINISH(bl);
}
};
ceph::encodeを確認する -> src/include/encoding.h。
基本uint8に変換してbufferlistにそのまま書き込んでいる。
template<class T>
inline void encode_raw(const T& t, bufferlist& bl)
{
bl.append((char*)&t, sizeof(t));
}
template<class T>
inline void decode_raw(T& t, bufferlist::const_iterator &p)
{
p.copy(sizeof(t), (char*)&t);
}
// -----------------------------------
// int types
#define WRITE_INTTYPE_ENCODER(type, etype) \\
inline void encode(type v, ::ceph::bufferlist& bl, uint64_t features=0) { \\
ceph_##etype e; \\
e = v; \\
::ceph::encode_raw(e, bl); \\
} \\
inline void decode(type &v, ::ceph::bufferlist::const_iterator& p) { \\
ceph_##etype e; \\
::ceph::decode_raw(e, p); \\
v = e; \\
}
WRITE_INTTYPE_ENCODER(uint64_t, le64)
WRITE_INTTYPE_ENCODER(int64_t, le64)
WRITE_INTTYPE_ENCODER(uint32_t, le32)
WRITE_INTTYPE_ENCODER(int32_t, le32)
WRITE_INTTYPE_ENCODER(uint16_t, le16)
WRITE_INTTYPE_ENCODER(int16_t, le16)
src/tools/rados/PoolDump.cc側の実装を確認する。
int PoolDump::dump(IoCtx *io_ctx)
{
ceph_assert(io_ctx != NULL);
int r = 0;
// 1️⃣
write_super();
// 2️⃣
r = write_simple(TYPE_POOL_BEGIN, file_fd);
librados::NObjectIterator i = io_ctx->nobjects_begin();
for (; i != i_end; ++i) {
// 3️⃣
r = write_section(TYPE_OBJECT_BEGIN, obj_begin, file_fd);
while (true) {
r = io_ctx->read(oid, outdata, op_size, offset);
if (r <= 0) {
// Error or no data
break;
}
// 4️⃣
r = write_section(TYPE_DATA,
data_section(offset, outdata.length(), outdata), file_fd);
}
// 5️⃣
r = write_section(TYPE_ATTRS, attr_section(xattrs), file_fd);
// 6️⃣
r = write_section(TYPE_OMAP_HDR, omap_hdr_section(omap_header), file_fd);
do {
// 7️⃣
r = write_section(TYPE_OMAP, omap_section(values), file_fd);
} while (r == MAX_READ);
// Close object
// =============
// 8️⃣
r = write_simple(TYPE_OBJECT_END, file_fd);
}
// 9️⃣
r = write_simple(TYPE_POOL_END, file_fd);
return r;
}
write_* callは9箇所しかなく、write_super/write_simple/write_sectionの三種類しかなかった。
write_simpleとwrite_sectionを再度確認する。
// Define this in .h because it's templated
template <typename T>
int write_section(sectiontype_t type, const T& obj, int fd) {
if (dry_run)
return 0;
bufferlist blhdr, bl, blftr;
obj.encode(bl);
header hdr(type, bl.length());
hdr.encode(blhdr);
footer ft;
ft.encode(blftr);
int ret = blhdr.write_fd(fd);
if (ret) return ret;
ret = bl.write_fd(fd);
if (ret) return ret;
ret = blftr.write_fd(fd);
return ret;
}
int write_simple(sectiontype_t type, int fd)
{
if (dry_run)
return 0;
bufferlist hbl;
header hdr(type, 0);
hdr.encode(hbl);
return hbl.write_fd(fd);
}
write_simpleはheader onlyで、write_sectionは header + 任意のobject + footerの形式のようだ。headerのencode処理を照らし合わせると、dumpデータのHex状態から以下の内容が見える。
enum {
TYPE_NONE = 0,
TYPE_PG_BEGIN, // 1
TYPE_PG_END, // 2
TYPE_OBJECT_BEGIN, // 3
TYPE_OBJECT_END, // 4
TYPE_DATA, // 5
TYPE_ATTRS, // 6
TYPE_OMAP_HDR, // 7
TYPE_OMAP, // 8
TYPE_PG_METADATA, // 9
TYPE_POOL_BEGIN, // 10
TYPE_POOL_END, // 11
END_OF_TYPES, //Keep at the end // 12
};

ここまで来たら、レイアウト自体parseできるようになった。
※ 以下は仮実装
//! DEMO decoding impl of ceph `rados export -p default.rgw.buckets.index rados_export.bin`
//! NOTE: error handling reduced for simplicity
use std::io::{Cursor, Read};
fn main() {
let data = include_bytes!("../data/rados_export.bin");
let mut cursor = Cursor::new(data.as_slice());
/*
- 16bytes super header
- loop
- 18bytes block header (simple) (type_tag + size)
- optional section
- $size bytes block content
- std::map<std::string, cepth::bufferlist> if type_tag == TYPE_OMAP
- std::string = object key
- meta
- 10bytes footer
*/
read_super(&mut cursor);
loop {
let (tag, size) = read_simple(&mut cursor);
match tag {
TypeTag::TYPE_NONE
| TypeTag::TYPE_PG_BEGIN
| TypeTag::TYPE_PG_END
| TypeTag::TYPE_OBJECT_BEGIN
| TypeTag::TYPE_DATA
| TypeTag::TYPE_ATTRS
| TypeTag::TYPE_OMAP_HDR
| TypeTag::TYPE_PG_METADATA
| TypeTag::END_OF_TYPES => {
read_section(&mut cursor, size);
}
TypeTag::TYPE_OMAP => {
let omap_buf = read_section(&mut cursor, size);
let mut omap_buf_cursor = Cursor::new(omap_buf);
read_omap(&mut omap_buf_cursor);
}
TypeTag::TYPE_POOL_BEGIN | TypeTag::TYPE_OBJECT_END => {}
TypeTag::TYPE_POOL_END => {
break;
}
}
}
}
#[allow(non_camel_case_types)]
#[derive(Debug)]
enum TypeTag {
TYPE_NONE = 0,
TYPE_PG_BEGIN, // 1
TYPE_PG_END, // 2
TYPE_OBJECT_BEGIN, // 3
TYPE_OBJECT_END, // 4
TYPE_DATA, // 5
TYPE_ATTRS, // 6
TYPE_OMAP_HDR, // 7
TYPE_OMAP, // 8
TYPE_PG_METADATA, // 9
TYPE_POOL_BEGIN, // 10
TYPE_POOL_END, // 11
END_OF_TYPES, // Keep at the end
}
impl From<u8> for TypeTag {
fn from(value: u8) -> Self {
match value {
0 => Self::TYPE_NONE,
1 => Self::TYPE_PG_BEGIN,
2 => Self::TYPE_PG_END,
3 => Self::TYPE_OBJECT_BEGIN,
4 => Self::TYPE_OBJECT_END,
5 => Self::TYPE_DATA,
6 => Self::TYPE_ATTRS,
7 => Self::TYPE_OMAP_HDR,
8 => Self::TYPE_OMAP,
9 => Self::TYPE_PG_METADATA,
10 => Self::TYPE_POOL_BEGIN,
11 => Self::TYPE_POOL_END,
12 => Self::END_OF_TYPES,
_ => unreachable!(),
}
}
}
impl From<&[u8; 4]> for TypeTag {
fn from(value: &[u8; 4]) -> Self {
((u32_from_le_slice(value) >> 24) as u8).into()
}
}
impl AsRef<str> for TypeTag {
fn as_ref(&self) -> &str {
match self {
TypeTag::TYPE_NONE => "TYPE_NONE",
TypeTag::TYPE_PG_BEGIN => "TYPE_PG_BEGIN",
TypeTag::TYPE_PG_END => "TYPE_PG_END",
TypeTag::TYPE_OBJECT_BEGIN => "TYPE_OBJECT_BEGIN",
TypeTag::TYPE_OBJECT_END => "TYPE_OBJECT_END",
TypeTag::TYPE_DATA => "TYPE_DATA",
TypeTag::TYPE_ATTRS => "TYPE_ATTRS",
TypeTag::TYPE_OMAP_HDR => "TYPE_OMAP_HDR",
TypeTag::TYPE_OMAP => "TYPE_OMAP",
TypeTag::TYPE_PG_METADATA => "TYPE_PG_METADATA",
TypeTag::TYPE_POOL_BEGIN => "TYPE_POOL_BEGIN",
TypeTag::TYPE_POOL_END => "TYPE_POOL_END",
TypeTag::END_OF_TYPES => "END_OF_TYPES",
}
}
}
// 16 bytes
fn read_super(cursor: &mut Cursor<&[u8]>) {
let mut header = [0; 16];
_ = cursor.read_exact(&mut header);
let magic = &header[..4];
let version = u32_from_le_slice(&header[4..8]);
let header_len = u32_from_le_slice(&header[8..12]);
let footer_len = u32_from_le_slice(&header[12..16]);
println!(
"{:X?} version={:?} header_len={:?} footer_len={:?}",
&magic, version, header_len, footer_len
);
}
// 6 bytes
fn read_meta(cursor: &mut Cursor<&[u8]>) -> i32 {
let mut starting = [0; 1 + 1]; // v + compact_v
let mut struct_len = [0; 4];
_ = cursor.read_exact(&mut starting);
_ = cursor.read_exact(&mut struct_len);
i32_from_le_slice(&struct_len)
}
// 18 bytes
fn read_simple(cursor: &mut Cursor<&[u8]>) -> (TypeTag, usize) {
read_meta(cursor);
read_header(cursor)
}
// 12 bytes
fn read_header(cursor: &mut Cursor<&[u8]>) -> (TypeTag, usize) {
let mut section_type = [0; 4];
_ = cursor.read_exact(&mut section_type);
let section_type_t = TypeTag::from(§ion_type);
println!("{}", section_type_t.as_ref());
let mut size_buf = [0; 8];
_ = cursor.read_exact(&mut size_buf);
let mysize_t = i64_from_le_slice(&size_buf) as usize;
// println!("header size={mysize_t}");
(section_type_t, mysize_t)
}
// 10 bytes
fn read_footer(cursor: &mut Cursor<&[u8]>) {
read_meta(cursor);
let mut magic = [0; 4];
_ = cursor.read_exact(&mut magic);
assert_eq!(magic, [0xCE, 0xFF, 0xFF, 0xEC])
}
fn read_section<'a>(cursor: &mut Cursor<&'a [u8]>, header_size: usize) -> &'a [u8] {
let buf = read_n_bytes(cursor, header_size);
if header_size > 0 {
read_footer(cursor);
}
buf
}
read_headerの実装は省くが、基本の考え方はgrpc/protobufに近い。append onlyとし、structのバージョンとheader内でencoding長を定義することで、後方互換性を維持しながら未知のフィールドをスキップできる。


ここから肝心なTYPE_OMAPをparseする。
struct omap_section {
std::map<std::string, bufferlist> omap;
explicit omap_section(const std::map<std::string, bufferlist> &omap) :
omap(omap) { }
omap_section() { }
void encode(bufferlist& bl) const {
ENCODE_START(1, 1, bl);
encode(omap, bl);
ENCODE_FINISH(bl);
}
void decode(bufferlist::const_iterator& bl) {
DECODE_START(1, bl);
decode(omap, bl);
DECODE_FINISH(bl);
}
};
std::mapに対応するencoding形式を調べたところ、map_sizeをencodeしてからkeyとvalueをlist化しているだけなので簡単だった。
// map
template<class T, class U, class Comp, class Alloc,
typename t_traits, typename u_traits>
inline std::enable_if_t<!t_traits::supported ||
!u_traits::supported>
encode(const std::map<T,U,Comp,Alloc>& m, bufferlist& bl)
{
__u32 n = (__u32)(m.size());
encode(n, bl);
for (auto p = m.begin(); p != m.end(); ++p) {
encode(p->first, bl);
encode(p->second, bl);
}
}
対応実装
fn read_omap(cursor: &mut Cursor<&[u8]>) {
read_meta(cursor);
let map_size = read_u32(cursor);
println!("map_size: {map_size}");
for _ in 0..map_size {
let entry = read_string(cursor);
// meta
let buf_len = read_u32(cursor);
let bytes = read_n_bytes(cursor, buf_len as usize);
let mut bytes_cursor = Cursor::new(bytes);
// 未知なfields、多分rgw_bucket_dir_entry_meta
_ = read_n_bytes(&mut bytes_cursor, 6);
_ = read_string(&mut bytes_cursor);
_ = read_n_bytes(&mut bytes_cursor, 32);
_ = read_string(&mut bytes_cursor);
let owner = read_string(&mut bytes_cursor);
// ...
}
}
注意が必要なのは、ここの string は std::string であり、utf8 を保証していないことだ。実際、試した限りでは Ceph の object version を使っているケースのみ string に 0x00 が混ざっていたため、それ以上の深追いはしなかった。それ以外に触れたデータ範囲内では、utf8 として扱っても特に問題はなかった。


let utf8_bytes = vec![
0xE3, 0x81, 0x93, 0xE3, 0x82, 0x93,
0xE3, 0x81, 0xAB, 0xE3, 0x81, 0xA1,
0xE3, 0x81, 0xAF, 0x2E, 0x6D, 0x64
];
println!("{:?}", String::from_utf8(utf8_bytes));
// Ok("こんにちは.md")
フォーマット概要は、以下のようになっている。
- 16bytes super header
- loop
- 18bytes block header (simple) (type_tag + size)
- optional section
- $size bytes block content
- std::map<std::string, cepth::bufferlist> if type_tag == TYPE_OMAP
- std::string = object key
- meta
- 10bytes footer
ここまですべてのomapのownerとentryをparseできた。おわり。
Appendix.1
RadosImport.cc をビルドできれば少し変えるだけで動きそうだったが、あんまり楽ではなかった。(特にBoost C++系の環境セットアップに時間かかりそうな気がした)
Appendix.2
librados のpython packageもあるが、OS限定だったり今回の目的に向いてなかった。
Discussion