summaryrefslogblamecommitdiffstats
path: root/src/main/java/org/openslx/util/vm/DiskImage.java
blob: 617fadd9555d456208978f9003c6dc2e76d6fd7a (plain) (tree)
1
2
3
4
5
6
7
8
9





                                     
                        

                                    
 
                               
                                                 
                                       
                             


                      
                                                                                 
           
                                                          

                                                         
                                                        



                                                             


                               
                                                                                       







                                                       
                                                                                   


                                            






                                                                                
                                                                  
                                            
                                                                      
                                           
                                                                
                                             
                                                                  
                                              





                                          
                                        
                                        
                                      
                                   
                                            
 





                                                                                                            
         
                                                                                  
                                                                                   
                               







                                                                                                 







                                                                                                
                                                                               
                                                                                  













                                                                                                                                         
                                         

                                 








































                                                                                                                             
                                                                 









                                                                                                             
                         
 
                                           
                                       
                                                             













































































































































                                                                                                                                                       
                                                                
                                                            
 
                                       
                         
                 
                                                        
         













                                                                                                      
 


















                                                                                                            








                                                           
                                                                            


                                     

                                                                               
                                                                                                                 





                                                                                   
 
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;
	}
}