目次
はじめに
仮想マシンのVMDKファイルに含まれているCID、parentCIDの紐づけが正常であるかをチェックする為の可視化ツールを考えました。
この考えに至った背景は、以下KBにあるようにCIDの不一致による問題が生じることがあります。
"The parent virtual disk has been modified since the child was created" error
VMDKファイルが多くなると、CIDのチェックが大変です。
そこで、可視化、不一致があれば警告するツールを作ることで判断材料としていただきたく思います。
ツール実行環境
os : Fedora 42
言語:
- D3.js
- HTML
- python
ツール実行
vmdkまたはログバンドル指定し、json形式のファイルを作成し、簡易的なwebサーバを立ち上げることでCID、parentCIDのステータスを可視化できます。
[事前準備]
仮想マシンに2つのハードディスクを設定
スナップショットを作成
[ツール実行手順]
仮想マシンのvmdkファイルのダウンロードまたはログバンドルを生成
ツール実行の端末からアクセス可能なディレクトリにダウンロードファイルを配置
以下コマンドでjsonファイルを生成
python3 parse_vmdk.py <vmdkファイルパス> -o vmdk_graph.json
* htmlファイルでvmdk_graph.jsonを直接指定している為変更しないでください。
webサーバ立ち上げ
python3 -m http.server 8000
ブラウザにてアクセスします
関係に問題が生じたvmdkはリンクされません。
そして警告にあるように問題のあるparentCIDが表示されます。

* 上記検証は参考例であるため、各々の環境に応じて設定してください。
作成したプログラム
Githubで公開しております。
GitHub - Hir0Lab/vmdk-cid-checker · GitHub
・parse_vmdk_bundle.py : d3.jsで取り扱えるようにjson形式に変換
・index.html : d3.jsを利用してjsonファイルを読み込み、可視化を実現
概要として以下で幾つかピックアップして説明します。
parse_vmdk_bundle.py
main()
引数としてログバンドル、vmdkファイルが格納されているディレクトリ、vmdkファイルとして指定が可能です。
それぞれの指定方法に合わせて[INFO]情報の出力を変えており、 build_graphにvmdkのデータを渡してjsonファイルを生成しています。
jsonファイル生成に必要なデータはbuild_graphの関数で作成しています。
def main() -> int:
parser = argparse.ArgumentParser(description="Parse VMDK CID/parentCID information and generate D3 graph JSON.")
parser.add_argument("input", help="Single .vmdk descriptor, support bundle archive, or extracted directory")
parser.add_argument("-o", "--output", default="vmdk_graph.json", help="Output JSON file")
args = parser.parse_args()
input_path = Path(args.input).expanduser().resolve()
output_path = Path(args.output).expanduser().resolve()
if not input_path.exists():
print(f"[ERROR] input not found: {input_path}")
return 1
with tempfile.TemporaryDirectory(prefix="vmdk_bundle_") as tmp:
tmpdir = Path(tmp)
if input_path.is_dir():
print(f"[INFO] scanning directory: {input_path}")
vmdks = collect_vmdks(input_path)
elif input_path.is_file() and input_path.suffix.lower() == ".vmdk":
print(f"[INFO] scanning single VMDK descriptor: {input_path}")
vmdks = collect_single_vmdk(input_path)
else:
print(f"[INFO] extracting archive: {input_path}")
root = extract_archive(input_path, tmpdir)
print(f"[INFO] scanning extracted archive: {root}")
vmdks = collect_vmdks(root)
print(f"[INFO] VMDK descriptors found: {len(vmdks)}")
graph = build_graph(vmdks)
output_path.write_text(json.dumps(graph, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"[INFO] output: {output_path}")
print("[INFO] summary:")
for k, v in graph["summary"].items():
print(f" - {k}: {v}")
if graph["warnings"]:
print("[WARN] warnings detected. Open index.html and check warning table.")
return 0
build_graph()
main()のjson.dumpsでjsonファイルを生成している。
本関数ではjsonファイルの生成に必要な元データを生成する。
def build_graph(vmdks: List[Dict]) -> Dict:
nodes = []
links = []
warnings = []
cid_to_nodes: Dict[str, List[Dict]] = {}
for v in vmdks:
if v.get("cid"):
cid_to_nodes.setdefault(v["cid"], []).append(v)
for cid, same_cid_nodes in cid_to_nodes.items():
if len(same_cid_nodes) > 1:
warnings.append({
"type": "duplicate_cid",
"cid": cid,
"files": [n["path"] for n in same_cid_nodes],
"message": f"Duplicate CID detected: {cid}"
})
for v in vmdks:
status = "normal"
if v.get("parentCID") == "ffffffff":
status = "base"
elif not v.get("parentCID"):
status = "unknown"
elif v["parentCID"] not in cid_to_nodes:
status = "missing_parent"
if v.get("cid") and len(cid_to_nodes.get(v["cid"], [])) > 1:
status = "duplicate_cid"
nodes.append({
**v,
"status": status,
"label": v["file"],
})
node_ids = {n["id"] for n in nodes}
for child in vmdks:
pcid = child.get("parentCID")
if not pcid or pcid == "ffffffff":
continue
candidates = cid_to_nodes.get(pcid, [])
if not candidates:
warnings.append({
"type": "missing_parent",
"child": child["path"],
"parentCID": pcid,
"parentFileNameHint": child.get("parentFileNameHint"),
"message": f"Parent CID not found for {child['path']}: parentCID={pcid}"
})
continue
parent = choose_parent_candidate(child, candidates)
if parent["id"] not in node_ids or child["id"] not in node_ids:
continue
links.append({
"source": parent["id"],
"target": child["id"],
"type": "cid_parentcid",
"parentCID": pcid,
"label": f"{child['file']}.parentCID = {parent['file']}.CID"
})
hint_index: Dict[Tuple[str, str], List[Dict]] = {}
for v in vmdks:
hint_index.setdefault((v["dir"], v["file"]), []).append(v)
for child in vmdks:
if child.get("parentCID") in (None, "ffffffff"):
continue
if child.get("parentCID") in cid_to_nodes:
continue
hint = child.get("parentFileNameHint")
if not hint:
continue
hint_name = os.path.basename(hint)
key = (child["dir"], hint_name)
hinted = hint_index.get(key, [])
if hinted:
warnings.append({
"type": "hint_parent_exists_but_cid_mismatch",
"child": child["path"],
"hintedParent": hinted[0]["path"],
"childParentCID": child.get("parentCID"),
"hintedParentCID": hinted[0].get("cid"),
"message": f"parentFileNameHint exists but CID does not match for {child['path']}"
})
return {
"nodes": nodes,
"links": links,
"warnings": warnings,
"summary": {
"vmdkDescriptors": len(nodes),
"links": len(links),
"warnings": len(warnings),
"baseDisks": len([n for n in nodes if n["status"] == "base"]),
"missingParents": len([n for n in nodes if n["status"] == "missing_parent"]),
"duplicateCidNodes": len([n for n in nodes if n["status"] == "duplicate_cid"]),
}
}
index.html
d3.json
生成したvmdk_graph.jsonを読み込んだ後に実行される処理について説明します。
jsonファイルから読み込んだデータを基に、ノードとリンクを描写するコードとなります。
d3.json("vmdk_graph.json").then(data => {
const nodes = data.nodes || [];
const links = data.links || [];
renderSummary(data.summary);
renderWarnings(data.warnings);
const link = g.append("g").attr("stroke-linecap", "round").selectAll("line").data(links).join("line").attr("class", "link");
const node = g.append("g").selectAll("g").data(nodes).join("g").attr("class", "node").call(drag());
node.append("circle").attr("r", radius).attr("fill", d => color(d.status));
node.append("text").attr("x", 12).attr("y", 4).text(d => d.file);
node.on("mouseover", (event, d) => {
tooltip.style("display", "block").text(`${d.file}
status: ${d.status}
CID: ${d.cid}
parentCID: ${d.parentCID}
parentFileNameHint: ${d.parentFileNameHint || "-"}
path: ${d.path}`);
})
.on("mousemove", (event) => tooltip.style("left", `${event.clientX + 12}px`).style("top", `${event.clientY + 12}px`))
.on("mouseout", () => tooltip.style("display", "none"))
.on("click", (event, d) => {
node.selectAll("circle").classed("selected", false);
d3.select(event.currentTarget).select("circle").classed("selected", true);
renderDetails(d);
});
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(140).strength(0.85))
.force("charge", d3.forceManyBody().strength(-550))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(d => radius(d) + 30))
.force("x", d3.forceX(width / 2).strength(0.03))
.force("y", d3.forceY(height / 2).strength(0.03))
.on("tick", () => {
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y).attr("x2", d => d.target.x).attr("y2", d => d.target.y);
node.attr("transform", d => `translate(${d.x},${d.y})`);
});
function drag() {
return d3.drag()
.on("start", (event, d) => { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
.on("drag", (event, d) => { d.fx = event.x; d.fy = event.y; })
.on("end", (event) => { if (!event.active) simulation.alphaTarget(0); });
}
document.getElementById("search").addEventListener("input", (e) => {
const q = e.target.value.trim().toLowerCase();
node.style("opacity", d => {
if (!q) return 1;
const haystack = [d.file, d.path, d.cid, d.parentCID, d.parentFileNameHint, d.status].join(" ").toLowerCase();
return haystack.includes(q) ? 1 : 0.15;
});
link.style("opacity", d => {
if (!q) return 0.75;
const s = d.source, t = d.target;
const haystack = [s.file, s.path, s.cid, s.parentCID, t.file, t.path, t.cid, t.parentCID].join(" ").toLowerCase();
return haystack.includes(q) ? 0.9 : 0.1;
});
});
}).catch(err => {
document.getElementById("summary").innerHTML = `
<div class="warning">vmdk_graph.json を読み込めませんでした。</div>
<p>同じディレクトリで HTTP サーバを起動してください。</p>
<pre>python3 -m http.server 8000</pre>
<p>その後、ブラウザで http://localhost:8000/ を開きます。</p>
<pre>${htmlEscape(err)}</pre>`;
});
まとめ
本ツールはvmdkが多くCID 、 parentCIDの関係を可視化、チェックを実施したい方向けとなります。
コード修正の余地はまだまだあります。
ご参考程度にご確認ください。