diff options
Diffstat (limited to 'src/main/java/org/openslx/vm')
16 files changed, 4921 insertions, 0 deletions
diff --git a/src/main/java/org/openslx/vm/DockerMetaDataDummy.java b/src/main/java/org/openslx/vm/DockerMetaDataDummy.java new file mode 100644 index 0000000..73cd92e --- /dev/null +++ b/src/main/java/org/openslx/vm/DockerMetaDataDummy.java @@ -0,0 +1,217 @@ +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.vm.disk.DiskImage; +import org.openslx.vm.disk.DiskImage.ImageFormat; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +class DockerSoundCardMeta +{ +} + +class DockerDDAccelMeta +{ +} + +class DockerHWVersionMeta +{ +} + +class DockerEthernetDevTypeMeta +{ +} + +class DockerUsbSpeedMeta +{ +} + +public class DockerMetaDataDummy extends VmMetaData<DockerSoundCardMeta, DockerDDAccelMeta, DockerHWVersionMeta, DockerEthernetDevTypeMeta, DockerUsbSpeedMeta> { + + /** + * List of supported image formats by the Docker hypervisor. + */ + private static final List<DiskImage.ImageFormat> SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableList( + Arrays.asList( ImageFormat.NONE ) ); + + private static final Logger LOGGER = Logger.getLogger( DockerMetaDataDummy.class); + + private final Virtualizer virtualizer = new Virtualizer(TConst.VIRT_DOCKER, "Docker"); + + /** + * containerDefinition is a serialized tar.gz archive and represents a + * ContainerDefinition. This archive contains a serialized Container Recipe (e.g. Dockerfile) + * and a ContainerMeta witch is serialized as a json file. + * <p> + * See ContainerDefintion in tutor-module (bwsuite). + * <p> + * This field is in vm context the machine description e.g. vmware = vmx. + * This field will be stored in table imageversion.virtualizerconfig + */ + private byte[] containerDefinition; + + @SuppressWarnings( "deprecation" ) + public DockerMetaDataDummy(List<OperatingSystem> osList, File file) throws UnsupportedVirtualizerFormatException { + super(osList); + + BufferedInputStream bis = null; + + try { + bis = new BufferedInputStream(new FileInputStream(file)); + containerDefinition = new byte[(int) file.length()]; + bis.read(containerDefinition); + + checkIsTarGz(); + } catch (IOException | UnsupportedVirtualizerFormatException e) { + LOGGER.error("Couldn't read dockerfile", e); + } finally { + IOUtils.closeQuietly( bis ); + } + } + + public DockerMetaDataDummy(List<OperatingSystem> osList, byte[] vmContent, int length) + throws UnsupportedVirtualizerFormatException { + super(osList); + + containerDefinition = vmContent; + + checkIsTarGz(); + } + + /* + TODO This is just a simple check to prevent the workflow from considering any content as acceptable. + */ + /** + * Checks if the first two bytes of the content identifies a tar.gz archive. + * The first byte is 31 == 0x1f, the second byte has to be -117 == 0x8b. + * + * @throws UnsupportedVirtualizerFormatException + */ + private void checkIsTarGz() throws UnsupportedVirtualizerFormatException { + if (!((31 == containerDefinition[0]) && (-117 == containerDefinition[1]))) { + LOGGER.warn("Not Supported Content."); + throw new UnsupportedVirtualizerFormatException( + "DockerMetaDataDummy: Not tar.gz encoded content!"); + } + } + + @Override public byte[] getFilteredDefinitionArray() { + return containerDefinition; + } + + @Override + public List<DiskImage.ImageFormat> getSupportedImageFormats() + { + return DockerMetaDataDummy.SUPPORTED_IMAGE_FORMATS; + } + + @Override public void applySettingsForLocalEdit() { + + } + + @Override public boolean addHddTemplate(File diskImage, String hddMode, String redoDir) { + return false; + } + + @Override public boolean addHddTemplate(String diskImagePath, String hddMode, String redoDir) { + return false; + } + + @Override public boolean addDefaultNat() { + return false; + } + + @Override public void setOs(String vendorOsId) { + + } + + @Override public boolean addDisplayName(String name) { + return false; + } + + @Override public boolean addRam(int mem) { + return false; + } + + @Override public void addFloppy(int index, String image, boolean readOnly) { + + } + + @Override public boolean addCdrom(String image) { + return false; + } + + @Override public boolean addCpuCoreCount(int nrOfCores) { + return false; + } + + @Override public void setSoundCard(SoundCardType type) { + + } + + @Override public SoundCardType getSoundCard() { + return SoundCardType.NONE; + } + + @Override public void setDDAcceleration(DDAcceleration type) { + + } + + @Override public DDAcceleration getDDAcceleration() { + return DDAcceleration.OFF; + } + + @Override public void setHWVersion(HWVersion type) { + + } + + @Override public HWVersion getHWVersion() { + return HWVersion.DEFAULT; + } + + @Override public void setEthernetDevType(int cardIndex, EthernetDevType type) { + + } + + @Override public EthernetDevType getEthernetDevType(int cardIndex) { + return EthernetDevType.NONE; + } + + @Override public void setMaxUsbSpeed(UsbSpeed speed) { + + } + + @Override public UsbSpeed getMaxUsbSpeed() { + return UsbSpeed.NONE; + } + + @Override public byte[] getDefinitionArray() { + return new byte[0]; + } + + @Override public boolean addEthernet(EtherType type) { + return false; + } + + @Override public Virtualizer getVirtualizer() { + return virtualizer; + } + + @Override public boolean tweakForNonPersistent() { + return false; + } + + @Override public void registerVirtualHW() { + + } +} diff --git a/src/main/java/org/openslx/vm/KeyValuePair.java b/src/main/java/org/openslx/vm/KeyValuePair.java new file mode 100644 index 0000000..c5650ec --- /dev/null +++ b/src/main/java/org/openslx/vm/KeyValuePair.java @@ -0,0 +1,13 @@ +package org.openslx.vm; + +class KeyValuePair +{ + public final String key; + public final String value; + + public KeyValuePair( String key, String value ) + { + this.key = key; + this.value = value; + } +}
\ No newline at end of file diff --git a/src/main/java/org/openslx/vm/QemuMetaData.java b/src/main/java/org/openslx/vm/QemuMetaData.java new file mode 100644 index 0000000..c780429 --- /dev/null +++ b/src/main/java/org/openslx/vm/QemuMetaData.java @@ -0,0 +1,855 @@ +package org.openslx.vm; + +import java.io.File; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; + +import org.openslx.bwlp.thrift.iface.OperatingSystem; +import org.openslx.bwlp.thrift.iface.Virtualizer; +import org.openslx.libvirt.domain.Domain; +import org.openslx.libvirt.domain.device.ControllerUsb; +import org.openslx.libvirt.domain.device.Disk.BusType; +import org.openslx.libvirt.domain.device.Disk.StorageType; +import org.openslx.libvirt.domain.device.DiskCdrom; +import org.openslx.libvirt.domain.device.DiskFloppy; +import org.openslx.libvirt.domain.device.DiskStorage; +import org.openslx.libvirt.domain.device.Graphics; +import org.openslx.libvirt.domain.device.GraphicsSpice; +import org.openslx.libvirt.domain.device.Interface; +import org.openslx.libvirt.domain.device.Sound; +import org.openslx.libvirt.domain.device.Video; +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.vm.disk.DiskImage; +import org.openslx.vm.disk.DiskImage.ImageFormat; + +/** + * Metadata to describe the hardware type of a QEMU sound card. + * + * @author Manuel Bentele + * @version 1.0 + */ +class QemuSoundCardMeta +{ + /** + * Stores the hardware model of the QEMU sound card. + */ + private final Sound.Model model; + + /** + * Creates metadata to describe the hardware model of a QEMU sound card. + * + * @param model hardware model of the QEMU sound card. + */ + public QemuSoundCardMeta( Sound.Model model ) + { + this.model = model; + } + + /** + * Returns hardware model of the QEMU sound card. + * + * @return hardware model of the QEMU sound card. + */ + public Sound.Model getModel() + { + return this.model; + } +} + +/** + * Metadata to describe the hardware acceleration state of QEMU virtual graphics. + * + * @author Manuel Bentele + * @version 1.0 + */ +class QemuDDAccelMeta +{ + /** + * Stores state of the hardware acceleration for QEMU virtual graphics. + */ + private final boolean enabled; + + /** + * Creates metadata to describe the hardware acceleration state of QEMU virtual graphics. + * + * @param enabled state of the hardware acceleration for QEMU virtual graphics. + */ + public QemuDDAccelMeta( boolean enabled ) + { + this.enabled = enabled; + } + + /** + * Returns state of the hardware acceleration of QEMU virtual graphics. + * + * @return state of the hardware acceleration for QEMU virtual graphics. + */ + public boolean isEnabled() + { + return this.enabled; + } +} + +/** + * Metadata to describe the version of a QEMU virtual machine configuration. + * + * @author Manuel Bentele + * @version 1.0 + */ +class QemuHWVersionMeta +{ + /** + * Stores the version of a QEMU virtual machine configuration. + */ + private final int version; + + /** + * Creates metadata to describe the version of a QEMU virtual machine configuration. + * + * @param version version of the QEMU virtual machine configuration. + */ + public QemuHWVersionMeta( int version ) + { + this.version = version; + } + + /** + * Returns version of the QEMU virtual machine configuration. + * + * @return version of the QEMU virtual machine configuration. + */ + public int getVersion() + { + return this.version; + } +} + +/** + * Metadata to describe the hardware type of a QEMU ethernet device. + * + * @author Manuel Bentele + * @version 1.0 + */ +class QemuEthernetDevTypeMeta +{ + /** + * Stores the hardware model of the QEMU ethernet device. + */ + private final Interface.Model model; + + /** + * Creates metadata to describe the hardware type of a QEMU ethernet device. + * + * @param model hardware type of the QEMU ethernet device. + */ + public QemuEthernetDevTypeMeta( Interface.Model model ) + { + this.model = model; + } + + /** + * Returns the hardware type of a QEMU ethernet device. + * + * @return hardware type of the QEMU ethernet device. + */ + public Interface.Model getModel() + { + return this.model; + } +} + +/** + * Metadata to describe a QEMU USB controller. + * + * @author Manuel Bentele + * @version 1.0 + */ +class QemuUsbSpeedMeta +{ + /** + * Stores the USB speed of the QEMU USB controller. + */ + private final int speed; + + /** + * Stores the QEMU hardware model of the USB controller. + */ + private final ControllerUsb.Model model; + + /** + * Creates metadata to describe a QEMU USB controller. + * + * @param speed USB speed of the QEMU USB controller. + * @param model QEMU hardware model of the USB controller. + */ + public QemuUsbSpeedMeta( int speed, ControllerUsb.Model model ) + { + this.speed = speed; + this.model = model; + } + + /** + * Returns the speed of the QEMU USB controller. + * + * @return speed of the QEMU USB controller. + */ + public int getSpeed() + { + return this.speed; + } + + /** + * Returns QEMU hardware model of the USB controller. + * + * @return hardware model of the QEMU USB controller. + */ + public ControllerUsb.Model getModel() + { + return this.model; + } +} + +/** + * Virtual machine configuration (managed by Libvirt) for the QEMU hypervisor. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class QemuMetaData extends + VmMetaData<QemuSoundCardMeta, QemuDDAccelMeta, QemuHWVersionMeta, QemuEthernetDevTypeMeta, QemuUsbSpeedMeta> +{ + /** + * Default bridge name of the network bridge connected to the LAN. + */ + public static final String NETWORK_DEFAULT_BRIDGE = "brBwLehrpool"; + + /** + * Default network name of the isolated host network (host only). + */ + public static final String NETWORK_DEFAULT_HOST_ONLY = "host"; + + /** + * Default network name of the NAT network. + */ + public static final String NETWORK_DEFAULT_NAT = "nat"; + + /** + * Default physical CDROM drive of the hypervisor host. + */ + public static final String CDROM_DEFAULT_PHYSICAL_DRIVE = "/dev/sr0"; + + /** + * List of supported image formats by the QEMU hypervisor. + */ + private static final List<DiskImage.ImageFormat> SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableList( + Arrays.asList( ImageFormat.QCOW2, ImageFormat.VMDK, ImageFormat.VDI ) ); + + /** + * Representation of a QEMU hypervisor (managed by Libvirt). + */ + private static final Virtualizer VIRTUALIZER = new Virtualizer( TConst.VIRT_QEMU, "QEMU" ); + + /** + * Libvirt XML configuration file to modify configuration of virtual machine for QEMU. + */ + private Domain vmConfig = null; + + /** + * Stores current index of added HDD device to the Libvirt XML configuration file. + */ + private int vmDeviceIndexHddAdd = 0; + + /** + * Stores current index of added CDROM device to the Libvirt XML configuration file. + */ + private int vmDeviceIndexCdromAdd = 0; + + /** + * Stores current index of added ethernet device to the Libvirt XML configuration file. + */ + private int vmDeviceIndexEthernetAdd = 0; + + /** + * Creates new virtual machine configuration (managed by Libvirt) for the QEMU hypervisor. + * + * @param osList list of operating systems. + * @param file image file for the QEMU hypervisor. + * @throws UnsupportedVirtualizerFormatException Libvirt XML configuration cannot be processed. + */ + public QemuMetaData( List<OperatingSystem> osList, File file ) throws UnsupportedVirtualizerFormatException + { + super( osList ); + + try { + // read and parse Libvirt domain XML configuration document + this.vmConfig = new Domain( file ); + } catch ( LibvirtXmlDocumentException e ) { + throw new UnsupportedVirtualizerFormatException( e.getLocalizedMessage() ); + } catch ( LibvirtXmlSerializationException e ) { + throw new UnsupportedVirtualizerFormatException( e.getLocalizedMessage() ); + } catch ( LibvirtXmlValidationException e ) { + throw new UnsupportedVirtualizerFormatException( e.getLocalizedMessage() ); + } + + // register virtual hardware models for graphical editing of virtual devices (GPU, sound, USB, ...) + this.registerVirtualHW(); + + // set display name of VM + this.displayName = vmConfig.getName(); + + // this property cannot be checked with the Libvirt domain XML configuration + // to check if machine is in a paused/suspended state, look in the QEMU qcow2 image for snapshots and machine states + this.isMachineSnapshot = false; + + // add HDDs, SSDs to QEMU metadata + for ( DiskStorage storageDiskDevice : this.vmConfig.getDiskStorageDevices() ) { + this.addHddMetaData( storageDiskDevice ); + } + } + + /** + * Adds an existing and observed storage disk device to the HDD metadata. + * + * @param storageDiskDevice existing and observed storage disk that should be added to the + * metadata. + */ + private void addHddMetaData( DiskStorage storageDiskDevice ) + { + String hddChipsetModel = null; + DriveBusType hddChipsetBus = QemuMetaDataUtils.convertBusType( storageDiskDevice.getBusType() ); + String hddImagePath = storageDiskDevice.getStorageSource(); + + this.hdds.add( new HardDisk( hddChipsetModel, hddChipsetBus, hddImagePath ) ); + } + + @Override + public byte[] getFilteredDefinitionArray() + { + // remove UUID in Libvirt domain XML configuration + this.vmConfig.removeUuid(); + + // removes all specified boot order entries + this.vmConfig.removeBootOrder(); + + // removes all referenced storage files of all specified CDROMs, Floppy drives and HDDs + this.vmConfig.removeDiskDevicesStorage(); + + // removes all source networks of all specified network interfaces + this.vmConfig.removeInterfaceDevicesSource(); + + // output filtered Libvirt domain XML configuration + String configuration = this.vmConfig.toString(); + return configuration.getBytes( StandardCharsets.UTF_8 ); + } + + @Override + public List<DiskImage.ImageFormat> getSupportedImageFormats() + { + return QemuMetaData.SUPPORTED_IMAGE_FORMATS; + } + + @Override + public void applySettingsForLocalEdit() + { + // NOT implemented yet + } + + @Override + public boolean addHddTemplate( File diskImage, String hddMode, String redoDir ) + { + return this.addHddTemplate( diskImage.getAbsolutePath(), hddMode, redoDir ); + } + + @Override + public boolean addHddTemplate( String diskImagePath, String hddMode, String redoDir ) + { + return this.addHddTemplate( this.vmDeviceIndexHddAdd++, diskImagePath, hddMode, redoDir ); + } + + /** + * Adds hard disk drive (HDD) to the QEMU virtual machine configuration. + * + * @param index current index of HDD to be added to the virtual machine configuration. + * @param diskImagePath path to the virtual disk image for the HDD. + * @param hddMode operation mode of the HDD. + * @param redoDir directory for the redo log if an independent non-persistent + * <code>hddMode</code> is set. + * @return result state of adding the HDD. + */ + public boolean addHddTemplate( int index, String diskImagePath, String hddMode, String redoDir ) + { + ArrayList<DiskStorage> storageDiskDevices = this.vmConfig.getDiskStorageDevices(); + DiskStorage storageDiskDevice = QemuMetaDataUtils.getArrayIndex( storageDiskDevices, index ); + + if ( storageDiskDevice == null ) { + // HDD does not exist, so create new storage (HDD) device + storageDiskDevice = this.vmConfig.addDiskStorageDevice(); + storageDiskDevice.setReadOnly( false ); + storageDiskDevice.setBusType( BusType.VIRTIO ); + String targetDevName = QemuMetaDataUtils.createAlphabeticalDeviceName( "vd", index ); + storageDiskDevice.setTargetDevice( targetDevName ); + storageDiskDevice.setStorage( StorageType.FILE, diskImagePath ); + + // add new created HDD to the metadata of the QemuMetaData object, too + this.addHddMetaData( storageDiskDevice ); + } else { + // HDD exists, so update existing storage (HDD) device + storageDiskDevice.setStorage( StorageType.FILE, diskImagePath ); + } + + return false; + } + + @Override + public boolean addDefaultNat() + { + return this.addEthernet( EtherType.NAT ); + } + + @Override + public void setOs( String vendorOsId ) + { + this.setOs( vendorOsId ); + } + + @Override + public boolean addDisplayName( String name ) + { + this.vmConfig.setName( name ); + + final boolean statusName = this.vmConfig.getName().equals( name ); + + return statusName; + } + + @Override + public boolean addRam( int mem ) + { + BigInteger memory = BigInteger.valueOf( mem ); + + this.vmConfig.setMemory( memory ); + this.vmConfig.setCurrentMemory( memory ); + + final boolean isMemorySet = this.vmConfig.getMemory().toString().equals( memory.toString() ); + final boolean isCurrentMemorySet = this.vmConfig.getCurrentMemory().toString().equals( memory.toString() ); + + return isMemorySet && isCurrentMemorySet; + } + + @Override + public void addFloppy( int index, String image, boolean readOnly ) + { + ArrayList<DiskFloppy> floppyDiskDevices = this.vmConfig.getDiskFloppyDevices(); + DiskFloppy floppyDiskDevice = QemuMetaDataUtils.getArrayIndex( floppyDiskDevices, index ); + + if ( floppyDiskDevice == null ) { + // floppy device does not exist, so create new floppy device + floppyDiskDevice = this.vmConfig.addDiskFloppyDevice(); + floppyDiskDevice.setBusType( BusType.FDC ); + String targetDevName = QemuMetaDataUtils.createAlphabeticalDeviceName( "fd", index ); + floppyDiskDevice.setTargetDevice( targetDevName ); + floppyDiskDevice.setReadOnly( readOnly ); + floppyDiskDevice.setStorage( StorageType.FILE, image ); + } else { + // floppy device exists, so update existing floppy device + floppyDiskDevice.setReadOnly( readOnly ); + floppyDiskDevice.setStorage( StorageType.FILE, image ); + } + } + + @Override + public boolean addCdrom( String image ) + { + return this.addCdrom( this.vmDeviceIndexCdromAdd++, image ); + } + + /** + * Adds CDROM drive to the QEMU virtual machine configuration. + * + * @param index current index of CDROM drive to be added to the virtual machine configuration. + * @param image path to a virtual image that will be inserted as CDROM into the drive. + * @return result state of adding the CDROM drive. + */ + public boolean addCdrom( int index, String image ) + { + ArrayList<DiskCdrom> cdromDiskDevices = this.vmConfig.getDiskCdromDevices(); + DiskCdrom cdromDiskDevice = QemuMetaDataUtils.getArrayIndex( cdromDiskDevices, index ); + + if ( cdromDiskDevice == null ) { + // CDROM device does not exist, so create new CDROM device + cdromDiskDevice = this.vmConfig.addDiskCdromDevice(); + cdromDiskDevice.setBusType( BusType.SATA ); + String targetDevName = QemuMetaDataUtils.createAlphabeticalDeviceName( "sd", index ); + cdromDiskDevice.setTargetDevice( targetDevName ); + cdromDiskDevice.setReadOnly( true ); + + if ( image == null ) { + cdromDiskDevice.setStorage( StorageType.BLOCK, CDROM_DEFAULT_PHYSICAL_DRIVE ); + } else { + cdromDiskDevice.setStorage( StorageType.FILE, image ); + } + } else { + // CDROM device exists, so update existing CDROM device + cdromDiskDevice.setReadOnly( true ); + + if ( image == null ) { + cdromDiskDevice.setStorage( StorageType.BLOCK, CDROM_DEFAULT_PHYSICAL_DRIVE ); + } else { + cdromDiskDevice.setStorage( StorageType.FILE, image ); + } + } + + return false; + } + + @Override + public boolean addCpuCoreCount( int nrOfCores ) + { + this.vmConfig.setVCpu( nrOfCores ); + + boolean isVCpuSet = this.vmConfig.getVCpu() == nrOfCores; + + return isVCpuSet; + } + + @Override + public void setSoundCard( SoundCardType type ) + { + QemuSoundCardMeta soundDeviceConfig = this.soundCards.get( type ); + ArrayList<Sound> soundDevices = this.vmConfig.getSoundDevices(); + Sound.Model soundDeviceModel = soundDeviceConfig.getModel(); + + if ( soundDevices.isEmpty() ) { + // create new sound device with 'soundDeviceModel' hardware + Sound soundDevice = this.vmConfig.addSoundDevice(); + soundDevice.setModel( soundDeviceModel ); + } else { + // update sound device model type of existing sound devices + for ( Sound soundDevice : soundDevices ) { + soundDevice.setModel( soundDeviceModel ); + } + } + } + + @Override + public SoundCardType getSoundCard() + { + ArrayList<Sound> soundDevices = this.vmConfig.getSoundDevices(); + SoundCardType soundDeviceType = SoundCardType.DEFAULT; + + if ( soundDevices.isEmpty() ) { + // the VM configuration does not contain a sound card device + soundDeviceType = SoundCardType.NONE; + } else { + // the VM configuration at least one sound card device, so return the type of the first one + Sound.Model soundDeviceModel = soundDevices.get( 0 ).getModel(); + soundDeviceType = QemuMetaDataUtils.convertSoundDeviceModel( soundDeviceModel ); + } + + return soundDeviceType; + } + + @Override + public void setDDAcceleration( DDAcceleration type ) + { + QemuDDAccelMeta accelerationConfig = this.ddacc.get( type ); + ArrayList<Graphics> graphicDevices = this.vmConfig.getGraphicDevices(); + ArrayList<Video> videoDevices = this.vmConfig.getVideoDevices(); + final boolean accelerationEnabled = accelerationConfig.isEnabled(); + + boolean acceleratedGraphicsAvailable = false; + + if ( graphicDevices.isEmpty() ) { + // add new graphics device with enabled acceleration to VM configuration + GraphicsSpice graphicSpiceDevice = this.vmConfig.addGraphicsSpiceDevice(); + graphicSpiceDevice.setOpenGl( true ); + acceleratedGraphicsAvailable = true; + } else { + // enable graphic acceleration of existing graphics devices + for ( Graphics graphicDevice : graphicDevices ) { + // set hardware acceleration for SPICE based graphics output + // other graphic devices do not support hardware acceleration + if ( graphicDevice instanceof GraphicsSpice ) { + GraphicsSpice.class.cast( graphicDevice ).setOpenGl( true ); + acceleratedGraphicsAvailable = true; + } + } + } + + // only configure hardware acceleration of video card(s) if graphics with hardware acceleration is available + if ( acceleratedGraphicsAvailable ) { + if ( videoDevices.isEmpty() ) { + // add new video device with enabled acceleration to VM configuration + Video videoDevice = this.vmConfig.addVideoDevice(); + videoDevice.setModel( Video.Model.VIRTIO ); + videoDevice.set2DAcceleration( true ); + videoDevice.set3DAcceleration( true ); + } else { + // enable graphic acceleration of existing graphics and video devices + for ( Video videoDevice : videoDevices ) { + // set hardware acceleration for Virtio GPUs + // other GPUs do not support hardware acceleration + if ( videoDevice.getModel() == Video.Model.VIRTIO ) { + videoDevice.set2DAcceleration( accelerationEnabled ); + videoDevice.set3DAcceleration( accelerationEnabled ); + } + } + } + } + } + + @Override + public DDAcceleration getDDAcceleration() + { + ArrayList<Graphics> graphicsDevices = this.vmConfig.getGraphicDevices(); + ArrayList<Video> videoDevices = this.vmConfig.getVideoDevices(); + DDAcceleration accelerationState = DDAcceleration.OFF; + + boolean acceleratedGraphicsAvailable = false; + boolean acceleratedVideoDevAvailable = false; + + // search for hardware accelerated graphics + for ( Graphics graphicDevice : graphicsDevices ) { + // only SPICE based graphic devices support hardware acceleration + if ( graphicDevice instanceof GraphicsSpice ) { + acceleratedGraphicsAvailable = true; + break; + } + } + + // search for hardware accelerated video devices + for ( Video videoDevice : videoDevices ) { + // only Virtio based video devices support hardware acceleration + if ( videoDevice.getModel() == Video.Model.VIRTIO ) { + acceleratedVideoDevAvailable = true; + break; + } + } + + // hardware acceleration is available if at least one accelerated graphics and video device is available + if ( acceleratedGraphicsAvailable && acceleratedVideoDevAvailable ) { + accelerationState = DDAcceleration.ON; + } else { + accelerationState = DDAcceleration.OFF; + } + + return accelerationState; + } + + @Override + public void setHWVersion( HWVersion type ) + { + // NOT supported by the QEMU hypervisor + } + + @Override + public HWVersion getHWVersion() + { + // NOT supported by the QEMU hypervisor + return null; + } + + @Override + public void setEthernetDevType( int cardIndex, EthernetDevType type ) + { + QemuEthernetDevTypeMeta networkDeviceConfig = this.networkCards.get( type ); + ArrayList<Interface> networkDevices = this.vmConfig.getInterfaceDevices(); + Interface networkDevice = QemuMetaDataUtils.getArrayIndex( networkDevices, cardIndex ); + Interface.Model networkDeviceModel = networkDeviceConfig.getModel(); + + if ( networkDevice != null ) { + networkDevice.setModel( networkDeviceModel ); + } + } + + @Override + public EthernetDevType getEthernetDevType( int cardIndex ) + { + ArrayList<Interface> networkDevices = this.vmConfig.getInterfaceDevices(); + Interface networkDevice = QemuMetaDataUtils.getArrayIndex( networkDevices, cardIndex ); + EthernetDevType networkDeviceType = EthernetDevType.NONE; + + if ( networkDevice == null ) { + // network interface device is not present + networkDeviceType = EthernetDevType.NONE; + } else { + // get model of existing network interface device + Interface.Model networkDeviceModel = networkDevice.getModel(); + networkDeviceType = QemuMetaDataUtils.convertNetworkDeviceModel( networkDeviceModel ); + } + + return networkDeviceType; + } + + @Override + public void setMaxUsbSpeed( UsbSpeed speed ) + { + QemuUsbSpeedMeta usbControllerConfig = this.usbSpeeds.get( speed ); + ArrayList<ControllerUsb> usbControllerDevices = this.vmConfig.getUsbControllerDevices(); + ControllerUsb.Model usbControllerModel = usbControllerConfig.getModel(); + + if ( usbControllerDevices.isEmpty() ) { + // add new USB controller with specified speed 'usbControllerModel' + ControllerUsb usbControllerDevice = this.vmConfig.addControllerUsbDevice(); + usbControllerDevice.setModel( usbControllerModel ); + } else { + // update model of all USB controller devices to support the maximum speed + for ( ControllerUsb usbControllerDevice : usbControllerDevices ) { + usbControllerDevice.setModel( usbControllerModel ); + } + } + } + + @Override + public UsbSpeed getMaxUsbSpeed() + { + ArrayList<ControllerUsb> usbControllerDevices = this.vmConfig.getUsbControllerDevices(); + UsbSpeed maxUsbSpeed = VmMetaData.UsbSpeed.NONE; + int maxUsbSpeedNumeric = 0; + + for ( ControllerUsb usbControllerDevice : usbControllerDevices ) { + ControllerUsb.Model usbControllerModel = usbControllerDevice.getModel(); + + for ( Entry<UsbSpeed, QemuUsbSpeedMeta> usbSpeedEntry : this.usbSpeeds.entrySet() ) { + QemuUsbSpeedMeta usbSpeed = usbSpeedEntry.getValue(); + if ( usbSpeed.getSpeed() > maxUsbSpeedNumeric && usbSpeed.getModel() == usbControllerModel ) { + maxUsbSpeed = usbSpeedEntry.getKey(); + maxUsbSpeedNumeric = usbSpeed.getSpeed(); + } + } + } + + return maxUsbSpeed; + } + + @Override + public byte[] getDefinitionArray() + { + String configuration = this.vmConfig.toString(); + + if ( configuration == null ) { + return null; + } else { + // append newline at the end of the XML content to match the structure of an original Libvirt XML file + configuration += System.lineSeparator(); + return configuration.getBytes( StandardCharsets.UTF_8 ); + } + } + + @Override + public boolean addEthernet( EtherType type ) + { + return this.addEthernet( this.vmDeviceIndexEthernetAdd++, type ); + } + + /** + * Adds an ethernet card to the QEMU virtual machine configuration. + * + * @param index current index of the ethernet card to be added to the virtual machine + * configuration. + * @param type card model of the ethernet card. + * @return result state of adding the ethernet card. + */ + public boolean addEthernet( int index, EtherType type ) + { + QemuEthernetDevTypeMeta defaultNetworkDeviceConfig = this.networkCards.get( EthernetDevType.AUTO ); + ArrayList<Interface> interfaceDevices = this.vmConfig.getInterfaceDevices(); + Interface interfaceDevice = QemuMetaDataUtils.getArrayIndex( interfaceDevices, index ); + + final Interface.Model defaultNetworkDeviceModel = defaultNetworkDeviceConfig.getModel(); + + if ( interfaceDevice == null ) { + // network interface device does not exist, so create new network interface device + switch ( type ) { + case BRIDGED: + // add network bridge interface device + interfaceDevice = this.vmConfig.addInterfaceBridgeDevice(); + interfaceDevice.setModel( defaultNetworkDeviceModel ); + interfaceDevice.setSource( QemuMetaData.NETWORK_DEFAULT_BRIDGE ); + break; + case HOST_ONLY: + // add network interface device with link to the isolated host network + interfaceDevice = this.vmConfig.addInterfaceNetworkDevice(); + interfaceDevice.setModel( defaultNetworkDeviceModel ); + interfaceDevice.setSource( QemuMetaData.NETWORK_DEFAULT_HOST_ONLY ); + break; + case NAT: + // add network interface device with link to the NAT network + interfaceDevice = this.vmConfig.addInterfaceNetworkDevice(); + interfaceDevice.setModel( defaultNetworkDeviceModel ); + interfaceDevice.setSource( QemuMetaData.NETWORK_DEFAULT_NAT ); + break; + } + } else { + // network interface device exists, so update existing network interface device + switch ( type ) { + case BRIDGED: + interfaceDevice.setType( Interface.Type.BRIDGE ); + interfaceDevice.setSource( QemuMetaData.NETWORK_DEFAULT_BRIDGE ); + break; + case HOST_ONLY: + interfaceDevice.setType( Interface.Type.NETWORK ); + interfaceDevice.setSource( QemuMetaData.NETWORK_DEFAULT_HOST_ONLY ); + break; + case NAT: + interfaceDevice.setType( Interface.Type.NETWORK ); + interfaceDevice.setSource( QemuMetaData.NETWORK_DEFAULT_NAT ); + break; + } + } + + return false; + } + + @Override + public Virtualizer getVirtualizer() + { + return QemuMetaData.VIRTUALIZER; + } + + @Override + public boolean tweakForNonPersistent() + { + // NOT implemented yet + return false; + } + + @Override + public void registerVirtualHW() + { + // @formatter:off + soundCards.put( VmMetaData.SoundCardType.NONE, new QemuSoundCardMeta( null ) ); + soundCards.put( VmMetaData.SoundCardType.DEFAULT, new QemuSoundCardMeta( Sound.Model.ICH9 ) ); + soundCards.put( VmMetaData.SoundCardType.SOUND_BLASTER, new QemuSoundCardMeta( Sound.Model.SB16 ) ); + soundCards.put( VmMetaData.SoundCardType.ES, new QemuSoundCardMeta( Sound.Model.ES1370 ) ); + soundCards.put( VmMetaData.SoundCardType.AC, new QemuSoundCardMeta( Sound.Model.AC97 ) ); + soundCards.put( VmMetaData.SoundCardType.HD_AUDIO, new QemuSoundCardMeta( Sound.Model.ICH9 ) ); + + ddacc.put( VmMetaData.DDAcceleration.OFF, new QemuDDAccelMeta( false ) ); + ddacc.put( VmMetaData.DDAcceleration.ON, new QemuDDAccelMeta( true ) ); + + hwversion.put( VmMetaData.HWVersion.DEFAULT, new QemuHWVersionMeta( 0 ) ); + + networkCards.put( VmMetaData.EthernetDevType.NONE, new QemuEthernetDevTypeMeta( null ) ); + networkCards.put( VmMetaData.EthernetDevType.AUTO, new QemuEthernetDevTypeMeta( Interface.Model.VIRTIO_NET_PCI ) ); + networkCards.put( VmMetaData.EthernetDevType.PCNETPCI2, new QemuEthernetDevTypeMeta( Interface.Model.PCNET ) ); + networkCards.put( VmMetaData.EthernetDevType.E1000, new QemuEthernetDevTypeMeta( Interface.Model.E1000 ) ); + networkCards.put( VmMetaData.EthernetDevType.E1000E, new QemuEthernetDevTypeMeta( Interface.Model.E1000E ) ); + networkCards.put( VmMetaData.EthernetDevType.VMXNET3, new QemuEthernetDevTypeMeta( Interface.Model.VMXNET3 ) ); + networkCards.put( VmMetaData.EthernetDevType.PARAVIRT, new QemuEthernetDevTypeMeta( Interface.Model.VIRTIO_NET_PCI ) ); + + usbSpeeds.put( VmMetaData.UsbSpeed.NONE, new QemuUsbSpeedMeta( 0, ControllerUsb.Model.NONE ) ); + usbSpeeds.put( VmMetaData.UsbSpeed.USB1_1, new QemuUsbSpeedMeta( 1, ControllerUsb.Model.ICH9_UHCI1 ) ); + usbSpeeds.put( VmMetaData.UsbSpeed.USB2_0, new QemuUsbSpeedMeta( 2, ControllerUsb.Model.ICH9_EHCI1 ) ); + usbSpeeds.put( VmMetaData.UsbSpeed.USB3_0, new QemuUsbSpeedMeta( 3, ControllerUsb.Model.QEMU_XHCI ) ); + // @formatter:on + } +} diff --git a/src/main/java/org/openslx/vm/QemuMetaDataUtils.java b/src/main/java/org/openslx/vm/QemuMetaDataUtils.java new file mode 100644 index 0000000..b6ac92e --- /dev/null +++ b/src/main/java/org/openslx/vm/QemuMetaDataUtils.java @@ -0,0 +1,188 @@ +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; + +/** + * Collection of utils to convert data types from bwLehrpool to Libvirt and vice versa. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class QemuMetaDataUtils +{ + /** + * Converts a Libvirt disk device bus type to a VM metadata driver bus type. + * + * @param busType Libvirt disk device bus type. + * @return VM metadata bus type of the disk drive. + */ + public static DriveBusType convertBusType( Disk.BusType busType ) + { + DriveBusType type = null; + + switch ( busType ) { + case IDE: + type = DriveBusType.IDE; + break; + case SATA: + type = DriveBusType.SATA; + break; + case SCSI: + type = DriveBusType.SCSI; + break; + default: + type = null; + break; + } + + return type; + } + + /** + * Converts a VM metadata driver bus type to a Libvirt disk device bus type. + * + * @param busType VM metadata bus type of the disk drive. + * @return Libvirt disk device bus type. + */ + public static Disk.BusType convertBusType( DriveBusType busType ) + { + Disk.BusType type = null; + + switch ( busType ) { + case IDE: + type = BusType.IDE; + break; + case SATA: + type = BusType.SATA; + break; + case SCSI: + type = BusType.SCSI; + break; + } + + return type; + } + + /** + * Converts a Libvirt sound device model to a VM metadata sound card type. + * + * @param soundDeviceModel Libvirt sound device model. + * @return VM metadata sound card type. + */ + public static SoundCardType convertSoundDeviceModel( Sound.Model soundDeviceModel ) + { + SoundCardType type = SoundCardType.NONE; + + switch ( soundDeviceModel ) { + case AC97: + type = SoundCardType.AC; + break; + case ES1370: + type = SoundCardType.ES; + break; + case ICH6: + type = SoundCardType.HD_AUDIO; + break; + case ICH9: + type = SoundCardType.HD_AUDIO; + break; + case SB16: + type = SoundCardType.SOUND_BLASTER; + break; + } + + return type; + } + + /** + * Converts a Libvirt network device model to a VM metadata ethernet device type. + * + * @param soundDeviceModel Libvirt network device model. + * @return VM metadata ethernet device type. + */ + public static EthernetDevType convertNetworkDeviceModel( Interface.Model networkDeviceModel ) + { + EthernetDevType type = EthernetDevType.NONE; + + switch ( networkDeviceModel ) { + case E1000: + type = EthernetDevType.E1000; + break; + case E1000E: + type = EthernetDevType.E1000E; + break; + case PCNET: + type = EthernetDevType.PCNETPCI2; + break; + case VIRTIO: + type = EthernetDevType.PARAVIRT; + break; + case VIRTIO_NET_PCI: + type = EthernetDevType.PARAVIRT; + break; + case VIRTIO_NET_PCI_NON_TRANSITIONAL: + type = EthernetDevType.PARAVIRT; + break; + case VIRTIO_NET_PCI_TRANSITIONAL: + type = EthernetDevType.PARAVIRT; + break; + case VMXNET3: + type = EthernetDevType.VMXNET3; + break; + default: + type = EthernetDevType.AUTO; + break; + } + + return type; + } + + /** + * Returns an item from a given {@link ArrayList}. + * + * The item is selected by a given index. If the item is not available within the + * {@link ArrayList}, <code>null</code> is returned. + * + * @param <T> type of the {@link ArrayList}. + * @param array {@link ArrayList} of type <code>T</code>. + * @param index selects the item from the {@link ArrayList}. + * @return selected item of the {@link ArrayList}. + */ + public static <T> T getArrayIndex( ArrayList<T> array, int index ) + { + T ret; + + try { + ret = array.get( index ); + } catch ( IndexOutOfBoundsException e ) { + ret = null; + } + + return ret; + } + + /** + * Creates an alphabetical device name constructed from a device prefix and a device number. + * + * @param devicePrefix prefix of the constructed device name. + * @param deviceNumber number of the device. + * @return alphabetical device name. + */ + public static String createAlphabeticalDeviceName( String devicePrefix, int deviceNumber ) + { + if ( deviceNumber < 0 || deviceNumber >= ( 'z' - 'a' ) ) { + String errorMsg = new String( "Device number is out of range to be able to create a valid device name." ); + throw new IllegalArgumentException( errorMsg ); + } + + return devicePrefix + ( 'a' + deviceNumber ); + } +} diff --git a/src/main/java/org/openslx/vm/UnsupportedVirtualizerFormatException.java b/src/main/java/org/openslx/vm/UnsupportedVirtualizerFormatException.java new file mode 100644 index 0000000..f19b1ff --- /dev/null +++ b/src/main/java/org/openslx/vm/UnsupportedVirtualizerFormatException.java @@ -0,0 +1,9 @@ +package org.openslx.vm; + +@SuppressWarnings( "serial" ) +public class UnsupportedVirtualizerFormatException extends Exception +{ + public UnsupportedVirtualizerFormatException(String message) { + super(message); + } +}
\ No newline at end of file diff --git a/src/main/java/org/openslx/vm/VboxConfig.java b/src/main/java/org/openslx/vm/VboxConfig.java new file mode 100644 index 0000000..9724b6a --- /dev/null +++ b/src/main/java/org/openslx/vm/VboxConfig.java @@ -0,0 +1,631 @@ +package org.openslx.vm; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.XMLConstants; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; + +import org.apache.log4j.Logger; +import org.openslx.util.Util; +import org.openslx.util.XmlHelper; +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; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Class handling the parsing of a .vbox machine description file + */ +public class VboxConfig +{ + private static final Logger LOGGER = Logger.getLogger( VboxConfig.class ); + + // key information set during initial parsing of the XML file + private String osName = new String(); + private ArrayList<HardDisk> hddsArray = new ArrayList<HardDisk>(); + + // XPath and DOM parsing related members + private Document doc = null; + + // list of nodes to automatically remove when reading the vbox file + private static String[] blacklist = { + "/VirtualBox/Machine/Hardware/GuestProperties", + "/VirtualBox/Machine/Hardware/VideoCapture", + "/VirtualBox/Machine/Hardware/HID", + "/VirtualBox/Machine/Hardware/LPT", + "/VirtualBox/Machine/Hardware/SharedFolders", + "/VirtualBox/Machine/Hardware/Network/Adapter[@enabled='true']/*", + "/VirtualBox/Machine/ExtraData", + "/VirtualBox/Machine/StorageControllers/StorageController/AttachedDevice[not(@type='HardDisk')]", + "/VirtualBox/Machine/MediaRegistry/FloppyImages", + "/VirtualBox/Machine/MediaRegistry/DVDImages" }; + + public static enum PlaceHolder + { + FLOPPYUUID( "%VM_FLOPPY_UUID%" ), FLOPPYLOCATION( "%VM_FLOPPY_LOCATION%" ), CPU( "%VM_CPU_CORES%" ), MEMORY( "%VM_RAM%" ), MACHINEUUID( "%VM_MACHINE_UUID%" ), NETWORKMAC( + "%VM_NIC_MAC%" ), HDDLOCATION( "%VM_HDD_LOCATION%" ), HDDUUID( "%VM_HDD_UUID_" ); + + private final String holderName; + + private PlaceHolder( String name ) + { + this.holderName = name; + } + + @Override + public String toString() + { + return holderName; + } + } + + /** + * Creates a vbox configuration by constructing a DOM from the given VirtualBox machine + * configuration file. + * Will validate the given file against the VirtualBox XSD schema and only proceed if it is + * valid. + * + * @param file the VirtualBox machine configuration file + * @throws IOException if an error occurs while reading the file + * @throws UnsupportedVirtualizerFormatException if the given file is not a valid VirtualBox + * configuration file. + */ + public VboxConfig( File file ) throws IOException, UnsupportedVirtualizerFormatException + { + // first validate xml + try { + SchemaFactory factory = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI ); + InputStream xsdStream = VboxConfig.class.getResourceAsStream( "/master-sync-shared/xml/VirtualBox-settings.xsd" ); + if ( xsdStream == null ) { + LOGGER.warn( "Cannot validate Vbox XML: No XSD found in JAR" ); + } else { + Schema schema = factory.newSchema( new StreamSource( xsdStream ) ); + Validator validator = schema.newValidator(); + validator.validate( new StreamSource( file ) ); + } + } catch ( SAXException e ) { + LOGGER.error( "Selected vbox file was not validated against the XSD schema: " + e.getMessage() ); + } + // valid xml, try to create the DOM + doc = XmlHelper.parseDocumentFromStream( new FileInputStream( file ) ); + doc = XmlHelper.removeFormattingNodes( doc ); + if ( doc == null ) + throw new UnsupportedVirtualizerFormatException( "Could not create DOM from given VirtualBox machine configuration file!" ); + init(); + } + + /** + * Creates an vbox configuration by constructing a DOM from the XML content given as a byte + * array. + * + * @param machineDescription content of the XML file saved as a byte array. + * @param length of the machine description byte array. + * @throws IOException if an + */ + public VboxConfig( byte[] machineDescription, int length ) throws UnsupportedVirtualizerFormatException + { + ByteArrayInputStream is = new ByteArrayInputStream( machineDescription ); + doc = XmlHelper.parseDocumentFromStream( is ); + if ( doc == null ) { + LOGGER.error( "Failed to create a DOM from given machine description." ); + throw new UnsupportedVirtualizerFormatException( "Could not create DOM from given machine description as. byte array." ); + } + init(); + } + + /** + * Main initialization functions parsing the document created during the constructor. + * @throws UnsupportedVirtualizerFormatException + */ + private void init() throws UnsupportedVirtualizerFormatException + { + if ( Util.isEmptyString( getDisplayName() ) ) { + throw new UnsupportedVirtualizerFormatException( "Machine doesn't have a name" ); + } + try { + ensureHardwareUuid(); + setOsType(); + fixUsb(); // Since we now support selecting specific speed + if ( checkForPlaceholders() ) { + return; + } + setHdds(); + removeBlacklistedElements(); + addPlaceHolders(); + } catch ( XPathExpressionException e ) { + LOGGER.debug( "Could not initialize VBoxConfig", e ); + return; + } + } + + private void fixUsb() + { + NodeList list = findNodes( "/VirtualBox/Machine/Hardware/USB/Controllers/Controller" ); + if ( list != null && list.getLength() != 0 ) { + LOGGER.info( "USB present, not fixing anything" ); + return; + } + // If there's no USB section, this can mean two things: + // 1) Old config that would always default to USB 2.0 for "USB enabled" or nothing for disabled + // 2) New config with USB disabled + list = findNodes( "/VirtualBox/OpenSLX/USB[@disabled]" ); + if ( list != null && list.getLength() != 0 ) { + LOGGER.info( "USB explicitly disabled" ); + return; // Explicitly marked as disabled, do nothing + } + // We assume case 1) and add USB 2.0 + LOGGER.info( "Fixing USB: Adding USB 2.0" ); + Element controller; + Element node = createNodeRecursive( "/VirtualBox/Machine/Hardware/USB/Controllers" ); + controller = addNewNode( node, "Controller" ); + controller.setAttribute( "name", "OHCI" ); + controller.setAttribute( "type", "OHCI" ); + controller = addNewNode( node, "Controller" ); + controller.setAttribute( "name", "EHCI" ); + controller.setAttribute( "type", "EHCI" ); + } + + /** + * Saves the machine's uuid as hardware uuid to prevent VMs from + * believing in a hardware change. + * + * @throws XPathExpressionException + * @throws UnsupportedVirtualizerFormatException + */ + private void ensureHardwareUuid() throws XPathExpressionException, UnsupportedVirtualizerFormatException + { + // we will need the machine uuid, so get it + String machineUuid = XmlHelper.XPath.compile( "/VirtualBox/Machine/@uuid" ).evaluate( this.doc ); + if ( machineUuid.isEmpty() ) { + LOGGER.error( "Machine UUID empty, should never happen!" ); + throw new UnsupportedVirtualizerFormatException( "XML doesn't contain a machine uuid" ); + } + + NodeList hwNodes = findNodes( "/VirtualBox/Machine/Hardware" ); + int count = hwNodes.getLength(); + if ( count != 1 ) { + throw new UnsupportedVirtualizerFormatException( "Zero or more '/VirtualBox/Machine/Hardware' node were found, should never happen!" ); + } + Element hw = (Element)hwNodes.item( 0 ); + String hwUuid = hw.getAttribute( "uuid" ); + if ( !hwUuid.isEmpty() ) { + LOGGER.info( "Found hardware uuid: " + hwUuid ); + return; + } else { + if ( !addAttributeToNode( hw, "uuid", machineUuid ) ) { + LOGGER.error( "Failed to set machine UUID '" + machineUuid + "' as hardware UUID." ); + return; + } + LOGGER.info( "Saved machine UUID as hardware UUID." ); + } + } + + /** + * Self-explanatory. + */ + public void addPlaceHolders() + { + // placeholder for the machine uuid + changeAttribute( "/VirtualBox/Machine", "uuid", PlaceHolder.MACHINEUUID.toString() ); + + // placeholder for the location of the virtual hdd + changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk", "location", PlaceHolder.HDDLOCATION.toString() ); + + // placeholder for the memory + changeAttribute( "/VirtualBox/Machine/Hardware/Memory", "RAMSize", PlaceHolder.MEMORY.toString() ); + + // placeholder for the CPU + changeAttribute( "/VirtualBox/Machine/Hardware/CPU", "count", PlaceHolder.CPU.toString() ); + + // placeholder for the MACAddress + changeAttribute( "/VirtualBox/Machine/Hardware/Network/Adapter", "MACAddress", PlaceHolder.NETWORKMAC.toString() ); + + NodeList hdds = findNodes( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk" ); + for ( int i = 0; i < hdds.getLength(); i++ ) { + Element hdd = (Element)hdds.item( i ); + if ( hdd == null ) + continue; + String hddUuid = hdd.getAttribute( "uuid" ); + hdd.setAttribute( "uuid", PlaceHolder.HDDUUID.toString() + i + "%" ); + NodeList images = findNodes( "/VirtualBox/Machine/StorageControllers/StorageController/AttachedDevice/Image" ); + for ( int j = 0; j < images.getLength(); j++ ) { + Element image = (Element)images.item( j ); + if ( image == null ) + continue; + if ( hddUuid.equals( image.getAttribute( "uuid" ) ) ) { + image.setAttribute( "uuid", PlaceHolder.HDDUUID.toString() + i + "%" ); + break; + } + } + } + } + + /** + * Function checks if the placeholders are present + * + * @return true if the placeholders are present, false otherwise + */ + private boolean checkForPlaceholders() + { + // TODO this should be more robust... + NodeList hdds = findNodes( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk" ); + for ( int i = 0; i < hdds.getLength(); i++ ) { + Element hdd = (Element)hdds.item( i ); + if ( hdd == null ) + continue; + if ( hdd.getAttribute( "location" ).equals( PlaceHolder.HDDLOCATION.toString() ) ) { + return true; + } + } + return false; + } + + /** + * Called during init(), prunes the DOM from the elements blacklisted defined + * in the member blacklist, a list of XPath expressions as String + * + * @throws XPathExpressionException + */ + private void removeBlacklistedElements() throws XPathExpressionException + { + // iterate over the blackList + for ( String blackedTag : blacklist ) { + XPathExpression blackedExpr = XmlHelper.XPath.compile( blackedTag ); + NodeList blackedNodes = (NodeList)blackedExpr.evaluate( this.doc, XPathConstants.NODESET ); + for ( int i = 0; i < blackedNodes.getLength(); i++ ) { + // go through the child nodes of the blacklisted ones -> why? + Element child = (Element)blackedNodes.item( i ); + removeNode( child ); + } + } + } + + /** + * Getter for the display name + * + * @return the display name of this VM + */ + public String getDisplayName() + { + try { + return XmlHelper.XPath.compile( "/VirtualBox/Machine/@name" ).evaluate( this.doc ); + } catch ( XPathExpressionException e ) { + return ""; + } + } + + /** + * Function finds and saves the name of the guest OS + * + * @throws XPathExpressionException + */ + public void setOsType() throws XPathExpressionException + { + String os = XmlHelper.XPath.compile( "/VirtualBox/Machine/@OSType" ).evaluate( this.doc ); + if ( os != null && !os.isEmpty() ) { + osName = os; + } + } + + /** + * Getter for the parsed guest OS name + * + * @return name of the guest OS + */ + public String getOsName() + { + return osName; + } + + /** + * Search for attached hard drives and determine their controller and their path. + * + * @throws XPathExpressionException + */ + public void setHdds() throws XPathExpressionException + { + XPathExpression hddsExpr = XmlHelper.XPath.compile( "/VirtualBox/Machine/StorageControllers/StorageController/AttachedDevice[@type='HardDisk']/Image" ); + NodeList nodes = (NodeList)hddsExpr.evaluate( this.doc, XPathConstants.NODESET ); + if ( nodes == null ) { + LOGGER.error( "Failed to find attached hard drives." ); + return; + } + for ( int i = 0; i < nodes.getLength(); i++ ) { + Element hddElement = (Element)nodes.item( i ); + if ( hddElement == null ) + continue; + String uuid = hddElement.getAttribute( "uuid" ); + if ( uuid.isEmpty() ) + continue; + // got uuid, check if it was registered + XPathExpression hddsRegistered = XmlHelper.XPath.compile( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk[@uuid='" + uuid + "']" ); + NodeList hddsRegisteredNodes = (NodeList)hddsRegistered.evaluate( this.doc, XPathConstants.NODESET ); + if ( hddsRegisteredNodes == null || hddsRegisteredNodes.getLength() != 1 ) { + LOGGER.error( "Found hard disk with uuid '" + uuid + "' which does not appear (unique) in the Media Registry. Skipping." ); + continue; + } + Element hddElementReg = (Element)hddsRegisteredNodes.item( 0 ); + if ( hddElementReg == null ) + continue; + String fileName = hddElementReg.getAttribute( "location" ); + String type = hddElementReg.getAttribute( "type" ); + if ( !type.equals( "Normal" ) && !type.equals( "Writethrough" ) ) { + LOGGER.warn( "Type of the disk file is neither 'Normal' nor 'Writethrough' but: " + type ); + LOGGER.warn( "This makes the image not directly modificable, which might lead to problems when editing it locally." ); + } + // search if it is also attached to a controller + Node hddDevice = hddElement.getParentNode(); + if ( hddDevice == null ) { + LOGGER.error( "HDD node had a null parent, shouldn't happen" ); + continue; + } + Element hddController = (Element)hddDevice.getParentNode(); + if ( hddController == null ) { + LOGGER.error( "HDD node had a null parent, shouldn't happen" ); + continue; + } + String controllerMode = hddController.getAttribute( "type" ); + String controllerType = hddController.getAttribute( "name" ); + DriveBusType busType; + if ( controllerType.equals( "NVMe" ) ) { + busType = DriveBusType.NVME; + } else { + try { + // This assumes the type in the xml matches our enum constants. + busType = DriveBusType.valueOf( controllerType ); + } catch (Exception e) { + LOGGER.warn( "Skipping unknown HDD controller type '" + controllerType + "'" ); + continue; + } + } + LOGGER.info( "Adding hard disk with controller: " + busType + " (" + controllerMode + ") from file '" + fileName + "'." ); + hddsArray.add( new HardDisk( controllerMode, busType, fileName ) ); + } + } + + /** + * Getter for the list of detected hard drives. + * + * @return list of disk drives. + */ + public ArrayList<HardDisk> getHdds() + { + return hddsArray; + } + + /** + * Detect if the vbox file has any machine snapshot by looking at + * the existance of '/VirtualBox/Machine/Snapshot' elements. + * + * @return true if a machine snapshot is present, false otherwise. + */ + public boolean isMachineSnapshot() + { + // check if the vbox configuration file contains some machine snapshots. + // by looking at the existance of /VirtualBox/Machine/Snapshot + NodeList machineSnapshots = findNodes( "/VirtualBox/Machine/Snapshot" ); + return machineSnapshots != null && machineSnapshots.getLength() > 0; + } + + /** + * Searches the DOM for the elements matching the given XPath expression. + * + * @param xpath expression to search the DOM with + * @return nodes found by evaluating given XPath expression + */ + public NodeList findNodes( String xpath ) + { + NodeList nodes = null; + try { + XPathExpression expr = XmlHelper.XPath.compile( xpath ); + Object nodesObject = expr.evaluate( this.doc, XPathConstants.NODESET ); + nodes = (NodeList)nodesObject; + } catch ( XPathExpressionException e ) { + LOGGER.error( "Could not build path", e ); + } + return nodes; + } + + /** + * Function used to change the value of an attribute of given element. + * The given xpath to the element needs to find a single node, or this function will return + * false. If only one element was found, it will return the result of calling addAttributeToNode. + * Note that due to the way setAttribute() works, this function to create the attribute if it + * doesn't exists. + * + * @param elementXPath given as an xpath expression + * @param attribute attribute to change + * @param value to set the attribute to + */ + public boolean changeAttribute( String elementXPath, String attribute, String value ) + { + NodeList nodes = findNodes( elementXPath ); + if ( nodes == null || nodes.getLength() != 1 ) { + LOGGER.error( "No unique node could be found for: " + elementXPath ); + return false; + } + return addAttributeToNode( nodes.item( 0 ), attribute, value ); + } + + /** + * Add given attribute with given value to the given node. + * NOTE: this will overwrite the attribute of the node if it already exists. + * + * @param node to add the attribute to + * @param attribute attribute to add to the node + * @param value of the attribute + * @return true if successful, false otherwise + */ + public boolean addAttributeToNode( Node node, String attribute, String value ) + { + if ( node == null || node.getNodeType() != Node.ELEMENT_NODE ) { + LOGGER.error( "Trying to change attribute of a non element node!" ); + return false; + } + try { + ( (Element)node ).setAttribute( attribute, value ); + } catch ( DOMException e ) { + LOGGER.error( "Failed set '" + attribute + "' to '" + value + "' of xml node '" + node.getNodeName() + "': ", e ); + return false; + } + return true; + } + + /** + * Adds a new node named nameOfNewNode to the given parent found by parentXPath. + * + * @param parentXPath XPath expression to the parent + * @param nameOfnewNode name of the node to be added + * @return the newly added Node + */ + public Node addNewNode( String parentXPath, String childName ) + { + NodeList possibleParents = findNodes( parentXPath ); + if ( possibleParents == null || possibleParents.getLength() != 1 ) { + LOGGER.error( "Could not find unique parent node to add new node to: " + parentXPath ); + return null; + } + return addNewNode( possibleParents.item( 0 ), childName ); + } + + public Element createNodeRecursive( String xPath ) + { + String[] nodeNames = xPath.split( "/" ); + Node parent = this.doc; + Element latest = null; + for ( int nodeIndex = 0; nodeIndex < nodeNames.length; ++nodeIndex ) { + if ( nodeNames[nodeIndex].length() == 0 ) + continue; + Node node = skipNonElementNodes( parent.getFirstChild() ); + while ( node != null ) { + if ( node.getNodeType() == Node.ELEMENT_NODE && nodeNames[nodeIndex].equals( node.getNodeName() ) ) + break; // Found existing + // Check next on same level + node = skipNonElementNodes( node.getNextSibling() ); + } + if ( node == null ) { + node = doc.createElement( nodeNames[nodeIndex] ); + parent.appendChild( node ); + } + parent = node; + latest = (Element)node; + } + return latest; + } + + private Element skipNonElementNodes( Node nn ) + { + while ( nn != null && nn.getNodeType() != Node.ELEMENT_NODE ) { + nn = nn.getNextSibling(); + } + return (Element)nn; + } + + public void setExtraData( String key, String value ) + { + NodeList nl = findNodes( "/VirtualBox/Machine/ExtraData/ExtraDataItem[@name='" + key + "']" ); + Element e = null; + for ( int i = 0; i < nl.getLength(); ++i ) { + Node n = nl.item( i ); + if ( n.getNodeType() == Node.ELEMENT_NODE ) { + e = (Element)n; + break; + } + } + if ( e == null ) { + Element p = createNodeRecursive( "/VirtualBox/Machine/ExtraData" ); + e = addNewNode( p, "ExtraDataItem" ); + e.setAttribute( "name", key ); + } + e.setAttribute( "value", value ); + } + + /** + * Creates a new element to the given parent node. + * + * @param parent to add the new element to + * @param childName name of the new element to create + * @return the newly created node + */ + public Element addNewNode( Node parent, String childName ) + { + if ( parent == null || parent.getNodeType() != Node.ELEMENT_NODE ) { + return null; + } + Element newNode = null; + try { + newNode = doc.createElement( childName ); + parent.appendChild( newNode ); + } catch ( DOMException e ) { + LOGGER.error( "Failed to add '" + childName + "' to '" + parent.getNodeName() + "'." ); + } + return newNode; + } + + /** + * Helper to remove given node from the DOM. + * + * @param node Node object to remove. + */ + private void removeNode( Node node ) + { + if ( node == null ) + return; + Node parent = node.getParentNode(); + if ( parent != null ) + parent.removeChild( node ); + } + + /** + * Helper to output the DOM as a String. + * + * @param prettyPrint sets whether to indent the output + * @return (un-)formatted XML + */ + public String toString( boolean prettyPrint ) + { + return XmlHelper.getXmlFromDocument( doc, prettyPrint ); + } + + /** + * Remove all nodes with name childName from parentPath + * @param parentPath XPath to parent node of where child nodes are to be deleted + * @param childName Name of nodes to delete + */ + public void removeNodes( String parentPath, String childName ) + { + NodeList parentNodes = findNodes( parentPath ); + // XPath might match multiple nodes + for ( int i = 0; i < parentNodes.getLength(); ++i ) { + Node parent = parentNodes.item( i ); + List<Node> delList = new ArrayList<>( 0 ); + // Iterate over child nodes + for ( Node child = parent.getFirstChild(); child != null; child = child.getNextSibling() ) { + if ( childName.equals( child.getNodeName() ) ) { + // Remember all to be deleted (don't delete while iterating) + delList.add( child ); + } + } + // Now delete them all + for ( Node child : delList ) { + parent.removeChild( child ); + } + } + } +} diff --git a/src/main/java/org/openslx/vm/VboxMetaData.java b/src/main/java/org/openslx/vm/VboxMetaData.java new file mode 100644 index 0000000..a6ac14d --- /dev/null +++ b/src/main/java/org/openslx/vm/VboxMetaData.java @@ -0,0 +1,536 @@ +package org.openslx.vm; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; +import java.util.UUID; + +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.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; +import org.w3c.dom.NodeList; + +class VBoxSoundCardMeta +{ + public final boolean isPresent; + public final String value; + + public VBoxSoundCardMeta( boolean present, String val ) + { + isPresent = present; + value = val; + } +} + +class VBoxDDAccelMeta +{ + public final boolean isPresent; + + public VBoxDDAccelMeta( boolean present ) + { + isPresent = present; + } +} + +class VBoxHWVersionMeta +{ + public final int version; + + public VBoxHWVersionMeta( int vers ) + { + version = vers; + } +} + +class VBoxEthernetDevTypeMeta +{ + public final String value; + public final boolean isPresent; + + public VBoxEthernetDevTypeMeta( boolean present, String val ) + { + value = val; + isPresent = present; + } +} + +class VBoxUsbSpeedMeta +{ + public final String value; + public final int speed; + public VBoxUsbSpeedMeta( String value, int speed ) + { + this.value = value; + this.speed = speed; + } +} + +public class VboxMetaData extends VmMetaData<VBoxSoundCardMeta, VBoxDDAccelMeta, VBoxHWVersionMeta, VBoxEthernetDevTypeMeta, VBoxUsbSpeedMeta> +{ + /** + * List of supported image formats by the VirtualBox hypervisor. + */ + private static final List<DiskImage.ImageFormat> SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableList( + Arrays.asList( ImageFormat.VDI ) ); + + private static final Logger LOGGER = Logger.getLogger( VboxMetaData.class ); + + private static final Virtualizer virtualizer = new Virtualizer( TConst.VIRT_VIRTUALBOX, "VirtualBox" ); + + private final VboxConfig config; + + public static enum EthernetType + { + NAT( "vboxnet1" ), BRIDGED( "vboxnet0" ), HOST_ONLY( "vboxnet2" ); + + public final String vnet; + + private EthernetType( String vnet ) + { + this.vnet = vnet; + } + } + + public VboxMetaData( List<OperatingSystem> osList, File file ) throws IOException, UnsupportedVirtualizerFormatException + { + super( osList ); + this.config = new VboxConfig( file ); + init(); + } + + public VboxMetaData( List<OperatingSystem> osList, byte[] vmContent, int length ) throws IOException, UnsupportedVirtualizerFormatException + { + super( osList ); + this.config = new VboxConfig( vmContent, length ); + init(); + } + + private void init() + { + registerVirtualHW(); + displayName = config.getDisplayName(); + setOs( config.getOsName() ); + this.isMachineSnapshot = config.isMachineSnapshot(); + for ( HardDisk hardDisk : config.getHdds() ) { + hdds.add( hardDisk ); + } + } + + @Override + public Virtualizer getVirtualizer() + { + return virtualizer; + } + + @Override + public List<DiskImage.ImageFormat> getSupportedImageFormats() + { + return VboxMetaData.SUPPORTED_IMAGE_FORMATS; + } + + @Override + public void applySettingsForLocalEdit() + { + // TODO Auto-generated method stub + } + + @Override + public byte[] getDefinitionArray() + { + return config.toString( false ).getBytes( StandardCharsets.UTF_8 ); + } + + @Override + public byte[] getFilteredDefinitionArray() + { + return config.toString( false ).getBytes( StandardCharsets.UTF_8 ); + } + + @Override + public boolean addHddTemplate( String diskImage, String hddMode, String redoDir ) + { + config.changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk[@location='" + PlaceHolder.HDDLOCATION.toString() + "']", "location", diskImage ); + config.changeAttribute( "/VirtualBox/Machine", "snapshotFolder", redoDir ); + return true; + } + + @Override + public boolean addHddTemplate( File diskImage, String hddMode, String redoDir ) + { + String diskImagePath = diskImage.getName(); + config.changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk", "location", diskImagePath ); + + UUID newhdduuid = UUID.randomUUID(); + + // patching the new uuid in the vbox config file here + String vboxUUid = "{" + newhdduuid.toString() + "}"; + config.changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk", "uuid", vboxUUid ); + config.changeAttribute( "/VirtualBox/Machine/StorageControllers/StorageController/AttachedDevice/Image", "uuid", vboxUUid ); + + // the order of the UUID is BIG_ENDIAN but we need to change the order of the first 8 Bytes + // to be able to write them to the vdi file... the PROBLEM here is that the first 8 + // are in LITTLE_ENDIAN order in pairs of 4-2-2 not the whole 8 so just changing the + // order when we are adding them to the bytebuffer won't help + // + // the following is a workaround that works + ByteBuffer buffer = ByteBuffer.wrap( new byte[ 16 ] ); + buffer.putLong( newhdduuid.getMostSignificantBits() ); + buffer.putLong( newhdduuid.getLeastSignificantBits() ); + byte[] oldOrder = buffer.array(); + // make a coppy here because the last 8 Bytes don't need to change position + byte[] bytesToWrite = Arrays.copyOf( oldOrder, oldOrder.length ); + // use an offset int[] to help with the shuffle + int[] offsets = { 3, 2, 1, 0, 5, 4, 7, 6 }; + for ( int index = 0; index < 8; index++ ) { + bytesToWrite[index] = oldOrder[offsets[index]]; + } + try ( RandomAccessFile file = new RandomAccessFile( diskImage, "rw" ) ) { + file.seek( 392 ); + file.write( bytesToWrite, 0, 16 ); + } catch ( Exception e ) { + LOGGER.warn( "could not patch new uuid in the vdi", e ); + } + + // we need a new machine uuid + UUID newMachineUuid = UUID.randomUUID(); + if ( newMachineUuid.equals( newhdduuid ) ) { + LOGGER.warn( "The new Machine UUID is the same as the new HDD UUID; tying again...this vm might not start" ); + newMachineUuid = UUID.randomUUID(); + } + String machineUUid = "{" + newMachineUuid.toString() + "}"; + return config.changeAttribute( "/VirtualBox/Machine", "uuid", machineUUid ); + } + + @Override + public boolean addDefaultNat() + { + if ( config.addNewNode( "/VirtualBox/Machine/Hardware/Network/Adapter", "NAT" ) == null ) { + LOGGER.error( "Failed to set network adapter to NAT." ); + return false; + } + return config.changeAttribute( "/VirtualBox/Machine/Hardware/Network/Adapter", "MACAddress", "080027B86D12" ); + } + + @Override + public void setOs( String vendorOsId ) + { + config.changeAttribute( "/VirtualBox/Machine", "OSType", vendorOsId ); + setOs( TConst.VIRT_VIRTUALBOX, vendorOsId ); + } + + @Override + public boolean addDisplayName( String name ) + { + return config.changeAttribute( "/VirtualBox/Machine", "name", name ); + } + + @Override + public boolean addRam( int mem ) + { + return config.changeAttribute( "/VirtualBox/Machine/Hardware/Memory", "RAMSize", Integer.toString( mem ) ); + } + + @Override + public void addFloppy( int index, String image, boolean readOnly ) + { + Element floppyController = null; + NodeList matches = (NodeList)config.findNodes( "/VirtualBox/Machine/StorageControllers/StorageController[@name='Floppy']" ); + if ( matches == null || matches.getLength() == 0 ) { + floppyController = (Element)config.addNewNode( "/VirtualBox/Machine/StorageControllers", "StorageController" ); + if ( floppyController == null ) { + LOGGER.error( "Failed to add <Image> to floppy device." ); + return; + } + floppyController.setAttribute( "name", "Floppy" ); + floppyController.setAttribute( "type", "I82078" ); + floppyController.setAttribute( "PortCount", "1" ); + floppyController.setAttribute( "useHostIOCache", "true" ); + floppyController.setAttribute( "Bootable", "false" ); + } + // virtualbox only allows one controller per type + if ( matches.getLength() > 1 ) { + LOGGER.error( "Multiple floppy controllers detected, this should never happen! " ); + return; + } + // so if we had any matches, we know we have exactly one + if ( floppyController == null ) + floppyController = (Element)matches.item( 0 ); + + // add the floppy device + Element floppyDevice = (Element)config.addNewNode( floppyController, "AttachedDevice" ); + if ( floppyDevice == null ) { + LOGGER.error( "Failed to add <Image> to floppy device." ); + return; + } + floppyDevice.setAttribute( "type", "Floppy" ); + floppyDevice.setAttribute( "hotpluggable", "false" ); + floppyDevice.setAttribute( "port", "0" ); + floppyDevice.setAttribute( "device", Integer.toString( index ) ); + + // finally add the image to it, if one was given + if ( image != null ) { + Element floppyImage = (Element)config.addNewNode( floppyDevice, "Image" ); + if ( floppyImage == null ) { + LOGGER.error( "Failed to add <Image> to floppy device." ); + return; + } + floppyImage.setAttribute( "uuid", VboxConfig.PlaceHolder.FLOPPYUUID.toString() ); + // register the image in the media registry + Element floppyImages = (Element)config.addNewNode( "/VirtualBox/Machine/MediaRegistry", "FloppyImages" ); + if ( floppyImages == null ) { + LOGGER.error( "Failed to add <FloppyImages> to media registry." ); + return; + } + Element floppyImageReg = (Element)config.addNewNode( "/VirtualBox/Machine/MediaRegistry/FloppyImages", "Image" ); + if ( floppyImageReg == null ) { + LOGGER.error( "Failed to add <Image> to floppy images in the media registry." ); + return; + } + floppyImageReg.setAttribute( "uuid", VboxConfig.PlaceHolder.FLOPPYUUID.toString() ); + floppyImageReg.setAttribute( "location", VboxConfig.PlaceHolder.FLOPPYLOCATION.toString() ); + } + } + + @Override + public boolean addCdrom( String image ) + { + // TODO - done in run-virt currently + return false; + } + + @Override + public boolean addCpuCoreCount( int nrOfCores ) + { + return config.changeAttribute( "/VirtualBox/Machine/Hardware/CPU", "count", Integer.toString( nrOfCores ) ); + } + + @Override + 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 ) ); + config.changeAttribute( "/VirtualBox/Machine/Hardware/AudioAdapter", "controller", sound.value ); + } + + @Override + public VmMetaData.SoundCardType getSoundCard() + { + // initialize here to type None to avoid all null pointer exceptions thrown for unknown user written sound cards + VmMetaData.SoundCardType returnsct = VmMetaData.SoundCardType.NONE; + Element x = (Element)config.findNodes( "/VirtualBox/Machine/Hardware/AudioAdapter" ).item( 0 ); + if ( !x.hasAttribute( "enabled" ) || ( x.hasAttribute( "enabled" ) && x.getAttribute( "enabled" ).equals( "false" ) ) ) { + return returnsct; + } else { + // extra separate case for the non-existing argument} + if ( !x.hasAttribute( "controller" ) ) { + returnsct = VmMetaData.SoundCardType.AC; + } else { + String controller = x.getAttribute( "controller" ); + VBoxSoundCardMeta soundMeta = null; + for ( VmMetaData.SoundCardType type : VmMetaData.SoundCardType.values() ) { + soundMeta = soundCards.get( type ); + if ( soundMeta != null ) { + if ( controller.equals( soundMeta.value ) ) { + returnsct = type; + } + } + } + } + } + return returnsct; + } + + @Override + public void setDDAcceleration( VmMetaData.DDAcceleration type ) + { + VBoxDDAccelMeta accel = ddacc.get( type ); + config.changeAttribute( "/VirtualBox/Machine/Hardware/Display", "accelerate3D", Boolean.toString( accel.isPresent ) ); + } + + @Override + public VmMetaData.DDAcceleration getDDAcceleration() + { + VmMetaData.DDAcceleration returndda = null; + Element x = (Element)config.findNodes( "/VirtualBox/Machine/Hardware/Display" ).item( 0 ); + if ( x.hasAttribute( "accelerate3D" ) ) { + if ( x.getAttribute( "accelerate3D" ).equals( "true" ) ) { + returndda = VmMetaData.DDAcceleration.ON; + } else { + returndda = VmMetaData.DDAcceleration.OFF; + } + } else { + returndda = VmMetaData.DDAcceleration.OFF; + } + return returndda; + } + + /** + * Function does nothing for Virtual Box; + * Virtual Box accepts per default only one hardware version and is hidden from the user + */ + @Override + public void setHWVersion( HWVersion type ) + { + } + + @Override + public VmMetaData.HWVersion getHWVersion() + { + // Virtual Box uses only one virtual hardware version and can't be changed + return VmMetaData.HWVersion.DEFAULT; + } + + @Override + public void setEthernetDevType( int cardIndex, EthernetDevType type ) + { + String index = "0"; + VBoxEthernetDevTypeMeta nic = networkCards.get( type ); + // cardIndex is not used yet...maybe later needed for different network cards + config.changeAttribute( "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='" + index + "']", "enabled", Boolean.toString( nic.isPresent ) ); + config.changeAttribute( "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='" + index + "']", "type", nic.value ); + } + + @Override + public VmMetaData.EthernetDevType getEthernetDevType( int cardIndex ) + { + VmMetaData.EthernetDevType returnedt = VmMetaData.EthernetDevType.NONE; + Element x = (Element)config.findNodes( "/VirtualBox/Machine/Hardware/Network/Adapter" ).item( 0 ); + if ( !x.hasAttribute( "enabled" ) || ( x.hasAttribute( "enabled" ) && x.getAttribute( "enabled" ).equals( "false" ) ) ) { + return returnedt; + } else { + // extra separate case for the non-existing argument} + if ( !x.hasAttribute( "type" ) ) { + returnedt = VmMetaData.EthernetDevType.PCNETFAST3; + } else { + String temp = x.getAttribute( "type" ); + VBoxEthernetDevTypeMeta etherMeta = null; + for ( VmMetaData.EthernetDevType type : VmMetaData.EthernetDevType.values() ) { + etherMeta = networkCards.get( type ); + if ( etherMeta != null ) { + if ( temp.equals( etherMeta.value ) ) { + returnedt = type; + } + } + } + } + } + return returnedt; + } + + public void registerVirtualHW() + { + // none type needs to have a valid value; it takes the value of AC97; if value is left null or empty vm will not start because value is not valid + // TODO: Maybe just remove the entire section from the XML? Same for ethernet... + soundCards.put( VmMetaData.SoundCardType.NONE, new VBoxSoundCardMeta( false, "AC97" ) ); + soundCards.put( VmMetaData.SoundCardType.SOUND_BLASTER, new VBoxSoundCardMeta( true, "SB16" ) ); + soundCards.put( VmMetaData.SoundCardType.HD_AUDIO, new VBoxSoundCardMeta( true, "HDA" ) ); + soundCards.put( VmMetaData.SoundCardType.AC, new VBoxSoundCardMeta( true, "AC97" ) ); + + ddacc.put( VmMetaData.DDAcceleration.OFF, new VBoxDDAccelMeta( false ) ); + ddacc.put( VmMetaData.DDAcceleration.ON, new VBoxDDAccelMeta( true ) ); + + hwversion.put( VmMetaData.HWVersion.DEFAULT, new VBoxHWVersionMeta( 0 ) ); + + // none type needs to have a valid value; it takes the value of pcnetcpi2; if value is left null or empty vm will not start because value is not valid + networkCards.put( VmMetaData.EthernetDevType.NONE, new VBoxEthernetDevTypeMeta( false, "Am79C970A" ) ); + networkCards.put( VmMetaData.EthernetDevType.PCNETPCI2, new VBoxEthernetDevTypeMeta( true, "Am79C970A" ) ); + networkCards.put( VmMetaData.EthernetDevType.PCNETFAST3, new VBoxEthernetDevTypeMeta( true, "Am79C973" ) ); + networkCards.put( VmMetaData.EthernetDevType.PRO1000MTD, new VBoxEthernetDevTypeMeta( true, "82540EM" ) ); + networkCards.put( VmMetaData.EthernetDevType.PRO1000TS, new VBoxEthernetDevTypeMeta( true, "82543GC" ) ); + networkCards.put( VmMetaData.EthernetDevType.PRO1000MTS, new VBoxEthernetDevTypeMeta( true, "82545EM" ) ); + networkCards.put( VmMetaData.EthernetDevType.PARAVIRT, new VBoxEthernetDevTypeMeta( true, "virtio" ) ); + + usbSpeeds.put( VmMetaData.UsbSpeed.NONE, new VBoxUsbSpeedMeta( null, 0 ) ); + usbSpeeds.put( VmMetaData.UsbSpeed.USB1_1, new VBoxUsbSpeedMeta( "OHCI", 1 ) ); + usbSpeeds.put( VmMetaData.UsbSpeed.USB2_0, new VBoxUsbSpeedMeta( "EHCI", 2 ) ); + usbSpeeds.put( VmMetaData.UsbSpeed.USB3_0, new VBoxUsbSpeedMeta( "XHCI", 3 ) ); + } + + @Override + public boolean addEthernet( VmMetaData.EtherType type ) + { + Node hostOnlyInterfaceNode = config.addNewNode( "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='0']", "HostOnlyInterface" ); + if ( hostOnlyInterfaceNode == null ) { + LOGGER.error( "Failed to create node for HostOnlyInterface." ); + return false; + } + return config.addAttributeToNode( hostOnlyInterfaceNode, "name", EthernetType.valueOf( type.name() ).vnet ); + } + + @Override + public boolean tweakForNonPersistent() + { + // Cannot disable suspend + // https://forums.virtualbox.org/viewtopic.php?f=6&t=77169 + // https://forums.virtualbox.org/viewtopic.php?f=8&t=80338 + // But some other stuff that won't make sense in non-persistent mode + config.setExtraData( "GUI/LastCloseAction", "PowerOff" ); + // Could use "Default" instead of "Last" above, but you won't get any confirmation dialog in that case + config.setExtraData( "GUI/RestrictedRuntimeHelpMenuActions", "All" ); + config.setExtraData( "GUI/RestrictedRuntimeMachineMenuActions", "TakeSnapshot,Pause,SaveState" ); + config.setExtraData( "GUI/RestrictedRuntimeMenus", "Help" ); + config.setExtraData( "GUI/PreventSnapshotOperations", "true" ); + config.setExtraData( "GUI/PreventApplicationUpdate", "true" ); + config.setExtraData( "GUI/RestrictedCloseActions", "SaveState,PowerOffRestoringSnapshot,Detach" ); + return true; + } + + @Override + public void setMaxUsbSpeed( VmMetaData.UsbSpeed speed ) + { + // Wipe existing ones + config.removeNodes( "/VirtualBox/Machine/Hardware", "USB" ); + if ( speed == null || speed == VmMetaData.UsbSpeed.NONE ) { + // Add marker so we know it's not an old config and we really want no USB + Element node = config.createNodeRecursive( "/VirtualBox/OpenSLX/USB" ); + if ( node != null ) { + node.setAttribute( "disabled", "true" ); + } + return; // NO USB + } + Element node = config.createNodeRecursive( "/VirtualBox/Machine/Hardware/USB/Controllers/Controller" ); + VBoxUsbSpeedMeta vboxSpeed = usbSpeeds.get( speed ); + node.setAttribute( "type", vboxSpeed.value ); + node.setAttribute( "name", vboxSpeed.value ); + if ( speed == UsbSpeed.USB2_0 ) { + // If EHCI (2.0) is selected, VBox adds an OHCI controller too... + node.setAttribute( "type", "OHCI" ); + node.setAttribute( "name", "OHCI" ); + } + } + + @Override + public VmMetaData.UsbSpeed getMaxUsbSpeed() + { + NodeList nodes = config.findNodes( "/VirtualBox/Machine/Hardware/USB/Controllers/Controller/@type" ); + int maxSpeed = 0; + VmMetaData.UsbSpeed maxItem = VmMetaData.UsbSpeed.NONE; + for ( int i = 0; i < nodes.getLength(); ++i ) { + if ( nodes.item( i ).getNodeType() != Node.ATTRIBUTE_NODE ) { + LOGGER.info( "Not ATTRIBUTE type" ); + continue; + } + String type = ((Attr)nodes.item( i )).getValue(); + for ( Entry<VmMetaData.UsbSpeed, VBoxUsbSpeedMeta> s : usbSpeeds.entrySet() ) { + if ( s.getValue().speed > maxSpeed && type.equals( s.getValue().value ) ) { + maxSpeed = s.getValue().speed; + maxItem = s.getKey(); + } + } + } + return maxItem; + } +} diff --git a/src/main/java/org/openslx/vm/VmMetaData.java b/src/main/java/org/openslx/vm/VmMetaData.java new file mode 100644 index 0000000..0be07e4 --- /dev/null +++ b/src/main/java/org/openslx/vm/VmMetaData.java @@ -0,0 +1,419 @@ +package org.openslx.vm; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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 + * description, like a *.vmx for VMware machines. + */ +public abstract class VmMetaData<T, U, V, W, X> +{ + private static final Logger LOGGER = Logger.getLogger( VmMetaData.class ); + + /* + * Helper types + */ + protected Map<SoundCardType, T> soundCards = new HashMap<>(); + protected Map<DDAcceleration, U> ddacc = new HashMap<>(); + protected Map<HWVersion, V> hwversion = new HashMap<>(); + protected Map<EthernetDevType, W> networkCards = new HashMap<>(); + protected Map<UsbSpeed, X> usbSpeeds = new HashMap<>(); + + /** + * Virtual sound cards types + */ + public static enum SoundCardType + { + NONE( "None" ), DEFAULT( "(default)" ), SOUND_BLASTER( "Sound Blaster 16" ), ES( "ES 1371" ), HD_AUDIO( "Intel Integrated HD Audio" ), AC( "Intel ICH Audio Codec 97" ); + + public final String displayName; + + private SoundCardType( String dName ) + { + this.displayName = dName; + } + } + + /** + * 3D acceleration types + */ + public static enum DDAcceleration + { + OFF( "Off" ), ON( "On" ); + + public final String displayName; + + private DDAcceleration( String dName ) + { + this.displayName = dName; + } + } + + /** + * Virtual hardware version - currently only in use for VMPlayer + */ + public static enum HWVersion + { + NONE( "(invalid)" ), + THREE( " 3 (Workstation 4/5, Player 1)" ), + FOUR( " 4 (Workstation 4/5, Player 1/2, Fusion 1)" ), + SIX( " 6 (Workstation 6)" ), + SEVEN( " 7 (Workstation 6.5/7, Player 3, Fusion 2/3)" ), + EIGHT( " 8 (Workstation 8, Player/Fusion 4)" ), + NINE( " 9 (Workstation 9, Player/Fusion 5)" ), + TEN( "10 (Workstation 10, Player/Fusion 6)" ), + ELEVEN( "11 (Workstation 11, Player/Fusion 7)" ), + TWELVE( "12 (Workstation/Player 12, Fusion 8)" ), + FOURTEEN( "14 (Workstation/Player 14, Fusion 10)"), + FIFTEEN( "15 (Workstation/Player 15, Fusion 11)"), + FIFTEEN_ONE( "16 (Workstation/Player 15.1, Fusion 11.1)"), + SIXTEEN( "17 (Workstation/Player 16, Fusion 12)"), + SIXTEEN_ONE( "18 (Workstation/Player 16.1, Fusion 12.1)"), + DEFAULT( "default" ); + + public final String displayName; + + private HWVersion( String dName ) + { + this.displayName = dName; + } + } + + /** + * Virtual network cards + */ + public static enum EthernetDevType + { + AUTO( "(default)" ), PCNET32( "AMD PCnet32" ), E1000( "Intel E1000 (PCI)" ), E1000E( "Intel E1000e (PCI-Express)" ), VMXNET( "VMXnet" ), VMXNET3( "VMXnet 3" ), PCNETPCI2( + "PCnet-PCI II" ), PCNETFAST3( "PCnet-FAST III" ), PRO1000MTD( "Intel PRO/1000 MT Desktop" ), PRO1000TS( + "Intel PRO/1000 T Server" ), PRO1000MTS( "Intel PRO/1000 MT Server" ), PARAVIRT( "Paravirtualized Network" ), NONE( "No Network Card" ); + + public final String displayName; + + private EthernetDevType( String dName ) + { + this.displayName = dName; + } + } + + public static enum UsbSpeed + { + NONE( "None" ), + USB1_1( "USB 1.1" ), + USB2_0( "USB 2.0" ), + USB3_0( "USB 3.0" ); + + public final String displayName; + + private UsbSpeed( String dName ) + { + this.displayName = dName; + } + } + + public static enum DriveBusType + { + SCSI, IDE, SATA, NVME; + } + + public static class HardDisk + { + public final String chipsetDriver; + public final DriveBusType bus; + public final String diskImage; + + public HardDisk( String chipsetDriver, DriveBusType bus, String diskImage ) + { + this.chipsetDriver = chipsetDriver; + this.bus = bus; + this.diskImage = diskImage; + } + } + + public static enum EtherType + { + NAT, BRIDGED, HOST_ONLY; + } + /* + * Members + */ + + protected final List<HardDisk> hdds = new ArrayList<>(); + + private final List<OperatingSystem> osList; + + private OperatingSystem os = null; + + protected String displayName = null; + + protected boolean isMachineSnapshot; + + /* + * Getters for virtual hardware + */ + public List<SoundCardType> getSupportedSoundCards() + { + ArrayList<SoundCardType> availables = new ArrayList<SoundCardType>( soundCards.keySet() ); + Collections.sort( availables ); + return availables; + } + + public List<DDAcceleration> getSupportedDDAccs() + { + ArrayList<DDAcceleration> availables = new ArrayList<DDAcceleration>( ddacc.keySet() ); + Collections.sort( availables ); + return availables; + } + + public List<HWVersion> getSupportedHWVersions() + { + ArrayList<HWVersion> availables = new ArrayList<HWVersion>( hwversion.keySet() ); + Collections.sort( availables ); + return availables; + } + + public List<EthernetDevType> getSupportedEthernetDevices() + { + ArrayList<EthernetDevType> availables = new ArrayList<EthernetDevType>( networkCards.keySet() ); + Collections.sort( availables ); + return availables; + } + + public List<UsbSpeed> getSupportedUsbSpeeds() + { + ArrayList<UsbSpeed> availables = new ArrayList<>( usbSpeeds.keySet() ); + Collections.sort( availables ); + return availables; + } + + /** + * Get operating system of this VM. + */ + public OperatingSystem getOs() + { + return os; + } + + /** + * Get all hard disks of this VM. + */ + public List<HardDisk> getHdds() + { + return Collections.unmodifiableList( hdds ); + } + + /** + * Get display name of VM. + */ + public String getDisplayName() + { + return displayName; + } + + /* + * Getter for isMachineSnapshot + */ + public boolean isMachineSnapshot() + { + return isMachineSnapshot; + } + + /** + * This method should return a minimal representation of the input meta data. + * The representation is platform dependent, and should be stripped of all + * non-essential configuration, such as CD/DVD/FLoppy drives, serial or parallel + * ports, shared folders, or anything else that could be considered sensible + * information (absolute paths containing the local user's name). + */ + public abstract byte[] getFilteredDefinitionArray(); + + public final ByteBuffer getFilteredDefinition() + { + return ByteBuffer.wrap( getFilteredDefinitionArray() ); + } + + /* + * Methods + */ + + public VmMetaData( List<OperatingSystem> osList ) + { + this.osList = osList; + } + + /** + * Called from subclass to set the OS. If the OS cannot be determined from the + * given parameters, it will not be set. + * + * @param virtId + * virtualizer, eg "vmware" for VMware + * @param virtOsId + * the os identifier used by the virtualizer, eg. windows7-64 for + * 64bit Windows 7 on VMware + */ + protected final void setOs( String virtId, String virtOsId ) + { + OperatingSystem lazyMatch = null; + for ( OperatingSystem os : osList ) { + if ( os.getVirtualizerOsId() == null ) + continue; + for ( Entry<String, String> entry : os.getVirtualizerOsId().entrySet() ) { + if ( !entry.getValue().equals( virtOsId ) ) + continue; + if ( entry.getKey().equals( virtId ) ) { + this.os = os; + return; + } else { + lazyMatch = os; + } + } + } + this.os = lazyMatch; + } + + /** + * Returns list of image formats supported by the VM's hypervisor. + * + * @return list of image formats. + */ + public abstract List<DiskImage.ImageFormat> getSupportedImageFormats(); + + /** + * Apply config options that are desired when locally editing a VM. for vmware, + * this disables automatic DPI scaling of the guest. + */ + public abstract void applySettingsForLocalEdit(); + + /** + * Returns a VmMetaData instance of the given machine description given as file + * + * @param osList List of supported operating systems + * @param file VM's machine description file to get the metadata instance from + * @return VmMetaData object representing the relevant parts of the given machine description + */ + public static VmMetaData<?, ?, ?, ?, ?> getInstance( List<OperatingSystem> osList, File file ) + throws IOException + { + try { + return new VmwareMetaData( osList, file ); + } catch ( UnsupportedVirtualizerFormatException e ) { + LOGGER.info( "Not a VMware file", e ); + } + try { + return new VboxMetaData( osList, file ); + } catch ( UnsupportedVirtualizerFormatException e ) { + LOGGER.info( "Not a VirtualBox file", e ); + } + try { + return new QemuMetaData( osList, file ); + } catch ( Exception e ) { + LOGGER.info( "Not a Qemu file", e ); + } + try { + return new DockerMetaDataDummy(osList, file); + } catch ( Exception e ) { + LOGGER.info( "Not a tar.gz file, for docker container", e ); + } + LOGGER.error( "Could not detect any known virtualizer format" ); + return null; + } + + /** + * Returns a VmMetaData instance of the given machine description given as a byte array + * + * @param osList List of supported operating systems + * @param vmContent VM's machine description as byte array (e.g. stored in DB) + * @param length length of the byte array given as vmContent + * @return VmMetaData object representing the relevant parts of the given machine description + * @throws IOException + */ + public static VmMetaData<?, ?, ?, ?, ?> getInstance( List<OperatingSystem> osList, byte[] vmContent, int length ) throws IOException + { + Map<String, Exception> exceptions = new HashMap<>(); + try { + return new VmwareMetaData( osList, vmContent, length ); + } catch ( UnsupportedVirtualizerFormatException e ) { + exceptions.put( "Not a VMware file", e ); + } + try { + return new VboxMetaData( osList, vmContent, length ); + } catch ( UnsupportedVirtualizerFormatException e ) { + exceptions.put( "Not a VirtualBox file", e ); + } + try { + return new DockerMetaDataDummy(osList, vmContent, length); + } catch (UnsupportedVirtualizerFormatException e) { + exceptions.put( "Not tar.gz file for DockerMetaDataDummy ", e); + } + // TODO QEmu -- hack above expects qcow2 file, so we can't do anything here yet + LOGGER.error( "Could not detect any known virtualizer format" ); + for ( Entry<String, Exception> e : exceptions.entrySet() ) { + LOGGER.error( e.getKey(), e.getValue() ); + } + return null; + } + + public abstract boolean addHddTemplate( File diskImage, String hddMode, String redoDir ); + + public abstract boolean addHddTemplate( String diskImagePath, String hddMode, String redoDir ); + + public abstract boolean addDefaultNat(); + + public abstract void setOs( String vendorOsId ); + + public abstract boolean addDisplayName( String name ); + + public abstract boolean addRam( int mem ); + + public abstract void addFloppy( int index, String image, boolean readOnly ); + + public abstract boolean addCdrom( String image ); + + public abstract boolean addCpuCoreCount( int nrOfCores ); + + public abstract void setSoundCard( SoundCardType type ); + + public abstract SoundCardType getSoundCard(); + + public abstract void setDDAcceleration( DDAcceleration type ); + + public abstract DDAcceleration getDDAcceleration(); + + public abstract void setHWVersion( HWVersion type ); + + public abstract HWVersion getHWVersion(); + + public abstract void setEthernetDevType( int cardIndex, EthernetDevType type ); + + public abstract EthernetDevType getEthernetDevType( int cardIndex ); + + public abstract void setMaxUsbSpeed( UsbSpeed speed ); + + public abstract UsbSpeed getMaxUsbSpeed(); + + public abstract byte[] getDefinitionArray(); + + public abstract boolean addEthernet( EtherType type ); + + public abstract Virtualizer getVirtualizer(); + + public abstract boolean tweakForNonPersistent(); + + /** + * Function used to register virtual devices + */ + public abstract void registerVirtualHW(); +} diff --git a/src/main/java/org/openslx/vm/VmwareConfig.java b/src/main/java/org/openslx/vm/VmwareConfig.java new file mode 100644 index 0000000..d98a1d4 --- /dev/null +++ b/src/main/java/org/openslx/vm/VmwareConfig.java @@ -0,0 +1,276 @@ +package org.openslx.vm; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.log4j.Logger; +import org.openslx.util.Util; + +public class VmwareConfig +{ + + private static final Logger LOGGER = Logger.getLogger( VmwareConfig.class ); + + private Map<String, ConfigEntry> entries = new TreeMap<>( String.CASE_INSENSITIVE_ORDER ); + + public VmwareConfig() + { + // (void) + } + + public VmwareConfig( File file ) throws IOException, UnsupportedVirtualizerFormatException + { + int todo = (int)Math.min( 100000, file.length() ); + int offset = 0; + byte[] data = new byte[ todo ]; + FileInputStream fr = null; + try { + fr = new FileInputStream( file ); + while ( todo > 0 ) { + int ret = fr.read( data, offset, todo ); + if ( ret <= 0 ) + break; + todo -= ret; + offset += ret; + } + } finally { + Util.safeClose( fr ); + } + init( data, offset ); + + } + + public VmwareConfig( InputStream is ) throws IOException, UnsupportedVirtualizerFormatException + { + int todo = Math.max( 4000, Math.min( 100000, is.available() ) ); + int offset = 0; + byte[] data = new byte[ todo ]; + while ( todo > 0 ) { + int ret = is.read( data, offset, todo ); + if ( ret <= 0 ) + break; + todo -= ret; + offset += ret; + } + init( data, offset ); + } + + public VmwareConfig( byte[] vmxContent, int length ) throws UnsupportedVirtualizerFormatException + { + init( vmxContent, length ); + } + + // function is used for both .vmx and .vmdk files + private void init( byte[] vmxContent, int length ) throws UnsupportedVirtualizerFormatException + { + try { + boolean isValid = false; + BufferedReader reader = getVmxReader( vmxContent, length ); + String line; + while ( ( line = reader.readLine() ) != null ) { + KeyValuePair entry = parse( line ); + + if ( entry != null ) { + if ( entry.key.equals( "virtualHW.version" ) || entry.key.equals( "ddb.virtualHWVersion" ) + // TODO: This is supposed to be case insensitive. + // Check if there are other consequences from lowercase entries in converted vmx files. + || entry.key.equals( "virtualhw.version" )) { + isValid = true; + } + set( entry.key, unescape( entry.value ) ); + } + } + if ( !isValid ) { + throw new UnsupportedVirtualizerFormatException( "Not in VMX format." ); + } + } catch ( IOException e ) { + LOGGER.warn( "Exception when loading vmx from byte array (how!?)", e ); + } + } + + public static BufferedReader getVmxReader( byte[] vmxContent, int length ) throws IOException + { + Charset cs = getCharset( vmxContent, length ); + return new BufferedReader( new InputStreamReader( new ByteArrayInputStream( vmxContent, 0, length ), cs ) ); + } + + public static Charset getCharset( byte[] vmxContent, int length ) + { + String csName = detectCharset( new ByteArrayInputStream( vmxContent, 0, length ) ); + Charset cs = null; + try { + cs = Charset.forName( csName ); + } catch ( Exception e ) { + LOGGER.warn( "Could not instantiate charset " + csName, e ); + } + if ( cs == null ) + cs = StandardCharsets.ISO_8859_1; + return cs; + } + + private String unescape( String value ) + { + String ret = value; + if ( ret.contains( "|22" ) ) { + ret = ret.replace( "|22", "\"" ); + } + if ( ret.contains( "|7C" ) ) { + ret = ret.replace( "|7C", "|" ); + } + return ret; + } + + public static String detectCharset( InputStream is ) + { + try { + BufferedReader csDetectReader = new BufferedReader( new InputStreamReader( is, StandardCharsets.ISO_8859_1 ) ); + String line; + while ( ( line = csDetectReader.readLine() ) != null ) { + KeyValuePair entry = parse( line ); + if ( entry == null ) + continue; + if ( entry.key.equals( ".encoding" ) || entry.key.equals( "encoding" ) ) { + return entry.value; + } + } + } catch ( Exception e ) { + LOGGER.warn( "Could not detect charset, fallback to latin1", e ); + } + // Dumb fallback + return "ISO-8859-1"; + } + + public Set<Entry<String, ConfigEntry>> entrySet() + { + return entries.entrySet(); + } + + private static final Pattern settingMatcher1 = Pattern.compile( "^\\s*(#?[a-z0-9\\.\\:_]+)\\s*=\\s*\"(.*)\"\\s*$", Pattern.CASE_INSENSITIVE ); + private static final Pattern settingMatcher2 = Pattern.compile( "^\\s*(#?[a-z0-9\\.\\:_]+)\\s*=\\s*([^\"]*)\\s*$", Pattern.CASE_INSENSITIVE ); + + private static KeyValuePair parse( String line ) + { + Matcher matcher = settingMatcher1.matcher( line ); + if ( !matcher.matches() ) { + matcher = settingMatcher2.matcher( line ); + } + if ( !matcher.matches() ) { + return null; + } + return new KeyValuePair( matcher.group( 1 ), matcher.group( 2 ) ); + + } + + public ConfigEntry set( String key, String value, boolean replace ) + { + if ( !replace && entries.containsKey( key ) ) + return null; + ConfigEntry ce = new ConfigEntry( value ); + entries.put( key, ce ); + return ce; + } + + public ConfigEntry set( String key, String value ) + { + return set( key, value, true ); + } + + public ConfigEntry set( KeyValuePair entry ) + { + return set( entry.key, entry.value ); + } + + public void remove( String key ) + { + entries.remove( key ); + } + + public String get( String key ) + { + ConfigEntry ce = entries.get( key ); + if ( ce == null ) + return null; + return ce.value; + } + + public String toString( boolean filteredRequired, boolean generatedRequired ) + { + set( ".encoding", "UTF-8" ).filtered( true ).generated( true ); + StringBuilder sb = new StringBuilder( 300 ); + for ( Entry<String, ConfigEntry> entry : entries.entrySet() ) { + ConfigEntry value = entry.getValue(); + if ( ( !filteredRequired || value.forFiltered ) && ( !generatedRequired || value.forGenerated ) ) { + sb.append( entry.getKey() ); + sb.append( " = \"" ); + sb.append( value.getEscaped() ); + sb.append( "\"\n" ); + } + } + return sb.toString(); + } + + @Override + public String toString() + { + return toString( false, false ); + } + + public static class ConfigEntry + { + private String value; + private boolean forFiltered; + private boolean forGenerated; + + public ConfigEntry( String value ) + { + this.value = value; + } + + public ConfigEntry filtered( boolean set ) + { + this.forFiltered = set; + return this; + } + + public ConfigEntry generated( boolean set ) + { + this.forGenerated = set; + return this; + } + + public String getEscaped() + { + String ret = value; + if ( ret.contains( "|" ) ) { + ret = ret.replace( "|", "|7C" ); + } + if ( ret.contains( "\"" ) ) { + ret = ret.replace( "\"", "|22" ); + } + return ret; + } + + public String getValue() + { + return value; + } + + public void setValue( String value ) + { + this.value = value; + } + } +} diff --git a/src/main/java/org/openslx/vm/VmwareMetaData.java b/src/main/java/org/openslx/vm/VmwareMetaData.java new file mode 100644 index 0000000..9bbe581 --- /dev/null +++ b/src/main/java/org/openslx/vm/VmwareMetaData.java @@ -0,0 +1,684 @@ +package org.openslx.vm; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.Util; +import org.openslx.vm.VmwareConfig.ConfigEntry; +import org.openslx.vm.disk.DiskImage; +import org.openslx.vm.disk.DiskImage.ImageFormat; + +class VmWareSoundCardMeta +{ + public final boolean isPresent; + public final String value; + + public VmWareSoundCardMeta( boolean present, String val ) + { + isPresent = present; + value = val; + } +} + +class VmWareDDAccelMeta +{ + public final boolean isPresent; + + public VmWareDDAccelMeta( boolean present ) + { + isPresent = present; + } +} + +class VmWareHWVersionMeta +{ + public final int version; + + public VmWareHWVersionMeta( int vers ) + { + version = vers; + } +} + +class VmWareEthernetDevTypeMeta +{ + public final String value; + + public VmWareEthernetDevTypeMeta( String val ) + { + value = val; + } +} + +class VmwareUsbSpeed +{ + public final String keyName; + public final int speedNumeric; + + public VmwareUsbSpeed( int speed, String key ) + { + this.keyName = key == null ? null : ( key + ".present" ); + this.speedNumeric = speed; + } +} + +public class VmwareMetaData extends VmMetaData<VmWareSoundCardMeta, VmWareDDAccelMeta, VmWareHWVersionMeta, VmWareEthernetDevTypeMeta, VmwareUsbSpeed> +{ + /** + * List of supported image formats by the VMware hypervisor. + */ + private static final List<DiskImage.ImageFormat> SUPPORTED_IMAGE_FORMATS = Collections.unmodifiableList( + Arrays.asList( ImageFormat.VMDK ) ); + + private static final Logger LOGGER = Logger.getLogger( VmwareMetaData.class ); + + private static final Virtualizer virtualizer = new Virtualizer( TConst.VIRT_VMWARE, "VMware" ); + + private static final Pattern hddKey = Pattern.compile( "^(ide\\d|scsi\\d|sata\\d|nvme\\d):?(\\d)?\\.(.*)", Pattern.CASE_INSENSITIVE ); + + // Lowercase list of allowed settings for upload (as regex) + private static final Pattern[] whitelist; + + private final VmwareConfig config; + + // Init static members + static { + String[] list = { "^guestos", "^uuid\\.bios", "^config\\.version", "^ehci[.:]", "^mks\\.enable3d", "^virtualhw\\.", + "^sound[.:]", "\\.pcislotnumber$", "^pcibridge", "\\.virtualdev$", "^tools\\.syncTime$", "^time\\.synchronize", + "^bios\\.bootDelay", "^rtc\\.", "^xhci[.:]", "^usb_xhci[.:]", "\\.deviceType$", "\\.port$", "\\.parent$", "^usb[.:]", + "^firmware", "^hpet", "^vm\\.genid" }; + whitelist = new Pattern[ list.length ]; + for ( int i = 0; i < list.length; ++i ) { + whitelist[i] = Pattern.compile( list[i].toLowerCase() ); + } + } + + public static enum EthernetType + { + NAT( "vmnet1" ), BRIDGED( "vmnet0" ), HOST_ONLY( "vmnet2" ); + + public final String vmnet; + + private EthernetType( String vnet ) + { + this.vmnet = vnet; + } + } + + private final Map<String, Controller> disks = new HashMap<>(); + + public VmwareMetaData( List<OperatingSystem> osList, File file ) throws IOException, UnsupportedVirtualizerFormatException + { + super( osList ); + this.config = new VmwareConfig( file ); + init(); + } + + public VmwareMetaData( List<OperatingSystem> osList, byte[] vmxContent, int length ) throws UnsupportedVirtualizerFormatException + { + super( osList ); + this.config = new VmwareConfig( vmxContent, length ); // still unfiltered + init(); // now filtered + } + + private void init() + { + registerVirtualHW(); + + for ( Entry<String, ConfigEntry> entry : config.entrySet() ) { + handleLoadEntry( entry ); + } + // Fix accidentally filtered USB config if we see EHCI is present + if ( isSetAndTrue( "ehci.present" ) && !isSetAndTrue( "usb.present" ) ) { + addFiltered( "usb.present", "TRUE" ); + } + // if we find this tag, we already went through the hdd's - so we're done. + if ( config.get( "#SLX_HDD_BUS" ) != null ) { + return; + } + // Now find the HDDs and add to list + for ( Entry<String, Controller> cEntry : disks.entrySet() ) { + Controller controller = cEntry.getValue(); + String controllerType = cEntry.getKey(); + if ( !controller.present ) + continue; + for ( Entry<String, Device> dEntry : controller.devices.entrySet() ) { + Device device = dEntry.getValue(); + if ( !device.present ) + continue; // Not present + if ( device.deviceType != null && !device.deviceType.toLowerCase().endsWith( "disk" ) ) + continue; // Not a HDD + DriveBusType bus = null; + if ( controllerType.startsWith( "ide" ) ) { + bus = DriveBusType.IDE; + } else if ( controllerType.startsWith( "scsi" ) ) { + bus = DriveBusType.SCSI; + } else if ( controllerType.startsWith( "sata" ) ) { + bus = DriveBusType.SATA; + } else if ( controllerType.startsWith( "nvme" ) ) { + bus = DriveBusType.NVME; + } + hdds.add( new HardDisk( controller.virtualDev, bus, device.filename ) ); + } + } + // TODO check if this machine is in a paused/suspended state + this.isMachineSnapshot = false; + + // Add HDD to cleaned vmx + if ( !hdds.isEmpty() ) { + HardDisk hdd = hdds.get( 0 ); + addFiltered( "#SLX_HDD_BUS", hdd.bus.toString() ); + if ( hdd.chipsetDriver != null ) { + addFiltered( "#SLX_HDD_CHIP", hdd.chipsetDriver ); + } + } + } + + private void addFiltered( String key, String value ) + { + config.set( key, value ).filtered( true ); + } + + private boolean isSetAndTrue( String key ) + { + String value = config.get( key ); + return value != null && value.equalsIgnoreCase( "true" ); + } + + private void handleLoadEntry( Entry<String, ConfigEntry> entry ) + { + String lowerKey = entry.getKey().toLowerCase(); + // Cleaned vmx construction + for ( Pattern exp : whitelist ) { + if ( exp.matcher( lowerKey ).find() ) { + entry.getValue().filtered( true ); + break; + } + } + // + // Dig Usable meta data + String value = entry.getValue().getValue(); + if ( lowerKey.equals( "guestos" ) ) { + setOs( value ); + return; + } + if ( lowerKey.equals( "displayname" ) ) { + displayName = value; + return; + } + Matcher hdd = hddKey.matcher( entry.getKey() ); + if ( hdd.find() ) { + handleHddEntry( hdd.group( 1 ).toLowerCase(), hdd.group( 2 ), hdd.group( 3 ), value ); + } + } + + private void handleHddEntry( String controllerStr, String deviceStr, String property, String value ) + { + Controller controller = disks.get( controllerStr ); + if ( controller == null ) { + controller = new Controller(); + disks.put( controllerStr, controller ); + } + if ( deviceStr == null || deviceStr.isEmpty() ) { + // Controller property + if ( property.equalsIgnoreCase( "present" ) ) { + controller.present = Boolean.parseBoolean( value ); + } else if ( property.equalsIgnoreCase( "virtualDev" ) ) { + controller.virtualDev = value; + } + return; + } + // Device property + Device device = controller.devices.get( deviceStr ); + if ( device == null ) { + device = new Device(); + controller.devices.put( deviceStr, device ); + } + if ( property.equalsIgnoreCase( "deviceType" ) ) { + device.deviceType = value; + } else if ( property.equalsIgnoreCase( "filename" ) ) { + device.filename = value; + } else if ( property.equalsIgnoreCase( "present" ) ) { + device.present = Boolean.parseBoolean( value ); + } + } + + @Override + public List<DiskImage.ImageFormat> getSupportedImageFormats() + { + return VmwareMetaData.SUPPORTED_IMAGE_FORMATS; + } + + @Override + public boolean addHddTemplate( File diskImage, String hddMode, String redoDir ) + { + return addHddTemplate( diskImage.getName(), hddMode, redoDir ); + } + + @Override + public boolean addHddTemplate( String diskImagePath, String hddMode, String redoDir ) + { + if ( diskImagePath.isEmpty() ) { + LOGGER.error( "Empty disk image path given!" ); + return false; + } + DriveBusType bus; + try { + bus = DriveBusType.valueOf( config.get( "#SLX_HDD_BUS" ) ); + } catch ( Exception e ) { + LOGGER.warn( "Unknown bus type: " + config.get( "#SLX_HDD_BUS" ) + ". Cannot add hdd config." ); + return false; + } + String chipset = config.get( "#SLX_HDD_CHIP" ); + String prefix; + switch ( bus ) { + case SATA: + // Cannot happen?... use lsisas1068 + prefix = "scsi0"; + chipset = "lsisas1068"; + break; + case IDE: + case SCSI: + case NVME: + prefix = bus.name().toLowerCase() + "0"; + break; + default: + LOGGER.warn( "Unknown HDD bus type: " + bus.toString() ); + return false; + } + // Gen + addFiltered( prefix + ".present", "TRUE" ); + if ( chipset != null ) { + addFiltered( prefix + ".virtualDev", chipset ); + } + addFiltered( prefix + ":0.present", "TRUE" ); + addFiltered( prefix + ":0.deviceType", "disk" ); + addFiltered( prefix + ":0.fileName", diskImagePath ); + if ( hddMode != null ) { + addFiltered( prefix + ":0.mode", hddMode ); + addFiltered( prefix + ":0.redo", "" ); + addFiltered( prefix + ":0.redoLogDir", redoDir ); + } + config.remove( "#SLX_HDD_BUS" ); + config.remove( "#SLX_HDD_CHIP" ); + return true; + } + + public boolean addDefaultNat() + { + addFiltered( "ethernet0.present", "TRUE" ); + addFiltered( "ethernet0.connectionType", "nat" ); + return true; + } + + public boolean addEthernet( VmMetaData.EtherType type ) + { + boolean ret = false; + int index = 0; + for ( ;; ++index ) { + if ( config.get( "ethernet" + index + ".present" ) == null ) + break; + } + switch ( type ) { + case NAT: + ret = addEthernet( index, EthernetType.NAT ); + break; + case BRIDGED: + ret = addEthernet( index, EthernetType.BRIDGED ); + break; + case HOST_ONLY: + ret = addEthernet( index, EthernetType.HOST_ONLY ); + break; + default: + // Should not come to this... + break; + } + return ret; + } + + public boolean addEthernet( int index, EthernetType type ) + { + String ether = "ethernet" + index; + addFiltered( ether + ".present", "TRUE" ); + addFiltered( ether + ".connectionType", "custom" ); + addFiltered( ether + ".vnet", type.vmnet ); + if ( config.get( ether + ".virtualDev" ) == null ) { + String dev = config.get( "ethernet0.virtualDev" ); + if ( dev != null ) { + addFiltered( ether + ".virtualDev", dev ); + } + } + return true; + } + + public void addFloppy( int index, String image, boolean readOnly ) + { + String pre = "floppy" + index; + addFiltered( pre + ".present", "TRUE" ); + if ( image == null ) { + addFiltered( pre + ".startConnected", "FALSE" ); + addFiltered( pre + ".fileType", "device" ); + config.remove( pre + ".fileName" ); + config.remove( pre + ".readonly" ); + addFiltered( pre + ".autodetect", "TRUE" ); + } else { + addFiltered( pre + ".startConnected", "TRUE" ); + addFiltered( pre + ".fileType", "file" ); + addFiltered( pre + ".fileName", image ); + addFiltered( pre + ".readonly", vmBoolean( readOnly ) ); + config.remove( pre + ".autodetect" ); + } + } + + public boolean addCdrom( String image ) + { + for ( String port : new String[] { "ide0:0", "ide0:1", "ide1:0", "ide1:1", "scsi0:1" } ) { + if ( !isSetAndTrue( port + ".present" ) ) { + addFiltered( port + ".present", "TRUE" ); + if ( image == null ) { + addFiltered( port + ".autodetect", "TRUE" ); + addFiltered( port + ".deviceType", "cdrom-raw" ); + config.remove( port + ".fileName" ); + } else { + config.remove( port + ".autodetect" ); + addFiltered( port + ".deviceType", "cdrom-image" ); + addFiltered( port + ".fileName", image ); + } + return true; + } + } + return false; + } + + private static String vmBoolean( boolean var ) + { + return Boolean.toString( var ).toUpperCase(); + } + + private static String vmInteger( int val ) + { + return Integer.toString( val ); + } + + @Override + public boolean tweakForNonPersistent() + { + addFiltered( "suspend.disabled", "TRUE" ); + return true; + } + + @Override + public boolean addDisplayName( String name ) + { + addFiltered( "displayName", name ); + return true; + } + + @Override + public boolean addRam( int mem ) + { + addFiltered( "memsize", Integer.toString( mem ) ); + return true; + } + + public void setOs( String vendorOsId ) + { + addFiltered( "guestOS", vendorOsId ); + setOs( TConst.VIRT_VMWARE, vendorOsId ); + } + + @Override + public byte[] getFilteredDefinitionArray() + { + return config.toString( true, false ).getBytes( StandardCharsets.UTF_8 ); + } + + public byte[] getDefinitionArray() + { + return config.toString( false, false ).getBytes( StandardCharsets.UTF_8 ); + } + + @Override + public Virtualizer getVirtualizer() + { + return virtualizer; + } + + private static class Device + { + public boolean present = false; + public String deviceType = null; + public String filename = null; + + @Override + public String toString() + { + return filename + " is " + deviceType + " (present: " + present + ")"; + } + } + + private static class Controller + { + public boolean present = true; // Seems to be implicit, seen at least for IDE... + public String virtualDev = null; + Map<String, Device> devices = new HashMap<>(); + + @Override + public String toString() + { + return virtualDev + " is (present: " + present + "): " + devices.toString(); + } + } + + @Override + public void applySettingsForLocalEdit() + { + addFiltered( "gui.applyHostDisplayScalingToGuest", "FALSE" ); + } + + public String getValue( String key ) + { + return config.get( key ); + } + + public void setSoundCard( VmMetaData.SoundCardType type ) + { + VmWareSoundCardMeta soundCardMeta = soundCards.get( type ); + addFiltered( "sound.present", vmBoolean( soundCardMeta.isPresent ) ); + if ( soundCardMeta.value != null ) { + addFiltered( "sound.virtualDev", soundCardMeta.value ); + } else { + config.remove( "sound.virtualDev" ); + } + } + + public VmMetaData.SoundCardType getSoundCard() + { + if ( !isSetAndTrue( "sound.present" ) || !isSetAndTrue( "sound.autodetect" ) ) { + return VmMetaData.SoundCardType.NONE; + } + String current = config.get( "sound.virtualDev" ); + if ( current != null ) { + VmWareSoundCardMeta soundCardMeta = null; + for ( VmMetaData.SoundCardType type : VmMetaData.SoundCardType.values() ) { + soundCardMeta = soundCards.get( type ); + if ( soundCardMeta != null ) { + if ( current.equals( soundCardMeta.value ) ) { + return type; + } + } + } + } + return VmMetaData.SoundCardType.DEFAULT; + } + + public void setDDAcceleration( VmMetaData.DDAcceleration type ) + { + VmWareDDAccelMeta ddaMeta = ddacc.get( type ); + addFiltered( "mks.enable3d", vmBoolean( ddaMeta.isPresent ) ); + } + + public VmMetaData.DDAcceleration getDDAcceleration() + { + if ( isSetAndTrue( "mks.enable3d" ) ) { + return VmMetaData.DDAcceleration.ON; + } else { + return VmMetaData.DDAcceleration.OFF; + } + } + + public void setHWVersion( VmMetaData.HWVersion type ) + { + VmWareHWVersionMeta hwVersionMeta = hwversion.get( type ); + addFiltered( "virtualHW.version", vmInteger( hwVersionMeta.version ) ); + } + + public VmMetaData.HWVersion getHWVersion() + { + int currentValue = Util.parseInt( config.get( "virtualHW.version" ), -1 ); + VmWareHWVersionMeta hwVersionMeta = null; + for ( VmMetaData.HWVersion ver : VmMetaData.HWVersion.values() ) { + hwVersionMeta = hwversion.get( ver ); + if ( hwVersionMeta == null ) { + continue; + } + if ( currentValue == hwVersionMeta.version ) { + return ver; + } + } + return HWVersion.NONE; + } + + public void setEthernetDevType( int cardIndex, VmMetaData.EthernetDevType type ) + { + VmWareEthernetDevTypeMeta ethernetDevTypeMeta = networkCards.get( type ); + if ( ethernetDevTypeMeta.value != null ) { + addFiltered( "ethernet" + cardIndex + ".virtualDev", ethernetDevTypeMeta.value ); + } else { + config.remove( "ethernet" + cardIndex + ".virtualDev" ); + } + } + + public VmMetaData.EthernetDevType getEthernetDevType( int cardIndex ) + { + String temp = config.get( "ethernet" + cardIndex + ".virtualDev" ); + if ( temp != null ) { + VmWareEthernetDevTypeMeta ethernetDevTypeMeta = null; + for ( VmMetaData.EthernetDevType type : VmMetaData.EthernetDevType.values() ) { + ethernetDevTypeMeta = networkCards.get( type ); + if ( ethernetDevTypeMeta == null ) { + continue; + } + if ( temp.equals( ethernetDevTypeMeta.value ) ) { + return type; + } + } + } + return VmMetaData.EthernetDevType.AUTO; + } + + @Override + public void setMaxUsbSpeed( VmMetaData.UsbSpeed newSpeed ) + { + if ( newSpeed == null ) { + newSpeed = VmMetaData.UsbSpeed.NONE; + } + VmwareUsbSpeed newSpeedMeta = usbSpeeds.get( newSpeed ); + if ( newSpeedMeta == null ) { + throw new RuntimeException( "USB Speed " + newSpeed.name() + " not registered with VMware" ); + } + for ( VmwareUsbSpeed meta : usbSpeeds.values() ) { + if ( meta == null ) + continue; // Should not happen + if ( meta.keyName == null ) + continue; // "No USB" has no config entry, obviously + if ( meta.speedNumeric <= newSpeedMeta.speedNumeric ) { + // Enable desired speed class, plus all lower ones + addFiltered( meta.keyName, "TRUE" ); + } else { + // This one is higher – remove + config.remove( meta.keyName ); + } + } + // VMware 14+ needs this to use USB 3.0 devices at USB 3.0 ports in VMs configured for < 3.0 + if ( newSpeedMeta.speedNumeric > 0 && newSpeedMeta.speedNumeric < 3 ) { + addFiltered( "usb.mangleUsb3Speed", "TRUE" ); + } + } + + @Override + public VmMetaData.UsbSpeed getMaxUsbSpeed() + { + int max = 0; + VmMetaData.UsbSpeed maxEnum = VmMetaData.UsbSpeed.NONE; + for ( Entry<VmMetaData.UsbSpeed, VmwareUsbSpeed> entry : usbSpeeds.entrySet() ) { + VmwareUsbSpeed v = entry.getValue(); + if ( v.speedNumeric > max && isSetAndTrue( v.keyName ) ) { + max = v.speedNumeric; + maxEnum = entry.getKey(); + } + } + return maxEnum; + } + + @Override + public boolean addCpuCoreCount( int numCores ) + { + // TODO actually add the cpu core count to the machine description + return false; + } + + public void registerVirtualHW() + { + soundCards.put( VmMetaData.SoundCardType.NONE, new VmWareSoundCardMeta( false, null ) ); + soundCards.put( VmMetaData.SoundCardType.DEFAULT, new VmWareSoundCardMeta( true, null ) ); + soundCards.put( VmMetaData.SoundCardType.SOUND_BLASTER, new VmWareSoundCardMeta( true, "sb16" ) ); + soundCards.put( VmMetaData.SoundCardType.ES, new VmWareSoundCardMeta( true, "es1371" ) ); + soundCards.put( VmMetaData.SoundCardType.HD_AUDIO, new VmWareSoundCardMeta( true, "hdaudio" ) ); + + ddacc.put( VmMetaData.DDAcceleration.OFF, new VmWareDDAccelMeta( false ) ); + ddacc.put( VmMetaData.DDAcceleration.ON, new VmWareDDAccelMeta( true ) ); + + hwversion.put( VmMetaData.HWVersion.NONE, new VmWareHWVersionMeta( 0 ) ); + hwversion.put( VmMetaData.HWVersion.THREE, new VmWareHWVersionMeta( 3 ) ); + hwversion.put( VmMetaData.HWVersion.FOUR, new VmWareHWVersionMeta( 4 ) ); + hwversion.put( VmMetaData.HWVersion.SIX, new VmWareHWVersionMeta( 6 ) ); + hwversion.put( VmMetaData.HWVersion.SEVEN, new VmWareHWVersionMeta( 7 ) ); + hwversion.put( VmMetaData.HWVersion.EIGHT, new VmWareHWVersionMeta( 8 ) ); + hwversion.put( VmMetaData.HWVersion.NINE, new VmWareHWVersionMeta( 9 ) ); + hwversion.put( VmMetaData.HWVersion.TEN, new VmWareHWVersionMeta( 10 ) ); + hwversion.put( VmMetaData.HWVersion.ELEVEN, new VmWareHWVersionMeta( 11 ) ); + hwversion.put( VmMetaData.HWVersion.TWELVE, new VmWareHWVersionMeta( 12 ) ); + hwversion.put( VmMetaData.HWVersion.FOURTEEN, new VmWareHWVersionMeta( 14 ) ); + hwversion.put( VmMetaData.HWVersion.FIFTEEN, new VmWareHWVersionMeta( 15 ) ); + hwversion.put( VmMetaData.HWVersion.FIFTEEN_ONE, new VmWareHWVersionMeta( 16 ) ); + hwversion.put( VmMetaData.HWVersion.SIXTEEN, new VmWareHWVersionMeta( 17 ) ); + hwversion.put( VmMetaData.HWVersion.SIXTEEN_ONE, new VmWareHWVersionMeta( 18 ) ); + + networkCards.put( VmMetaData.EthernetDevType.AUTO, new VmWareEthernetDevTypeMeta( null ) ); + networkCards.put( VmMetaData.EthernetDevType.PCNET32, new VmWareEthernetDevTypeMeta( "vlance" ) ); + networkCards.put( VmMetaData.EthernetDevType.E1000, new VmWareEthernetDevTypeMeta( "e1000" ) ); + networkCards.put( VmMetaData.EthernetDevType.E1000E, new VmWareEthernetDevTypeMeta( "e1000e" ) ); + networkCards.put( VmMetaData.EthernetDevType.VMXNET, new VmWareEthernetDevTypeMeta( "vmxnet" ) ); + networkCards.put( VmMetaData.EthernetDevType.VMXNET3, new VmWareEthernetDevTypeMeta( "vmxnet3" ) ); + + usbSpeeds.put( VmMetaData.UsbSpeed.NONE, new VmwareUsbSpeed( 0, null )); + usbSpeeds.put( VmMetaData.UsbSpeed.USB1_1, new VmwareUsbSpeed( 1, "usb" ) ); + usbSpeeds.put( VmMetaData.UsbSpeed.USB2_0, new VmwareUsbSpeed( 2, "ehci" ) ); + usbSpeeds.put( VmMetaData.UsbSpeed.USB3_0, new VmwareUsbSpeed( 3, "usb_xhci" ) ); + } + +} 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; + } +} |