VMware Player + NHM は便利だけど複雑な派生とか全くしない私の使い方にはやや面倒なので車輪の再発明。
monolithicSparse な disk にしか適用できない上に親のファイル名を書き換えて差分が参照されるようにするので注意。
これを .vmdk に関連づけさせて楽になった。
"""vmdk_delta.py - make and delete delta vmdk file. Make delta ---------- > vmdk_delta.py make sd0.vmdk Then `sd0.vmdk` is renamed `sd0-000000.vmdk`, and you get new delta file `sd0.vmdk`. Delete delta ------------ > vmdk_delta.py delete sd0.vmdk Then `sd0.vmdk` is removed, and `sd0-000000.vmdk` is renamed `sd0.vmdk`. If `sd0.vmdk` is not delta file, this tool doesn't do anything. vmdk format ----------- see http://www.vmware.com/app/vmdk/?src=vmdk """ import sys import os import random import struct from ctypes import * char = c_char Bool = uint8 = c_byte uint16 = c_ushort uint32 = c_ulong SectorType = c_ulonglong # sector size is 512 bytes. class SparseExtentHeader(Structure): _pack_ = 1 _fields_ = [ ('magicNumber', uint32), # 0x564d444b ('version', uint32), # may be 1 ('flags', uint32), ('capacity', SectorType), ('grainSize', SectorType), ('descriptorOffset', SectorType), ('descriptorSize', SectorType), ('numGTEsPerGT', uint32), # number of entries in a grain table ('rgdOffset', SectorType), # redundant level 0 of metadata ('gdOffset', SectorType), # level 0 of metadata ('overHead', SectorType), # size of total vmdk hdr ('uncleanShutdown', Bool), ('singleEndLineChar', char), ('nonEndLineChar', char), ('doubleEndLineChar1', char), ('doubleEndLineChar2', char), ('compressAlgorithm', uint16), ('pad', uint8 * 433), ] def readstruct(st, fp): memmove(addressof(st), fp.read(sizeof(st)), sizeof(st)) def getinfo(path): fp = open(path, 'rb') hdr = SparseExtentHeader() readstruct(hdr, fp) assert hdr.magicNumber == 0x564d444b assert hdr.version == 1 fp.seek(hdr.descriptorOffset * 512) lines = fp.read(hdr.descriptorSize * 512).split('\0', 1)[0] descriptor = {}; section = '' for line in lines.splitlines(): line = line.strip() if line.startswith('# '): section = line[1:].strip() elif line: descriptor.setdefault(section, []).append(line) for section in ('Disk DescriptorFile', 'The Disk Data Base'): d = {} for line in descriptor[section]: if line.startswith('#'): continue k, v = line.split('=', 1) k = k.strip(); v = v.strip() d[k] = v descriptor[section] = d fp.seek(hdr.rgdOffset * 512) rgt0 = struct.unpack('<L', fp.read(4))[0] # Redundant grain table #0 fp.seek(hdr.rgdOffset * 512) rgd = [] while fp.tell() < rgt0 * 512: rgd.append(struct.unpack('<L', fp.read(4))[0]) fp.seek(hdr.gdOffset * 512) gt0 = struct.unpack('<L', fp.read(4))[0] # Grain table #0 fp.seek(hdr.gdOffset * 512) gd = [] while fp.tell() < gt0 * 512: gd.append(struct.unpack('<L', fp.read(4))[0]) fp.seek(0) overhead = fp.read(hdr.overHead * 512) return type('SparseExtent', (), { 'header': hdr, 'descriptor': descriptor, 'rgd': rgd, 'gd': gd, 'overhead': overhead, }) def mkchild(parentPath, childName): """only supported monolithicSparse""" e = getinfo(parentPath) assert e.header.descriptorOffset > 0 assert not e.header.uncleanShutdown fp = open(os.path.join(os.path.dirname(parentPath), childName), 'wb') fp.write(e.overhead) fp.seek(e.header.descriptorOffset * 512) fp.write('\0' * e.header.descriptorSize * 512) fp.seek(e.header.descriptorOffset * 512) fp.write('# Disk DescriptorFile\n') items = ( ('version', e.descriptor['Disk DescriptorFile']['version']), ('CID', '%.8x' % random.randint(0, 0xffffffff)), ('parentCID', e.descriptor['Disk DescriptorFile']['CID']), ('createType', e.descriptor['Disk DescriptorFile']['createType']), ('parentFileNameHint', '"%s"' % os.path.basename(parentPath).encode('string_escape')), ) for i in items: fp.write('%s=%s\n' % i) fp.write('\n') fp.write('# Extent description\n') fp.write('RW %d SPARSE "%s"\n' % ( e.header.capacity, os.path.basename(childName).encode('string_escape'), )) fp.write('\n') fp.write('# The Disk Data Base\n') fp.write('#DDB\n') fp.write('\n') fp.seek(e.rgd[0] * 512) fp.write('\0' * (e.header.gdOffset - e.rgd[0]) * 512) fp.seek(e.gd[0] * 512) fp.write('\0' * (e.header.overHead - e.gd[0]) * 512) def usage(): print 'Usage to make delta: %s make vmdk-path' % sys.argv[0] print 'Usage to delete delta: %s delete vmdk-path' % sys.argv[0] raise SystemExit(-1) if __name__ == '__main__': try: action = sys.argv[1] path = sys.argv[2] except LookupError: usage() ancestors = [path] while 1: e = getinfo(ancestors[0]) if 'parentFileNameHint' not in e.descriptor['Disk DescriptorFile']: break basename = e.descriptor['Disk DescriptorFile']['parentFileNameHint'] basename = basename[1:-1].decode('string_escape') ancestors.insert( 0, os.path.join(os.path.dirname(path), basename)) if action == 'make': i = os.path.splitext(ancestors[-1]) ancestors[-1] = '%s-%.6d%s' % (i[0], len(ancestors) - 1, i[1]) os.rename(path, ancestors[-1]) mkchild(ancestors[-1], os.path.basename(path)) elif action == 'delete': if len(ancestors) < 2: raise Exception('%r is not disk delta file.' % path) os.unlink(path) os.rename(ancestors[-2], path) else: usage()