package org.zstack.simulator.kvm; import org.apache.commons.lang.StringUtils; import org.junit.Assert; import org.springframework.beans.factory.annotation.Autowired; import org.zstack.core.Platform; import org.zstack.core.db.DatabaseFacade; import org.zstack.core.db.SimpleQuery; import org.zstack.core.db.SimpleQuery.Op; import org.zstack.header.storage.snapshot.VolumeSnapshotInventory; import org.zstack.header.storage.snapshot.VolumeSnapshotTree; import org.zstack.header.storage.snapshot.VolumeSnapshotTree.SnapshotLeaf; import org.zstack.header.storage.snapshot.VolumeSnapshotVO; import org.zstack.header.storage.snapshot.VolumeSnapshotVO_; import org.zstack.kvm.KVMAgentCommands.TakeSnapshotCmd; import org.zstack.kvm.KVMAgentCommands.TakeSnapshotResponse; import org.zstack.storage.primary.nfs.NfsPrimaryStorageKVMBackendCommands.RevertVolumeFromSnapshotCmd; import org.zstack.storage.primary.nfs.NfsPrimaryStorageKVMBackendCommands.RevertVolumeFromSnapshotResponse; import org.zstack.utils.CollectionUtils; import org.zstack.utils.DebugUtils; import org.zstack.utils.Utils; import org.zstack.utils.function.Function; import org.zstack.utils.logging.CLogger; import org.zstack.utils.path.PathUtil; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** */ public class VolumeSnapshotKvmSimulator { private static CLogger logger = Utils.getLogger(VolumeSnapshotKvmSimulator.class); public static class Qcow2 { private String installPath; private List<Qcow2> next = new ArrayList<Qcow2>(); private Qcow2 prev; public String getInstallPath() { return installPath; } public void setInstallPath(String installPath) { this.installPath = installPath; } public void addNext(Qcow2 n) { next.add(n); } public void deleteNext(Qcow2 n) { next.remove(n); } public List<Qcow2> getNext() { return next; } public void setNext(List<Qcow2> next) { this.next = next; } public Qcow2 getPrev() { return prev; } public void setPrev(Qcow2 prev) { this.prev = prev; } private Qcow2 walkDown(Qcow2 s, Function<Qcow2, Qcow2> func) { Qcow2 ret = null; if (s == null) { return null; } ret = func.call(s); if (ret != null) { return ret; } if (s.next.isEmpty()) { return null; } for (Qcow2 q : s.next) { ret = walkDown(q, func); if (ret != null) { return ret; } } return ret; } public Qcow2 walkDown(Function<Qcow2, Qcow2> func) { return walkDown(this, func); } private Qcow2 walkUp(Qcow2 s, Function<Qcow2, Qcow2> func) { Qcow2 ret = null; if (s == null) { return null; } ret = func.call(s); if (ret != null) { return ret; } if (s.prev == null) { return null; } return walkDown(s.prev, func); } public Qcow2 walkUp(Function<Qcow2, Qcow2> func) { return walkUp(this, func); } public Qcow2 find(final String installPath) { return walkDown(new Function<Qcow2, Qcow2>() { @Override public Qcow2 call(Qcow2 arg) { if (installPath.equals(arg.getInstallPath())) { return arg; } return null; } }); } public static Qcow2 newQcow2(String installPath) { return newQcow2(installPath, null); } public static Qcow2 newQcow2(String installPath, Qcow2 parent) { DebugUtils.Assert(installPath!=null, "installPath cannot be null"); Qcow2 s = new Qcow2(); s.setInstallPath(installPath); if (parent != null) { parent.addNext(s); s.setPrev(parent); } return s; } private Qcow2 findRoot(Qcow2 qcow2) { if (qcow2.prev == null) { return qcow2; } return findRoot(qcow2.prev); } public Qcow2 findRoot() { return findRoot(this); } public void print(PrintWriter writer) { print(writer, "", true); } private void print(PrintWriter writer, String prefix, boolean isTail) { writer.println(prefix + (isTail ? "|__ " : "|---") + installPath); for (int i = 0; i < next.size() - 1; i++) { next.get(i).print(writer, prefix + (isTail ? " " : "| "), false); } if (next.size() >= 1) { next.get(next.size() - 1).print(writer, prefix + (isTail ? " " : "| "), true); } } } @Autowired private DatabaseFacade dbf; private Map<String, Qcow2> snapshots = new HashMap<String, Qcow2>(); private Qcow2 findByInstallPath(String installPath) { for (Qcow2 s : snapshots.values()) { Qcow2 ret = s.find(installPath); if (ret != null) { return ret; } } return null; } private void dumpQcow2Tree(Qcow2 current, String msg) { Qcow2 root = current.findRoot(); StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); printWriter.println(String.format("======== %s ==============", msg)); root.print(printWriter); printWriter.println(String.format("======== End of %s =======", msg)); logger.debug("\n\n" + stringWriter.toString()); } public synchronized TakeSnapshotResponse takeSnapshot(TakeSnapshotCmd cmd) { Qcow2 current = findByInstallPath(cmd.getVolumeInstallPath()); if (cmd.isFullSnapshot() && current == null) { dumpAllQcow2(); String err = String.format("cannot find snapshot[%s] and it's full snapshot", cmd.getVolumeInstallPath()); DebugUtils.dumpStackTrace(err); Assert.fail(err); } if (current == null) { current = Qcow2.newQcow2(cmd.getVolumeInstallPath()); snapshots.put(current.getInstallPath(), current); } else { if (cmd.isFullSnapshot()) { String dir = PathUtil.parentFolder(cmd.getVolumeInstallPath()); String newVolumePath = String.format("%s/%s.qcow2", dir, Platform.getUuid()); current = Qcow2.newQcow2(newVolumePath); snapshots.put(current.getInstallPath(), current); } } Qcow2 n = Qcow2.newQcow2(cmd.getInstallPath(), current); logger.debug(String.format("created new volume[%s] for taking snapshot", n.getInstallPath())); dumpQcow2Tree(current, "Taking Snapshot"); TakeSnapshotResponse rsp = new TakeSnapshotResponse(); rsp.setSize(100000); rsp.setNewVolumeInstallPath(n.getInstallPath()); rsp.setSnapshotInstallPath(current.getInstallPath()); return rsp; } private void dumpAllQcow2() { int i = 0; for (Qcow2 q : snapshots.values()) { logger.debug("\n ------------------ All snapshots dump ------------------- \n"); dumpQcow2Tree(q, String.valueOf(i++)); logger.debug("\n ------------------ End of all snapshots dump ------------ \n"); } } public synchronized void merge(String src, String dest, boolean fullRebase) { Qcow2 qsrc = findByInstallPath(src); DebugUtils.Assert(qsrc!=null, String.format("cannot find source snapshot[%s]", src)); Qcow2 qdest = qsrc.find(dest); if (qdest == null) { dumpAllQcow2(); Assert.fail(String.format("cannot find target volume[%s] to merge", dest)); } if (fullRebase) { snapshots.remove(qsrc.getInstallPath()); qdest.setPrev(null); snapshots.put(qdest.getInstallPath(), qdest); return; } if (qsrc.getPrev() == null) { // base if (!qsrc.getNext().contains(qdest)) { qsrc.getNext().add(qdest); qdest.setPrev(qsrc); } } else { // intermediate Qcow2 toDelete = qdest.getPrev(); while (toDelete != null) { if (qsrc.getNext().contains(toDelete)) { qsrc.deleteNext(toDelete); break; } toDelete = toDelete.getPrev(); } qsrc.addNext(qdest); qdest.setPrev(qsrc); } dumpQcow2Tree(qdest, "Merging Snapshot"); } public synchronized void delete(String installPath) { Qcow2 q = findByInstallPath(installPath); if (q == null) { return; } if (q.getPrev() == null) { // base snapshots.remove(q.getInstallPath()); } else { // intermediate Qcow2 parent = q.getPrev(); parent.deleteNext(q); dumpQcow2Tree(parent, "Deleting Snapshot"); } } public synchronized RevertVolumeFromSnapshotResponse revert(RevertVolumeFromSnapshotCmd cmd) { RevertVolumeFromSnapshotResponse rsp = new RevertVolumeFromSnapshotResponse(); Qcow2 current = findByInstallPath(cmd.getSnapshotInstallPath()); String dir = PathUtil.parentFolder(cmd.getSnapshotInstallPath()); String newVolumeInstallPath = String.format("%s/%s.qcow2", dir, Platform.getUuid()); logger.debug(String.format("created new volume[%s] for reverting snapshot", newVolumeInstallPath)); Qcow2.newQcow2(newVolumeInstallPath, current); dumpQcow2Tree(current, "Reverting Snapshot"); rsp.setNewVolumeInstallPath(newVolumeInstallPath); return rsp; } private void walkDownLeaf(SnapshotLeaf leaf, List<VolumeSnapshotInventory> path, List<List<VolumeSnapshotInventory>> ret) { if (leaf.getChildren().isEmpty()) { List<VolumeSnapshotInventory> copy = new ArrayList<VolumeSnapshotInventory>(); copy.addAll(path); copy.add(leaf.getInventory()); ret.add(copy); return; } path.add(leaf.getInventory()); for (SnapshotLeaf l : leaf.getChildren()) { walkDownLeaf(l, path, ret); } path.remove(leaf.getInventory()); } private List<List<VolumeSnapshotInventory>> findOutAllChains(SnapshotLeaf leaf) { List<List<VolumeSnapshotInventory>> ret = new ArrayList<List<VolumeSnapshotInventory>>(); List<VolumeSnapshotInventory> paths = new ArrayList<VolumeSnapshotInventory>(); walkDownLeaf(leaf, paths, ret); return ret; } private void walkDownQcow2(Qcow2 qcow2, List<String> paths, List<List<String>> ret) { if (qcow2.getNext().isEmpty()) { List<String> copy = new ArrayList<String>(); copy.addAll(paths); copy.add(qcow2.getInstallPath()); ret.add(copy); return; } paths.add(qcow2.getInstallPath()); for (Qcow2 q : qcow2.getNext()) { walkDownQcow2(q, paths, ret); } paths.remove(qcow2.getInstallPath()); } private List<List<String>> findOutAllQcow2Chains(Qcow2 qcow2) { final List<String> paths = new ArrayList<String>(); final List<List<String>> ret = new ArrayList<List<String>>(); walkDownQcow2(qcow2, paths, ret); return ret; } private void validate(List<VolumeSnapshotInventory> chain) { VolumeSnapshotInventory start = chain.get(0); Qcow2 root = findByInstallPath(start.getPrimaryStorageInstallPath()); if (root == null) { dumpAllQcow2(); Assert.fail(String.format("cannot find root qcow2 with path[%s]", start.getPrimaryStorageInstallPath())); } List<List<String>> qcowChains = findOutAllQcow2Chains(root); List<String> expected = CollectionUtils.transformToList(chain, new Function<String, VolumeSnapshotInventory>() { @Override public String call(VolumeSnapshotInventory arg) { return arg.getPrimaryStorageInstallPath(); } }); boolean success = false; for (List<String> paths : qcowChains) { boolean tempSuccess = true; if (paths.size() >= chain.size()) { for (VolumeSnapshotInventory s : chain) { int index = chain.indexOf(s); String actual = paths.get(index); if (!actual.equals(s.getPrimaryStorageInstallPath())) { tempSuccess = false; break; } } } else { tempSuccess = false; } if (tempSuccess) { success = true; break; } } if (!success) { StringBuilder sb = new StringBuilder("cannot find snapshot chain on backend:\n\n"); sb.append(String.format("expected:\n\n")); sb.append(StringUtils.join(expected, "\n")); sb.append("\n\nactual:\n"); for (List<String> paths : qcowChains) { sb.append(String.format("chain%s\n", qcowChains.indexOf(paths))); sb.append(StringUtils.join(paths, "\n")); sb.append("\n"); } String err = sb.toString(); logger.warn(err); Assert.fail(err); } } public void validateNotExisting(String installPath) { Qcow2 q = findByInstallPath(installPath); if (q != null) { Assert.fail(String.format("still found snapshot[%s]", q.getInstallPath())); } } public void validate(SnapshotLeaf leaf) { List<List<VolumeSnapshotInventory>> chains = findOutAllChains(leaf); for (List<VolumeSnapshotInventory> chain : chains) { validate(chain); } } public void validate(VolumeSnapshotInventory root) { logger.debug(String.format("validating volume snapshot chain starting with root[uuid:%s, installPath:%s]", root.getUuid(), root.getPrimaryStorageInstallPath())); SnapshotLeaf leaf = buildRootLeaf(root.getUuid()); validate(leaf); } public SnapshotLeaf buildRootLeaf(String uuid) { VolumeSnapshotVO s = dbf.findByUuid(uuid, VolumeSnapshotVO.class); SimpleQuery<VolumeSnapshotVO> q = dbf.createQuery(VolumeSnapshotVO.class); q.add(VolumeSnapshotVO_.treeUuid, Op.EQ, s.getTreeUuid()); List<VolumeSnapshotVO> vos = q.list(); VolumeSnapshotTree tree = VolumeSnapshotTree.fromVOs(vos); SnapshotLeaf leaf = tree.getRoot(); if (!leaf.getInventory().getUuid().equals(uuid)) { Assert.fail(String.format("snapshot[%s] is not root snapshot", uuid)); } return leaf; } }