summaryrefslogtreecommitdiffstats
path: root/src/main/java/org/openslx/virtualization/disk
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/openslx/virtualization/disk')
-rw-r--r--src/main/java/org/openslx/virtualization/disk/DiskImage.java253
-rw-r--r--src/main/java/org/openslx/virtualization/disk/DiskImageException.java25
-rw-r--r--src/main/java/org/openslx/virtualization/disk/DiskImageQcow2.java232
-rw-r--r--src/main/java/org/openslx/virtualization/disk/DiskImageUtils.java144
-rw-r--r--src/main/java/org/openslx/virtualization/disk/DiskImageVdi.java112
-rw-r--r--src/main/java/org/openslx/virtualization/disk/DiskImageVmdk.java282
6 files changed, 1048 insertions, 0 deletions
diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImage.java b/src/main/java/org/openslx/virtualization/disk/DiskImage.java
new file mode 100644
index 0000000..dfb242a
--- /dev/null
+++ b/src/main/java/org/openslx/virtualization/disk/DiskImage.java
@@ -0,0 +1,253 @@
+package org.openslx.virtualization.disk;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.List;
+import java.util.function.Predicate;
+
+import org.openslx.bwlp.thrift.iface.Virtualizer;
+import org.openslx.thrifthelper.TConst;
+import org.openslx.util.Util;
+import org.openslx.virtualization.Version;
+
+/**
+ * Disk image for virtual machines.
+ *
+ * @implNote This class is the abstract base class to implement various specific disk images (like
+ * QCOW2 or VMDK).
+ *
+ * @author Manuel Bentele
+ * @version 1.0
+ */
+public abstract class DiskImage implements Closeable
+{
+ /**
+ * Stores the image file of the disk.
+ */
+ private RandomAccessFile diskImage = null;
+
+ /**
+ * Creates a new disk image from an existing image file with a known disk image format.
+ *
+ * @param diskImage file to a disk storing the image content.
+ *
+ * @implNote Do not use this constructor to create a new disk image from an image file with
+ * unknown disk image format. Instead, use the factory method
+ * {@link #newInstance(File)} to probe unknown disk image files before creation.
+ */
+ protected DiskImage( RandomAccessFile diskImage )
+ {
+ this.diskImage = diskImage;
+ }
+
+ /**
+ * Returns the disk image file.
+ *
+ * @return the disk image file.
+ */
+ protected RandomAccessFile getDiskImage()
+ {
+ return this.diskImage;
+ }
+
+ /**
+ * Checks whether disk image is standalone and do not depend on other files (e.g. snapshot
+ * files).
+ *
+ * @return state whether disk image is standalone or not.
+ *
+ * @throws DiskImageException unable to check if disk image is standalone.
+ */
+ public abstract boolean isStandalone() throws DiskImageException;
+
+ /**
+ * Checks whether disk image is compressed.
+ *
+ * @return state whether disk image is compressed or not.
+ *
+ * @throws DiskImageException unable to check whether disk image is compressed.
+ */
+ public abstract boolean isCompressed() throws DiskImageException;
+
+ /**
+ * Checks whether disk image is a snapshot.
+ *
+ * @return state whether disk image is a snapshot or not.
+ *
+ * @throws DiskImageException unable to check whether disk image is a snapshot.
+ */
+ public abstract boolean isSnapshot() throws DiskImageException;
+
+ /**
+ * Returns the version of the disk image format.
+ *
+ * @return version of the disk image format.
+ *
+ * @throws DiskImageException unable to obtain version of the disk image format.
+ */
+ public abstract Version getVersion() throws DiskImageException;
+
+ /**
+ * Returns the disk image description.
+ *
+ * @return description of the disk image.
+ *
+ * @throws DiskImageException unable to obtain description of the disk image.
+ */
+ public abstract String getDescription() throws DiskImageException;
+
+ /**
+ * Returns the format of the disk image.
+ *
+ * @return format of the disk image.
+ */
+ public abstract ImageFormat getFormat();
+
+ /**
+ * Creates a new disk image from an existing image file with an unknown disk image format.
+ *
+ * @param diskImagePath file to a disk storing the image content.
+ * @return concrete disk image instance.
+ *
+ * @throws FileNotFoundException cannot find specified disk image file.
+ * @throws IOException cannot access the content of the disk image file.
+ * @throws DiskImageException disk image file has an invalid and unknown disk image format.
+ */
+ public static DiskImage newInstance( File diskImagePath )
+ throws FileNotFoundException, IOException, DiskImageException
+ {
+ // Make sure this doesn't escape the scope, in case instantiation fails - we can't know when the GC
+ // would come along and close this file, which is problematic on Windows (blocking rename/delete)
+ final RandomAccessFile fileHandle = new RandomAccessFile( diskImagePath, "r" );
+
+ try {
+ if ( DiskImageQcow2.probe( fileHandle ) ) {
+ return new DiskImageQcow2( fileHandle );
+ } else if ( DiskImageVdi.probe( fileHandle ) ) {
+ return new DiskImageVdi( fileHandle );
+ } else if ( DiskImageVmdk.probe( fileHandle ) ) {
+ return new DiskImageVmdk( fileHandle );
+ }
+ } catch ( Exception e ) {
+ Util.safeClose( fileHandle );
+ throw e;
+ }
+ Util.safeClose( fileHandle );
+ final String errorMsg = "File '" + diskImagePath.getAbsolutePath() + "' is not a valid disk image!";
+ throw new DiskImageException( errorMsg );
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ Util.safeClose( diskImage );
+ }
+
+ @Override
+ protected void finalize() throws Throwable
+ {
+ close();
+ }
+
+ /**
+ * Format of a disk image.
+ *
+ * @author Manuel Bentele
+ * @version 1.0
+ */
+ public enum ImageFormat
+ {
+ // @formatter:off
+ NONE ( "none" ),
+ QCOW2( "qcow2" ),
+ VDI ( "vdi" ),
+ VMDK ( "vmdk" );
+ // @formatter:on
+
+ /**
+ * Stores filename extension of the disk image format.
+ */
+ public final String extension;
+
+ /**
+ * Create new disk image format.
+ *
+ * @param extension filename extension of the disk image format.
+ */
+ ImageFormat( String extension )
+ {
+ this.extension = extension;
+ }
+
+ /**
+ * Returns filename extension of the disk image.
+ *
+ * @return filename extension of the disk image.
+ */
+ public String getExtension()
+ {
+ return this.extension;
+ }
+
+ /**
+ * Checks if the disk image format is supported by a virtualizer.
+ *
+ * @param supportedImageFormats list of supported disk image formats of a virtualizer.
+ * @return <code>true</code> if image type is supported by the virtualizer; otherwise
+ * <code>false</code>.
+ */
+ public boolean isSupportedbyVirtualizer( List<ImageFormat> supportedImageFormats )
+ {
+ Predicate<ImageFormat> matchDiskFormat = supportedImageFormat -> supportedImageFormat.toString()
+ .equalsIgnoreCase( this.toString() );
+ return supportedImageFormats.stream().anyMatch( matchDiskFormat );
+ }
+
+ /**
+ * Returns default (preferred) disk image format for the specified virtualizer.
+ *
+ * @param virt virtualizer for that the default disk image should be determined.
+ * @return default (preferred) disk image format.
+ */
+ public static ImageFormat defaultForVirtualizer( Virtualizer virt )
+ {
+ if ( virt == null ) {
+ return null;
+ } else {
+ return ImageFormat.defaultForVirtualizer( virt.virtId );
+ }
+ }
+
+ /**
+ * Returns default (preferred) disk image format for the specified virtualizer.
+ *
+ * @param virtId ID of a virtualizer for that the default disk image should be determined.
+ * @return default (preferred) disk image format.
+ */
+ public static ImageFormat defaultForVirtualizer( String virtId )
+ {
+ ImageFormat imgFormat = null;
+
+ if ( TConst.VIRT_DOCKER.equals( virtId ) ) {
+ imgFormat = NONE;
+ } else if ( TConst.VIRT_QEMU.equals( virtId ) ) {
+ imgFormat = QCOW2;
+ } else if ( TConst.VIRT_VIRTUALBOX.equals( virtId ) ) {
+ imgFormat = VDI;
+ } else if ( TConst.VIRT_VMWARE.equals( virtId ) ) {
+ imgFormat = VMDK;
+ }
+
+ return imgFormat;
+ }
+
+ @Override
+ public String toString()
+ {
+ return this.getExtension();
+ }
+ }
+}
diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImageException.java b/src/main/java/org/openslx/virtualization/disk/DiskImageException.java
new file mode 100644
index 0000000..db62917
--- /dev/null
+++ b/src/main/java/org/openslx/virtualization/disk/DiskImageException.java
@@ -0,0 +1,25 @@
+package org.openslx.virtualization.disk;
+
+/**
+ * An exception for faulty disk image handling.
+ *
+ * @author Manuel Bentele
+ * @version 1.0
+ */
+public class DiskImageException extends Exception
+{
+ /**
+ * Version number for serialization.
+ */
+ private static final long serialVersionUID = 5464286488698331909L;
+
+ /**
+ * Creates a disk image exception including an error message.
+ *
+ * @param errorMsg message to describe a disk image error.
+ */
+ public DiskImageException( String errorMsg )
+ {
+ super( errorMsg );
+ }
+}
diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImageQcow2.java b/src/main/java/org/openslx/virtualization/disk/DiskImageQcow2.java
new file mode 100644
index 0000000..a6b3b73
--- /dev/null
+++ b/src/main/java/org/openslx/virtualization/disk/DiskImageQcow2.java
@@ -0,0 +1,232 @@
+package org.openslx.virtualization.disk;
+
+import java.io.RandomAccessFile;
+
+import org.openslx.virtualization.Version;
+
+/**
+ * QCOW2 disk image for virtual machines.
+ *
+ * A QCOW2 disk image consists of a header, one L1 table and several L2 tables used for lookup data
+ * clusters in the file via a two-level lookup. The QCOW2 header contains the following fields:
+ *
+ * <pre>
+ * QCOW2 (version 2 and 3) header format:
+ *
+ * magic (4 byte)
+ * version (4 byte)
+ * backing_file_offset (8 byte)
+ * backing_file_size (4 byte)
+ * cluster_bits (4 byte)
+ * size (8 byte)
+ * crypt_method (4 byte)
+ * l1_size (4 byte)
+ * l1_table_offset (8 byte)
+ * refcount_table_offset (8 byte)
+ * refcount_table_clusters (4 byte)
+ * nb_snapshots (4 byte)
+ * snapshots_offset (8 byte)
+ * incompatible_features (8 byte) [*]
+ * compatible_features (8 byte) [*]
+ * autoclear_features (8 byte) [*]
+ * refcount_order (8 byte) [*]
+ * header_length (4 byte) [*]
+ *
+ * [*] these fields are only available in the QCOW2 version 3 header format
+ * </pre>
+ *
+ * @author Manuel Bentele
+ * @version 1.0
+ */
+public class DiskImageQcow2 extends DiskImage
+{
+ /**
+ * Big endian representation of the big endian QCOW2 magic bytes <code>QFI\xFB</code>.
+ */
+ private static final int QCOW2_MAGIC = 0x514649fb;
+
+ /**
+ * Creates a new QCOW2 disk image from an existing QCOW2 image file.
+ *
+ * @param diskImage file to a QCOW2 disk storing the image content.
+ */
+ DiskImageQcow2( RandomAccessFile diskImage )
+ {
+ super( diskImage );
+ }
+
+ /**
+ * Probe specified file with unknown format to be a QCOW2 disk image file.
+ *
+ * @param diskImage file with unknown format that should be probed.
+ * @return state whether file is a QCOW2 disk image or not.
+ *
+ * @throws DiskImageException cannot probe specified file with unknown format.
+ */
+ public static boolean probe( RandomAccessFile diskImage ) throws DiskImageException
+ {
+ final boolean isQcow2ImageFormat;
+
+ // goto the beginning of the disk image to read the disk image
+ final int diskImageMagic = DiskImageUtils.readInt( diskImage, 0 );
+
+ // check if disk image's magic bytes can be found
+ if ( diskImageMagic == DiskImageQcow2.QCOW2_MAGIC ) {
+ isQcow2ImageFormat = true;
+ } else {
+ isQcow2ImageFormat = false;
+ }
+
+ return isQcow2ImageFormat;
+ }
+
+ @Override
+ public boolean isStandalone() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+ final long qcowBackingFileOffset = DiskImageUtils.readLong( diskFile, 8 );
+ final boolean qcowStandalone;
+
+ // check if QCOW2 image does not refer to any backing file
+ if ( qcowBackingFileOffset == 0 ) {
+ qcowStandalone = true;
+ } else {
+ qcowStandalone = false;
+ }
+
+ return qcowStandalone;
+ }
+
+ @Override
+ public boolean isCompressed() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+ final boolean qcowUseExtendedL2;
+ boolean qcowCompressed = false;
+
+ // check if QCOW2 image uses extended L2 tables
+ // extended L2 tables are only possible in QCOW2 version 3 header format
+ if ( this.getVersion().getMajor() >= Short.valueOf( "3" ) ) {
+ // read incompatible feature bits
+ final long qcowIncompatibleFeatures = DiskImageUtils.readLong( diskFile, 72 );
+
+ // support for extended L2 tables is enabled if bit 4 is set
+ qcowUseExtendedL2 = ( ( ( qcowIncompatibleFeatures & 0x000000000010 ) >>> 4 ) == 1 );
+ } else {
+ qcowUseExtendedL2 = false;
+ }
+
+ // get number of entries in L1 table
+ final int qcowL1TableSize = DiskImageUtils.readInt( diskFile, 36 );
+
+ // check if a valid L1 table is present
+ if ( qcowL1TableSize > 0 ) {
+ // QCOW2 image contains active L1 table with more than 0 entries: l1_size > 0
+ // search for first L2 table and its first entry to get compression bit
+
+ // get cluster bits to calculate the cluster size
+ final int qcowClusterBits = DiskImageUtils.readInt( diskFile, 20 );
+ final int qcowClusterSize = ( 1 << qcowClusterBits );
+
+ // entries of a L1 table have always the size of 8 byte (64 bit)
+ final int qcowL1TableEntrySize = 8;
+
+ // entries of a L2 table have either the size of 8 or 16 byte (64 or 128 bit)
+ final int qcowL2TableEntrySize = ( qcowUseExtendedL2 ) ? 16 : 8;
+
+ // calculate number of L2 table entries
+ final int qcowL2TableSize = qcowClusterSize / qcowL2TableEntrySize;
+
+ // get offset of L1 table
+ final long qcowL1TableOffset = DiskImageUtils.readLong( diskFile, 40 );
+
+ // check for each L2 table referenced from an L1 table its entries
+ // until a compressed cluster descriptor is found
+ for ( long i = 0; i < qcowL1TableSize; i++ ) {
+ // get offset of current L2 table from the current L1 table entry
+ final long qcowL1TableEntryOffset = qcowL1TableOffset + ( i * qcowL1TableEntrySize );
+ final long qcowL1TableEntry = DiskImageUtils.readLong( diskFile, qcowL1TableEntryOffset );
+
+ // extract offset (bits 9 - 55) from L1 table entry
+ final long qcowL2TableOffset = ( qcowL1TableEntry & 0x00fffffffffffe00L );
+
+ if ( qcowL2TableOffset == 0 ) {
+ // L2 table and all clusters described by this L2 table are unallocated
+ continue;
+ }
+
+ // get each L2 table entry and check if it is a compressed cluster descriptor
+ for ( long j = 0; j < qcowL2TableSize; j++ ) {
+ // get current L2 table entry
+ final long qcowL2TableEntryOffset = qcowL2TableOffset + ( j * qcowL2TableEntrySize );
+ final long qcowL2TableEntry = DiskImageUtils.readLong( diskFile, qcowL2TableEntryOffset );
+
+ // extract cluster type (standard or compressed) (bit 62)
+ boolean qcowClusterCompressed = ( ( ( qcowL2TableEntry & 0x4000000000000000L ) >>> 62 ) == 1 );
+
+ // check if QCOW2 disk image contains at least one compressed cluster descriptor
+ if ( qcowClusterCompressed ) {
+ qcowCompressed = true;
+ break;
+ }
+ }
+
+ // terminate if one compressed cluster descriptor is already found
+ if ( qcowCompressed ) {
+ break;
+ }
+ }
+ } else {
+ // QCOW2 image does not contain an active L1 table with any entry: l1_size = 0
+ qcowCompressed = false;
+ }
+
+ return qcowCompressed;
+ }
+
+ @Override
+ public boolean isSnapshot() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+ final int qcowNumSnapshots = DiskImageUtils.readInt( diskFile, 56 );
+ final boolean qcowSnapshot;
+
+ // check if QCOW2 image contains at least one snapshot
+ if ( qcowNumSnapshots == 0 ) {
+ qcowSnapshot = true;
+ } else {
+ qcowSnapshot = false;
+ }
+
+ return qcowSnapshot;
+ }
+
+ @Override
+ public Version getVersion() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+ final int qcowVersion = DiskImageUtils.readInt( diskFile, 4 );
+
+ // check QCOW2 file format version
+ if ( qcowVersion < 2 || qcowVersion > 3 ) {
+ // QCOW2 disk image does not contain a valid QCOW2 version
+ final String errorMsg = "Invalid QCOW2 version in header found!";
+ throw new DiskImageException( errorMsg );
+ }
+
+ return new Version( Integer.valueOf( qcowVersion ).shortValue() );
+ }
+
+ @Override
+ public String getDescription() throws DiskImageException
+ {
+ // QCOW2 disk image format does not support any disk description
+ return null;
+ }
+
+ @Override
+ public ImageFormat getFormat()
+ {
+ return ImageFormat.QCOW2;
+ }
+}
diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImageUtils.java b/src/main/java/org/openslx/virtualization/disk/DiskImageUtils.java
new file mode 100644
index 0000000..6bcef7f
--- /dev/null
+++ b/src/main/java/org/openslx/virtualization/disk/DiskImageUtils.java
@@ -0,0 +1,144 @@
+package org.openslx.virtualization.disk;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * Utilities to parse disk image format elements and control versions of disk images.
+ *
+ * @author Manuel Bentele
+ * @version 1.0
+ */
+public class DiskImageUtils
+{
+ /**
+ * Returns the size of a specified disk image file.
+ *
+ * @param diskImage file to a disk storing the image content.
+ * @return size of the disk image file in bytes.
+ *
+ * @throws DiskImageException unable to obtain the size of the disk image file.
+ */
+ public static long getImageSize( RandomAccessFile diskImage ) throws DiskImageException
+ {
+ final long imageSize;
+
+ try {
+ imageSize = diskImage.length();
+ } catch ( IOException e ) {
+ throw new DiskImageException( e.getLocalizedMessage() );
+ }
+
+ return imageSize;
+ }
+
+ /**
+ * Reads two bytes ({@link Short}) at a given <code>offset</code> from the specified disk image
+ * file.
+ *
+ * @param diskImage file to a disk storing the image content.
+ * @param offset offset in bytes for reading the two bytes.
+ * @return value of the two bytes from the disk image file as {@link Short}.
+ *
+ * @throws DiskImageException unable to read two bytes from the disk image file.
+ */
+ public static short readShort( RandomAccessFile diskImage, long offset ) throws DiskImageException
+ {
+ final long imageSize = DiskImageUtils.getImageSize( diskImage );
+ short value = 0;
+
+ if ( imageSize >= ( offset + Short.BYTES ) ) {
+ try {
+ diskImage.seek( offset );
+ value = diskImage.readShort();
+ } catch ( IOException e ) {
+ throw new DiskImageException( e.getLocalizedMessage() );
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * Reads four bytes ({@link Integer}) at a given <code>offset</code> from the specified disk
+ * image file.
+ *
+ * @param diskImage file to a disk storing the image content.
+ * @param offset offset in bytes for reading the four bytes.
+ * @return value of the four bytes from the disk image file as {@link Integer}.
+ *
+ * @throws DiskImageException unable to read four bytes from the disk image file.
+ */
+ public static int readInt( RandomAccessFile diskImage, long offset ) throws DiskImageException
+ {
+ final long imageSize = DiskImageUtils.getImageSize( diskImage );
+ int value = 0;
+
+ if ( imageSize >= ( offset + Integer.BYTES ) ) {
+ try {
+ diskImage.seek( offset );
+ value = diskImage.readInt();
+ } catch ( IOException e ) {
+ throw new DiskImageException( e.getLocalizedMessage() );
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * Reads eight bytes ({@link Long}) at a given <code>offset</code> from the specified disk image
+ * file.
+ *
+ * @param diskImage file to a disk storing the image content.
+ * @param offset offset in bytes for reading the eight bytes.
+ * @return value of the eight bytes from the disk image file as {@link Long}.
+ *
+ * @throws DiskImageException unable to read eight bytes from the disk image file.
+ */
+ public static long readLong( RandomAccessFile diskImage, long offset ) throws DiskImageException
+ {
+ final long imageSize = DiskImageUtils.getImageSize( diskImage );
+ long value = 0;
+
+ if ( imageSize >= ( offset + Long.BYTES ) ) {
+ try {
+ diskImage.seek( offset );
+ value = diskImage.readLong();
+ } catch ( IOException e ) {
+ throw new DiskImageException( e.getLocalizedMessage() );
+ }
+ }
+
+ return value;
+ }
+
+ /**
+ * Reads a variable number of bytes (<code>numBytes</code>) at a given <code>offset</code> from the specified disk image file.
+ *
+ * @param diskImage file to a disk storing the image content.
+ * @param offset offset in bytes for reading <code>numBytes</code> bytes.
+ * @param numBytes number of bytes to read at <code>offset</code>.
+ * @return read bytes from the disk image file as {@link byte[]}.
+ *
+ * @throws DiskImageException unable to read bytes from the disk image file.
+ */
+ public static byte[] readBytesAsArray( RandomAccessFile diskImage, long offset, int numBytes )
+ throws DiskImageException
+ {
+ final long imageSize = DiskImageUtils.getImageSize( diskImage );
+ byte values[] = {};
+
+ if ( imageSize >= ( offset + numBytes ) ) {
+ try {
+ diskImage.seek( offset );
+ values = new byte[ numBytes ];
+ diskImage.readFully( values );
+ } catch ( IOException e ) {
+ throw new DiskImageException( e.getLocalizedMessage() );
+ }
+ }
+
+ return values;
+ }
+}
diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImageVdi.java b/src/main/java/org/openslx/virtualization/disk/DiskImageVdi.java
new file mode 100644
index 0000000..1e5b20b
--- /dev/null
+++ b/src/main/java/org/openslx/virtualization/disk/DiskImageVdi.java
@@ -0,0 +1,112 @@
+package org.openslx.virtualization.disk;
+
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.openslx.virtualization.Version;
+
+/**
+ * VDI disk image for virtual machines.
+ *
+ * @author Manuel Bentele
+ * @version 1.0
+ */
+public class DiskImageVdi extends DiskImage
+{
+ /**
+ * Big endian representation of the little endian VDI magic bytes (signature).
+ */
+ private static final int VDI_MAGIC = 0x7f10dabe;
+ /**
+ * Just 16 null-bytes
+ */
+ private static final byte[] ZERO_UUID = new byte[16];
+
+ /**
+ * Creates a new VDI disk image from an existing VDI image file.
+ *
+ * @param diskImage file to a VDI disk storing the image content.
+ */
+ DiskImageVdi( RandomAccessFile diskImage )
+ {
+ super( diskImage );
+ }
+
+ /**
+ * Probe specified file with unknown format to be a VDI disk image file.
+ *
+ * @param diskImage file with unknown format that should be probed.
+ * @return state whether file is a VDI disk image or not.
+ *
+ * @throws DiskImageException cannot probe specified file with unknown format.
+ */
+ public static boolean probe( RandomAccessFile diskImage ) throws DiskImageException
+ {
+ final boolean isVdiImageFormat;
+
+ // goto the beginning of the disk image to read the magic bytes
+ // skip first 64 bytes (opening tag)
+ final int diskImageMagic = DiskImageUtils.readInt( diskImage, 64 );
+
+ // check if disk image's magic bytes can be found
+ if ( diskImageMagic == DiskImageVdi.VDI_MAGIC ) {
+ isVdiImageFormat = true;
+ } else {
+ isVdiImageFormat = false;
+ }
+
+ return isVdiImageFormat;
+ }
+
+ @Override
+ public boolean isStandalone() throws DiskImageException
+ {
+ // VDI does not seem to support split VDI files, so VDI files are always standalone
+ return true;
+ }
+
+ @Override
+ public boolean isCompressed() throws DiskImageException
+ {
+ // compression is done by sparsifying the disk files, there is no flag for it
+ return false;
+ }
+
+ @Override
+ public boolean isSnapshot() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+
+ // if parent UUID is set, the VDI file is a snapshot
+ byte[] parentUuid = DiskImageUtils.readBytesAsArray( diskFile, 440, 16 );
+
+ return !Arrays.equals( ZERO_UUID, parentUuid );
+ }
+
+ @Override
+ public Version getVersion() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+
+ final short vdiVersionMajor = Short.reverseBytes( DiskImageUtils.readShort( diskFile, 68 ) );
+ final short vdiVersionMinor = Short.reverseBytes( DiskImageUtils.readShort( diskFile, 70 ) );
+
+ return new Version( vdiVersionMajor, vdiVersionMinor );
+ }
+
+ @Override
+ public String getDescription() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+ byte[] data = DiskImageUtils.readBytesAsArray( diskFile, 84, 256 );
+ // This will replace invalid chars. Maybe use CharsetDecoder and fall back to latin1 on error
+ return new String( data, StandardCharsets.UTF_8 );
+ }
+
+ @Override
+ public ImageFormat getFormat()
+ {
+ return ImageFormat.VDI;
+ }
+}
diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImageVmdk.java b/src/main/java/org/openslx/virtualization/disk/DiskImageVmdk.java
new file mode 100644
index 0000000..77986ef
--- /dev/null
+++ b/src/main/java/org/openslx/virtualization/disk/DiskImageVmdk.java
@@ -0,0 +1,282 @@
+package org.openslx.virtualization.disk;
+
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+
+import org.openslx.util.Util;
+import org.openslx.virtualization.configuration.VirtualizationConfigurationVmwareFileFormat;
+import org.openslx.virtualization.Version;
+import org.openslx.virtualization.configuration.VirtualizationConfigurationException;
+
+/**
+ * VMDK (sparse extent) disk image for virtual machines.
+ *
+ * @author Manuel Bentele
+ * @version 1.0
+ */
+public class DiskImageVmdk extends DiskImage
+{
+ /**
+ * Big endian representation of the little endian magic bytes <code>KDMV</code>.
+ */
+ private static final int VMDK_MAGIC = 0x4b444d56;
+
+ /**
+ * Size of a VMDK disk image data cluster in bytes.
+ */
+ private static final int VMDK_SECTOR_SIZE = 512;
+
+ /**
+ * Default hardware version of a VMDK disk image.
+ */
+ private static final int VMDK_DEFAULT_HW_VERSION = 10;
+
+ /**
+ * Stores disk configuration if VMDK disk image contains an embedded descriptor file.
+ */
+ private final VirtualizationConfigurationVmwareFileFormat vmdkConfig;
+
+ /**
+ * Creates a new VMDK disk image from an existing VMDK image file.
+ *
+ * @param diskImage file to a VMDK disk storing the image content.
+ *
+ * @throws DiskImageException parsing of the VMDK's embedded descriptor file failed.
+ */
+ DiskImageVmdk( RandomAccessFile diskImage ) throws DiskImageException
+ {
+ super( diskImage );
+
+ this.vmdkConfig = this.parseVmdkConfig();
+ }
+
+ /**
+ * Probe specified file with unknown format to be a VMDK disk image file.
+ *
+ * @param diskImage file with unknown format that should be probed.
+ * @return state whether file is a VMDK disk image or not.
+ *
+ * @throws DiskImageException cannot probe specified file with unknown format.
+ */
+ public static boolean probe( RandomAccessFile diskImage ) throws DiskImageException
+ {
+ final boolean isVmdkImageFormat;
+
+ // goto the beginning of the disk image to read the magic bytes
+ final int diskImageMagic = DiskImageUtils.readInt( diskImage, 0 );
+
+ // check if disk image's magic bytes can be found
+ if ( diskImageMagic == DiskImageVmdk.VMDK_MAGIC ) {
+ isVmdkImageFormat = true;
+ } else {
+ isVmdkImageFormat = false;
+ }
+
+ return isVmdkImageFormat;
+ }
+
+ /**
+ * Returns the creation type from the VMDK's embedded descriptor file.
+ *
+ * @return creation type from the VMDK's embedded descriptor file.
+ */
+ private String getCreationType()
+ {
+ final VirtualizationConfigurationVmwareFileFormat vmdkConfig = this.getVmdkConfig();
+ final String vmdkCreationType;
+
+ if ( vmdkConfig == null ) {
+ // VMDK disk image does not contain any descriptor file
+ // assume that the file is not stand alone
+ vmdkCreationType = null;
+ } else {
+ // VMDK disk image contains a descriptor file
+ // get creation type from the content of the descriptor file
+ vmdkCreationType = this.vmdkConfig.get( "createType" );
+ }
+
+ return vmdkCreationType;
+ }
+
+ /**
+ * Parse the configuration of the VMDK's embedded descriptor file.
+ *
+ * @return parsed configuration of the VMDK's embedded descriptor file.
+ *
+ * @throws DiskImageException parsing of the VMDK's embedded descriptor file failed.
+ */
+ protected VirtualizationConfigurationVmwareFileFormat parseVmdkConfig() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+ final VirtualizationConfigurationVmwareFileFormat vmdkConfig;
+
+ // get offset and size of descriptor file embedded into the VMDK disk image
+ final long vmdkDescriptorSectorOffset = Long.reverseBytes( DiskImageUtils.readLong( diskFile, 28 ) );
+ final long vmdkDescriptorSectorSize = Long.reverseBytes( DiskImageUtils.readLong( diskFile, 36 ) );
+
+ if ( vmdkDescriptorSectorOffset > 0 ) {
+ // get content of descriptor file embedded into the VMDK disk image
+ final long vmdkDescriptorOffset = vmdkDescriptorSectorOffset * DiskImageVmdk.VMDK_SECTOR_SIZE;
+ final long vmdkDescriptorSizeMax = vmdkDescriptorSectorSize * DiskImageVmdk.VMDK_SECTOR_SIZE;
+ final String descriptorStr = new String ( DiskImageUtils.readBytesAsArray( diskFile, vmdkDescriptorOffset,
+ Long.valueOf( vmdkDescriptorSizeMax ).intValue() ), StandardCharsets.US_ASCII );
+
+ // get final length of the content within the sectors to be able to trim all 'zero' characters
+ final int vmdkDescriptorSize = descriptorStr.indexOf( 0 );
+
+ // if final length of the content is invalid, throw an exception
+ if ( vmdkDescriptorSize > vmdkDescriptorSizeMax || vmdkDescriptorSize < 0 ) {
+ final String errorMsg = "Embedded descriptor size in VMDK disk image is invalid!";
+ throw new DiskImageException( errorMsg );
+ }
+
+ // trim all 'zero' characters at the end of the descriptor content to avoid errors during parsing
+ final String configStr = descriptorStr.substring( 0, vmdkDescriptorSize );
+
+ // create configuration instance from content of the descriptor file
+ try {
+ vmdkConfig = new VirtualizationConfigurationVmwareFileFormat( configStr.getBytes(), vmdkDescriptorSize );
+ } catch ( VirtualizationConfigurationException e ) {
+ throw new DiskImageException( e.getLocalizedMessage() );
+ }
+ } else {
+ // there is no descriptor file embedded into the VMDK disk image
+ vmdkConfig = null;
+ }
+
+ return vmdkConfig;
+ }
+
+ /**
+ * Returns parsed configuration of the VMDK's embedded descriptor file.
+ *
+ * @return parsed configuration of the VMDK's embedded descriptor file.
+ */
+ protected VirtualizationConfigurationVmwareFileFormat getVmdkConfig()
+ {
+ return this.vmdkConfig;
+ }
+
+ /**
+ * Returns the hardware version from the VMDK's embedded descriptor file.
+ *
+ * If the VMDK's embedded descriptor file does not contain any hardware version configuration
+ * entry, the default hardware version (see {@link #VMDK_DEFAULT_HW_VERSION}) is returned.
+ *
+ * @return hardware version from the VMDK's embedded descriptor file.
+ *
+ * @throws DiskImageException unable to obtain the VMDK's hardware version of the disk image
+ * format.
+ */
+ public Version getHwVersion() throws DiskImageException
+ {
+ final VirtualizationConfigurationVmwareFileFormat vmdkConfig = this.getVmdkConfig();
+ final Version hwVersion;
+
+ if ( vmdkConfig != null ) {
+ // VMDK image contains a hardware version, so return parsed hardware version
+ // if hardware version cannot be parsed, return default hardware version
+ final String hwVersionStr = vmdkConfig.get( "ddb.virtualHWVersion" );
+
+ final int hwVersionMajor = Util.parseInt( hwVersionStr, DiskImageVmdk.VMDK_DEFAULT_HW_VERSION );
+ hwVersion = new Version( Integer.valueOf( hwVersionMajor ).shortValue() );
+ } else {
+ // VMDK image does not contain any hardware version, so return default hardware version
+ final int hwVersionMajor = DiskImageVmdk.VMDK_DEFAULT_HW_VERSION;
+ hwVersion = new Version( Integer.valueOf( hwVersionMajor ).shortValue() );
+ }
+
+ return hwVersion;
+ }
+
+ @Override
+ public boolean isStandalone() throws DiskImageException
+ {
+ final String vmdkCreationType = this.getCreationType();
+ final boolean vmdkStandalone;
+
+ if ( vmdkCreationType != null ) {
+ // creation type is defined, so check if VMDK disk image is a snapshot
+ if ( this.isSnapshot() ) {
+ // VMDK disk image is a snapshot and not stand alone
+ vmdkStandalone = false;
+ } else {
+ // VMDK disk image is not a snapshot
+ // determine stand alone disk image property
+ vmdkStandalone = vmdkCreationType.equalsIgnoreCase( "streamOptimized" ) ||
+ vmdkCreationType.equalsIgnoreCase( "monolithicSparse" );
+ }
+ } else {
+ // creation type is not defined
+ // assume that the file is not stand alone
+ vmdkStandalone = false;
+ }
+
+ return vmdkStandalone;
+ }
+
+ @Override
+ public boolean isCompressed() throws DiskImageException
+ {
+ final String vmdkCreationType = this.getCreationType();
+ final boolean vmdkCompressed;
+
+ if ( vmdkCreationType != null && vmdkCreationType.equalsIgnoreCase( "streamOptimized" ) ) {
+ // creation type is defined, and VMDK disk image is compressed
+ vmdkCompressed = true;
+ } else {
+ // creation type for compression is not defined
+ // assume that the file is not compressed
+ vmdkCompressed = false;
+ }
+
+ return vmdkCompressed;
+ }
+
+ @Override
+ public boolean isSnapshot() throws DiskImageException
+ {
+ final VirtualizationConfigurationVmwareFileFormat vmdkConfig = this.getVmdkConfig();
+ final boolean vmdkSnapshot;
+
+ if ( vmdkConfig == null ) {
+ // VMDK disk image does not contain any descriptor file
+ // assume that the file is not a snapshot
+ vmdkSnapshot = false;
+ } else {
+ // get parent CID to determine snapshot disk image property
+ final String parentCid = vmdkConfig.get( "parentCID" );
+
+ if ( parentCid != null && !parentCid.equalsIgnoreCase( "ffffffff" ) ) {
+ // link to parent content identifier is defined, so VMDK disk image is a snapshot
+ vmdkSnapshot = true;
+ } else {
+ // link to parent content identifier is not defined, so VMDK disk image is not a snapshot
+ vmdkSnapshot = false;
+ }
+ }
+
+ return vmdkSnapshot;
+ }
+
+ @Override
+ public Version getVersion() throws DiskImageException
+ {
+ final RandomAccessFile diskFile = this.getDiskImage();
+ final int vmdkVersion = Integer.reverseBytes( DiskImageUtils.readInt( diskFile, 4 ) );
+
+ return new Version( Integer.valueOf( vmdkVersion ).shortValue() );
+ }
+
+ @Override
+ public String getDescription() throws DiskImageException
+ {
+ return null;
+ }
+
+ @Override
+ public ImageFormat getFormat()
+ {
+ return ImageFormat.VMDK;
+ }
+}