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 osList, File file ) throws IOException, VirtualizationConfigurationException { super( new VirtualizerVirtualBox(), osList ); this.config = new VirtualizationConfigurationVirtualboxFileFormat( file ); init(); } public VirtualizationConfigurationVirtualBox( List 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 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 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 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 to media registry." ); return; } Element floppyImageReg = (Element)config.addNewNode( "/VirtualBox/Machine/MediaRegistry/FloppyImages", "Image" ); if ( floppyImageReg == null ) { LOGGER.error( "Failed to add 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 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(); } }