diff options
author | Manuel Bentele | 2021-02-25 15:00:38 +0100 |
---|---|---|
committer | Manuel Bentele | 2021-03-10 15:05:56 +0100 |
commit | be40e979e03e41ddcd831d9c330902f76908ca64 (patch) | |
tree | 0b5d66d2e01bfb7b96c76170db788b5f36fd8b2d | |
parent | [vmware] Stop creating 'null.present = "TRUE"' entries (diff) | |
download | master-sync-shared-be40e979e03e41ddcd831d9c330902f76908ca64.tar.gz master-sync-shared-be40e979e03e41ddcd831d9c330902f76908ca64.tar.xz master-sync-shared-be40e979e03e41ddcd831d9c330902f76908ca64.zip |
Refactor disk image representation and add unit tests
36 files changed, 1608 insertions, 670 deletions
diff --git a/src/main/java/org/openslx/util/ThriftUtil.java b/src/main/java/org/openslx/util/ThriftUtil.java index 327f2d3..3c2c9ea 100644 --- a/src/main/java/org/openslx/util/ThriftUtil.java +++ b/src/main/java/org/openslx/util/ThriftUtil.java @@ -7,7 +7,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; -import org.openslx.util.vm.VmwareConfig; +import org.openslx.vm.VmwareConfig; public class ThriftUtil { diff --git a/src/main/java/org/openslx/util/vm/DiskImage.java b/src/main/java/org/openslx/util/vm/DiskImage.java deleted file mode 100644 index 617fadd..0000000 --- a/src/main/java/org/openslx/util/vm/DiskImage.java +++ /dev/null @@ -1,379 +0,0 @@ -package org.openslx.util.vm; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.Arrays; -import java.util.List; -import java.util.function.Predicate; - -import org.apache.log4j.Logger; -import org.openslx.bwlp.thrift.iface.Virtualizer; -import org.openslx.thrifthelper.TConst; -import org.openslx.util.Util; - -public class DiskImage -{ - private static final Logger LOGGER = Logger.getLogger( DiskImage.class ); - /** - * Big endian representation of the 4 bytes 'KDMV' - */ - private static final int VMDK_MAGIC = 0x4b444d56; - private static final int VDI_MAGIC = 0x7f10dabe; - /** - * Big endian representation of the 4 bytes 'QFI\xFB' - */ - private static final int QEMU_MAGIC = 0x514649fb; - - public enum ImageFormat - { - VMDK( "vmdk" ), QCOW2( "qcow2" ), VDI( "vdi" ), DOCKER( "dockerfile" ); - - public final String extension; - - private ImageFormat( String extension ) - { - this.extension = extension; - } - - public static ImageFormat defaultForVirtualizer( Virtualizer virt ) - { - if ( virt == null ) - return null; - return defaultForVirtualizer( virt.virtId ); - } - - public static ImageFormat defaultForVirtualizer( String virtId ) - { - if ( virtId == null ) - return null; - if ( virtId.equals( TConst.VIRT_VMWARE ) ) - return VMDK; - if ( virtId.equals( TConst.VIRT_VIRTUALBOX ) ) - return VDI; - if ( virtId.equals( TConst.VIRT_QEMU ) ) - return QCOW2; - if ( virtId.equals( TConst.VIRT_DOCKER ) ) - return DOCKER; - return null; - } - } - - public final boolean isStandalone; - public final boolean isCompressed; - public final boolean isSnapshot; - public final ImageFormat format; - public final String subFormat; - public final int hwVersion; - public final String diskDescription; - - public ImageFormat getImageFormat() - { - return format; - } - - public DiskImage( File disk ) throws FileNotFoundException, IOException, UnknownImageFormatException - { - LOGGER.debug( "Validating disk file: " + disk.getAbsolutePath() ); - try ( RandomAccessFile file = new RandomAccessFile( disk, "r" ) ) { - // vmdk - boolean isVmdkMagic = ( file.readInt() == VMDK_MAGIC ); - if ( isVmdkMagic || file.length() < 4096 ) { - if ( isVmdkMagic ) { - file.seek( 512 ); - } else { - file.seek( 0 ); - } - byte[] buffer = new byte[ (int)Math.min( 2048, file.length() ) ]; - file.readFully( buffer ); - VmwareConfig config; - try { - config = new VmwareConfig( buffer, findNull( buffer ) ); - } catch ( UnsupportedVirtualizerFormatException e ) { - config = null; - } - if ( config != null ) { - String sf = config.get( "createType" ); - String parent = config.get( "parentCID" ); - if ( sf != null || parent != null ) { - subFormat = sf; - this.isStandalone = isStandaloneCreateType( subFormat, parent ); - this.isCompressed = subFormat != null && subFormat.equalsIgnoreCase( "streamOptimized" ); - this.isSnapshot = parent != null && !parent.equalsIgnoreCase( "ffffffff" ); - this.format = ImageFormat.VMDK; - String hwv = config.get( "ddb.virtualHWVersion" ); - if ( hwv == null ) { - this.hwVersion = 10; - } else { - this.hwVersion = Util.parseInt( hwv, 10 ); - } - this.diskDescription = null; - return; - } - } - } - // Format spec from: https://forums.virtualbox.org/viewtopic.php?t=8046 - // First 64 bytes are the opening tag: <<< .... >>> - // which we don't care about, then comes the VDI signature - file.seek( 64 ); - if ( file.readInt() == VDI_MAGIC ) { - // skip the next 4 ints as they don't interest us: - // - VDI version - // - size of header, strangely irrelevant? - // - image type, 1 for dynamic allocated storage, 2 for fixed size - // - image flags, seem to be always 00 00 00 00 - file.skipBytes( 4 * 4 ); - - // next 256 bytes are image description - byte[] imageDesc = new byte[ 256 ]; - file.readFully( imageDesc ); - // next sections are irrelevant (int if not specified): - // - offset blocks - // - offset data - // - cylinders - // - heads - // - sectors - // - sector size - // - <unused> - // - disk size (long = 8 bytes) - // - block size - // - block extra data - // - blocks in hdd - // - blocks allocated - file.skipBytes( 4 * 13 ); - - // now it gets interesting, UUID of VDI - byte[] diskUuid = new byte[ 16 ]; - file.readFully( diskUuid ); - // last snapshot uuid, mostly uninteresting since we don't support snapshots -> skip 16 bytes - // TODO: meaning of "uuid link"? for now, skip 16 - file.skipBytes( 32 ); - - // parent uuid, indicator if this VDI is a snapshot or not - byte[] parentUuid = new byte[ 16 ]; - file.readFully( parentUuid ); - byte[] zeroUuid = new byte[ 16 ]; - Arrays.fill( zeroUuid, (byte)0 ); - this.isSnapshot = !Arrays.equals( parentUuid, zeroUuid ); - // VDI does not seem to support split VDI files so always standalone - this.isStandalone = true; - // compression is done by sparsifying the disk files, there is no flag for it - this.isCompressed = false; - this.format = ImageFormat.VDI; - this.subFormat = null; - this.diskDescription = new String( imageDesc ); - this.hwVersion = 0; - return; - } - - // qcow2 disk image - file.seek( 0 ); - if ( file.readInt() == QEMU_MAGIC ) { - // - // 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 - // - - // - // check qcow2 file format version - // - file.seek( 4 ); - final int qcowVersion = file.readInt(); - if ( qcowVersion < 2 || qcowVersion > 3 ) { - // disk image format is not a qcow2 disk format - throw new UnknownImageFormatException(); - } else { - // disk image format is a valid qcow2 disk format - this.hwVersion = qcowVersion; - this.subFormat = null; - } - - // - // check if qcow2 image does not refer to any backing file - // - file.seek( 8 ); - this.isStandalone = ( file.readLong() == 0 ) ? true : false; - - // - // check if qcow2 image does not contain any snapshot - // - file.seek( 56 ); - this.isSnapshot = ( file.readInt() == 0 ) ? true : false; - - // - // check if qcow2 image uses extended L2 tables - // - boolean qcowUseExtendedL2 = false; - - // extended L2 tables are only possible in qcow2 version 3 header format - if ( qcowVersion == 3 ) { - // read incompatible feature bits - file.seek( 72 ); - final long qcowIncompatibleFeatures = file.readLong(); - - // support for extended L2 tables is enabled if bit 4 is set - qcowUseExtendedL2 = ( ( ( qcowIncompatibleFeatures & 0x000000000010 ) >>> 4 ) == 1 ); - } - - // - // check if qcow2 image contains compressed clusters - // - boolean qcowCompressed = false; - - // get number of entries in L1 table - file.seek( 36 ); - final int qcowL1TableSize = file.readInt(); - - // 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 - file.seek( 20 ); - final int qcowClusterBits = file.readInt(); - 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 - file.seek( 40 ); - long qcowL1TableOffset = file.readLong(); - - // 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 - long qcowL1TableEntryOffset = qcowL1TableOffset + ( i * qcowL1TableEntrySize ); - file.seek( qcowL1TableEntryOffset ); - long qcowL1TableEntry = file.readLong(); - - // extract offset (bits 9 - 55) from L1 table entry - 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 - long qcowL2TableEntryOffset = qcowL2TableOffset + ( j * qcowL2TableEntrySize ); - file.seek( qcowL2TableEntryOffset ); - long qcowL2TableEntry = file.readLong(); - - // 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; - } - - this.isCompressed = qcowCompressed; - this.format = ImageFormat.QCOW2; - this.diskDescription = null; - - return; - } - } - throw new UnknownImageFormatException(); - } - - /** - * Creates new disk image and checks if image is supported by hypervisor's image formats. - * - * @param disk file to a disk storing the virtual machine content. - * @param supportedImageTypes list of supported image types of a hypervisor's image format. - * @throws FileNotFoundException cannot find virtual machine image file. - * @throws IOException cannot access the virtual machine image file. - * @throws UnknownImageFormatException virtual machine image file has an unknown image format. - */ - public DiskImage( File disk, List<ImageFormat> supportedImageTypes ) - throws FileNotFoundException, IOException, UnknownImageFormatException - { - this( disk ); - - if ( !this.isSupportedByHypervisor( supportedImageTypes ) ) { - throw new UnknownImageFormatException(); - } - } - - /** - * Checks if image format is supported by a list of supported hypervisor image formats. - * - * @param supportedImageTypes list of supported image types of a hypervisor. - * @return <code>true</code> if image type is supported by the hypervisor; otherwise - * <code>false</code>. - */ - public boolean isSupportedByHypervisor( List<ImageFormat> supportedImageTypes ) - { - Predicate<ImageFormat> matchDiskFormat = supportedImageType -> supportedImageType.toString() - .equalsIgnoreCase( this.getImageFormat().toString() ); - return supportedImageTypes.stream().anyMatch( matchDiskFormat ); - } - - private int findNull( byte[] buffer ) - { - for ( int i = 0; i < buffer.length; ++i ) { - if ( buffer[i] == 0 ) - return i; - } - return buffer.length; - } - - private boolean isStandaloneCreateType( String type, String parent ) - { - if ( type == null ) - return false; - if ( parent != null && !parent.equalsIgnoreCase( "ffffffff" ) ) - return false; - return type.equalsIgnoreCase( "streamOptimized" ) || type.equalsIgnoreCase( "monolithicSparse" ); - } - - public static class UnknownImageFormatException extends Exception - { - private static final long serialVersionUID = -6647935235475007171L; - } -} diff --git a/src/main/java/org/openslx/util/vm/DockerMetaDataDummy.java b/src/main/java/org/openslx/vm/DockerMetaDataDummy.java index 38388ce..73cd92e 100644 --- a/src/main/java/org/openslx/util/vm/DockerMetaDataDummy.java +++ b/src/main/java/org/openslx/vm/DockerMetaDataDummy.java @@ -1,11 +1,12 @@ -package org.openslx.util.vm; +package org.openslx.vm; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.openslx.bwlp.thrift.iface.OperatingSystem; import org.openslx.bwlp.thrift.iface.Virtualizer; import org.openslx.thrifthelper.TConst; -import org.openslx.util.vm.DiskImage.ImageFormat; +import org.openslx.vm.disk.DiskImage; +import org.openslx.vm.disk.DiskImage.ImageFormat; import java.io.BufferedInputStream; import java.io.File; @@ -41,7 +42,7 @@ public class DockerMetaDataDummy extends VmMetaData<DockerSoundCardMeta, DockerD * List of supported image formats by the Docker hypervisor. */ private static final List<DiskImage.ImageFormat> SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableList( - Arrays.asList( ImageFormat.DOCKER ) ); + Arrays.asList( ImageFormat.NONE ) ); private static final Logger LOGGER = Logger.getLogger( DockerMetaDataDummy.class); diff --git a/src/main/java/org/openslx/util/vm/KeyValuePair.java b/src/main/java/org/openslx/vm/KeyValuePair.java index d89d51b..c5650ec 100644 --- a/src/main/java/org/openslx/util/vm/KeyValuePair.java +++ b/src/main/java/org/openslx/vm/KeyValuePair.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; class KeyValuePair { diff --git a/src/main/java/org/openslx/util/vm/QemuMetaData.java b/src/main/java/org/openslx/vm/QemuMetaData.java index d3b8451..c780429 100644 --- a/src/main/java/org/openslx/util/vm/QemuMetaData.java +++ b/src/main/java/org/openslx/vm/QemuMetaData.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; import java.io.File; import java.math.BigInteger; @@ -27,7 +27,8 @@ import org.openslx.libvirt.xml.LibvirtXmlDocumentException; import org.openslx.libvirt.xml.LibvirtXmlSerializationException; import org.openslx.libvirt.xml.LibvirtXmlValidationException; import org.openslx.thrifthelper.TConst; -import org.openslx.util.vm.DiskImage.ImageFormat; +import org.openslx.vm.disk.DiskImage; +import org.openslx.vm.disk.DiskImage.ImageFormat; /** * Metadata to describe the hardware type of a QEMU sound card. diff --git a/src/main/java/org/openslx/util/vm/QemuMetaDataUtils.java b/src/main/java/org/openslx/vm/QemuMetaDataUtils.java index 42c3fb6..b6ac92e 100644 --- a/src/main/java/org/openslx/util/vm/QemuMetaDataUtils.java +++ b/src/main/java/org/openslx/vm/QemuMetaDataUtils.java @@ -1,14 +1,14 @@ -package org.openslx.util.vm; +package org.openslx.vm; import java.util.ArrayList; import org.openslx.libvirt.domain.device.Disk; import org.openslx.libvirt.domain.device.Interface; import org.openslx.libvirt.domain.device.Disk.BusType; +import org.openslx.vm.VmMetaData.DriveBusType; +import org.openslx.vm.VmMetaData.EthernetDevType; +import org.openslx.vm.VmMetaData.SoundCardType; import org.openslx.libvirt.domain.device.Sound; -import org.openslx.util.vm.VmMetaData.DriveBusType; -import org.openslx.util.vm.VmMetaData.EthernetDevType; -import org.openslx.util.vm.VmMetaData.SoundCardType; /** * Collection of utils to convert data types from bwLehrpool to Libvirt and vice versa. diff --git a/src/main/java/org/openslx/util/vm/UnsupportedVirtualizerFormatException.java b/src/main/java/org/openslx/vm/UnsupportedVirtualizerFormatException.java index 08c9673..f19b1ff 100644 --- a/src/main/java/org/openslx/util/vm/UnsupportedVirtualizerFormatException.java +++ b/src/main/java/org/openslx/vm/UnsupportedVirtualizerFormatException.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; @SuppressWarnings( "serial" ) public class UnsupportedVirtualizerFormatException extends Exception diff --git a/src/main/java/org/openslx/util/vm/VboxConfig.java b/src/main/java/org/openslx/vm/VboxConfig.java index f405991..9724b6a 100644 --- a/src/main/java/org/openslx/util/vm/VboxConfig.java +++ b/src/main/java/org/openslx/vm/VboxConfig.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; import java.io.ByteArrayInputStream; import java.io.File; @@ -20,8 +20,8 @@ import javax.xml.xpath.XPathExpressionException; import org.apache.log4j.Logger; import org.openslx.util.Util; import org.openslx.util.XmlHelper; -import org.openslx.util.vm.VmMetaData.DriveBusType; -import org.openslx.util.vm.VmMetaData.HardDisk; +import org.openslx.vm.VmMetaData.DriveBusType; +import org.openslx.vm.VmMetaData.HardDisk; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; diff --git a/src/main/java/org/openslx/util/vm/VboxMetaData.java b/src/main/java/org/openslx/vm/VboxMetaData.java index 82936a7..a6ac14d 100644 --- a/src/main/java/org/openslx/util/vm/VboxMetaData.java +++ b/src/main/java/org/openslx/vm/VboxMetaData.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; import java.io.File; import java.io.IOException; @@ -15,8 +15,9 @@ import org.apache.log4j.Logger; import org.openslx.bwlp.thrift.iface.OperatingSystem; import org.openslx.bwlp.thrift.iface.Virtualizer; import org.openslx.thrifthelper.TConst; -import org.openslx.util.vm.DiskImage.ImageFormat; -import org.openslx.util.vm.VboxConfig.PlaceHolder; +import org.openslx.vm.VboxConfig.PlaceHolder; +import org.openslx.vm.disk.DiskImage; +import org.openslx.vm.disk.DiskImage.ImageFormat; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -317,7 +318,7 @@ public class VboxMetaData extends VmMetaData<VBoxSoundCardMeta, VBoxDDAccelMeta, } @Override - public void setSoundCard( org.openslx.util.vm.VmMetaData.SoundCardType type ) + public void setSoundCard( org.openslx.vm.VmMetaData.SoundCardType type ) { VBoxSoundCardMeta sound = soundCards.get( type ); config.changeAttribute( "/VirtualBox/Machine/Hardware/AudioAdapter", "enabled", Boolean.toString( sound.isPresent ) ); diff --git a/src/main/java/org/openslx/util/vm/VmMetaData.java b/src/main/java/org/openslx/vm/VmMetaData.java index 4b754c3..0be07e4 100644 --- a/src/main/java/org/openslx/util/vm/VmMetaData.java +++ b/src/main/java/org/openslx/vm/VmMetaData.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; import java.io.File; import java.io.IOException; @@ -13,6 +13,7 @@ import java.util.Map.Entry; import org.apache.log4j.Logger; import org.openslx.bwlp.thrift.iface.OperatingSystem; import org.openslx.bwlp.thrift.iface.Virtualizer; +import org.openslx.vm.disk.DiskImage; /** * Describes a configured virtual machine. This class is parsed from a machine diff --git a/src/main/java/org/openslx/util/vm/VmwareConfig.java b/src/main/java/org/openslx/vm/VmwareConfig.java index 62a014d..d98a1d4 100644 --- a/src/main/java/org/openslx/util/vm/VmwareConfig.java +++ b/src/main/java/org/openslx/vm/VmwareConfig.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; import java.io.BufferedReader; import java.io.ByteArrayInputStream; diff --git a/src/main/java/org/openslx/util/vm/VmwareMetaData.java b/src/main/java/org/openslx/vm/VmwareMetaData.java index a20e0a2..9bbe581 100644 --- a/src/main/java/org/openslx/util/vm/VmwareMetaData.java +++ b/src/main/java/org/openslx/vm/VmwareMetaData.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; import java.io.File; import java.io.IOException; @@ -17,8 +17,9 @@ import org.openslx.bwlp.thrift.iface.OperatingSystem; import org.openslx.bwlp.thrift.iface.Virtualizer; import org.openslx.thrifthelper.TConst; import org.openslx.util.Util; -import org.openslx.util.vm.DiskImage.ImageFormat; -import org.openslx.util.vm.VmwareConfig.ConfigEntry; +import org.openslx.vm.VmwareConfig.ConfigEntry; +import org.openslx.vm.disk.DiskImage; +import org.openslx.vm.disk.DiskImage.ImageFormat; class VmWareSoundCardMeta { diff --git a/src/main/java/org/openslx/vm/disk/DiskImage.java b/src/main/java/org/openslx/vm/disk/DiskImage.java new file mode 100644 index 0000000..38964f4 --- /dev/null +++ b/src/main/java/org/openslx/vm/disk/DiskImage.java @@ -0,0 +1,251 @@ +package org.openslx.vm.disk; + +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; + +/** + * 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 +{ + /** + * 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. + * + * @throws FileNotFoundException cannot find specified disk image file. + * @throws IOException cannot access the content of the disk image file. + * + * @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. + */ + public DiskImage( File diskImage ) throws FileNotFoundException, IOException + { + final RandomAccessFile diskFile = new RandomAccessFile( diskImage, "r" ); + this.diskImage = diskFile; + } + + /** + * 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. + */ + public 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 int 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 diskImage 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 diskImage ) throws FileNotFoundException, IOException, DiskImageException + { + final RandomAccessFile diskFile = new RandomAccessFile( diskImage, "r" ); + final DiskImage diskImageInstance; + + if ( DiskImageQcow2.probe( diskFile ) ) { + diskImageInstance = new DiskImageQcow2( diskFile ); + } else if ( DiskImageVdi.probe( diskFile ) ) { + diskImageInstance = new DiskImageVdi( diskFile ); + } else if ( DiskImageVmdk.probe( diskFile ) ) { + diskImageInstance = new DiskImageVmdk( diskFile ); + } else { + final String errorMsg = new String( "File '" + diskImage.getAbsolutePath() + "' is not a valid disk image!" ); + throw new DiskImageException( errorMsg ); + } + + return diskImageInstance; + } + + /** + * 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 supportedImageTypes 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/vm/disk/DiskImageException.java b/src/main/java/org/openslx/vm/disk/DiskImageException.java new file mode 100644 index 0000000..a98f963 --- /dev/null +++ b/src/main/java/org/openslx/vm/disk/DiskImageException.java @@ -0,0 +1,25 @@ +package org.openslx.vm.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/vm/disk/DiskImageQcow2.java b/src/main/java/org/openslx/vm/disk/DiskImageQcow2.java new file mode 100644 index 0000000..5f72a00 --- /dev/null +++ b/src/main/java/org/openslx/vm/disk/DiskImageQcow2.java @@ -0,0 +1,246 @@ +package org.openslx.vm.disk; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * 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. + * + * @throws FileNotFoundException cannot find specified QCOW2 disk image file. + * @throws IOException cannot access the content of the QCOW2 disk image file. + */ + public DiskImageQcow2( File disk ) throws FileNotFoundException, IOException + { + super( disk ); + } + + /** + * Creates a new QCOW2 disk image from an existing QCOW2 image file. + * + * @param diskImage file to a QCOW2 disk storing the image content. + */ + public DiskImageQcow2( RandomAccessFile disk ) + { + super( disk ); + } + + /** + * 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() >= 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 int 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 = new String( "Invalid QCOW2 version in header found!" ); + throw new DiskImageException( errorMsg ); + } + + return DiskImageUtils.versionFromMajor( 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/vm/disk/DiskImageUtils.java b/src/main/java/org/openslx/vm/disk/DiskImageUtils.java new file mode 100644 index 0000000..fbed6f9 --- /dev/null +++ b/src/main/java/org/openslx/vm/disk/DiskImageUtils.java @@ -0,0 +1,154 @@ +package org.openslx.vm.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 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 String readBytesAsString( 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 new String( values ); + } + + public static int versionFromMajorMinor( final short major, final short minor ) + { + return ( ( Integer.valueOf( major ) << 16 ) | minor ); + } + + public static int versionFromMajor( final short major ) + { + return DiskImageUtils.versionFromMajorMinor( major, Short.valueOf( "0" ) ); + } +} diff --git a/src/main/java/org/openslx/vm/disk/DiskImageVdi.java b/src/main/java/org/openslx/vm/disk/DiskImageVdi.java new file mode 100644 index 0000000..1c34c1d --- /dev/null +++ b/src/main/java/org/openslx/vm/disk/DiskImageVdi.java @@ -0,0 +1,119 @@ +package org.openslx.vm.disk; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * 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; + + /** + * Creates a new VDI disk image from an existing VDI image file. + * + * @param diskImage file to a VDI disk storing the image content. + * + * @throws FileNotFoundException cannot find specified VDI disk image file. + * @throws IOException cannot access the content of the VDI disk image file. + */ + public DiskImageVdi( File diskImage ) throws FileNotFoundException, IOException + { + super( diskImage ); + } + + /** + * Creates a new VDI disk image from an existing VDI image file. + * + * @param diskImage file to a VDI disk storing the image content. + */ + public 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 + final String parentUuid = DiskImageUtils.readBytesAsString( diskFile, 440, 16 ); + final String zeroUuid = new String( new byte[ 16 ] ); + + return !zeroUuid.equals( parentUuid ); + } + + @Override + public int 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 DiskImageUtils.versionFromMajorMinor( vdiVersionMajor, vdiVersionMinor ); + } + + @Override + public String getDescription() throws DiskImageException + { + final RandomAccessFile diskFile = this.getDiskImage(); + return DiskImageUtils.readBytesAsString( diskFile, 84, 256 ); + } + + @Override + public ImageFormat getFormat() + { + return ImageFormat.VDI; + } +} diff --git a/src/main/java/org/openslx/vm/disk/DiskImageVmdk.java b/src/main/java/org/openslx/vm/disk/DiskImageVmdk.java new file mode 100644 index 0000000..c9bfdbf --- /dev/null +++ b/src/main/java/org/openslx/vm/disk/DiskImageVmdk.java @@ -0,0 +1,298 @@ +package org.openslx.vm.disk; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +import org.openslx.util.Util; +import org.openslx.vm.UnsupportedVirtualizerFormatException; +import org.openslx.vm.VmwareConfig; + +/** + * 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 VmwareConfig 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. + * @throws FileNotFoundException cannot find specified VMDK disk image file. + * @throws IOException cannot access the content of the VMDK disk image file. + */ + public DiskImageVmdk( File diskImage ) throws DiskImageException, FileNotFoundException, IOException + { + super( diskImage ); + + this.vmdkConfig = this.parseVmdkConfig(); + } + + /** + * 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. + */ + public 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 VmwareConfig 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 VmwareConfig parseVmdkConfig() throws DiskImageException + { + final RandomAccessFile diskFile = this.getDiskImage(); + final VmwareConfig 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 = DiskImageUtils.readBytesAsString( diskFile, vmdkDescriptorOffset, + Long.valueOf( vmdkDescriptorSizeMax ).intValue() ); + + // 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 = new String( "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 VmwareConfig( configStr.getBytes(), vmdkDescriptorSize ); + } catch ( UnsupportedVirtualizerFormatException 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 VmwareConfig 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 + */ + public int getHwVersion() throws DiskImageException + { + final VmwareConfig vmdkConfig = this.getVmdkConfig(); + final int 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 = DiskImageUtils.versionFromMajor( 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 = DiskImageUtils.versionFromMajor( 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 VmwareConfig 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 int getVersion() throws DiskImageException + { + final RandomAccessFile diskFile = this.getDiskImage(); + final int vmdkVersion = Integer.reverseBytes( DiskImageUtils.readInt( diskFile, 4 ) ); + + return DiskImageUtils.versionFromMajor( Integer.valueOf( vmdkVersion ).shortValue() ); + } + + @Override + public String getDescription() throws DiskImageException + { + return null; + } + + @Override + public ImageFormat getFormat() + { + return ImageFormat.VMDK; + } +} diff --git a/src/test/java/org/openslx/util/vm/DiskImageTest.java b/src/test/java/org/openslx/util/vm/DiskImageTest.java deleted file mode 100644 index e1105d8..0000000 --- a/src/test/java/org/openslx/util/vm/DiskImageTest.java +++ /dev/null @@ -1,260 +0,0 @@ -package org.openslx.util.vm; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; - -import org.apache.log4j.Level; -import org.apache.log4j.LogManager; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.openslx.util.vm.DiskImage.ImageFormat; -import org.openslx.util.vm.DiskImage.UnknownImageFormatException; - -public class DiskImageTest -{ - @BeforeAll - public static void setUp() - { - // disable logging with log4j - LogManager.getRootLogger().setLevel( Level.OFF ); - } - - @Test - @DisplayName( "Test detection of VMDK disk image" ) - public void testVmdkDiskImage() throws FileNotFoundException, IOException, UnknownImageFormatException - { - File file = DiskImageTestResources.getDiskFile( "image-default.vmdk" ); - DiskImage image = new DiskImage( file ); - - assertEquals( ImageFormat.VMDK.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 18, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of VDI disk image" ) - public void testVdiDiskImage() throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image-default.vdi" ) ); - - assertEquals( ImageFormat.VDI.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 0, image.hwVersion ); - assertNotNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of default QCOW2 disk image" ) - public void testQcow2DiskImage() throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image-default.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of compressed, 16384 byte cluster QCOW2 disk image with extended L2 tables" ) - public void testQcow2DetectionL2Compressed16384DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-16384_cp-on_l2-on.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( true, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of compressed, 16384 byte cluster QCOW2 disk image without extended L2 tables" ) - public void testQcow2DetectionNonL2Compressed16384DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-16384_cp-on_l2-off.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( true, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of non-compressed, 16384 byte cluster QCOW2 disk image with extended L2 tables" ) - public void testQcow2DetectionL2NonCompressed16384DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-16384_cp-off_l2-on.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of non-compressed, 16384 byte cluster QCOW2 disk image without extended L2 tables" ) - public void testQcow2DetectionNonL2NonCompressed16384DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-16384_cp-off_l2-off.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of compressed, 65536 byte cluster QCOW2 disk image with extended L2 tables" ) - public void testQcow2DetectionL2Compressed65536DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-65536_cp-on_l2-on.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( true, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of compressed, 65536 byte cluster QCOW2 disk image without extended L2 tables" ) - public void testQcow2DetectionNonL2Compressed65536DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-65536_cp-on_l2-off.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( true, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of non-compressed, 65536 byte cluster QCOW2 disk image with extended L2 tables" ) - public void testQcow2DetectionL2NonCompressed65536DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-65536_cp-off_l2-on.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of non-compressed, 65536 byte cluster QCOW2 disk image without extended L2 tables" ) - public void testQcow2DetectionNonL2NonCompressed65536DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-65536_cp-off_l2-off.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of compressed, 2097152 byte cluster QCOW2 disk image with extended L2 tables" ) - public void testQcow2DetectionL2Compressed2097152DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-2097152_cp-on_l2-on.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( true, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of compressed, 2097152 byte cluster QCOW2 disk image without extended L2 tables" ) - public void testQcow2DetectionNonL2Compressed2097152DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-2097152_cp-on_l2-off.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( true, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of non-compressed, 2097152 byte cluster QCOW2 disk image with extended L2 tables" ) - public void testQcow2DetectionL2NonCompressed2097152DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-2097152_cp-off_l2-on.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test detection of non-compressed, 2097152 byte cluster QCOW2 disk image without extended L2 tables" ) - public void testQcow2DetectionNonL2NonCompressed2097152DiskImage() - throws FileNotFoundException, IOException, UnknownImageFormatException - { - DiskImage image = new DiskImage( DiskImageTestResources.getDiskFile( "image_cs-2097152_cp-off_l2-off.qcow2" ) ); - - assertEquals( ImageFormat.QCOW2.toString(), image.getImageFormat().toString() ); - assertEquals( true, image.isStandalone ); - assertEquals( false, image.isSnapshot ); - assertEquals( false, image.isCompressed ); - assertEquals( 3, image.hwVersion ); - assertNull( image.diskDescription ); - } - - @Test - @DisplayName( "Test of invalid disk image" ) - public void testInvalidDiskImage() throws FileNotFoundException, IOException, UnknownImageFormatException - { - Assertions.assertThrows( UnknownImageFormatException.class, () -> { - new DiskImage( DiskImageTestResources.getDiskFile( "image-default.invalid" ) ); - } ); - } -} diff --git a/src/test/java/org/openslx/util/vm/QemuMetaDataTest.java b/src/test/java/org/openslx/vm/QemuMetaDataTest.java index f201a77..3217fda 100644 --- a/src/test/java/org/openslx/util/vm/QemuMetaDataTest.java +++ b/src/test/java/org/openslx/vm/QemuMetaDataTest.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -31,11 +31,13 @@ import org.openslx.libvirt.domain.device.DiskStorage; import org.openslx.libvirt.domain.device.Interface; import org.openslx.libvirt.domain.device.Sound; import org.openslx.libvirt.xml.LibvirtXmlTestResources; -import org.openslx.util.vm.DiskImage.ImageFormat; -import org.openslx.util.vm.VmMetaData.EtherType; -import org.openslx.util.vm.VmMetaData.EthernetDevType; -import org.openslx.util.vm.VmMetaData.SoundCardType; -import org.openslx.util.vm.VmMetaData.UsbSpeed; +import org.openslx.vm.VmMetaData.EtherType; +import org.openslx.vm.VmMetaData.EthernetDevType; +import org.openslx.vm.VmMetaData.SoundCardType; +import org.openslx.vm.VmMetaData.UsbSpeed; +import org.openslx.vm.disk.DiskImage; +import org.openslx.vm.disk.DiskImageTestResources; +import org.openslx.vm.disk.DiskImage.ImageFormat; public class QemuMetaDataTest { diff --git a/src/test/java/org/openslx/vm/disk/DiskImageQcow2Test.java b/src/test/java/org/openslx/vm/disk/DiskImageQcow2Test.java new file mode 100644 index 0000000..530cd60 --- /dev/null +++ b/src/test/java/org/openslx/vm/disk/DiskImageQcow2Test.java @@ -0,0 +1,220 @@ +package org.openslx.vm.disk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.openslx.vm.disk.DiskImage.ImageFormat; + +public class DiskImageQcow2Test +{ + @Test + @DisplayName( "Test detection of default QCOW2 disk image" ) + public void testQcow2DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image-default.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of compressed, 16384 byte cluster QCOW2 disk image with extended L2 tables" ) + public void testQcow2DetectionL2Compressed16384DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-16384_cp-on_l2-on.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( true, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of compressed, 16384 byte cluster QCOW2 disk image without extended L2 tables" ) + public void testQcow2DetectionNonL2Compressed16384DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-16384_cp-on_l2-off.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( true, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of non-compressed, 16384 byte cluster QCOW2 disk image with extended L2 tables" ) + public void testQcow2DetectionL2NonCompressed16384DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-16384_cp-off_l2-on.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of non-compressed, 16384 byte cluster QCOW2 disk image without extended L2 tables" ) + public void testQcow2DetectionNonL2NonCompressed16384DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-16384_cp-off_l2-off.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of compressed, 65536 byte cluster QCOW2 disk image with extended L2 tables" ) + public void testQcow2DetectionL2Compressed65536DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-65536_cp-on_l2-on.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( true, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of compressed, 65536 byte cluster QCOW2 disk image without extended L2 tables" ) + public void testQcow2DetectionNonL2Compressed65536DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-65536_cp-on_l2-off.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( true, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of non-compressed, 65536 byte cluster QCOW2 disk image with extended L2 tables" ) + public void testQcow2DetectionL2NonCompressed65536DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-65536_cp-off_l2-on.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of non-compressed, 65536 byte cluster QCOW2 disk image without extended L2 tables" ) + public void testQcow2DetectionNonL2NonCompressed65536DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-65536_cp-off_l2-off.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of compressed, 2097152 byte cluster QCOW2 disk image with extended L2 tables" ) + public void testQcow2DetectionL2Compressed2097152DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-2097152_cp-on_l2-on.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( true, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of compressed, 2097152 byte cluster QCOW2 disk image without extended L2 tables" ) + public void testQcow2DetectionNonL2Compressed2097152DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-2097152_cp-on_l2-off.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( true, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of non-compressed, 2097152 byte cluster QCOW2 disk image with extended L2 tables" ) + public void testQcow2DetectionL2NonCompressed2097152DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-2097152_cp-off_l2-on.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of non-compressed, 2097152 byte cluster QCOW2 disk image without extended L2 tables" ) + public void testQcow2DetectionNonL2NonCompressed2097152DiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage + .newInstance( DiskImageTestResources.getDiskFile( "image_cs-2097152_cp-off_l2-off.qcow2" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + + assertEquals( ImageFormat.QCOW2.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + } +} diff --git a/src/test/java/org/openslx/vm/disk/DiskImageTest.java b/src/test/java/org/openslx/vm/disk/DiskImageTest.java new file mode 100644 index 0000000..2572c58 --- /dev/null +++ b/src/test/java/org/openslx/vm/disk/DiskImageTest.java @@ -0,0 +1,19 @@ +package org.openslx.vm.disk; + +import java.io.IOException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class DiskImageTest +{ + @Test + @DisplayName( "Test of invalid disk image" ) + public void testInvalidDiskImage() throws IOException + { + Assertions.assertThrows( DiskImageException.class, () -> { + DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image-default.invalid" ) ); + } ); + } +} diff --git a/src/test/java/org/openslx/util/vm/DiskImageTestResources.java b/src/test/java/org/openslx/vm/disk/DiskImageTestResources.java index 1f164bd..2ec2e05 100644 --- a/src/test/java/org/openslx/util/vm/DiskImageTestResources.java +++ b/src/test/java/org/openslx/vm/disk/DiskImageTestResources.java @@ -1,4 +1,4 @@ -package org.openslx.util.vm; +package org.openslx.vm.disk; import java.io.File; import java.net.URL; diff --git a/src/test/java/org/openslx/vm/disk/DiskImageVdiTest.java b/src/test/java/org/openslx/vm/disk/DiskImageVdiTest.java new file mode 100644 index 0000000..492c6aa --- /dev/null +++ b/src/test/java/org/openslx/vm/disk/DiskImageVdiTest.java @@ -0,0 +1,43 @@ +package org.openslx.vm.disk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.IOException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.openslx.vm.disk.DiskImage.ImageFormat; + +public class DiskImageVdiTest +{ + @Test + @DisplayName( "Test detection of default VDI disk image" ) + public void testVdiDiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image-default.vdi" ) ); + final int imageVersion = DiskImageUtils.versionFromMajorMinor( Short.valueOf( "1" ), Short.valueOf( "1" ) ); + + assertEquals( ImageFormat.VDI.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNotNull( image.getDescription() ); + } + + @Test + @DisplayName( "Test detection of VDI disk image snapshot" ) + public void testVdiDiskImageSnapshot() throws DiskImageException, IOException + { + final DiskImage image = DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image-default_snapshot.vdi" ) ); + final int imageVersion = DiskImageUtils.versionFromMajorMinor( Short.valueOf( "1" ), Short.valueOf( "1" ) ); + + assertEquals( ImageFormat.VDI.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( true, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNotNull( image.getDescription() ); + } +} diff --git a/src/test/java/org/openslx/vm/disk/DiskImageVmdkTest.java b/src/test/java/org/openslx/vm/disk/DiskImageVmdkTest.java new file mode 100644 index 0000000..00cf561 --- /dev/null +++ b/src/test/java/org/openslx/vm/disk/DiskImageVmdkTest.java @@ -0,0 +1,110 @@ +package org.openslx.vm.disk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.openslx.vm.disk.DiskImage.ImageFormat; + +public class DiskImageVmdkTest +{ + @Test + @DisplayName( "Test detection of default VMDK disk image" ) + public void testVmdkDiskImage() throws DiskImageException, IOException + { + final DiskImage image = DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image-default.vmdk" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "1" ) ); + final int imageHwVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "18" ) ); + + assertEquals( ImageFormat.VMDK.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + + // test special features of the VMDK disk image format + final DiskImageVmdk vmdkImage = DiskImageVmdk.class.cast( image ); + assertEquals( imageHwVersion, vmdkImage.getHwVersion() ); + } + + @Test + @DisplayName( "Test detection of VMDK disk image (type 0: single growable virtual disk)" ) + public void testVmdkDiskImageType0() throws DiskImageException, IOException + { + final DiskImage image = DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image_t0.vmdk" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "1" ) ); + final int imageHwVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "18" ) ); + + assertEquals( ImageFormat.VMDK.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( false, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + + // test special features of the VMDK disk image format + final DiskImageVmdk vmdkImage = DiskImageVmdk.class.cast( image ); + assertEquals( imageHwVersion, vmdkImage.getHwVersion() ); + } + + @Test + @DisplayName( "Test detection of VMDK disk image (type 1: growable virtual disk split into multiple files)" ) + public void testVmdkDiskImageType1() throws DiskImageException, IOException + { + Assertions.assertThrows( DiskImageException.class, () -> { + DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image_t1.vmdk" ) ); + } ); + } + + @Test + @DisplayName( "Test detection of VMDK disk image (type 2: preallocated virtual disk)" ) + public void testVmdkDiskImageType2() throws DiskImageException, IOException + { + Assertions.assertThrows( DiskImageException.class, () -> { + DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image_t2.vmdk" ) ); + } ); + } + + @Test + @DisplayName( "Test detection of VMDK disk image (type 3: preallocated virtual disk split into multiple files)" ) + public void testVmdkDiskImageType3() throws DiskImageException, IOException + { + Assertions.assertThrows( DiskImageException.class, () -> { + DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image_t3.vmdk" ) ); + } ); + } + + @Test + @DisplayName( "Test detection of VMDK disk image (type 4: preallocated ESX-type virtual disk)" ) + public void testVmdkDiskImageType4() throws DiskImageException, IOException + { + Assertions.assertThrows( DiskImageException.class, () -> { + DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image_t4.vmdk" ) ); + } ); + } + + @Test + @DisplayName( "Test detection of VMDK disk image (type 5: compressed disk optimized for streaming)" ) + public void testVmdkDiskImageType5() throws DiskImageException, IOException + { + final DiskImage image = DiskImage.newInstance( DiskImageTestResources.getDiskFile( "image_t5.vmdk" ) ); + final int imageVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "3" ) ); + final int imageHwVersion = DiskImageUtils.versionFromMajor( Short.valueOf( "18" ) ); + + assertEquals( ImageFormat.VMDK.toString(), image.getFormat().toString() ); + assertEquals( true, image.isStandalone() ); + assertEquals( false, image.isSnapshot() ); + assertEquals( true, image.isCompressed() ); + assertEquals( imageVersion, image.getVersion() ); + assertNull( image.getDescription() ); + + // test special features of the VMDK disk image format + final DiskImageVmdk vmdkImage = DiskImageVmdk.class.cast( image ); + assertEquals( imageHwVersion, vmdkImage.getHwVersion() ); + } +} diff --git a/src/test/resources/disk/image-default_snapshot.vdi b/src/test/resources/disk/image-default_snapshot.vdi Binary files differnew file mode 100644 index 0000000..f07502a --- /dev/null +++ b/src/test/resources/disk/image-default_snapshot.vdi diff --git a/src/test/resources/disk/image_t0.vmdk b/src/test/resources/disk/image_t0.vmdk Binary files differnew file mode 100644 index 0000000..08047a0 --- /dev/null +++ b/src/test/resources/disk/image_t0.vmdk diff --git a/src/test/resources/disk/image_t1-s001.vmdk b/src/test/resources/disk/image_t1-s001.vmdk Binary files differnew file mode 100644 index 0000000..a3ce425 --- /dev/null +++ b/src/test/resources/disk/image_t1-s001.vmdk diff --git a/src/test/resources/disk/image_t1.vmdk b/src/test/resources/disk/image_t1.vmdk new file mode 100644 index 0000000..7f780c2 --- /dev/null +++ b/src/test/resources/disk/image_t1.vmdk @@ -0,0 +1,21 @@ +# Disk DescriptorFile +version=1 +CID=f879aea6 +parentCID=ffffffff +createType="twoGbMaxExtentSparse" + +# Extent description +RW 20480 SPARSE "image_t1-s001.vmdk" + +# The Disk Data Base +#DDB + +ddb.adapterType = "ide" +ddb.deletable = "true" +ddb.encoding = "UTF-8" +ddb.geometry.cylinders = "20" +ddb.geometry.heads = "16" +ddb.geometry.sectors = "63" +ddb.longContentID = "a586da03e45ef91444741b5e03dd9850" +ddb.uuid = "60 00 C2 95 f2 fc 95 8b-59 71 f0 58 a4 63 3d d9" +ddb.virtualHWVersion = "18" diff --git a/src/test/resources/disk/image_t2-flat.vmdk b/src/test/resources/disk/image_t2-flat.vmdk Binary files differnew file mode 100644 index 0000000..3cfe668 --- /dev/null +++ b/src/test/resources/disk/image_t2-flat.vmdk diff --git a/src/test/resources/disk/image_t2.vmdk b/src/test/resources/disk/image_t2.vmdk new file mode 100644 index 0000000..2907c8d --- /dev/null +++ b/src/test/resources/disk/image_t2.vmdk @@ -0,0 +1,21 @@ +# Disk DescriptorFile +version=1 +CID=f879aea6 +parentCID=ffffffff +createType="monolithicFlat" + +# Extent description +RW 20480 FLAT "image_t2-flat.vmdk" 0 + +# The Disk Data Base +#DDB + +ddb.adapterType = "ide" +ddb.deletable = "true" +ddb.encoding = "UTF-8" +ddb.geometry.cylinders = "20" +ddb.geometry.heads = "16" +ddb.geometry.sectors = "63" +ddb.longContentID = "b104a6d8dbe1d6adfe5c3c2422159584" +ddb.uuid = "60 00 C2 91 13 fb 32 ae-b1 94 6c 90 7e a7 dc fd" +ddb.virtualHWVersion = "18" diff --git a/src/test/resources/disk/image_t3-f001.vmdk b/src/test/resources/disk/image_t3-f001.vmdk Binary files differnew file mode 100644 index 0000000..3cfe668 --- /dev/null +++ b/src/test/resources/disk/image_t3-f001.vmdk diff --git a/src/test/resources/disk/image_t3.vmdk b/src/test/resources/disk/image_t3.vmdk new file mode 100644 index 0000000..3a9755a --- /dev/null +++ b/src/test/resources/disk/image_t3.vmdk @@ -0,0 +1,21 @@ +# Disk DescriptorFile +version=1 +CID=f879aea6 +parentCID=ffffffff +createType="twoGbMaxExtentFlat" + +# Extent description +RW 20480 FLAT "image_t3-f001.vmdk" 0 + +# The Disk Data Base +#DDB + +ddb.adapterType = "ide" +ddb.deletable = "true" +ddb.encoding = "UTF-8" +ddb.geometry.cylinders = "20" +ddb.geometry.heads = "16" +ddb.geometry.sectors = "63" +ddb.longContentID = "1d2f0fc53996e4dd85d6348a9cb54a70" +ddb.uuid = "60 00 C2 92 6f 18 ff eb-66 28 54 8e f4 fb 0d e6" +ddb.virtualHWVersion = "18" diff --git a/src/test/resources/disk/image_t4-flat.vmdk b/src/test/resources/disk/image_t4-flat.vmdk Binary files differnew file mode 100644 index 0000000..3cfe668 --- /dev/null +++ b/src/test/resources/disk/image_t4-flat.vmdk diff --git a/src/test/resources/disk/image_t4.vmdk b/src/test/resources/disk/image_t4.vmdk new file mode 100644 index 0000000..c9b56db --- /dev/null +++ b/src/test/resources/disk/image_t4.vmdk @@ -0,0 +1,22 @@ +# Disk DescriptorFile +version=1 +CID=f879aea6 +parentCID=ffffffff +createType="vmfs" + +# Extent description +RW 20480 VMFS "image_t4-flat.vmdk" 0 + +# The Disk Data Base +#DDB + +ddb.adapterType = "ide" +ddb.deletable = "true" +ddb.encoding = "UTF-8" +ddb.geometry.cylinders = "20" +ddb.geometry.heads = "16" +ddb.geometry.sectors = "63" +ddb.longContentID = "c1c7f78748a6d9a7f9233c312a8362c6" +ddb.thinProvisioned = "1" +ddb.uuid = "60 00 C2 9a 0f 63 7a 30-b4 d5 1c 32 b3 3c e6 bb" +ddb.virtualHWVersion = "18" diff --git a/src/test/resources/disk/image_t5.vmdk b/src/test/resources/disk/image_t5.vmdk Binary files differnew file mode 100644 index 0000000..318ab09 --- /dev/null +++ b/src/test/resources/disk/image_t5.vmdk |