diff options
Diffstat (limited to 'src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualBox.java')
-rw-r--r-- | src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualBox.java | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualBox.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualBox.java new file mode 100644 index 0000000..153fffa --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualBox.java @@ -0,0 +1,601 @@ +package org.openslx.virtualization.configuration; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openslx.bwlp.thrift.iface.OperatingSystem; +import org.openslx.thrifthelper.TConst; +import org.openslx.util.Util; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.configuration.VirtualizationConfigurationVirtualboxFileFormat.MatchMode; +import org.openslx.virtualization.hardware.ConfigurationGroups; +import org.openslx.virtualization.hardware.Ethernet; +import org.openslx.virtualization.hardware.SoundCard; +import org.openslx.virtualization.hardware.Usb; +import org.openslx.virtualization.hardware.VirtOptionValue; +import org.openslx.virtualization.virtualizer.VirtualizerVirtualBox; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class VirtualizationConfigurationVirtualBox extends VirtualizationConfiguration +{ + /** + * File name extension for VirtualBox virtualization configuration files.. + */ + public static final String FILE_NAME_EXTENSION = "vbox"; + + private static final Logger LOGGER = LogManager.getLogger( VirtualizationConfigurationVirtualBox.class ); + + private final VirtualizationConfigurationVirtualboxFileFormat config; + + public static enum EthernetType + { + NAT( "vboxnet1" ), BRIDGED( "vboxnet0" ), HOST_ONLY( "vboxnet2" ); + + public final String vnet; + + private EthernetType( String vnet ) + { + this.vnet = vnet; + } + } + + public VirtualizationConfigurationVirtualBox( List<OperatingSystem> osList, File file ) + throws IOException, VirtualizationConfigurationException + { + super( new VirtualizerVirtualBox(), osList ); + this.config = new VirtualizationConfigurationVirtualboxFileFormat( file ); + init(); + } + + public VirtualizationConfigurationVirtualBox( List<OperatingSystem> osList, byte[] vmContent, int length ) + throws IOException, VirtualizationConfigurationException + { + super( new VirtualizerVirtualBox(), osList ); + this.config = new VirtualizationConfigurationVirtualboxFileFormat( vmContent, length ); + init(); + } + + private void init() + { + displayName = config.getDisplayName(); + setOs( config.getOsName() ); + this.isMachineSnapshot = config.isMachineSnapshot(); + for ( HardDisk hardDisk : config.getHdds() ) { + hdds.add( hardDisk ); + } + } + + private void disableEnhancedNetworkAdapters() + { + final NodeList disableAdapters = this.config.findNodes( "/VirtualBox/Machine/Hardware/Network/Adapter[not(@slot='0')]" ); + + if ( disableAdapters != null ) { + for ( int i = 0; i < disableAdapters.getLength(); i++ ) { + final Element disableAdapter = (Element)disableAdapters.item( i ); + disableAdapter.setAttribute( "enabled", "false" ); + } + } + } + + private void removeEnhancedNetworkAdapters() + { + final NodeList removeAdapters = this.config.findNodes( "/VirtualBox/Machine/Hardware/Network/Adapter[not(@slot='0')]" ); + + if ( removeAdapters != null ) { + for ( int i = 0; i < removeAdapters.getLength(); i++ ) { + final Node removeAdapter = removeAdapters.item( i ); + removeAdapter.getParentNode().removeChild( removeAdapter ); + } + } + } + + @Override + public void transformEditable() throws VirtualizationConfigurationException + { + this.disableEnhancedNetworkAdapters(); + } + + @Override + public void transformPrivacy() throws VirtualizationConfigurationException + { + config.addPlaceHolders(); + } + + @Override + public byte[] getConfigurationAsByteArray() + { + return config.toString( true ).getBytes( StandardCharsets.UTF_8 ); + } + + @Override + public boolean addEmptyHddTemplate() + { + return this.addHddTemplate( VirtualizationConfigurationVirtualboxFileFormat.DUMMY_VALUE, + VirtualizationConfigurationVirtualboxFileFormat.DUMMY_VALUE, + VirtualizationConfigurationVirtualboxFileFormat.DUMMY_VALUE ); + } + + @Override + public boolean addHddTemplate( String diskImage, String hddMode, String redoDir ) + { + config.changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk", "location", diskImage, MatchMode.FIRST_ONLY ); + config.changeAttribute( "/VirtualBox/Machine", "snapshotFolder", redoDir, MatchMode.FIRST_ONLY ); + 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, MatchMode.FIRST_ONLY ); + + 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, MatchMode.FIRST_ONLY ); + config.changeAttribute( config.storageControllersPath() + "/StorageController/AttachedDevice/Image", "uuid", + vboxUUid, MatchMode.FIRST_ONLY ); + + // 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, MatchMode.EXACTLY_ONE ); + } + + @Override + public boolean addDefaultNat() + { + final boolean status; + + final Node adapterSlot0 = config.findNodes( "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='0']" ).item( 0 ); + if ( adapterSlot0 != null ) { + // remove all child node to wipe existing networking mode + final NodeList adapterSlot0SettingNodes = adapterSlot0.getChildNodes(); + while ( adapterSlot0.getChildNodes().getLength() > 0 ) { + adapterSlot0.removeChild( adapterSlot0SettingNodes.item( 0 ) ); + } + + // add networking mode 'NAT' + if ( config.addNewNode( "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='0']", "NAT" ) == null ) { + LOGGER.error( "Failed to set network adapter to NAT." ); + status = false; + } else { + status = config.changeAttribute( "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='0']", "MACAddress", + "080027B86D12", MatchMode.EXACTLY_ONE ); + } + } else { + status = false; + } + + return status; + } + + @Override + public void setOs( String vendorOsId ) + { + config.changeAttribute( "/VirtualBox/Machine", "OSType", vendorOsId, MatchMode.EXACTLY_ONE ); + + final OperatingSystem os = VirtualizationConfigurationUtils.getOsOfVirtualizerFromList( this.osList, + TConst.VIRT_VIRTUALBOX, vendorOsId ); + this.setOs( os ); + } + + @Override + public boolean addDisplayName( String name ) + { + return config.changeAttribute( "/VirtualBox/Machine", "name", name, MatchMode.EXACTLY_ONE ); + } + + @Override + public boolean addRam( int mem ) + { + return config.changeAttribute( "/VirtualBox/Machine/Hardware/Memory", "RAMSize", + Integer.toString( mem ), MatchMode.EXACTLY_ONE ); + } + + @Override + public void addFloppy( int index, String image, boolean readOnly ) + { + Element floppyController = null; + NodeList matches = config.findNodes( config.storageControllersPath() + "/StorageController[@name='Floppy']" ); + if ( matches == null || matches.getLength() == 0 ) { + floppyController = (Element)config.addNewNode( config.storageControllersPath(), "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", + VirtualizationConfigurationVirtualboxFileFormat.DUMMY_VALUE ); + // 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", + VirtualizationConfigurationVirtualboxFileFormat.DUMMY_VALUE ); + floppyImageReg.setAttribute( "location", + VirtualizationConfigurationVirtualboxFileFormat.DUMMY_VALUE ); + } + } + + @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 ), MatchMode.EXACTLY_ONE ); + } + + class VBoxSoundCardModel extends VirtOptionValue + { + + public VBoxSoundCardModel( String id, String displayName ) + { + super( id, displayName ); + } + + @Override + public void apply() + { + // XXX I guess this "present" hack will be nicer with enum too + if ( Util.isEmptyString( this.id ) ) { + config.changeAttribute( "/VirtualBox/Machine/Hardware/AudioAdapter", "enabled", "false", MatchMode.MULTIPLE ); + return; + } + config.changeAttribute( "/VirtualBox/Machine/Hardware/AudioAdapter", "enabled", "true", MatchMode.FIRST_ONLY ); + config.changeAttribute( "/VirtualBox/Machine/Hardware/AudioAdapter", "controller", this.id, MatchMode.FIRST_ONLY ); + } + + @Override + public boolean isActive() + { + Element x = (Element)config.findNodes( "/VirtualBox/Machine/Hardware/AudioAdapter" ).item( 0 ); + if ( !x.hasAttribute( "enabled" ) + || ( x.hasAttribute( "enabled" ) && x.getAttribute( "enabled" ).equals( "false" ) ) ) { + return Util.isEmptyString( this.id ); // XXX enum + } + String val = "AC97"; + if ( x.hasAttribute( "controller" ) ) { + val = x.getAttribute( "controller" ); + } + return val.equals( this.id ); + } + + } + + class VBoxAccel3D extends VirtOptionValue + { + + public VBoxAccel3D( String id, String displayName ) + { + super( id, displayName ); + } + + @Override + public void apply() + { + config.changeAttribute( "/VirtualBox/Machine/Hardware/Display", "accelerate3D", this.id, MatchMode.EXACTLY_ONE ); + } + + @Override + public boolean isActive() + { + Element x = (Element)config.findNodes( "/VirtualBox/Machine/Hardware/Display" ).item( 0 ); + String val = "false"; + if ( x.hasAttribute( "accelerate3D" ) ) { + val = x.getAttribute( "accelerate3D" ); + } + return val.equalsIgnoreCase( this.id ); + } + + } + + /** + * Function does nothing for Virtual Box; + * Virtual Box accepts per default only one hardware version and is hidden from the user + */ + @Override + public void setVirtualizerVersion( Version type ) + { + } + + public Version getConfigurationVersion() + { + return this.config.getVersion(); + } + + @Override + public Version getVirtualizerVersion() + { + // Virtual Box uses only one virtual hardware version and can't be changed + return null; + } + + class VBoxNicModel extends VirtOptionValue + { + + private final int cardIndex; + + public VBoxNicModel( int cardIndex, String id, String displayName ) + { + super( id, displayName ); + this.cardIndex = cardIndex; + } + + @Override + public void apply() + { + String index = Integer.toString( this.cardIndex ); + String dev = this.id; + boolean present = true; + if ( "".equals( this.id ) ) { + // 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 + dev = "Am79C970A"; + present = false; + } + config.changeAttribute( "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='" + index + "']", "enabled", + Boolean.toString( present ), MatchMode.EXACTLY_ONE ); + config.changeAttribute( "/VirtualBox/Machine/Hardware/Network/Adapter[@slot='" + index + "']", "type", + dev, MatchMode.EXACTLY_ONE ); + } + + @Override + public boolean isActive() + { + Element x = (Element)config.findNodes( "/VirtualBox/Machine/Hardware/Network/Adapter" ).item( 0 ); + if ( !x.hasAttribute( "enabled" ) + || ( x.hasAttribute( "enabled" ) && x.getAttribute( "enabled" ).equalsIgnoreCase( "false" ) ) ) { + return Util.isEmptyString( this.id ); + } + // Has NIC + if ( !x.hasAttribute( "type" ) ) { + return "Am79C973".equals( this.id ); + } + return x.getAttribute( "type" ).equals( this.id ); + } + + } + + public void registerVirtualHW() + { + List<VirtOptionValue> list; + // 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... + list = new ArrayList<>(); + list.add( new VBoxSoundCardModel( "AC97", SoundCard.NONE ) ); + list.add( new VBoxSoundCardModel( "SB16", SoundCard.SOUND_BLASTER ) ); + list.add( new VBoxSoundCardModel( "HDA", SoundCard.HD_AUDIO ) ); + list.add( new VBoxSoundCardModel( "AC97", SoundCard.AC ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.SOUND_CARD_MODEL, list ) ); + + list = new ArrayList<>(); + list.add( new VBoxAccel3D( "true", "3D" ) ); + list.add( new VBoxAccel3D( "false", "2D" ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.GFX_TYPE, list ) ); + + list = new ArrayList<>(); + list.add( new VBoxNicModel( 0, "", Ethernet.NONE ) ); + list.add( new VBoxNicModel( 0, "Am79C970A", Ethernet.PCNETPCI2 ) ); + list.add( new VBoxNicModel( 0, "Am79C973", Ethernet.PCNETFAST3 ) ); + list.add( new VBoxNicModel( 0, "82540EM", Ethernet.PRO1000MTD ) ); + list.add( new VBoxNicModel( 0, "82543GC", Ethernet.PRO1000TS ) ); + list.add( new VBoxNicModel( 0, "82545EM", Ethernet.PRO1000MTS ) ); + list.add( new VBoxNicModel( 0, "virtio", Ethernet.PARAVIRT ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.NIC_MODEL, list ) ); + + list = new ArrayList<>(); + list.add( new VBoxUsbSpeed( null, Usb.NONE ) ); + list.add( new VBoxUsbSpeed( "OHCI", Usb.USB1_1 ) ); + list.add( new VBoxUsbSpeed( "EHCI", Usb.USB2_0 ) ); + list.add( new VBoxUsbSpeed( "XHCI", Usb.USB3_0 ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.USB_SPEED, list ) ); + } + + @Override + public boolean addEthernet( VirtualizationConfiguration.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 void transformNonPersistent() throws VirtualizationConfigurationException + { + // 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" ); + + this.removeEnhancedNetworkAdapters(); + } + + class VBoxUsbSpeed extends VirtOptionValue + { + + public VBoxUsbSpeed( String id, String displayName ) + { + super( id, displayName ); + } + + @Override + public void apply() + { + // Wipe existing ones + config.removeNodes( "/VirtualBox/Machine/Hardware", "USB" ); + if ( Util.isEmptyString( this.id ) ) { + // 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" ); + node.setAttribute( "type", this.id ); + node.setAttribute( "name", this.id ); + if ( this.id.equals( "EHCI" ) ) { // XXX "mechanically" ported, could make a special class for this special case + // If EHCI (2.0) is selected, VBox adds an OHCI controller too... + node = config.addNewNode( "/VirtualBox/Machine/Hardware/USB/Controllers", "Controller" ); + node.setAttribute( "type", "OHCI" ); + node.setAttribute( "name", "OHCI" ); + } + } + + @Override + public boolean isActive() + { + NodeList nodes = config.findNodes( "/VirtualBox/Machine/Hardware/USB/Controllers/Controller/@type" ); + LOGGER.info( "Found USB attribute nodes:" + nodes.getLength() ); + /* Again, we need an ugly special case here for USB 2.0, which creates two entries in the + * XML, one EHCI and one OHCI. If we simply look for "our" type and return true, both + * OHCI and EHCI would return true, and what ends up getting selected in the UI is more or + * less undefined. So we need to put more brains in here and special-case the whole EHCI/OHCI + * detection, and not return true if this.id is OHCI, and we have a controller node with type + * OHCI, but also another node with type EHCI... + */ + boolean ohci = false, ehci = false; + for ( int i = 0; i < nodes.getLength(); ++i ) { + if ( nodes.item( i ).getNodeType() != Node.ATTRIBUTE_NODE ) { + LOGGER.info( "Not ATTRIBUTE type (" + nodes.item( i ).getClass().getSimpleName() + ")" ); + continue; + } + String type = ( (Attr)nodes.item( i ) ).getValue(); + LOGGER.info( "Found USB node with type " + type ); + if ( type.equals( "EHCI" ) ) { + ehci = true; + } else if ( type.equals( "OHCI" ) ) { + ohci = true; + } else if ( type.equals( this.id ) ) + return true; + } + if ( ehci && "EHCI".equals( this.id ) ) + return true; + if ( ohci && !ehci && "OHCI".equals( this.id ) ) + return true; + if ( config.findNodes( "/VirtualBox/OpenSLX/USB/@disabled" ).getLength() > 0 ) { + return Util.isEmptyString( this.id ); + } + return false; + } + + } + + @Override + public String getFileNameExtension() + { + return VirtualizationConfigurationVirtualBox.FILE_NAME_EXTENSION; + } + + @Override + public void validate() throws VirtualizationConfigurationException + { + this.config.validate(); + } + + @Override + public void disableUsb() + { + new VBoxUsbSpeed( null, Usb.NONE ).apply(); + } +} |