summaryrefslogtreecommitdiffstats
path: root/src/main/java/org/openslx/util/vm/DiskImage.java
blob: 617fadd9555d456208978f9003c6dc2e76d6fd7a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
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;
	}
}