diff options
Diffstat (limited to 'src/main/java/org/openslx/virtualization')
44 files changed, 7547 insertions, 0 deletions
diff --git a/src/main/java/org/openslx/virtualization/Version.java b/src/main/java/org/openslx/virtualization/Version.java new file mode 100644 index 0000000..c823324 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/Version.java @@ -0,0 +1,321 @@ +package org.openslx.virtualization; + +import java.util.List; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.openslx.util.Util; + +/** + * Represents a version information. + * + * The version information is used in the field of virtualization (for virtualizers, disk images, + * virtualization configuration files, ...). + * + * @author Manuel Bentele + * @version 1.0 + */ +public class Version implements Comparable<Version> +{ + /** + * Regular expression to parse a version from a {@link String}. + * <p> + * The regular expression matches a version if its textual version information is well-formed + * according to the following examples: + * + * <pre> + * 52 + * 4.31 + * 5.10.13 + * </pre> + */ + private static final Pattern VERSION_NUMBER_REGEX = Pattern.compile( "^(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?$" ); + + /** + * Major number of the version. + */ + private final short major; + + /** + * Minor number of the version. + */ + private final short minor; + + /** + * Name or description of the version. + */ + private final String name; + + /** + * Creates a new version. + * + * The version consists of a major version, whereas the minor version is set to the value + * <code>0</code> and the version name is undefined. + * + * @param major major version. + */ + public Version( short major ) + { + this( major, Short.valueOf( "0" ), null ); + } + + /** + * Creates a new version. + * + * The version consists of a major version labeled with a version name, whereas the minor version + * is set to the value <code>0</code>. + * + * @param major major version. + * @param name version name. + */ + public Version( short major, String name ) + { + this( major, Short.valueOf( "0" ), name ); + } + + /** + * Creates a new version. + * + * The version consists of a major and a minor version, whereas the version name is undefined. + * + * @param major major version. + * @param minor minor version. + */ + public Version( short major, short minor ) + { + this( major, minor, null ); + } + + /** + * Creates a new version. + * + * The version consists of a major and a minor version labeled with a version name. + * + * @param major major version. + * @param minor minor version. + * @param name version name. + */ + public Version( short major, short minor, String name ) + { + this.major = major; + this.minor = minor; + this.name = name; + } + + /** + * Returns the major version. + * + * @return major version. + */ + public short getMajor() + { + return this.major; + } + + /** + * Returns the minor version. + * + * @return minor version. + */ + public short getMinor() + { + return this.minor; + } + + /** + * Returns the full version as {@link Integer}. + * + * The full version consists of the major and minor version where both are combined in one + * {@link Integer} value. The upper 16-bits of the value represent the major number, whereas + * the lower 16-bits represent the minor number. + * + * @return full version as {@link Integer}. + */ + public int getVersion() + { + final int major = this.major; + final int minor = this.minor; + + return ( major << Short.SIZE ) | minor; + } + + /** + * Returns the name of the version. + * + * @return name of the version. + */ + public String getName() + { + return this.name; + } + + /** + * Checks if version is supported by a version from a list of supported versions. + * + * @param supportedVersions list of supported versions. + * @return state whether version is supported by a version from the list of versions or not. + */ + public boolean isSupported( List<Version> supportedVersions ) + { + return supportedVersions.contains( this ); + } + + /** + * Returns a version from a list of supported versions filtered by its given filter predicate. + * + * @param byFilter filter predicate. + * @param supportedVersions list of supported versions. + * @return version from a list of supported versions filtered by its given filter predicate. + */ + private static Version getInstanceByPredicateFromVersions( Predicate<Version> byFilter, + List<Version> supportedVersions ) + { + return supportedVersions.stream().filter( byFilter ).findFirst().orElse( null ); + } + + /** + * Returns a version from a list of supported versions by its given major version. + * + * @param major version. + * @param supportedVersions list of supported versions. + * @return version from a list of supported versions by its given major version. + */ + public static Version getInstanceByMajorFromVersions( short major, List<Version> supportedVersions ) + { + final Predicate<Version> byMajor = version -> major == version.getMajor(); + return Version.getInstanceByPredicateFromVersions( byMajor, supportedVersions ); + } + + /** + * Returns a version from a list of supported versions by its given major and minor version. + * + * @param major version. + * @param minor version. + * @param supportedVersions list of supported versions. + * @return version from a list of supported versions by its given major and minor version. + */ + public static Version getInstanceByMajorMinorFromVersions( short major, short minor, + List<Version> supportedVersions ) + { + final Predicate<Version> byMajorMinor = version -> major == version.getMajor() && minor == version.getMinor(); + return supportedVersions.stream().filter( byMajorMinor ).findFirst().orElse( null ); + } + + /** + * Checks if this version is smaller than a specified {@code version}. + * + * @param version for comparison. + * @return state whether this version is smaller than the specified {@code version} or not. + */ + public boolean isSmallerThan( Version version ) + { + return ( this.compareTo( version ) < 0 ) ? true : false; + } + + /** + * Checks if this version is greater than a specified {@code version}. + * + * @param version for comparison. + * @return state whether this version is greater than the specified {@code version} or not. + */ + public boolean isGreaterThan( Version version ) + { + return ( this.compareTo( version ) > 0 ) ? true : false; + } + + /** + * Creates a new version parsed from a {@link String}. + * + * The version consists of a major and a minor version parsed from the specified {@link String}. + * + * @param version textual information containing a version as {@link String}. The textual + * version should be well-formed according to the defined regular expression + * {@link #VERSION_NUMBER_REGEX}. + * @return version instance. + */ + public static Version valueOf( String version ) + { + final Version parsedVersion; + + if ( Util.isEmptyString( version ) ) { + parsedVersion = null; + } else { + final Matcher versionMatcher = VERSION_NUMBER_REGEX.matcher( version ); + + if ( versionMatcher.find() ) { + final String majorStr = versionMatcher.group( 1 ); + final String minorStr = versionMatcher.group( 2 ); + + final short major = ( majorStr != null ) ? Short.valueOf( majorStr ) : 0; + final short minor = ( minorStr != null ) ? Short.valueOf( minorStr ) : 0; + + parsedVersion = new Version( major, minor ); + + } else { + parsedVersion = null; + } + } + + return parsedVersion; + } + + @Override + public String toString() + { + if ( Util.isEmptyString( this.getName() ) ) { + return String.format( "%d.%d", this.getMajor(), this.getMinor() ); + } else { + return String.format( "%d.%d %s", this.getMajor(), this.getMinor(), this.getName() ); + } + } + + @Override + public boolean equals( Object obj ) + { + if ( obj == null ) { + return false; + } else if ( this.getClass() != obj.getClass() ) { + return false; + } else if ( this.compareTo( Version.class.cast( obj ) ) == 0 ) { + return true; + } else { + return false; + } + } + + @Override + public int compareTo( Version v ) + { + // compare the current version to the specified version + if ( this.getMajor() < v.getMajor() ) { + // current major version is smaller than the given major version + return -1; + } else if ( this.getMajor() > v.getMajor() ) { + // current major version is larger than the given major version + return 1; + } else { + // current major version is equal to the given major version + // so compare the current minor version to the specified minor version + if ( this.getMinor() < v.getMinor() ) { + // current minor version is smaller than the given minor version + // so the entire version is smaller than the given version + return -1; + } else if ( this.getMinor() > v.getMinor() ) { + // current minor version is larger than the given minor version + // so the entire version is larger than the given version + return 1; + } else { + // current minor version is equal to the given minor version + // so the entire version is equal to the given version + return 0; + } + } + } + + @Override + public int hashCode() + { + return ( Short.valueOf( this.getMajor() ).hashCode() ) ^ ( Short.valueOf( this.getMinor() ).hashCode() ); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfiguration.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfiguration.java new file mode 100644 index 0000000..f548597 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfiguration.java @@ -0,0 +1,378 @@ +package org.openslx.virtualization.configuration; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openslx.bwlp.thrift.iface.OperatingSystem; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.hardware.VirtOptionValue; +import org.openslx.virtualization.hardware.ConfigurationGroups; +import org.openslx.virtualization.virtualizer.Virtualizer; + +/** + * Describes a configured virtual machine. This class is parsed from a machine + * description, like a *.vmx for VMware machines. + */ +public abstract class VirtualizationConfiguration +{ + private static final Logger LOGGER = LogManager.getLogger( VirtualizationConfiguration.class ); + + private final Virtualizer virtualizer; + + + 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; + } + + public static class ConfigurableOptionGroup + { + public final ConfigurationGroups groupIdentifier; + + public final List<VirtOptionValue> availableOptions; + + public ConfigurableOptionGroup( ConfigurationGroups groupIdentifier, List<VirtOptionValue> availableOptions ) + { + this.groupIdentifier = groupIdentifier; + this.availableOptions = Collections.unmodifiableList( availableOptions ); + } + + public VirtOptionValue getSelected() + { + for (VirtOptionValue hw : availableOptions) { + if ( hw.isActive() ) + return hw; + } + return null; + } + + } + + /* + * Members + */ + + protected final List<HardDisk> hdds = new ArrayList<HardDisk>(); + + protected final List<OperatingSystem> osList; + + private OperatingSystem os = null; + + protected String displayName = null; + + protected boolean isMachineSnapshot; + + protected final List<ConfigurableOptionGroup> configurableOptions = new ArrayList<>(); + + /** + * Get operating system of this VM. + * + * @return operating system of the VM. + */ + public OperatingSystem getOs() + { + return os; + } + + /** + * Sets the operating system for the virtualization configuration. + * + * @param os operating system for the virtualization configuration. + */ + public void setOs( OperatingSystem os ) + { + this.os = os; + } + + /** + * Get all hard disks of this VM. + * + * @return list of hard disks of the VM. + */ + public List<HardDisk> getHdds() + { + return Collections.unmodifiableList( hdds ); + } + + /** + * Get display name of VM. + * + * @return display name of the VM. + */ + public String getDisplayName() + { + return displayName; + } + + public boolean isMachineSnapshot() + { + return isMachineSnapshot; + } + + /** + * Return name of file that indicates this VM is currently suspended. + * The existence of this file means the VM is currently suspended. + * Can be null if this virtualizer doesn't support suspension, or if + * it's already known the VM is not in a suspended state. + */ + public String getSuspendedFile() + { + return null; + } + + private class VersionOption extends VirtOptionValue { + + private final Version version; + + public VersionOption( Version version ) + { + super( Integer.toString( version.getVersion() ), version.getName() ); + this.version = version; + } + + @Override + public boolean isActive() + { + try { + return getVirtualizerVersion().equals( version ); + } catch (NullPointerException e) { + return false; + } + } + + @Override + public void apply() + { + setVirtualizerVersion( version ); + } + } + + /* + * Methods + */ + + public VirtualizationConfiguration( Virtualizer virtualizer, List<OperatingSystem> osList ) + { + this.virtualizer = virtualizer; + + if ( osList == null ) { + // create empty operating system list if none is specified + this.osList = new ArrayList<OperatingSystem>(); + } else { + this.osList = osList; + } + + // register virtual hardware models for graphical editing of virtual devices (GPU, sound, USB, ...) + final List<Version> availables = this.getVirtualizer().getSupportedVersions(); + //Collections.sort( availables ); // XXX WTF? How did this not break before? It's an unmodifiable collection + if ( availables != null ) { + // XXX List is null for qemu? + List<VirtOptionValue> list = new ArrayList<>(); + for ( Version ver : availables ) { + list.add( new VersionOption( ver ) ); + } + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.HW_VERSION, list ) ); + } + this.registerVirtualHW(); + } + + /** + * 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 + * @throws IOException failed to read machine description from specified file. + */ + public static VirtualizationConfiguration getInstance( List<OperatingSystem> osList, File file ) + throws IOException + { + try { + return new VirtualizationConfigurationVmware( osList, file ); + } catch ( VirtualizationConfigurationException e ) { + LOGGER.debug( "Not a VMware file", e ); + } + try { + return new VirtualizationConfigurationVirtualBox( osList, file ); + } catch ( VirtualizationConfigurationException e ) { + LOGGER.debug( "Not a VirtualBox file", e ); + } + try { + return new VirtualizationConfigurationQemu( osList, file ); + } catch ( VirtualizationConfigurationException e ) { + LOGGER.debug( "Not a Libvirt file", e ); + } + try { + return new VirtualizationConfigurationDocker( osList, file ); + } catch ( VirtualizationConfigurationException e ) { + LOGGER.debug( "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 failed to read machine description from specified byte stream. + * @throws VirtualizationConfigurationException + */ + public static VirtualizationConfiguration getInstance( List<OperatingSystem> osList, byte[] vmContent, + int length ) + throws IOException, VirtualizationConfigurationException + { + try { + return new VirtualizationConfigurationVmware( osList, vmContent, length ); + } catch ( VirtualizationConfigurationException e ) { + LOGGER.debug( "Not a VMware file", e ); + } + try { + return new VirtualizationConfigurationDocker( osList, vmContent, length ); + } catch ( VirtualizationConfigurationException e ) { + LOGGER.debug( "Not a tar.gz file, for docker container", e ); + } + try { + return new VirtualizationConfigurationVirtualBox( osList, vmContent, length ); + } catch ( VirtualizationConfigurationException e ) { + LOGGER.debug( "Not a VirtualBox file", e ); + } + try { + return new VirtualizationConfigurationQemu( osList, vmContent, length ); + } catch ( VirtualizationConfigurationException e ) { + LOGGER.debug( "Not a Libvirt file", e ); + } + throw new VirtualizationConfigurationException( "Unknown virtualizer config format" ); + } + + /** + * Returns the file name extension for the virtualization configuration file. + * + * @return file name extension for the virtualization configuration file. + */ + public abstract String getFileNameExtension(); + + public abstract boolean addEmptyHddTemplate(); + + 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 disableUsb(); + + public abstract void setVirtualizerVersion( Version type ); + + public abstract Version getVirtualizerVersion(); + + public abstract byte[] getConfigurationAsByteArray(); + + public String getConfigurationAsString() + { + return new String( this.getConfigurationAsByteArray(), StandardCharsets.UTF_8 ); + } + + @Override + public String toString() + { + return this.getConfigurationAsString(); + } + + public abstract boolean addEthernet( EtherType type ); + + public Virtualizer getVirtualizer() + { + return this.virtualizer; + } + + /** + * Validates the virtualization configuration and reports errors if its content is not a valid + * virtualization configuration. + * + * @throws VirtualizationConfigurationException validation of the virtualization configuration + * failed. + */ + public abstract void validate() throws VirtualizationConfigurationException; + + /** + * Transforms the virtualization configuration in terms of a privacy filter to filter out + * sensitive information like name of users in absolute paths. + * + * @throws VirtualizationConfigurationException transformation of the virtualization + * configuration failed. + */ + public abstract void transformPrivacy() throws VirtualizationConfigurationException; + + /** + * Transforms the virtualization configuration applying options that are desired when locally + * editing a virtualized system (e.g. disables automatic DPI scaling). + * + * @throws VirtualizationConfigurationException transformation of the virtualization + * configuration failed. + */ + public abstract void transformEditable() throws VirtualizationConfigurationException; + + /** + * Transforms the virtualization configuration applying options that are desired when running a + * virtualized system in a stateless manner. + * + * @throws VirtualizationConfigurationException transformation of the virtualization + * configuration failed. + */ + public abstract void transformNonPersistent() throws VirtualizationConfigurationException; + + /** + * Function used to register virtual devices. + */ + public abstract void registerVirtualHW(); + + /** + * Get all config options this virtualizer supports, with all available options. + */ + public List<ConfigurableOptionGroup> getConfigurableOptions() + { + return Collections.unmodifiableList( configurableOptions ); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationDocker.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationDocker.java new file mode 100644 index 0000000..3163ed2 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationDocker.java @@ -0,0 +1,174 @@ +package org.openslx.virtualization.configuration; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openslx.bwlp.thrift.iface.OperatingSystem; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.virtualizer.VirtualizerDocker; + +public class VirtualizationConfigurationDocker extends VirtualizationConfiguration { + + /** + * File name extension for Docker virtualization configuration files. + */ + private static final String FILE_NAME_EXTENSION = null; + + private static final Logger LOGGER = LogManager.getLogger( VirtualizationConfigurationDocker.class); + + /** + * 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; + + public VirtualizationConfigurationDocker(List<OperatingSystem> osList, File file) throws VirtualizationConfigurationException { + super(new VirtualizerDocker(), osList); + + BufferedInputStream bis = null; + + try { + bis = new BufferedInputStream(new FileInputStream(file)); + containerDefinition = new byte[(int) file.length()]; + bis.read(containerDefinition); + + checkIsTarGz(); + } catch (IOException | VirtualizationConfigurationException e) { + LOGGER.error("Couldn't read dockerfile", e); + } finally { + try { + bis.close(); + } catch ( IOException e ) { + LOGGER.warn( "Could not close the input stream!" ); + } + } + } + + public VirtualizationConfigurationDocker(List<OperatingSystem> osList, byte[] vmContent, int length) + throws VirtualizationConfigurationException { + super(new VirtualizerDocker(), 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 VirtualizationConfigurationException + */ + private void checkIsTarGz() throws VirtualizationConfigurationException { + if (!((31 == containerDefinition[0]) && (-117 == containerDefinition[1]))) { + LOGGER.debug("Not Supported Content."); + throw new VirtualizationConfigurationException( + "DockerMetaDataDummy: Not tar.gz encoded content!"); + } + } + + @Override public void transformEditable() throws VirtualizationConfigurationException { + + } + + @Override + public void transformPrivacy() throws VirtualizationConfigurationException { + } + + @Override + public boolean addEmptyHddTemplate() { + return true; + } + + @Override public boolean addHddTemplate(File diskImage, String hddMode, String redoDir) { + return true; + } + + @Override public boolean addHddTemplate(String diskImagePath, String hddMode, String redoDir) { + return false; + } + + @Override public boolean addDefaultNat() { + return true; + } + + @Override public void setOs(String vendorOsId) { + + } + + @Override public boolean addDisplayName(String name) { + return true; + } + + @Override public boolean addRam(int mem) { + return true; + } + + @Override public void addFloppy(int index, String image, boolean readOnly) { + + } + + @Override public boolean addCdrom(String image) { + return true; + } + + @Override public boolean addCpuCoreCount(int nrOfCores) { + return true; + } + + @Override public void setVirtualizerVersion( Version type ) + { + } + + @Override public Version getVirtualizerVersion() + { + return null; + } + + @Override public byte[] getConfigurationAsByteArray() { + return this.containerDefinition; + } + + @Override public boolean addEthernet(EtherType type) { + return true; + } + + @Override public void transformNonPersistent() throws VirtualizationConfigurationException { + + } + + @Override public void registerVirtualHW() { + + } + + @Override + public String getFileNameExtension() { + return VirtualizationConfigurationDocker.FILE_NAME_EXTENSION; + } + + @Override + public void validate() throws VirtualizationConfigurationException + { + } + + @Override + public void disableUsb() + { + // Not applicable, maybe some day via USBguard? + } + +} diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationException.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationException.java new file mode 100644 index 0000000..2d401b1 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationException.java @@ -0,0 +1,13 @@ +package org.openslx.virtualization.configuration; + +public class VirtualizationConfigurationException extends Exception +{ + /** + * Version for serialization. + */ + private static final long serialVersionUID = 5794121065945636839L; + + public VirtualizationConfigurationException(String message) { + super(message); + } +}
\ No newline at end of file diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationQemu.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationQemu.java new file mode 100644 index 0000000..6c62a96 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationQemu.java @@ -0,0 +1,903 @@ +package org.openslx.virtualization.configuration; + +import java.io.File; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.openslx.bwlp.thrift.iface.OperatingSystem; +import org.openslx.firmware.FirmwareException; +import org.openslx.firmware.QemuFirmwareUtil; +import org.openslx.libvirt.domain.Domain; +import org.openslx.libvirt.domain.DomainUtils; +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.libosinfo.LibOsInfo; +import org.openslx.libvirt.libosinfo.os.Os; +import org.openslx.libvirt.xml.LibvirtXmlDocumentException; +import org.openslx.libvirt.xml.LibvirtXmlSerializationException; +import org.openslx.libvirt.xml.LibvirtXmlValidationException; +import org.openslx.util.LevenshteinDistance; +import org.openslx.util.Util; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.hardware.VirtOptionValue; +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.virtualizer.VirtualizerQemu; + +/** + * Virtual machine configuration (managed by Libvirt) for the QEMU hypervisor. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class VirtualizationConfigurationQemu extends VirtualizationConfiguration +{ + /** + * Name of the network bridge for the LAN. + */ + public static final String NETWORK_BRIDGE_LAN_DEFAULT = "br0"; + + /** + * Name of the network bridge for the default NAT network. + */ + public static final String NETWORK_BRIDGE_NAT_DEFAULT = "nat1"; + + /** + * Name of the network for the isolated host network (host only). + */ + public static final String NETWORK_BRIDGE_HOST_ONLY_DEFAULT = "vsw2"; + + /** + * Default physical CDROM drive of the hypervisor host. + */ + public static final String CDROM_DEFAULT_PHYSICAL_DRIVE = "/dev/sr0"; + + /** + * File name extension for QEMU (Libvirt) virtualization configuration files. + */ + public static final String FILE_NAME_EXTENSION = "xml"; + + /** + * Libvirt XML configuration file to modify configuration of virtual machine for QEMU. + */ + private Domain vmConfig = null; + + /** + * 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 VirtualizationConfigurationException Libvirt XML configuration cannot be processed. + */ + public VirtualizationConfigurationQemu( List<OperatingSystem> osList, File file ) + throws VirtualizationConfigurationException + { + super( new VirtualizerQemu(), osList ); + + try { + // read and parse Libvirt domain XML configuration document + this.vmConfig = new Domain( file ); + } catch ( LibvirtXmlDocumentException | LibvirtXmlSerializationException | LibvirtXmlValidationException e ) { + throw new VirtualizationConfigurationException( e.getLocalizedMessage() ); + } + + // parse VM config and initialize fields of QemuMetaData class + this.parseVmConfig(); + } + + /** + * Creates new virtual machine configuration (managed by Libvirt) for the QEMU hypervisor. + * + * @param osList list of operating systems. + * @param vmContent file content for the QEMU hypervisor. + * @param length number of bytes of the file content. + * + * @throws VirtualizationConfigurationException Libvirt XML configuration cannot be processed. + */ + public VirtualizationConfigurationQemu( List<OperatingSystem> osList, byte[] vmContent, int length ) + throws VirtualizationConfigurationException + { + super( new VirtualizerQemu(), osList ); + + try { + // read and parse Libvirt domain XML configuration document + this.vmConfig = new Domain( new String( vmContent, StandardCharsets.UTF_8 ) ); + } catch ( LibvirtXmlDocumentException | LibvirtXmlSerializationException | LibvirtXmlValidationException e ) { + throw new VirtualizationConfigurationException( e.getLocalizedMessage() ); + } + + // parse VM config and initialize fields of QemuMetaData class + this.parseVmConfig(); + } + + /** + * Parses Libvirt domain XML configuration to initialize QEMU metadata. + */ + private void parseVmConfig() + { + // 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 ); + } + + // detect the operating system from the optional embedded libosinfo metadata + this.setOs( this.vmConfig.getLibOsInfoOsId() ); + } + + /** + * 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 = VirtualizationConfigurationQemuUtils + .convertBusType( storageDiskDevice.getBusType() ); + String hddImagePath = storageDiskDevice.getStorageSource(); + + this.hdds.add( new HardDisk( hddChipsetModel, hddChipsetBus, hddImagePath ) ); + } + + /** + * Detects the operating system by the specified libosinfo operating system identifier. + * + * @param osId libosinfo operating system identifier. + */ + private OperatingSystem detectOperatingSystem( String osId ) + { + OperatingSystem os = null; + + if ( osId != null && !osId.isEmpty() ) { + // lookup operating system identifier in the libosinfo database + final Os osLookup = LibOsInfo.lookupOs( osId ); + + // check if entry in the database was found + if ( osLookup != null ) { + // operating system entry was found + // so determine OpenSLX OS name with the smallest distance to the libosinfo OS name + final LevenshteinDistance distance = new LevenshteinDistance( 2, 1, 1 ); + int smallestDistance = Integer.MAX_VALUE; + + // get name of the OS and combine it with the optional available architecture + String osLookupOsName = osLookup.getName(); + final int osArchSize = VirtualizationConfigurationQemuUtils.getOsArchSize( this.vmConfig.getOsArch() ); + + if ( osArchSize > 0 ) { + // append architecture size in bit if information is available from the specified architecture + osLookupOsName += " (" + osArchSize + " Bit)"; + } + + for ( final OperatingSystem osCandidate : this.osList ) { + final int currentDistance = distance.calculateDistance( osLookupOsName, osCandidate.getOsName() ); + + if ( currentDistance < smallestDistance ) { + // if the distance is smaller save the current distance and operating system as best candidate + smallestDistance = currentDistance; + os = osCandidate; + } + } + } + } + + return os; + } + + @Override + public void transformEditable() throws VirtualizationConfigurationException + { + } + + public void transformOsLoader() throws VirtualizationConfigurationException + { + final String sourceOsLoader = this.vmConfig.getOsLoader(); + final String sourceOsArch = this.vmConfig.getOsArch(); + final String sourceOsMachine = this.vmConfig.getOsMachine(); + + // transform OS loader for local editing + // check if OS loader is specified + if ( sourceOsLoader != null && !sourceOsLoader.isEmpty() ) { + // OS loader is specified so transform path to specified firmware path + // First, lookup QEMU firmware loader for target + String targetOsLoader = null; + try { + targetOsLoader = QemuFirmwareUtil.lookupTargetOsLoaderDefaultFwSpecDir( sourceOsLoader, sourceOsArch, + sourceOsMachine ); + } catch ( FirmwareException e ) { + throw new VirtualizationConfigurationException( e.getLocalizedMessage() ); + } + + // Second, set target QEMU firmware loader if specified + if ( targetOsLoader != null && !targetOsLoader.isEmpty() ) { + this.vmConfig.setOsLoader( targetOsLoader ); + } + } + } + + @Override + public boolean addEmptyHddTemplate() + { + return this.addHddTemplate( "", null, null ); + } + + @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 ) + { + int index = this.vmConfig.getDiskStorageDevices().size() - 1; + index = ( index > 0 ) ? index : 0; + return this.addHddTemplate( index, 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 = VirtualizationConfigurationQemuUtils.getArrayIndex( storageDiskDevices, index ); + + if ( storageDiskDevice == null ) { + // HDD does not exist, so create new storage (HDD) device + final BusType devBusType = BusType.VIRTIO; + final String targetDevName = VirtualizationConfigurationQemuUtils.createDeviceName( this.vmConfig, + devBusType ); + storageDiskDevice = this.vmConfig.addDiskStorageDevice(); + storageDiskDevice.setReadOnly( false ); + storageDiskDevice.setBusType( devBusType ); + storageDiskDevice.setTargetDevice( targetDevName ); + + if ( diskImagePath == null || diskImagePath.isEmpty() ) { + storageDiskDevice.removeStorage(); + } else { + 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 + if ( diskImagePath == null || diskImagePath.isEmpty() ) { + storageDiskDevice.removeStorage(); + } else { + storageDiskDevice.setStorage( StorageType.FILE, diskImagePath ); + } + } + + return true; + } + + @Override + public boolean addDefaultNat() + { + // since network interface was not filtered during VM upload, + // do not add or configure any network interface here + return true; + } + + @Override + public void setOs( String vendorOsId ) + { + final OperatingSystem os = this.detectOperatingSystem( vendorOsId ); + this.setOs( os ); + } + + @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 ) + { + // convert given memory in MiB to memory in bytes for Libvirt XML Domain API functions + final BigInteger memory = DomainUtils.decodeMemory( Integer.toString( mem ), "MiB" ); + + this.vmConfig.setMemory( memory ); + this.vmConfig.setCurrentMemory( memory ); + + final boolean isMemorySet = this.vmConfig.getMemory().equals( memory ); + final boolean isCurrentMemorySet = this.vmConfig.getCurrentMemory().equals( memory ); + + return isMemorySet && isCurrentMemorySet; + } + + @Override + public void addFloppy( int index, String image, boolean readOnly ) + { + ArrayList<DiskFloppy> floppyDiskDevices = this.vmConfig.getDiskFloppyDevices(); + DiskFloppy floppyDiskDevice = VirtualizationConfigurationQemuUtils.getArrayIndex( floppyDiskDevices, index ); + + if ( floppyDiskDevice == null ) { + // floppy device does not exist, so create new floppy device + final BusType devBusType = BusType.FDC; + final String targetDevName = VirtualizationConfigurationQemuUtils.createDeviceName( this.vmConfig, + devBusType ); + floppyDiskDevice = this.vmConfig.addDiskFloppyDevice(); + floppyDiskDevice.setBusType( devBusType ); + floppyDiskDevice.setTargetDevice( targetDevName ); + floppyDiskDevice.setReadOnly( readOnly ); + + if ( image == null || image.isEmpty() ) { + floppyDiskDevice.removeStorage(); + } else { + floppyDiskDevice.setStorage( StorageType.FILE, image ); + } + } else { + // floppy device exists, so update existing floppy device + floppyDiskDevice.setReadOnly( readOnly ); + + if ( image == null || image.isEmpty() ) { + floppyDiskDevice.removeStorage(); + } else { + floppyDiskDevice.setStorage( StorageType.FILE, image ); + } + } + } + + @Override + public boolean addCdrom( String image ) + { + int index = this.vmConfig.getDiskCdromDevices().size() - 1; + index = ( index > 0 ) ? index : 0; + return this.addCdrom( index, 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 = VirtualizationConfigurationQemuUtils.getArrayIndex( cdromDiskDevices, index ); + + if ( cdromDiskDevice == null ) { + // CDROM device does not exist, so create new CDROM device + final BusType devBusType = BusType.SATA; + final String targetDevName = VirtualizationConfigurationQemuUtils.createDeviceName( this.vmConfig, + devBusType ); + cdromDiskDevice = this.vmConfig.addDiskCdromDevice(); + cdromDiskDevice.setBusType( devBusType ); + cdromDiskDevice.setTargetDevice( targetDevName ); + cdromDiskDevice.setReadOnly( true ); + + if ( image == null ) { + cdromDiskDevice.setStorage( StorageType.BLOCK, CDROM_DEFAULT_PHYSICAL_DRIVE ); + } else { + if ( image.isEmpty() ) { + cdromDiskDevice.removeStorage(); + } 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 { + if ( image.isEmpty() ) { + cdromDiskDevice.removeStorage(); + } else { + cdromDiskDevice.setStorage( StorageType.FILE, image ); + } + } + } + + return true; + } + + @Override + public boolean addCpuCoreCount( int nrOfCores ) + { + this.vmConfig.setVCpu( nrOfCores ); + + boolean isVCpuSet = this.vmConfig.getVCpu() == nrOfCores; + + return isVCpuSet; + } + + class QemuGfxModel extends VirtOptionValue + { + public QemuGfxModel( Video.Model model, String displayName ) + { + super( model.toString(), displayName ); + } + + @Override + public void apply() + { + final ArrayList<Video> videoDevices = vmConfig.getVideoDevices(); + + if ( videoDevices.isEmpty() ) { + // add new video device with disabled acceleration to VM configuration + final Video videoDevice = vmConfig.addVideoDevice(); + videoDevice.setModel( Video.Model.fromString( this.getId() ) ); + videoDevice.set2DAcceleration( false ); + videoDevice.set3DAcceleration( false ); + } else { + // change graphics model of existing video devices + for ( final Video videoDevice : videoDevices ) { + // remove all old model-related XML attributes + videoDevice.removeXmlElement( "model" ); + // set new model + videoDevice.setModel( Video.Model.fromString( this.getId() ) ); + } + } + } + + @Override + public boolean isActive() + { + final ArrayList<Video> videoDevices = vmConfig.getVideoDevices(); + boolean isActive = true; + + for ( final Video videoDevice : videoDevices ) { + if ( !videoDevice.getModel().toString().equals( this.getId() ) ) { + isActive = false; + break; + } + } + + return isActive; + } + } + + class QemuGfxType extends VirtOptionValue + { + public QemuGfxType( String id, String displayName ) + { + super( id, displayName ); + } + + @Override + public void apply() + { + ArrayList<Graphics> graphicDevices = vmConfig.getGraphicDevices(); + ArrayList<Video> videoDevices = vmConfig.getVideoDevices(); + final boolean accelerationEnabled = this.id.equals( "true" ); + + boolean acceleratedGraphicsAvailable = false; + + if ( graphicDevices.isEmpty() ) { + // add new graphics device with enabled acceleration to VM configuration + GraphicsSpice graphicSpiceDevice = 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 = 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 boolean isActive() + { + ArrayList<Graphics> graphicsDevices = vmConfig.getGraphicDevices(); + ArrayList<Video> videoDevices = vmConfig.getVideoDevices(); + + 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 ) { + return this.id.equals( "true" ); + } else { + return this.id.equals( "false" ); + } + } + } + + @Override + public void setVirtualizerVersion( Version type ) + { + if ( type != null ) { + final String osMachine = this.vmConfig.getOsMachine(); + final String osMachineName = VirtualizationConfigurationQemuUtils.getOsMachineName( osMachine ); + + if ( osMachineName != null && !osMachineName.isEmpty() ) { + final String modifiedOsMachineVersion = VirtualizationConfigurationQemuUtils.getOsMachineVersion( type ); + final String modifiedOsMachine = VirtualizationConfigurationQemuUtils.getOsMachine( osMachineName, + modifiedOsMachineVersion ); + this.vmConfig.setOsMachine( modifiedOsMachine ); + } + } + } + + @Override + public Version getVirtualizerVersion() + { + final String osMachine = this.vmConfig.getOsMachine(); + final Version uncheckedVersion = VirtualizationConfigurationQemuUtils.getOsMachineVersion( osMachine ); + final Version checkedVersion; + + if ( uncheckedVersion == null ) { + checkedVersion = null; + } else { + checkedVersion = Version.getInstanceByMajorMinorFromVersions( uncheckedVersion.getMajor(), + uncheckedVersion.getMinor(), this.getVirtualizer().getSupportedVersions() ); + } + + return checkedVersion; + } + + class QemuNicModel extends VirtOptionValue + { + private final int cardIndex; + + public QemuNicModel( int cardIndex, Interface.Model model, String displayName ) + { + super( model.toString(), displayName ); // XXX: toString/fromString would disappear if + this.cardIndex = cardIndex; // this were AbstractConfigurableOption<T extends Enum<T>> + } + + @Override + public void apply() + { + ArrayList<Interface> networkDevices = vmConfig.getInterfaceDevices(); + Interface networkDevice = VirtualizationConfigurationQemuUtils.getArrayIndex( networkDevices, cardIndex ); + + if ( networkDevice != null ) { + networkDevice.setModel( Interface.Model.fromString( id ) ); + } + } + + @Override + public boolean isActive() + { + ArrayList<Interface> networkDevices = vmConfig.getInterfaceDevices(); + Interface networkDevice = VirtualizationConfigurationQemuUtils.getArrayIndex( networkDevices, cardIndex ); + + if ( networkDevice == null ) { + // network interface device is not present + return Util.isEmptyString( this.id ); // XXX: would be more explicit with enum.NONE + } + // get model of existing network interface device + Interface.Model networkDeviceModel = networkDevice.getModel(); + if ( networkDeviceModel == null ) { + return Util.isEmptyString( this.id ); // see above + } + // Success + return networkDeviceModel.toString().equals( this.id ); // XXX: enum would allow simple == + } + } + + class QemuSoundCardModel extends VirtOptionValue + { + public QemuSoundCardModel( Sound.Model id, String displayName ) + { + super( id.toString(), displayName ); + } + + @Override + public void apply() + { + ArrayList<Sound> soundDevices = vmConfig.getSoundDevices(); + Sound.Model soundDeviceModel = Sound.Model.fromString( this.id ); + + if ( soundDevices.isEmpty() ) { + // create new sound device with 'soundDeviceModel' hardware + Sound soundDevice = vmConfig.addSoundDevice(); + soundDevice.setModel( soundDeviceModel ); + } else { + // update sound device model type of existing sound devices + for ( Sound soundDevice : soundDevices ) { + soundDevice.setModel( soundDeviceModel ); + } + } + } + + @Override + public boolean isActive() + { + ArrayList<Sound> soundDevices = vmConfig.getSoundDevices(); + + if ( soundDevices.isEmpty() ) { + // the VM configuration does not contain a sound card device + return Util.isEmptyString( this.id ); + } + // the VM configuration at least one sound card device, so return the type of the first one + Sound.Model soundDeviceModel = soundDevices.get( 0 ).getModel(); + return soundDeviceModel != null && soundDeviceModel.toString().equals( this.id ); + } + + } + + class QemuUsbSpeed extends VirtOptionValue + { + public QemuUsbSpeed( ControllerUsb.Model id, String displayName ) + { + super( id.toString(), displayName ); + } + + @Override + public void apply() + { + ArrayList<ControllerUsb> usbControllerDevices = vmConfig.getUsbControllerDevices(); + ControllerUsb.Model usbControllerModel = ControllerUsb.Model.fromString( this.id ); + + if ( usbControllerDevices.isEmpty() ) { + // add new USB controller with specified speed 'usbControllerModel' + ControllerUsb usbControllerDevice = 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 boolean isActive() + { + ArrayList<ControllerUsb> usbControllerDevices = vmConfig.getUsbControllerDevices(); + + for ( ControllerUsb usbControllerDevice : usbControllerDevices ) { + ControllerUsb.Model usbControllerModel = usbControllerDevice.getModel(); + + // TODO Need something to map from chip to usb speed. But this is conceptually broken anyways since + // it's modeled after vmware, where you only cannot configure different controllers at the same time + // anyways XXX + if ( usbControllerModel.toString().equals( this.id ) ) + return true; + } + + return false; + } + } + + @Override + public byte[] getConfigurationAsByteArray() + { + 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 += LibvirtXmlDocument.LINE_SEPARATOR; + return configuration.getBytes( StandardCharsets.UTF_8 ); + } + } + + @Override + public boolean addEthernet( EtherType type ) + { + int index = this.vmConfig.getInterfaceDevices().size() - 1; + index = ( index > 0 ) ? index : 0; + return this.addEthernet( index, 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 ) + { + ArrayList<Interface> interfaceDevices = this.vmConfig.getInterfaceDevices(); + Interface interfaceDevice = VirtualizationConfigurationQemuUtils.getArrayIndex( interfaceDevices, index ); + + final Interface.Model defaultNetworkDeviceModel = Interface.Model.VIRTIO_NET_PCI; + + 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( VirtualizationConfigurationQemu.NETWORK_BRIDGE_LAN_DEFAULT ); + break; + case HOST_ONLY: + // add network interface device with link to the isolated host network + interfaceDevice = this.vmConfig.addInterfaceBridgeDevice(); + interfaceDevice.setModel( defaultNetworkDeviceModel ); + interfaceDevice.setSource( VirtualizationConfigurationQemu.NETWORK_BRIDGE_HOST_ONLY_DEFAULT ); + break; + case NAT: + // add network interface device with link to the NAT network + interfaceDevice = this.vmConfig.addInterfaceBridgeDevice(); + interfaceDevice.setModel( defaultNetworkDeviceModel ); + interfaceDevice.setSource( VirtualizationConfigurationQemu.NETWORK_BRIDGE_NAT_DEFAULT ); + break; + } + } else { + // network interface device exists, so update existing network interface device + switch ( type ) { + case BRIDGED: + interfaceDevice.setType( Interface.Type.BRIDGE ); + interfaceDevice.setSource( VirtualizationConfigurationQemu.NETWORK_BRIDGE_LAN_DEFAULT ); + break; + case HOST_ONLY: + interfaceDevice.setType( Interface.Type.BRIDGE ); + interfaceDevice.setSource( VirtualizationConfigurationQemu.NETWORK_BRIDGE_HOST_ONLY_DEFAULT ); + break; + case NAT: + interfaceDevice.setType( Interface.Type.BRIDGE ); + interfaceDevice.setSource( VirtualizationConfigurationQemu.NETWORK_BRIDGE_NAT_DEFAULT ); + break; + } + } + + return true; + } + + @Override + public void transformNonPersistent() throws VirtualizationConfigurationException + { + // NOT implemented yet + } + + @Override + public void transformPrivacy() throws VirtualizationConfigurationException + { + // removes all referenced storage files of all specified CDROMs, Floppy drives and HDDs + this.vmConfig.removeDiskDevicesStorage(); + + // remove specified NVRAM file of OS loader (firmware) + this.vmConfig.removeOsNvram(); + } + + @Override + public void registerVirtualHW() + { + // XXX Add missing qemu-only types/models + List<VirtOptionValue> list; + // @formatter:off + list = new ArrayList<>(); + //list.add( new QemuSoundCardModel( Sound.Model.NONE, SoundCard.NONE ) ); // XXX TODO + list.add( new QemuSoundCardModel( Sound.Model.ICH9, SoundCard.DEFAULT ) ); + list.add( new QemuSoundCardModel( Sound.Model.SB16, SoundCard.SOUND_BLASTER ) ); + list.add( new QemuSoundCardModel( Sound.Model.ES1370, SoundCard.ES ) ); + list.add( new QemuSoundCardModel( Sound.Model.AC97, SoundCard.AC ) ); + list.add( new QemuSoundCardModel( Sound.Model.ICH9, SoundCard.HD_AUDIO ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.SOUND_CARD_MODEL, list ) ); + + list = new ArrayList<>(); + list.add( new QemuGfxModel( Video.Model.VGA, "VGA" ) ); + list.add( new QemuGfxModel( Video.Model.QXL, "QXL" ) ); + list.add( new QemuGfxModel( Video.Model.VMVGA, "VMware VGA" ) ); + list.add( new QemuGfxModel( Video.Model.VIRTIO, "virtio-gpu" ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.GFX_MODEL, list ) ); + + list = new ArrayList<>(); + list.add( new QemuGfxType( "false", "2D" ) ); + list.add( new QemuGfxType( "true", "3D" ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.GFX_TYPE, list ) ); + + list = new ArrayList<>(); + // XXX Represent NONE; can add missing models now with new approach (add human readable strings) + list.add( new QemuNicModel( 0, Interface.Model.VIRTIO, Ethernet.AUTO ) ); + list.add( new QemuNicModel( 0, Interface.Model.PCNET, Ethernet.PCNETPCI2 ) ); + list.add( new QemuNicModel( 0, Interface.Model.E1000, Ethernet.E1000 ) ); + list.add( new QemuNicModel( 0, Interface.Model.E1000E, Ethernet.E1000E ) ); + list.add( new QemuNicModel( 0, Interface.Model.VMXNET3, Ethernet.VMXNET3 ) ); + list.add( new QemuNicModel( 0, Interface.Model.VIRTIO_NET_PCI, Ethernet.PARAVIRT ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.NIC_MODEL, list ) ); + + list = new ArrayList<>(); + list.add( new QemuUsbSpeed( ControllerUsb.Model.NONE, Usb.NONE ) ); + list.add( new QemuUsbSpeed( ControllerUsb.Model.ICH9_UHCI1, Usb.USB1_1 ) ); + list.add( new QemuUsbSpeed( ControllerUsb.Model.ICH9_EHCI1, Usb.USB2_0 ) ); + list.add( new QemuUsbSpeed( ControllerUsb.Model.QEMU_XHCI, Usb.USB3_0 ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.USB_SPEED, list ) ); + // @formatter:on + } + + @Override + public String getFileNameExtension() + { + return VirtualizationConfigurationQemu.FILE_NAME_EXTENSION; + } + + @Override + public void validate() throws VirtualizationConfigurationException + { + try { + this.vmConfig.validateXml(); + } catch ( LibvirtXmlValidationException e ) { + throw new VirtualizationConfigurationException( e.getLocalizedMessage() ); + } + } + + @Override + public void disableUsb() + { + new QemuUsbSpeed( ControllerUsb.Model.NONE, Usb.NONE ).apply(); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationQemuUtils.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationQemuUtils.java new file mode 100644 index 0000000..86216a0 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationQemuUtils.java @@ -0,0 +1,404 @@ +package org.openslx.virtualization.configuration; + +import java.util.ArrayList; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.openslx.libvirt.domain.Domain; +import org.openslx.libvirt.domain.device.Disk; +import org.openslx.libvirt.domain.device.Disk.BusType; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.configuration.VirtualizationConfiguration.DriveBusType; + +/** + * Collection of utils to convert data types from bwLehrpool to Libvirt and vice versa. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class VirtualizationConfigurationQemuUtils +{ + /** + * Separator symbol between Libvirt/QEMU machine name and machine version. + */ + private static final String OS_MACHINE_NAME_VERSION_SEPARATOR = "-"; + + /** + * 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; + + if ( busType == null ) + return 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 NVME: + type = null; + break; + case SATA: + type = BusType.SATA; + break; + case SCSI: + type = BusType.SCSI; + 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. + */ + private static String createAlphabeticalDeviceName( String devicePrefix, int deviceNumber ) + { + if ( deviceNumber < 0 || deviceNumber >= ( 'z' - 'a' ) ) { + String errorMsg = "Device number is out of range to be able to create a valid device name."; + throw new IllegalArgumentException( errorMsg ); + } + + return devicePrefix + Character.valueOf( (char) ( 'a' + deviceNumber ) ).toString(); + } + + /** + * Creates an alphabetical device name for a disk device with a bus <i>type</i> that is unique in + * a Libvirt domain XML configuration. + * + * @param config Libvirt domain XML configuration. + * @param type device type for device name. + * @return alphabetical device name. + */ + public static String createDeviceName( final Domain config, final BusType type ) throws IllegalArgumentException + { + final String devicePrefix; + final int deviceNumber; + + switch ( type ) { + case FDC: + devicePrefix = "fd"; + break; + case IDE: + devicePrefix = "hd"; + break; + case SATA: + devicePrefix = "sd"; + break; + case VIRTIO: + devicePrefix = "vd"; + break; + default: + return null; + } + + final Predicate<Disk> bySpecifiedBusType = d -> d.getBusType() == type; + deviceNumber = Long.valueOf( config.getDiskDevices().stream().filter( bySpecifiedBusType ).count() ).intValue(); + + return VirtualizationConfigurationQemuUtils.createAlphabeticalDeviceName( devicePrefix, deviceNumber ); + } + + /** + * Data container to store a Libvirt/QEMU machine name with version information. + * + * @author Manuel Bentele + * @version 1.0 + */ + static class OsMachineNameAndVersion + { + /** + * Stores the machine name. + */ + final private String osMachineName; + + /** + * Stores the machine version. + */ + final private Version osMachineVersion; + + /** + * Creates a data container for a machine name with version information. + * + * @param osMachineName name of the machine. + * @param osMachineVersion version of the machine. + */ + public OsMachineNameAndVersion( String osMachineName, Version osMachineVersion ) + { + this.osMachineName = osMachineName; + this.osMachineVersion = osMachineVersion; + } + + /** + * Returns the machine name. + * + * @return machine name. + */ + public String getOsMachineName() + { + return this.osMachineName; + } + + /** + * Returns the version information. + * + * @return version information. + */ + public Version getOsMachineVersion() + { + return this.osMachineVersion; + } + } + + /** + * Parses a machine name with version information from a Libvirt/QEMU machine description. + * + * @param osMachine Libvirt/QEMU machine description as {@link String}. + * @return data container containing the parsed machine name with version information. + */ + private static OsMachineNameAndVersion parseOsMachineNameAndVersion( String osMachine ) + { + final String osMachineName; + final Version osMachineVersion; + + if ( osMachine == null || osMachine.isEmpty() ) { + // there is no machine description given, so we can not parse anything + osMachineName = null; + osMachineVersion = null; + } else { + // create regular expression based matcher to extract machine name and version number + final Pattern osMachineNameAndVersionPattern = Pattern.compile( "^([a-z0-9\\-]+)" + + VirtualizationConfigurationQemuUtils.OS_MACHINE_NAME_VERSION_SEPARATOR + "([0-9]+).([0-9]+)$" ); + final Matcher osMachineNameAndVersionMatcher = osMachineNameAndVersionPattern.matcher( osMachine ); + + final boolean matches = osMachineNameAndVersionMatcher.find(); + + if ( matches ) { + // get results of regular expression based matcher + osMachineName = osMachineNameAndVersionMatcher.group( 1 ); + final String osMachineMajorString = osMachineNameAndVersionMatcher.group( 2 ); + final String osMachineMinorString = osMachineNameAndVersionMatcher.group( 3 ); + + // create version representation + final short osMachineMajor = Short.valueOf( osMachineMajorString ); + final short osMachineMinor = Short.valueOf( osMachineMinorString ); + osMachineVersion = new Version( osMachineMajor, osMachineMinor ); + } else { + osMachineName = null; + osMachineVersion = null; + } + } + + return new OsMachineNameAndVersion( osMachineName, osMachineVersion ); + } + + /** + * Parses a machine name from a Libvirt/QEMU machine description. + * + * @param osMachine Libvirt/QEMU machine description as {@link String}. + * @return parsed machine name. + */ + public static String getOsMachineName( String osMachine ) + { + final OsMachineNameAndVersion machineNameAndVersion = VirtualizationConfigurationQemuUtils + .parseOsMachineNameAndVersion( osMachine ); + return machineNameAndVersion.getOsMachineName(); + } + + /** + * Parses a machine version from a Libvirt/QEMU machine description. + * + * @param osMachine Libvirt/QEMU machine description as {@link String}. + * @return parsed machine version. + */ + public static Version getOsMachineVersion( String osMachine ) + { + final OsMachineNameAndVersion machineNameAndVersion = VirtualizationConfigurationQemuUtils + .parseOsMachineNameAndVersion( osMachine ); + return machineNameAndVersion.getOsMachineVersion(); + } + + /** + * Combines a machine name with a machine version and returns a Libvirt/QEMU machine description. + * + * @param osMachineName name of the machine. + * @param osMachineVersion version of the machine. + * @return Libvirt/QEMU machine description. + */ + public static String getOsMachine( String osMachineName, String osMachineVersion ) + { + return osMachineName + VirtualizationConfigurationQemuUtils.OS_MACHINE_NAME_VERSION_SEPARATOR + osMachineVersion; + } + + /** + * Converts a {@link Version} to a Libvirt/QEMU machine version. + * + * @param version Libvirt/QEMU machine version as {@link Version}. + * @return Libvirt/QEMU machine version. + */ + public static String getOsMachineVersion( Version version ) + { + return String.format( "%d.%d", version.getMajor(), version.getMinor() ); + } + + /** + * Returns the size of a given architecture {@link String}. + * + * @param osArch Libvirt/QEMU machine architecture as {@link String}. + * @return Size of the Libvirt/QEMU machine architecture. + */ + public static int getOsArchSize( String osArch ) + { + final int archSize; + + if ( osArch == null || osArch.isEmpty() ) { + archSize = 0; + } else { + switch ( osArch ) { + case "alpha": + archSize = 64; + break; + case "armv6l": + archSize = 32; + break; + case "armv7l": + archSize = 32; + break; + case "aarch64": + archSize = 64; + break; + case "cris": + archSize = 32; + break; + case "i686": + archSize = 32; + break; + case "m68k": + archSize = 32; + break; + case "microblaze": + archSize = 32; + break; + case "microblazeel": + archSize = 32; + break; + case "mips": + archSize = 32; + break; + case "mipsel": + archSize = 32; + break; + case "mips64": + archSize = 64; + break; + case "mips64el": + archSize = 64; + break; + case "ppc": + archSize = 32; + break; + case "ppc64": + archSize = 64; + break; + case "ppc64le": + archSize = 64; + break; + case "riscv32": + archSize = 32; + break; + case "riscv64": + archSize = 64; + break; + case "s390x": + archSize = 64; + break; + case "sh4": + archSize = 32; + break; + case "sh4eb": + archSize = 64; + break; + case "sparc": + archSize = 32; + break; + case "sparc64": + archSize = 64; + break; + case "x86_64": + archSize = 64; + break; + case "xtensa": + archSize = 32; + break; + case "xtensaeb": + archSize = 32; + break; + default: + archSize = 0; + } + } + + return archSize; + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationUtils.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationUtils.java new file mode 100644 index 0000000..2427001 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationUtils.java @@ -0,0 +1,52 @@ +package org.openslx.virtualization.configuration; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.openslx.bwlp.thrift.iface.OperatingSystem; + +/** + * Utilities to set up and edit virtualization configurations. + * + * @author Manuel Bentele + * @version 1.0 + */ +public final class VirtualizationConfigurationUtils +{ + /** + * Returns an operating system from a given list of operating systems determined by the + * virtualizer specific operating system parameters. + * + * @param osList list of available operating systems. + * @param virtId virtualizer identifier, e.g. <code>vmware</code> for VMware + * @param virtOsId operating system identifier used by the virtualizer, eg. + * <code>windows7-64</code> for 64bit Windows 7 on VMware. + */ + public static OperatingSystem getOsOfVirtualizerFromList( List<OperatingSystem> osList, String virtId, + String virtOsId ) + { + OperatingSystem os = null; + + for ( final OperatingSystem osCandidate : osList ) { + final Map<String, String> osVirtualizerMapping = osCandidate.getVirtualizerOsId(); + if ( osVirtualizerMapping != null ) { + for ( final Entry<String, String> entry : osVirtualizerMapping.entrySet() ) { + // check if suitable OS has been found + if ( entry.getKey().equals( virtId ) && entry.getValue().equals( virtOsId ) ) { + // save OS and exit inner loop since OS has been found + os = osCandidate; + break; + } + } + + // exit outer loop if OS has been found + if ( os != null ) { + break; + } + } + } + + return os; + } +} 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(); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualboxFileFormat.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualboxFileFormat.java new file mode 100644 index 0000000..b5b3180 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVirtualboxFileFormat.java @@ -0,0 +1,771 @@ +package org.openslx.virtualization.configuration; + +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.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.XMLConstants; +import javax.xml.transform.dom.DOMSource; +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.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openslx.util.Resources; +import org.openslx.util.Util; +import org.openslx.util.XmlHelper; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.configuration.VirtualizationConfiguration.DriveBusType; +import org.openslx.virtualization.configuration.VirtualizationConfiguration.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 VirtualizationConfigurationVirtualboxFileFormat +{ + private static final Logger LOGGER = LogManager.getLogger( VirtualizationConfigurationVirtualboxFileFormat.class ); + + // key information set during initial parsing of the XML file + private String osName = ""; + private ArrayList<HardDisk> hddsArray = new ArrayList<HardDisk>(); + + // XPath and DOM parsing related members + private Document doc = null; + + /** + * Version of the configuration file format. + */ + private Version version = null; + + /** + * File names of XML schema files for different file format versions. + */ + private final static HashMap<Version, String> FILE_FORMAT_SCHEMA_VERSIONS = new HashMap<Version, String>() { + + private static final long serialVersionUID = -3163681758191475625L; + + { + put( Version.valueOf( "1.15" ), "VirtualBox-settings_v1-15.xsd" ); + put( Version.valueOf( "1.16" ), "VirtualBox-settings_v1-16.xsd" ); + put( Version.valueOf( "1.17" ), "VirtualBox-settings_v1-17.xsd" ); + put( Version.valueOf( "1.18" ), "VirtualBox-settings_v1-18.xsd" ); + } + }; + + /** + * Path to the VirtualBox file format schemas within the *.jar file. + */ + private final static String FILE_FORMAT_SCHEMA_PREFIX_PATH = Resources.PATH_SEPARATOR + "virtualbox" + + Resources.PATH_SEPARATOR + "xsd"; + + public static final String DUMMY_VALUE = "[dummy]"; + + // list of nodes to automatically remove when reading the vbox file + private static final 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[@slot='0']/*", + "/VirtualBox/Machine/ExtraData", + "/VirtualBox/Machine/StorageControllers/StorageController/AttachedDevice[not(@type='HardDisk')]", + "/VirtualBox/Machine/Hardware/StorageControllers/StorageController/AttachedDevice[not(@type='HardDisk')]", + "/VirtualBox/Machine/MediaRegistry/FloppyImages", + "/VirtualBox/Machine/MediaRegistry/DVDImages" }; + + public static enum MatchMode + { + EXACTLY_ONE, + MULTIPLE, + FIRST_ONLY, + }; + + /** + * 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 VirtualizationConfigurationException if the given file is not a valid VirtualBox + * configuration file. + */ + public VirtualizationConfigurationVirtualboxFileFormat( File file ) throws IOException, VirtualizationConfigurationException + { + doc = XmlHelper.parseDocumentFromStream( new FileInputStream( file ) ); + doc = XmlHelper.removeFormattingNodes( doc ); + if ( doc == null ) + throw new VirtualizationConfigurationException( "Could not parse given VirtualBox machine configuration file!" ); + + this.parseConfigurationVersion(); + this.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 VirtualizationConfigurationException creation of VirtualBox configuration file representation failed. + */ + public VirtualizationConfigurationVirtualboxFileFormat( byte[] machineDescription, int length ) throws VirtualizationConfigurationException + { + ByteArrayInputStream is = new ByteArrayInputStream( machineDescription ); + + doc = XmlHelper.parseDocumentFromStream( is ); + if ( doc == null ) { + final String errorMsg = "Could not parse given VirtualBox machine description from byte array!"; + LOGGER.debug( errorMsg ); + throw new VirtualizationConfigurationException( errorMsg ); + } + + this.parseConfigurationVersion(); + this.init(); + } + + public void validate() throws VirtualizationConfigurationException + { + this.validateFileFormatVersion( this.getVersion() ); + } + + public void validateFileFormatVersion( Version version ) throws VirtualizationConfigurationException + { + if ( this.getVersion() != null && this.doc != null ) { + // check if specified version is supported + final String fileName = FILE_FORMAT_SCHEMA_VERSIONS.get( version ); + + if ( fileName == null ) { + final String errorMsg = "File format version " + version.toString() + " is not supported!"; + LOGGER.debug( errorMsg ); + throw new VirtualizationConfigurationException( errorMsg ); + } else { + // specified version is supported, so validate document with corresponding schema file + final InputStream schemaResource = VirtualizationConfigurationVirtualboxFileFormat + .getSchemaResource( fileName ); + + if ( schemaResource != null ) { + try { + final SchemaFactory factory = SchemaFactory.newInstance( XMLConstants.W3C_XML_SCHEMA_NS_URI ); + final Schema schema = factory.newSchema( new StreamSource( schemaResource ) ); + final Validator validator = schema.newValidator(); + validator.validate( new DOMSource( this.doc ) ); + } catch ( SAXException | IOException e ) { + final String errorMsg = "XML configuration is not a valid VirtualBox v" + version.toString() + + " configuration: " + e.getLocalizedMessage(); + LOGGER.debug( errorMsg ); + throw new VirtualizationConfigurationException( errorMsg ); + } + } + } + } + } + + private static InputStream getSchemaResource( String fileName ) + { + final String schemaFilePath = FILE_FORMAT_SCHEMA_PREFIX_PATH + File.separator + fileName; + return VirtualizationConfigurationVirtualboxFileFormat.class.getResourceAsStream( schemaFilePath ); + } + + /** + * Main initialization functions parsing the document created during the constructor. + * @throws VirtualizationConfigurationException + */ + private void init() throws VirtualizationConfigurationException + { + try { + this.validate(); + } catch ( VirtualizationConfigurationException e ) { + // do not print output of failed validation if placeholders are available + // since those placeholder values violate the defined UUID pattern + if ( !this.checkForPlaceholders() ) { + final String errorMsg = "XML configuration is not a valid VirtualBox v" + version.toString() + + " configuration: " + e.getLocalizedMessage(); + LOGGER.debug( errorMsg ); + } + } + + if ( Util.isEmptyString( getDisplayName() ) ) { + throw new VirtualizationConfigurationException( "Machine doesn't have a name" ); + } + try { + ensureHardwareUuid(); + setOsType(); + fixUsb(); // Since we now support selecting specific speed + removeUnusedHdds(); + if ( checkForPlaceholders() ) { + return; + } + setHdds(); + removeBlacklistedElements(); + addPlaceHolders(); + } catch ( XPathExpressionException e ) { + LOGGER.debug( "Could not initialize VBoxConfig", e ); + return; + } + } + + private void parseConfigurationVersion() throws VirtualizationConfigurationException + { + String versionText; + try { + versionText = XmlHelper.compileXPath( "/VirtualBox/@version" ).evaluate( this.doc ); + } catch ( XPathExpressionException e ) { + throw new VirtualizationConfigurationException( + "Failed to parse the version number of the configuration file" ); + } + + if ( versionText == null || versionText.isEmpty() ) { + throw new VirtualizationConfigurationException( "Configuration file does not contain any version number!" ); + } else { + // parse version information from textual description + final Pattern versionPattern = Pattern.compile( "^(\\d+\\.\\d+).*$" ); + final Matcher versionMatcher = versionPattern.matcher( versionText ); + + if ( versionMatcher.find() ) { + this.version = Version.valueOf( versionMatcher.group( 1 ) ); + } + + if ( this.version == null ) { + throw new VirtualizationConfigurationException( "Configuration file version number is not valid!" ); + } + } + } + + 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" ); + } + + /** + * Remove any HDDs from MediaRegistry that aren't being used + */ + private void removeUnusedHdds() + { + Set<String> existing = new HashSet<>(); + NodeList list = findNodes( storageControllersPath() + "/StorageController/AttachedDevice/Image" ); + if ( list != null && list.getLength() != 0 ) { + for ( int i = 0; i < list.getLength(); ++i ) { + Node item = list.item( i ); + if ( ! ( item instanceof Element ) ) + continue; + Element e = (Element)item; + String uuid = e.getAttribute( "uuid" ); + if ( Util.isEmptyString( uuid ) ) + continue; + existing.add( uuid ); + } + } + // Now check registry + list = findNodes( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk" ); + if ( list != null && list.getLength() != 0 ) { + for ( int i = 0; i < list.getLength(); ++i ) { + Node item = list.item( i ); + if ( ! ( item instanceof Element ) ) + continue; + Element e = (Element)item; + String uuid = e.getAttribute( "uuid" ); + if ( !existing.contains( uuid ) ) { + LOGGER.info( "Removing unused HDD " + uuid + " from MediaRegistry" ); + e.getParentNode().removeChild( e ); + } + } + } + } + + public String storageControllersPath() + { + if ( this.getVersion().isSmallerThan( Version.valueOf( "1.17" ) ) ) + return "/VirtualBox/Machine/StorageControllers"; + return "/VirtualBox/Machine/Hardware/StorageControllers"; + } + + /** + * Saves the machine's uuid as hardware uuid to prevent VMs from + * believing in a hardware change. + * + * @throws XPathExpressionException + * @throws VirtualizationConfigurationException + */ + private void ensureHardwareUuid() throws XPathExpressionException, VirtualizationConfigurationException + { + // we will need the machine uuid, so get it + String machineUuid = XmlHelper.compileXPath( "/VirtualBox/Machine/@uuid" ).evaluate( this.doc ); + if ( machineUuid.isEmpty() ) { + LOGGER.error( "Machine UUID empty, should never happen!" ); + throw new VirtualizationConfigurationException( "XML doesn't contain a machine uuid" ); + } + + NodeList hwNodes = findNodes( "/VirtualBox/Machine/Hardware" ); + int count = hwNodes.getLength(); + if ( count != 1 ) { + throw new VirtualizationConfigurationException( "Zero or > 1 '/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." ); + } + } + + public Version getVersion() + { + return this.version; + } + + /** + * Self-explanatory. + */ + public void addPlaceHolders() + { + // placeholder for the location of the virtual hdd + changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk", "location", DUMMY_VALUE, MatchMode.MULTIPLE ); + // in case it already has a snapshot + changeAttribute( "/VirtualBox/Machine/MediaRegistry/HardDisks/HardDisk/HardDisk", "location", DUMMY_VALUE, MatchMode.MULTIPLE ); + changeAttribute( "/VirtualBox/Machine", "snapshotFolder", DUMMY_VALUE, MatchMode.FIRST_ONLY ); + } + + /** + * 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( DUMMY_VALUE ) ) { + 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.compileXPath( 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.compileXPath( "/VirtualBox/Machine/@name" ).evaluate( this.doc ); + } catch ( XPathExpressionException e ) { + return ""; + } + } + + /** + * Function finds and saves the name of the guest OS + * + * @throws XPathExpressionException failed to find and retrieve name of the guest OS. + */ + public void setOsType() throws XPathExpressionException + { + String os = XmlHelper.compileXPath( "/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 failed to find attached hard drives and their controllers. + */ + public void setHdds() throws XPathExpressionException + { + final XPathExpression hddsExpr = XmlHelper.compileXPath( storageControllersPath() + + "/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.compileXPath( "/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.compileXPath( 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(s). + * + * @param elementXPath given as an xpath expression + * @param attribute attribute to change + * @param value to set the attribute to + * @param mode what to do if multiple nodes match XPath + * @return state of the change operation whether the attribute was changed successfully or not. + */ + public boolean changeAttribute( String elementXPath, String attribute, String value, MatchMode mode ) + { + NodeList nodes = findNodes( elementXPath ); + if ( nodes == null || nodes.getLength() == 0 ) { + if ( mode != MatchMode.MULTIPLE ) { + LOGGER.error( "No node could be found for: " + elementXPath ); + } + return false; + } + if ( nodes.getLength() != 1 && ( mode == MatchMode.EXACTLY_ONE ) ) { + LOGGER.error( "Multiple nodes found for: " + elementXPath ); + return false; + } + boolean ret = true; + for ( int i = 0; i < nodes.getLength(); ++i ) { + if ( !addAttributeToNode( nodes.item( i ), attribute, value ) ) { + ret = false; + } + if ( mode == MatchMode.FIRST_ONLY ) + break; + } + return ret; + } + + /** + * 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 childName name of the node to be added + * @return the newly added Node + */ + public Element 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" ); + Element e = null; + if ( nl != null ) { + for ( int i = 0; i < nl.getLength(); ++i ) { + Node n = nl.item( i ); + if ( n.getNodeType() == Node.ELEMENT_NODE ) { + final Element ne = (Element)n; + final String keyValue = ne.getAttribute( "name" ); + if ( keyValue != null && keyValue.equals( key ) ) { + e = ne; + 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/virtualization/configuration/VirtualizationConfigurationVmware.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVmware.java new file mode 100644 index 0000000..107014f --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVmware.java @@ -0,0 +1,741 @@ +package org.openslx.virtualization.configuration; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +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.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.VirtualizationConfigurationVmwareFileFormat.ConfigEntry; +import org.openslx.virtualization.hardware.VirtOptionValue; +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.virtualizer.VirtualizerVmware; + +public class VirtualizationConfigurationVmware extends VirtualizationConfiguration +{ + /** + * File name extension for VMware virtualization configuration files. + */ + public static final String FILE_NAME_EXTENSION = "vmx"; + + private static final Logger LOGGER = LogManager.getLogger( VirtualizationConfigurationVmware.class ); + + private static final Pattern HDD_PATTERN = Pattern.compile( "^(ide\\d|scsi\\d|sata\\d|nvme\\d):?(\\d?)\\.(.*)", + Pattern.CASE_INSENSITIVE ); + + /** Lowercase regex of allowed settings for stateless execution */ + private static final Pattern STATELESS_WHITELIST_PATTERN; + + /** Lowercase regex of forbidden settings when uploading (privacy concerns) */ + private static final Pattern PRIVACY_BLACKLIST_PATTERN; + + private final VirtualizationConfigurationVmwareFileFormat config; + + // Init static members + static { + // LOWERCASE - Client execution whitelist + String[] list1 = { "^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", + "^svga\\.graphicsMemoryKB$" }; + STATELESS_WHITELIST_PATTERN = Pattern.compile( String.join( "|", list1 ), Pattern.CASE_INSENSITIVE ); + // LOWERCASE - Upload privacy filter + String[] list2 = { "^displayname$", "^extendedconfigfile$", "^gui\\.", "^nvram$", "^memsize$" }; + PRIVACY_BLACKLIST_PATTERN = Pattern.compile( String.join( "|", list2 ), Pattern.CASE_INSENSITIVE ); + } + + public static enum EthernetType + { + NAT( "vmnet1" ), BRIDGED( "vmnet0" ), HOST_ONLY( "vmnet2" ); + + public final String vmnet; + + private EthernetType( String vnet ) + { + this.vmnet = vnet; + } + } + + public VirtualizationConfigurationVmware( List<OperatingSystem> osList, File file ) + throws IOException, VirtualizationConfigurationException + { + super( new VirtualizerVmware(), osList ); + this.config = new VirtualizationConfigurationVmwareFileFormat( file ); + init(); + } + + public VirtualizationConfigurationVmware( List<OperatingSystem> osList, byte[] vmxContent, int length ) + throws VirtualizationConfigurationException + { + super( new VirtualizerVmware(), osList ); + this.config = new VirtualizationConfigurationVmwareFileFormat( vmxContent, length ); // still unfiltered + init(); // now filtered + } + + private void init() + { + Map<String, Controller> disks = new HashMap<>(); + for ( Entry<String, ConfigEntry> entry : config.entrySet() ) { + handleLoadEntry( entry, disks ); + } + // 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 ) { + try { + hdds.add( new HardDisk( config.get( "#SLX_HDD_CHIP" ), + DriveBusType.valueOf( config.get( "#SLX_HDD_BUS" ) ), "empty" ) ); + } catch ( Exception e ) { + LOGGER.debug( "Error adding HDD object when parsing #SLX_HDD_BUS. Meta-data will be incorrect.", e ); + } + 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() ) { + String deviceId = dEntry.getKey(); + 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 ) ); + // Remove original entries from VMX + removeEntriesStartingWith( controllerType + ":" + deviceId + "." ); + } + } + 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 removeEntriesStartingWith( String start ) + { + for ( Iterator<Entry<String, ConfigEntry>> it = config.entrySet().iterator(); it.hasNext(); ) { + Entry<String, ConfigEntry> entry = it.next(); + if ( entry.getKey().startsWith( start ) ) { + it.remove(); + } + } + } + + private void addFiltered( String key, String value ) + { + config.set( key, value ); + } + + private boolean isSetAndTrue( String key ) + { + String value = config.get( key ); + return value != null && value.equalsIgnoreCase( "true" ); + } + + private void handleLoadEntry( Entry<String, ConfigEntry> entry, Map<String, Controller> disks ) + { + String lowerKey = entry.getKey().toLowerCase(); + // 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 = HDD_PATTERN.matcher( entry.getKey() ); + if ( hdd.find() ) { + handleHddEntry( disks, hdd.group( 1 ).toLowerCase(), hdd.group( 2 ), hdd.group( 3 ), value ); + } + } + + private void handleHddEntry( Map<String, Controller> disks, 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 boolean addEmptyHddTemplate() + { + return this.addHddTemplate( "%VM_DISK_PATH%", "%VM_DISK_MODE%", "%VM_DISK_REDOLOGDIR%" ); + } + + @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; + } + + if ( hdds.isEmpty() ) { + LOGGER.warn( "No HDDs found in configuration" ); + return false; + } + + HardDisk hdd = hdds.get( 0 ); + + String chipset = hdd.chipsetDriver; + String prefix; + switch ( hdd.bus ) { + case SATA: + // Cannot happen?... use lsisas1068 + prefix = "scsi0"; + chipset = "lsisas1068"; + break; + case IDE: + case SCSI: + case NVME: + prefix = hdd.bus.name().toLowerCase() + "0"; + break; + default: + LOGGER.warn( "Unknown HDD bus type: " + hdd.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( VirtualizationConfiguration.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 void transformNonPersistent() throws VirtualizationConfigurationException + { + // Cleaned vmx construction + for ( Iterator<Entry<String, ConfigEntry>> it = config.entrySet().iterator(); it.hasNext(); ) { + Entry<String, ConfigEntry> elem = it.next(); + if ( !STATELESS_WHITELIST_PATTERN.matcher( elem.getKey() ).find() ) { + it.remove(); + } + } + addFiltered( "suspend.disabled", "TRUE" ); + } + + @Override + public void transformEditable() throws VirtualizationConfigurationException + { + addFiltered( "gui.applyHostDisplayScalingToGuest", "FALSE" ); + // This is for a very old bug: Check we have at lerast USB 2.0, as + // a buggy dmsd removed all USB controllers + List<ConfigurableOptionGroup> groups = getConfigurableOptions(); + for ( ConfigurableOptionGroup group : groups ) { + if ( group.groupIdentifier != ConfigurationGroups.USB_SPEED ) + continue; + int currentSpeed = 0; + VirtOptionValue twoPointOh = null; + for ( VirtOptionValue option : group.availableOptions ) { + int s = Util.parseInt( option.getId(), 0 ); + if ( option.isActive() && s > currentSpeed ) { + currentSpeed = s; + } + if ( s == 2 ) { + twoPointOh = option; + } + } + if ( currentSpeed < 3 && twoPointOh != null ) { + twoPointOh.apply(); + } + } + } + + @Override + public void transformPrivacy() throws VirtualizationConfigurationException + { + for ( Iterator<Entry<String, ConfigEntry>> it = config.entrySet().iterator(); it.hasNext(); ) { + Entry<String, ConfigEntry> elem = it.next(); + String key = elem.getKey(); + String value = elem.getValue().getValue(); + if ( key.endsWith( ".fileName" ) && !value.startsWith( "-" ) + && ( value.contains( "." ) || value.contains( "/" ) || value.contains( "\\" ) ) ) { + it.remove(); + } else if ( PRIVACY_BLACKLIST_PATTERN.matcher( key ).find() ) { + it.remove(); + } + } + } + + @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 ); + + final OperatingSystem os = VirtualizationConfigurationUtils.getOsOfVirtualizerFromList( this.osList, + TConst.VIRT_VMWARE, vendorOsId ); + this.setOs( os ); + } + + public byte[] getConfigurationAsByteArray() + { + return config.toString().getBytes( StandardCharsets.UTF_8 ); + } + + 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(); + } + } + + public String getValue( String key ) + { + return config.get( key ); + } + + class VmwareNoSoundCard extends VirtOptionValue + { + + public VmwareNoSoundCard( String displayName ) + { + super( "", displayName ); + } + + @Override + public void apply() + { + addFiltered( "sound.present", vmBoolean( false ) ); + config.remove( "sound.virtualDev" ); + } + + @Override + public boolean isActive() + { + return !isSetAndTrue( "sound.present" ); + } + + } + + class VmWareSoundCardModelNone extends VirtOptionValue + { + + public VmWareSoundCardModelNone( String displayName ) + { + super( "none", displayName ); + } + + @Override + public void apply() + { + addFiltered( "sound.present", vmBoolean( false ) ); + addFiltered( "sound.autodetect", vmBoolean( false ) ); + config.remove( "sound.virtualDev" ); + } + + @Override + public boolean isActive() + { + return !isSetAndTrue( "sound.present" ); + } + + } + + class VmWareSoundCardModel extends VirtOptionValue + { + + public VmWareSoundCardModel( String id, String displayName ) + { + super( id, displayName ); + } + + @Override + public void apply() + { + addFiltered( "sound.present", vmBoolean( true ) ); + addFiltered( "sound.autodetect", vmBoolean( true ) ); + addFiltered( "sound.virtualDev", this.id ); + } + + @Override + public boolean isActive() + { + return isSetAndTrue( "sound.present" ) && isSetAndTrue( "sound.autodetect" ) + && this.id.equals( config.get( "sound.virtualDev" ) ); + } + + } + + class VmWareAccel3D extends VirtOptionValue + { + + public VmWareAccel3D( String id, String displayName ) + { + super( id, displayName ); + } + + @Override + public void apply() + { + addFiltered( "mks.enable3d", this.id ); + } + + @Override + public boolean isActive() + { + return Boolean.parseBoolean( this.id ) == isSetAndTrue( "mks.enable3d" ); + } + + } + + public void setVirtualizerVersion( Version type ) + { + addFiltered( "virtualHW.version", vmInteger( type.getMajor() ) ); + } + + public Version getVirtualizerVersion() + { + final short major = Integer.valueOf( Util.parseInt( config.get( "virtualHW.version" ), -1 ) ).shortValue(); + return Version.getInstanceByMajorFromVersions( major, this.getVirtualizer().getSupportedVersions() ); + } + + class VmwareNicModel extends VirtOptionValue + { + + private final int cardIndex; + + public VmwareNicModel( int cardIndex, String id, String displayName ) + { + super( id, displayName ); + this.cardIndex = cardIndex; + } + + @Override + public void apply() + { + if ( Util.isEmptyString( id ) ) { + config.remove( "ethernet" + cardIndex + ".virtualDev" ); + } else { + addFiltered( "ethernet" + cardIndex + ".virtualDev", id ); + } + } + + @Override + public boolean isActive() + { + String temp = config.get( "ethernet" + cardIndex + ".virtualDev" ); + if ( temp == null ) + return Util.isEmptyString( this.id ); + return temp.equals( this.id ); + } + + } + + class VmWareUsbSpeed extends VirtOptionValue + { + private final String[] SPEED = { null, "usb", "ehci", "usb_xhci" }; + private final int speed; + + public VmWareUsbSpeed( int speed, String displayName ) + { + super( Integer.toString( speed ), displayName ); + this.speed = speed; + } + + @Override + public void apply() + { + // XXX TODO This sucks, qnd + for ( int i = 1; i < SPEED.length; ++i ) { + String key = SPEED[i] + ".present"; + if ( i <= speed ) { + // Enable desired speed class, plus all lower ones + addFiltered( key, "TRUE" ); + } else { + config.remove( key ); + } + } + // VMware 14+ needs this to use USB 3.0 devices at USB 3.0 ports in VMs configured for < 3.0 + if ( speed > 0 && speed < 3 ) { + addFiltered( "usb.mangleUsb3Speed", "TRUE" ); + } + } + + @Override + public boolean isActive() + { + int max = 0; + for ( int i = 1; i < SPEED.length; ++i ) { + if ( isSetAndTrue( SPEED[i] + ".present" ) ) { + max = i; + } + } + return speed == max; + } + + } + + @Override + public boolean addCpuCoreCount( int numCores ) + { + addFiltered( "numvcpus", vmInteger( numCores ) ); + return true; + } + + public void registerVirtualHW() + { + List<VirtOptionValue> list; + list = new ArrayList<>(); + list.add( new VmWareSoundCardModelNone( SoundCard.NONE ) ); + list.add( new VmWareSoundCardModel( "", SoundCard.DEFAULT ) ); + list.add( new VmWareSoundCardModel( "sb16", SoundCard.SOUND_BLASTER ) ); + list.add( new VmWareSoundCardModel( "es1371", SoundCard.ES ) ); + list.add( new VmWareSoundCardModel( "hdaudio", SoundCard.HD_AUDIO ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.SOUND_CARD_MODEL, list ) ); + + list = new ArrayList<>(); + list.add( new VmWareAccel3D( "FALSE", "2D" ) ); + list.add( new VmWareAccel3D( "TRUE", "3D" ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.GFX_TYPE, list ) ); + + list = new ArrayList<>(); + list.add( new VmwareNicModel( 0, "", Ethernet.AUTO ) ); + list.add( new VmwareNicModel( 0, "vlance", Ethernet.PCNET32 ) ); + list.add( new VmwareNicModel( 0, "e1000", Ethernet.E1000 ) ); + list.add( new VmwareNicModel( 0, "e1000e", Ethernet.E1000E ) ); + list.add( new VmwareNicModel( 0, "vmxnet", Ethernet.VMXNET ) ); + list.add( new VmwareNicModel( 0, "vmxnet3", Ethernet.VMXNET3 ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.NIC_MODEL, list ) ); + + list = new ArrayList<>(); + list.add( new VmWareUsbSpeed( 0, Usb.NONE ) ); + list.add( new VmWareUsbSpeed( 1, Usb.USB1_1 ) ); + list.add( new VmWareUsbSpeed( 2, Usb.USB2_0 ) ); + list.add( new VmWareUsbSpeed( 3, Usb.USB3_0 ) ); + configurableOptions.add( new ConfigurableOptionGroup( ConfigurationGroups.USB_SPEED, list ) ); + } + + @Override + public String getFileNameExtension() + { + return VirtualizationConfigurationVmware.FILE_NAME_EXTENSION; + } + + @Override + public void validate() throws VirtualizationConfigurationException + { + } + + @Override + public void disableUsb() + { + new VmWareUsbSpeed( 0, Usb.NONE ).apply(); + } + + @Override + public String getSuspendedFile() + { + return config.get( "checkpoint.vmState" ); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVmwareFileFormat.java b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVmwareFileFormat.java new file mode 100644 index 0000000..356a034 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/VirtualizationConfigurationVmwareFileFormat.java @@ -0,0 +1,284 @@ +package org.openslx.virtualization.configuration; + +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.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openslx.util.Util; + +class KeyValuePair +{ + public final String key; + public final String value; + + public KeyValuePair( String key, String value ) + { + this.key = key; + this.value = value; + } +} + +public class VirtualizationConfigurationVmwareFileFormat +{ + + private static final Logger LOGGER = LogManager.getLogger( VirtualizationConfigurationVmwareFileFormat.class ); + + private Map<String, ConfigEntry> entries = new TreeMap<>( String.CASE_INSENSITIVE_ORDER ); + + public VirtualizationConfigurationVmwareFileFormat() + { + // (void) + } + + public VirtualizationConfigurationVmwareFileFormat( File file ) throws IOException, VirtualizationConfigurationException + { + 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 VirtualizationConfigurationVmwareFileFormat( InputStream is ) throws IOException, VirtualizationConfigurationException + { + 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 VirtualizationConfigurationVmwareFileFormat( byte[] vmxContent, int length ) throws VirtualizationConfigurationException + { + init( vmxContent, length ); + } + + // function is used for both .vmx and .vmdk files + private void init( byte[] vmxContent, int length ) throws VirtualizationConfigurationException + { + try { + boolean isValid = false; + BufferedReader reader = getVmxReader( vmxContent, length ); + String line; + while ( ( line = reader.readLine() ) != null ) { + KeyValuePair entry = parse( line ); + + if ( entry != null ) { + // TODO: This is supposed to be case insensitive. + // Check if there are other consequences from lowercase entries in converted vmx files. + if ( entry.key.equals( "virtualHW.version" ) || entry.key.equals( "ddb.virtualHWVersion" ) + || entry.key.equals( "virtualhw.version" ) ) { + isValid = true; + } + set( entry.key, unescape( entry.value ) ); + } + } + if ( !isValid ) { + throw new VirtualizationConfigurationException( "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 ); + if ( cs == null ) + cs = StandardCharsets.UTF_8; // YES BECAUSE THIS IS NOT VMX AND EVERYTHING IS SHIT + return new BufferedReader( new InputStreamReader( new ByteArrayInputStream( vmxContent, 0, length ), cs ) ); + } + + /** + * Get charset of config. Returns null if input doesn't look like a vmx file. + * @param vmxContent + * @param length + * @return + */ + public static Charset getCharset( byte[] vmxContent, int length ) + { + String csName = detectCharset( new ByteArrayInputStream( vmxContent, 0, length ) ); + if ( csName == null ) + return null; + 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; + } + + private static String detectCharset( InputStream is ) + { + boolean isVmware = false; + 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; + } + if ( entry.key.equals( "virtualHW.version" ) || entry.key.equals( "memsize" ) || entry.key.equals( "displayName") ) { + isVmware = true; + } + } + } catch ( Exception e ) { + LOGGER.warn( "Could not detect charset, fallback to latin1", e ); + } + if ( !isVmware ) + return null; + // 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; + } + + @Override + public String toString() + { + set( ".encoding", "UTF-8" ); + StringBuilder sb = new StringBuilder( 300 ); + for ( Entry<String, ConfigEntry> entry : entries.entrySet() ) { + ConfigEntry value = entry.getValue(); + sb.append( entry.getKey() ); + sb.append( " = \"" ); + sb.append( value.getEscaped() ); + sb.append( "\"\n" ); + } + return sb.toString(); + } + + public static class ConfigEntry + { + private String value; + + public ConfigEntry( String value ) + { + this.value = value; + } + + 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/virtualization/configuration/container/ContainerBindMount.java b/src/main/java/org/openslx/virtualization/configuration/container/ContainerBindMount.java new file mode 100644 index 0000000..0c1788e --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/container/ContainerBindMount.java @@ -0,0 +1,74 @@ +package org.openslx.virtualization.configuration.container; + +import java.util.Objects; + +/** + * This class implements a model for a bind mount entry in the docker context + * (eg. docker run ... --mount type=bind,source=source,target=target,options ... ). A list of objects of this class is stored in + * {@link ContainerMeta}. + */ +public class ContainerBindMount { + + public enum ContainerMountType { + DEFAULT, + CONTAINER_IMAGE + } + + private ContainerMountType mount_type = ContainerMountType.DEFAULT; + private String source = ""; + private String target = ""; + private String options = ""; + + public ContainerBindMount() { + } + + public ContainerBindMount(String source, String target, String options) { + this(ContainerMountType.DEFAULT,source,target,options); + } + + public ContainerBindMount(ContainerMountType mount_type, String source, String target, String options) { + this.mount_type = mount_type; + this.source = source; + this.target = target; + this.options = options; + } + + public String getSource() { + return source; + } + + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } + + public String getOptions() { + return options; + } + + public void setOptions(String options) { + this.options = options; + } + + public ContainerMountType getMountType() { + return this.mount_type; + } + + + @Override public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ContainerBindMount that = (ContainerBindMount) o; + return Objects.equals(source, that.source) && Objects.equals(mount_type, that.mount_type) + && Objects.equals(target, that.target) && Objects.equals(options, that.options); + } + + @Override public int hashCode() { + return Objects.hash(source, target, options); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/container/ContainerDefinition.java b/src/main/java/org/openslx/virtualization/configuration/container/ContainerDefinition.java new file mode 100644 index 0000000..825d0c3 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/container/ContainerDefinition.java @@ -0,0 +1,195 @@ +package org.openslx.virtualization.configuration.container; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.stream.JsonReader; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openslx.util.Util; +import org.openslx.util.TarArchiveUtil.TarArchiveReader; +import org.openslx.util.TarArchiveUtil.TarArchiveWriter; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +public class ContainerDefinition { + + // TODO database needs a refactoring to store container details + // TODO refatoring: tar.gz of this object is not useful, for smaller dockerfiles it makes the package lager. + // remove the containerRecipe, ContainerMeta holds in build_context the dockerfile. + + protected static final Logger LOGGER = LogManager.getLogger(ContainerDefinition.class); + + protected static final String CONTAINER_FILE = "dockerfile"; + protected static final String CONTAINER_META_FILE = "container_meta.json"; + + /** + * The file to construct a real container image, could be an dockerfile or a singularity recipe. + */ + public String containerRecipe = ""; + + /** + * Further container information, see {@link ContainerMeta}. + */ + public ContainerMeta containerMeta; + + public ContainerDefinition() { + containerMeta = new ContainerMeta(); + } + + /** + * Copy Constructor + * + * @param containerDef {@link ContainerDefinition} from which to make a deep copy. + */ + public ContainerDefinition(ContainerDefinition containerDef) { + containerRecipe = String.valueOf(containerDef.getContainerRecipe()); + containerMeta = new ContainerMeta(containerDef.getContainerMeta()); + } + + /** + * Utility function to create a {@link ContainerDefinition} object for a byte array downloaded from the server. + * + * @param rawTarData Downloaded tar.gz file from the server as a byte array. + * @return New object of ContainerDefinition. + */ + public static ContainerDefinition fromByteArray(byte[] rawTarData) { + + ContainerDefinition containerDef = new ContainerDefinition(); + + try { + TarArchiveReader tarReader = new TarArchiveReader(new ByteArrayInputStream(rawTarData), true, true); + + while (tarReader.hasNextEntry()) { + if (tarReader.getEntryName().equals(CONTAINER_FILE)) + containerDef.setContainerRecipe(tarReader.readCurrentEntry()); + if (tarReader.getEntryName().equals(CONTAINER_META_FILE)) + containerDef.setContainerMeta(tarReader.readCurrentEntry()); + } + tarReader.close(); + + } catch (IOException e) { + LOGGER.error("Could not create a ContainerDefinition Object for rawTarData", e); + } + + return containerDef; + } + + public String getContainerRecipe() { + return containerRecipe; + } + + public void setContainerRecipe(String containerRecipe) { + this.containerRecipe = containerRecipe; + } + + public void setContainerRecipe(File containerRecipeFile) { + this.containerRecipe = readContainerRecipe(containerRecipeFile); + } + + public void setContainerRecipe(byte[] rawContainerRecipe) { + this.containerRecipe = new String(rawContainerRecipe, StandardCharsets.UTF_8); + } + + public ContainerMeta getContainerMeta() { + return containerMeta; + } + + public void setContainerMeta(byte[] containerMeta) { + Gson gson = new GsonBuilder().create(); + this.containerMeta = gson.fromJson(new JsonReader( + new InputStreamReader(new ByteArrayInputStream(containerMeta), StandardCharsets.UTF_8)), + ContainerMeta.class); + } + + /** + * Serializes the ContainerMeta and Container Description (e.g. dockerfile) into an tar.gz archive. + * + * @return A ByteBuffer object of the container definition. Can be uploaded so satellite server. + */ + public ByteBuffer toByteBuffer() { + + ByteBuffer containerDef = null; + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + TarArchiveWriter tarWriter = new TarArchiveWriter(baos); + tarWriter.writeFile(CONTAINER_META_FILE, gson.toJson(containerMeta)); + tarWriter.writeFile(CONTAINER_FILE, containerRecipe); + Util.safeClose(tarWriter); + + containerDef = ByteBuffer.wrap(baos.toByteArray()); + } catch (IOException e) { + LOGGER.warn("Could not create a tar file", e); + } + + return containerDef; + } + + private String readContainerRecipe(File file) { + String recipe = null; + try { + + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + ByteArrayOutputStream rawFile = new ByteArrayOutputStream(); + int count; + byte[] data = new byte[1024]; + while ((count = bis.read(data)) != -1) { + rawFile.write(data, 0, count); + } + + String rawRecipe = new String(rawFile.toByteArray(), StandardCharsets.UTF_8); + + // replace windows by unix EOL + recipe = rawRecipe.replaceAll("\\r\\n", "\n"); + + bis.close(); + + } catch (IOException e) { + LOGGER.error("Could not read Container Recipe", e); + } + return recipe; + } + + /** + * Saves containerRecipe and containerMeta at the provided location. + * + * @param destDir destination directory for containerRecipe and containerMeta. + */ + public void saveLocal(File destDir) { + writeFile(destDir, containerRecipe, CONTAINER_FILE); + } + + private void writeFile(File destDir, String fileContent, String filename) { + File output = new File(destDir, filename); + try { + FileWriter fw = new FileWriter(output); + fw.write(fileContent); + fw.flush(); + fw.close(); + } catch (IOException e) { + e.printStackTrace(); + LOGGER.error("Could not write File", e); + } + } + + public ContainerImageContext getContainerImageContext() { + return ContainerImageContext.fromInt(containerMeta.getContainerImageContext()); + } + + @Override public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ContainerDefinition that = (ContainerDefinition) o; + return containerRecipe.equals(that.containerRecipe) && containerMeta.equals(that.containerMeta); + } + + @Override public int hashCode() { + return Objects.hash(containerRecipe, containerMeta); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/container/ContainerImageContext.java b/src/main/java/org/openslx/virtualization/configuration/container/ContainerImageContext.java new file mode 100644 index 0000000..f19d419 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/container/ContainerImageContext.java @@ -0,0 +1,10 @@ +package org.openslx.virtualization.configuration.container; + +public enum ContainerImageContext { + + DOCKERFILE, GIT_REPOSITORY, IMAGE_REPOSITORY, DOCKER_ARCHIVE; + + public static ContainerImageContext fromInt(int index) { + return values()[index]; + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/container/ContainerMeta.java b/src/main/java/org/openslx/virtualization/configuration/container/ContainerMeta.java new file mode 100644 index 0000000..4dbf64b --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/container/ContainerMeta.java @@ -0,0 +1,166 @@ +package org.openslx.virtualization.configuration.container; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * ContainerMeta is used to store container specific information. An object of + * this class will be serialized with gson to a json file. + * <p> + * TODO rename build_context_method to container_image_context, requires update + * in database(json) + * <p> + * TODO rename build_context_url to build_context TODO refactoring build_context + * is either a dockerfile or a git url with a dockerfile. + */ +public class ContainerMeta { + + private int build_context_method; + private String image_repo; + private String build_context_url; + private String image_name; + private String run_options; + private String run_command; + private String image_type; + private List<ContainerBindMount> bind_mount_config = new ArrayList<>(); + + public ContainerMeta() { + + image_repo = ""; + build_context_method = ContainerImageContext.DOCKERFILE.ordinal(); + build_context_url = ""; + image_name = ""; + run_options = ""; + run_command = ""; + image_type = ContainerImageType.LECTURE.toString(); + bind_mount_config = new ArrayList<>(); + } + + public ContainerMeta(ContainerMeta containerMeta) { + build_context_method = containerMeta.build_context_method; + build_context_url = containerMeta.build_context_url; + image_name = containerMeta.image_name; + run_options = containerMeta.run_options; + run_command = containerMeta.run_command; + image_repo = containerMeta.image_repo; + + for (ContainerBindMount bm : containerMeta.bind_mount_config) + bind_mount_config.add(new ContainerBindMount(bm.getSource(), bm.getTarget(), bm.getOptions())); + + } + + public int getContainerImageContext() { + return build_context_method; + } + + public void setContainerImageContext(int buildContextMethod) { + this.build_context_method = buildContextMethod; + } + + public String getBuildContextUrl() { + return build_context_url; + } + + public void setBuildContextUrl(String buildContextUrl) { + this.build_context_url = buildContextUrl; + } + + public String getRunOptions() { + return run_options; + } + + public void setRunOptions(String run_options) { + this.run_options = run_options; + } + + public String getRunCommand() { + return this.run_command; + } + + public void setRunCommand(String run_command) { + this.run_command = run_command; + } + + public String getImageName() { + return image_name; + } + + public void setImageName(String image_name) { + this.image_name = image_name; + } + + public List<ContainerBindMount> getBindMountConfig() { + return bind_mount_config; + } + + public void setBindMountConfig(List<ContainerBindMount> bindMountConfig) { + this.bind_mount_config = bindMountConfig; + } + + public String getImageRepo() { + return image_repo; + } + + public void setImageRepo(String from_image) { + this.image_repo = from_image; + } + + public ContainerImageType getImageType() { + if (image_type == null || image_type.length() == 0) + return ContainerImageType.LECTURE; + + // turn string representation into enum-var 'LECTURE' -> + // ContainerImageType.LECTURE + return ContainerImageType.valueOf(image_type); + } + + public void setImageType(ContainerImageType image_type) { + // set constant representation of the enum-var e.g. ContainerImageType.LECTURE + // -> 'LECTURE' + this.image_type = image_type.name(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ContainerMeta that = (ContainerMeta) o; + return Objects.equals(build_context_url, that.build_context_url) && Objects.equals(image_name, that.image_name) + && Objects.equals(run_options, that.run_options) && Objects.equals(run_command, that.run_command) + && Objects.equals(bind_mount_config, that.bind_mount_config) + && Objects.equals(image_repo, that.image_repo) && Objects.equals(image_type, that.image_type); + } + + @Override + public int hashCode() { + return Objects.hash(build_context_url, image_name, run_options, run_command, bind_mount_config, image_repo, + image_type); + } + + public enum ContainerImageType implements org.apache.thrift.TEnum { + LECTURE("Lecture"), BATCH("Batch"), DATA("Data"); + + private final String displayLable; + + ContainerImageType(String name) { + this.displayLable = name; + } + + public boolean equalNames(String other) { + return displayLable.equals(other); + } + + @Override + public String toString() { + return this.displayLable; + } + + @Override + public int getValue() { + return this.ordinal(); + } + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModClientToDozModServer.java b/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModClientToDozModServer.java new file mode 100644 index 0000000..8a08d05 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModClientToDozModServer.java @@ -0,0 +1,21 @@ +package org.openslx.virtualization.configuration.data; + +/** + * Data container to collect and store input arguments for a + * {@link org.openslx.virtualization.configuration.logic.ConfigurationLogicDozModClientToDozModServer} + * transformation. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class ConfigurationDataDozModClientToDozModServer +{ + /** + * Creates a new data container to collect and store input arguments for a + * {@link org.openslx.virtualization.configuration.logic.ConfigurationLogicDozModClientToDozModServer} + * transformation. + */ + public ConfigurationDataDozModClientToDozModServer() + { + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModServerToDozModClient.java b/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModServerToDozModClient.java new file mode 100644 index 0000000..4e18d48 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModServerToDozModClient.java @@ -0,0 +1,116 @@ +package org.openslx.virtualization.configuration.data; + +import java.io.File; + +import org.openslx.bwlp.thrift.iface.OperatingSystem; + +/** + * Data container to collect and store input arguments for a + * {@link org.openslx.virtualization.configuration.logic.ConfigurationLogicDozModServerToDozModClient} + * transformation. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class ConfigurationDataDozModServerToDozModClient +{ + /** + * Display name for a transformation of a virtualization configuration. + */ + private final String displayName; + + /** + * Disk image file for a transformation of a virtualization configuration. + */ + private final File diskImage; + + /** + * Guest operating system for a transformation of a virtualization configuration. + */ + private final OperatingSystem guestOs; + + /** + * Virtualizer identifier for a transformation of a virtualization configuration. + */ + private final String virtualizerId; + + /** + * Total amount of available memory for a transformation of a virtualization configuration. + */ + private final int totalMemory; + + /** + * Creates a new data container to collect and store input arguments for a + * {@link org.openslx.virtualization.configuration.logic.ConfigurationLogicDozModServerToDozModClient} + * transformation. + * + * @param displayName display name for a transformation of a virtualization configuration. + * @param diskImage disk image file for a transformation of a virtualization configuration. + * @param guestOs guest operating system for a transformation of a virtualization configuration. + * @param virtualizerId virtualizer identifier for a transformation of a virtualization + * configuration. + * @param totalMemory total amount of available memory for a transformation of a virtualization + * configuration. + */ + public ConfigurationDataDozModServerToDozModClient( String displayName, File diskImage, OperatingSystem guestOs, + String virtualizerId, int totalMemory ) + { + this.displayName = displayName; + this.diskImage = diskImage; + this.guestOs = guestOs; + this.virtualizerId = virtualizerId; + this.totalMemory = totalMemory; + } + + /** + * Returns the display name for a transformation of a virtualization configuration. + * + * @return display name for a transformation of a virtualization configuration. + */ + public String getDisplayName() + { + return this.displayName; + } + + /** + * Returns the disk image file for a transformation of a virtualization configuration. + * + * @return disk image file for a transformation of a virtualization configuration. + */ + public File getDiskImage() + { + return this.diskImage; + } + + /** + * Returns the guest operating system for a transformation of a virtualization configuration. + * + * @return guest operating system for a transformation of a virtualization configuration. + */ + public OperatingSystem getGuestOs() + { + return this.guestOs; + } + + /** + * Returns the virtualizer identifier for a transformation of a virtualization configuration. + * + * @return virtualizer identifier for a transformation of a virtualization configuration. + */ + public String getVirtualizerId() + { + return this.virtualizerId; + } + + /** + * Returns the total amount of available memory for a transformation of a virtualization + * configuration. + * + * @return total amount of available memory for a transformation of a virtualization + * configuration. + */ + public int getTotalMemory() + { + return this.totalMemory; + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModServerToStatelessClient.java b/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModServerToStatelessClient.java new file mode 100644 index 0000000..65cc7ce --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/data/ConfigurationDataDozModServerToStatelessClient.java @@ -0,0 +1,79 @@ +package org.openslx.virtualization.configuration.data; + +/** + * Data container to collect and store input arguments for a + * {@link org.openslx.virtualization.configuration.logic.ConfigurationLogicDozModServerToStatelessClient} + * transformation. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class ConfigurationDataDozModServerToStatelessClient +{ + /** + * Display name for a transformation of a virtualization configuration. + */ + private final String displayName; + + /** + * Operating system identifier for a transformation of a virtualization configuration. + */ + private final String osId; + + /** + * State whether USB access is allowed or not for a transformation of a virtualization + * configuration. + */ + private final boolean hasUsbAccess; + + /** + * Creates a new data container to collect and store input arguments for a + * {@link org.openslx.virtualization.configuration.logic.ConfigurationLogicDozModServerToStatelessClient} + * transformation. + * + * @param displayName display name for a transformation of a virtualization configuration. + * @param osId operating system identifier for a transformation of a virtualization + * configuration. + * @param hasUsbAccess state whether USB access is allowed or not for a transformation of a + * virtualization configuration. + */ + public ConfigurationDataDozModServerToStatelessClient( String displayName, String osId, boolean hasUsbAccess ) + { + this.displayName = displayName; + this.osId = osId; + this.hasUsbAccess = hasUsbAccess; + } + + /** + * Returns the display name for a transformation of a virtualization configuration. + * + * @return display name for a transformation of a virtualization configuration. + */ + public String getDisplayName() + { + return this.displayName; + } + + /** + * Returns the operating system identifier for a transformation of a virtualization + * configuration. + * + * @return operating system identifier for a transformation of a virtualization configuration. + */ + public String getOsId() + { + return this.osId; + } + + /** + * Returns the state whether USB access is allowed or not for a transformation of a + * virtualization configuration. + * + * @return state whether USB access is allowed or not for a transformation of a virtualization + * configuration. + */ + public boolean hasUsbAccess() + { + return this.hasUsbAccess; + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogic.java b/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogic.java new file mode 100644 index 0000000..90319e2 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogic.java @@ -0,0 +1,28 @@ +package org.openslx.virtualization.configuration.logic; + +import org.openslx.virtualization.configuration.VirtualizationConfiguration; +import org.openslx.virtualization.configuration.transformation.TransformationGeneric; + +/** + * Generic transformation logic for virtualization configurations. + * <p> + * This transformation logic represents an encapsulated transformation logic to transform + * virtualization configurations as part of server or client implementations. + * + * @author Manuel Bentele + * @version 1.0 + * + * @param <T> type of configuration data used as input arguments for a transformation. + */ +public abstract class ConfigurationLogic<T> extends TransformationGeneric<VirtualizationConfiguration, T> +{ + /** + * Creates a new generic transformation logic for virtualization configurations. + * + * @param name generic transformation logic name. + */ + public ConfigurationLogic( String name ) + { + super( name ); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModClientToDozModServer.java b/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModClientToDozModServer.java new file mode 100644 index 0000000..f5d9a76 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModClientToDozModServer.java @@ -0,0 +1,76 @@ +package org.openslx.virtualization.configuration.logic; + +import org.openslx.virtualization.configuration.VirtualizationConfiguration; +import org.openslx.virtualization.configuration.VirtualizationConfigurationException; +import org.openslx.virtualization.configuration.data.ConfigurationDataDozModClientToDozModServer; +import org.openslx.virtualization.configuration.transformation.TransformationException; + +/** + * Transformation logic for virtualization configurations between a dozmod-client and a + * dozmod-server. + * <p> + * This transformation logic is applied while uploading a new virtualization configuration from a + * dozmod-client to a dozmod-server. + * + * <pre> + * +------------------------------+ DozModClientToDozModServer +------------------------------+ + * | virtualization configuration | ----------------------------â–¶ | virtualization configuration | + * +---------------+--------------+ transformation logic +---------------+--------------+ + * | dozmod-client | | dozmod-server | + * +---------------+ +---------------+ + * </pre> + * + * @author Manuel Bentele + * @version 1.0 + */ +public class ConfigurationLogicDozModClientToDozModServer + extends ConfigurationLogic<ConfigurationDataDozModClientToDozModServer> +{ + /** + * Name of the transformation logic for virtualization configurations. + */ + private static final String CONFIGURATION_LOGIC_NAME = "Transformation of virtualization configuration during upload from DozMod client to DozMod server"; + + /** + * Creates a new transformation logic for virtualization configurations between a dozmod-client + * and a dozmod-server. + */ + public ConfigurationLogicDozModClientToDozModServer() + { + super( ConfigurationLogicDozModClientToDozModServer.CONFIGURATION_LOGIC_NAME ); + } + + /** + * Validates a virtualization configuration and input arguments for a transformation. + * + * @param config virtualization configuration for the validation. + * @param args input arguments for the validation. + * @throws TransformationException validation has failed. + */ + private void validateInputs( VirtualizationConfiguration config, + ConfigurationDataDozModClientToDozModServer args ) + throws TransformationException + { + if ( config == null || args == null ) { + throw new TransformationException( "Virtualization configuration or input arguments are missing!" ); + } else if ( config.getDisplayName() == null ) { + throw new TransformationException( "Display name is missing in virtualization configuration!" ); + } + } + + @Override + public void transform( VirtualizationConfiguration config, + ConfigurationDataDozModClientToDozModServer args ) + throws TransformationException + { + // check if input parameters for a transformation are valid + this.validateInputs( config, args ); + + // apply the privacy filter on the given virtualization configuration + try { + config.transformPrivacy(); + } catch ( VirtualizationConfigurationException e ) { + throw new TransformationException( e.getLocalizedMessage() ); + } + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModServerToDozModClient.java b/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModServerToDozModClient.java new file mode 100644 index 0000000..acbf4fc --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModServerToDozModClient.java @@ -0,0 +1,197 @@ +package org.openslx.virtualization.configuration.logic; + +import java.util.Map; + +import org.openslx.bwlp.thrift.iface.OperatingSystem; +import org.openslx.virtualization.configuration.VirtualizationConfiguration; +import org.openslx.virtualization.configuration.VirtualizationConfigurationException; +import org.openslx.virtualization.configuration.data.ConfigurationDataDozModServerToDozModClient; +import org.openslx.virtualization.configuration.transformation.TransformationException; + +/** + * Transformation logic for virtualization configurations between a dozmod-server and a + * dozmod-client. + * <p> + * This transformation logic is applied while downloading an existing virtualization configuration + * from a dozmod-server to a dozmod-client. + * + * <pre> + * +------------------------------+ DozModServerToDozModClient +------------------------------+ + * | virtualization configuration | ----------------------------â–¶ | virtualization configuration | + * +---------------+--------------+ transformation logic +---------------+--------------+ + * | dozmod-server | | dozmod-client | + * +---------------+ +---------------+ + * </pre> + * + * @author Manuel Bentele + * @version 1.0 + */ +public class ConfigurationLogicDozModServerToDozModClient + extends ConfigurationLogic<ConfigurationDataDozModServerToDozModClient> +{ + /** + * Name of the transformation logic for virtualization configurations. + */ + private static final String CONFIGURATION_LOGIC_NAME = "Transformation of virtualization configuration during download from DozMod server to DozMod client"; + + /** + * Default number of CPU cores set by the configuration logic for the virtualization + * configuration's virtualizer. + */ + private static final int CONFIGURATION_LOGIC_NUM_CPU_CORES = 1; + + /** + * Default memory in megabytes set by the configuration logic for the virtualization + * configuration's virtualizer. + */ + private static final int CONFIGURATION_LOGIC_MEMORY_MIN = 1024; + + /** + * Creates a new transformation logic for virtualization configurations between a dozmod-server + * and a dozmod-client. + */ + public ConfigurationLogicDozModServerToDozModClient() + { + super( ConfigurationLogicDozModServerToDozModClient.CONFIGURATION_LOGIC_NAME ); + } + + /** + * Validates a virtualization configuration and input arguments for a transformation. + * + * @param config virtualization configuration for the validation. + * @param args input arguments for the validation. + * @throws TransformationException validation has failed. + */ + private void validateInputs( VirtualizationConfiguration config, + ConfigurationDataDozModServerToDozModClient args ) + throws TransformationException + { + if ( config == null || args == null ) { + throw new TransformationException( "Virtualization configuration or input arguments are missing!" ); + } else if ( args.getDisplayName() == null || args.getDisplayName().isEmpty() ) { + throw new TransformationException( "Valid display name is not specified!" ); + } else if ( args.getDiskImage() == null || !args.getDiskImage().exists() ) { + throw new TransformationException( "Valid disk image file is not specified!" ); + } else if ( ! ( args.getTotalMemory() > 0 ) ) { + throw new TransformationException( "Total memory amount is not specified!" ); + } + } + + /** + * Rounds a given value to the nearest factor. + * + * @param value input value for the rounding. + * @param nearestFactor nearest factor for the rounding. + * @return rounded value as a multiple of the nearest factor. + * + * @apiNote This utility method rounds the given value to an integer value and no to a floating + * point value. + */ + private static int roundToNearest( int value, int nearestFactor ) + { + return ( value / nearestFactor ) * nearestFactor; + } + + /** + * Calculates the amount of memory for the virtualization configuration depending on the + * available resources of the dozmod-client's host system. + * + * @param totalMemory maximum memory available on the dozmod-client's host system. + * @param osMaxMemory maximum memory supported by the defined operating system in the + * virtualization configuration. + * @return amount of memory for the virtualization configuration in megabytes + */ + private static int calculateVirtualizationMemoryOnDozmodClient( int totalMemory, int osMaxMemory ) + { + // calculate the amount of memory + int memory = totalMemory / 2 - 512; + + // increase calculated memory if lower memory limit is undercut + if ( memory < ConfigurationLogicDozModServerToDozModClient.CONFIGURATION_LOGIC_MEMORY_MIN ) { + memory = ConfigurationLogicDozModServerToDozModClient.CONFIGURATION_LOGIC_MEMORY_MIN; + } + + // limit virtualization memory if the available host's system memory amount is smaller + if ( osMaxMemory > 0 && memory > osMaxMemory ) { + memory = osMaxMemory; + } + + // round to nearest factor of 4, otherwise VMware virtualization configuration files are invalid + return ConfigurationLogicDozModServerToDozModClient.roundToNearest( memory, 4 ); + } + + @Override + public void transform( VirtualizationConfiguration config, + ConfigurationDataDozModServerToDozModClient args ) + throws TransformationException + { + // check if input parameters for a transformation are valid + this.validateInputs( config, args ); + + // set display name + if ( !config.addDisplayName( args.getDisplayName() ) ) { + throw new TransformationException( "Can not set display name in virtualization configuration!" ); + } + + // append hard disk drive + if ( !config.addHddTemplate( args.getDiskImage(), null, null ) ) { + throw new TransformationException( "Can not configure hard disk in virtualization configuration!" ); + } + + // append default NAT interface + if ( !config.addDefaultNat() ) { + throw new TransformationException( "Can not configure NAT interface in virtualization configuration!" ); + } + + // set the guest OS if specified + final OperatingSystem guestOs = args.getGuestOs(); + final String virtualizerId = args.getVirtualizerId(); + int osMaxMemory = 0; + + if ( guestOs != null && virtualizerId != null ) { + final Map<String, String> virtOsIdMap = guestOs.getVirtualizerOsId(); + if ( virtOsIdMap != null ) { + // set guest operating system if possible + final String virtOsId = virtOsIdMap.get( virtualizerId ); + if ( virtOsId != null ) { + config.setOs( virtOsId ); + } + + // get maximum memory of editable host for guestOs if possible + final int maxMemMb = guestOs.getMaxMemMb(); + if ( maxMemMb > 0 ) { + osMaxMemory = maxMemMb; + } + } + } + + // set CPU core count + if ( !config.addCpuCoreCount( ConfigurationLogicDozModServerToDozModClient.CONFIGURATION_LOGIC_NUM_CPU_CORES ) ) { + throw new TransformationException( "Can not set CPU core count in virtualization configuration!" ); + } + + // calculate and set memory + final int virtualizationMemory = ConfigurationLogicDozModServerToDozModClient + .calculateVirtualizationMemoryOnDozmodClient( args.getTotalMemory(), osMaxMemory ); + if ( !config.addRam( virtualizationMemory ) ) { + throw new TransformationException( "Can not set memory in virtualization configuration!" ); + } + + // append first empty floppy drive + config.addFloppy( 0, null, true ); + // append second empty floppy drive + config.addFloppy( 1, null, true ); + + // append first empty (ISO-based) CDROM drive + config.addCdrom( "" ); + // append second CDROM drive connected to the host's physical drive + config.addCdrom( null ); + + // apply settings to edit virtualized system locally + try { + config.transformEditable(); + } catch ( VirtualizationConfigurationException e ) { + throw new TransformationException( e.getLocalizedMessage() ); + } + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModServerToStatelessClient.java b/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModServerToStatelessClient.java new file mode 100644 index 0000000..2d3e861 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/logic/ConfigurationLogicDozModServerToStatelessClient.java @@ -0,0 +1,108 @@ +package org.openslx.virtualization.configuration.logic; + +import org.openslx.virtualization.configuration.VirtualizationConfiguration; +import org.openslx.virtualization.configuration.VirtualizationConfiguration.EtherType; +import org.openslx.virtualization.configuration.VirtualizationConfigurationException; +import org.openslx.virtualization.configuration.data.ConfigurationDataDozModServerToStatelessClient; +import org.openslx.virtualization.configuration.transformation.TransformationException; + +/** + * Transformation logic for virtualization configurations between a dozmod-server and a stateless + * client. + * <p> + * This transformation logic is applied while downloading an existing virtualization configuration + * from a dozmod-server to a stateless client. + * + * <pre> + * +------------------------------+ DozModServerToStatelessClient +------------------------------+ + * | virtualization configuration | -------------------------------â–¶ | virtualization configuration | + * +---------------+--------------+ transformation logic +------------------+-----------+ + * | dozmod-server | | stateless client | + * +---------------+ +------------------+ + * </pre> + * + * @author Manuel Bentele + * @version 1.0 + */ +public class ConfigurationLogicDozModServerToStatelessClient + extends ConfigurationLogic<ConfigurationDataDozModServerToStatelessClient> +{ + /** + * Name of the transformation logic for virtualization configurations. + */ + private static final String CONFIGURATION_LOGIC_NAME = "Transformation of virtualization configuration during download from DozMod server to stateless client"; + + /** + * Default type for an ethernet interface in a virtualization configuration. + */ + private static final EtherType CONFIGURATION_DEFAULT_ETHERNET_TYPE = EtherType.NAT; + + /** + * Creates a new transformation logic for virtualization configurations between a dozmod-server + * and a stateless client. + */ + public ConfigurationLogicDozModServerToStatelessClient() + { + super( ConfigurationLogicDozModServerToStatelessClient.CONFIGURATION_LOGIC_NAME ); + } + + /** + * Validates a virtualization configuration and input arguments for a transformation. + * + * @param config virtualization configuration for the validation. + * @param args input arguments for the validation. + * @throws TransformationException validation has failed. + */ + private void validateInputs( VirtualizationConfiguration config, + ConfigurationDataDozModServerToStatelessClient args ) + throws TransformationException + { + if ( config == null || args == null ) { + throw new TransformationException( "Virtualization configuration or input arguments are missing!" ); + } else if ( args.getDisplayName() == null || args.getDisplayName().isEmpty() ) { + throw new TransformationException( "Valid display name is not specified!" ); + } + } + + @Override + public void transform( VirtualizationConfiguration config, + ConfigurationDataDozModServerToStatelessClient args ) + throws TransformationException + { + // check if input parameters for a transformation are valid + this.validateInputs( config, args ); + + // apply settings to run virtualized system in a stateless manner + try { + config.transformNonPersistent(); + } catch ( VirtualizationConfigurationException e ) { + throw new TransformationException( e.getLocalizedMessage() ); + } + + // set display name of lecture + if ( !config.addDisplayName( args.getDisplayName() ) ) { + throw new TransformationException( "Can not set display name in virtualization configuration!" ); + } + + // append hard disk drive (with no referenced image as content) + if ( !config.addEmptyHddTemplate() ) { + throw new TransformationException( "Can not configure hard disk in virtualization configuration!" ); + } + + // append default NAT interface + if ( !config.addEthernet( + ConfigurationLogicDozModServerToStatelessClient.CONFIGURATION_DEFAULT_ETHERNET_TYPE ) ) { + throw new TransformationException( "Can not configure NAT interface in virtualization configuration!" ); + } + + // set the guest OS if specified + if ( args.getOsId() != null ) { + config.setOs( args.getOsId() ); + } + + // disable USB if necessary + if ( !args.hasUsbAccess() ) { + config.disableUsb(); + } + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/transformation/Transformation.java b/src/main/java/org/openslx/virtualization/configuration/transformation/Transformation.java new file mode 100644 index 0000000..af0e181 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/transformation/Transformation.java @@ -0,0 +1,65 @@ +package org.openslx.virtualization.configuration.transformation; + +/** + * Represents a transformation that transforms (alters) a given configuration with specified input + * arguments. + * + * @author Manuel Bentele + * @version 1.0 + * + * @param <T> type of the configuration which will be transformed. + * @param <R> type of input arguments for the transformation. + */ +public abstract class Transformation<T, R> implements TransformationFunction<T, R> +{ + /** + * Name of the transformation. + */ + private final String name; + + /** + * State of the transformation. + */ + private boolean enabled; + + /** + * Creates a transformation. + * + * @param name comprehensible name for the transformation. + */ + public Transformation( String name ) + { + this.name = name; + this.setEnabled( true ); + } + + /** + * Returns the name of the transformation. + * + * @return name of the transformation. + */ + public String getName() + { + return this.name; + } + + /** + * Returns the state of the transformation. + * + * @return state of the transformation. + */ + public boolean isEnabled() + { + return this.enabled; + } + + /** + * Sets the state for the transformation. + * + * @param enabled state for the transformation. + */ + public void setEnabled( boolean enabled ) + { + this.enabled = enabled; + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationException.java b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationException.java new file mode 100644 index 0000000..72f320d --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationException.java @@ -0,0 +1,25 @@ +package org.openslx.virtualization.configuration.transformation; + +/** + * An exception of a transformation error. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class TransformationException extends Exception +{ + /** + * Version for serialization. + */ + private static final long serialVersionUID = 7293420658901349154L; + + /** + * Creates a transformation exception including an error message. + * + * @param errorMsg message to describe the exception. + */ + public TransformationException( String errorMsg ) + { + super( errorMsg ); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationFunction.java b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationFunction.java new file mode 100644 index 0000000..b5be7a0 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationFunction.java @@ -0,0 +1,39 @@ +package org.openslx.virtualization.configuration.transformation; + +/** + * Represents a transformation operation that transforms (alters) a given configuration with + * specified input arguments and returns no result. + * + * @author Manuel Bentele + * @version 1.0 + * + * @param <T> type of the configuration which will be transformed. + * @param <R> type of input arguments for the transformation. + */ +@FunctionalInterface +public interface TransformationFunction<T, R> +{ + /** + * Transforms a given configuration with the specified input arguments. + * + * @param config configuration which will be transformed. + * @param args input arguments for the transformation. + * + * @throws TransformationException transformation of the configuration failed. + */ + public void transform( T config, R args ) throws TransformationException; + + /** + * Applies the transformation function {@link #transform(Object, Object)} to the given + * configuration and specified input arguments. + * + * @param config configuration which will be transformed. + * @param args input arguments for the transformation. + * + * @throws TransformationException transformation of the configuration failed. + */ + public default void apply( T config, R args ) throws TransformationException + { + this.transform( config, args ); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationGeneric.java b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationGeneric.java new file mode 100644 index 0000000..4ad36c7 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationGeneric.java @@ -0,0 +1,25 @@ +package org.openslx.virtualization.configuration.transformation; + +/** + * Represents a generic transformation that transforms (alters) a given configuration with specified + * input arguments. The generic transformation does not depend on any external states of a + * virtualizer. + * + * @author Manuel Bentele + * @version 1.0 + * + * @param <T> type of the configuration which will be transformed. + * @param <R> type of input arguments for the transformation. + */ +public abstract class TransformationGeneric<T, R> extends Transformation<T, R> +{ + /** + * Create a generic transformation. + * + * @param name comprehensible name for the transformation. + */ + public TransformationGeneric( String name ) + { + super( name ); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationManager.java b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationManager.java new file mode 100644 index 0000000..18d7e43 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationManager.java @@ -0,0 +1,158 @@ +package org.openslx.virtualization.configuration.transformation; + +import java.util.ArrayList; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A transformation manager is a class to manage several transformations and their application. + * + * Transformations can be registered at the transformation manager. The transformation manager has + * the ability to apply all registered transformations on a given configuration and specified input + * arguments. + * + * @author Manuel Bentele + * @version 1.0 + * + * @param <T> type of the configuration which will be transformed by all transformations. + * @param <R> type of input arguments for all transformations. + */ +public class TransformationManager<T, R> +{ + /** + * List of registered transformations. + */ + private ArrayList<Transformation<T, R>> transformations; + + /** + * Reference to the configuration that will be transformed (altered). + */ + private T config; + + /** + * Reference to the input arguments for all registered transformations. + */ + private R args; + + /** + * Logger instance to log messages. + */ + private static final Logger LOGGER = LogManager.getLogger( TransformationManager.class ); + + /** + * Create a transformation manager. + * + * @param config configuration which will be transformed. + * @param args input arguments for all registered transformations. + */ + public TransformationManager( T config, R args ) + { + this.transformations = new ArrayList<Transformation<T, R>>(); + this.config = config; + this.args = args; + } + + /** + * Registers and enables a transformation. + * + * @param transformation existing transformation that will be registered and enabled. + */ + public void register( Transformation<T, R> transformation ) + { + this.register( transformation, true ); + } + + /** + * Registers a transformation and sets its state. + * + * @param transformation existing transformation that will be registered. + * @param enabled state for the existing transformation that will be set. + */ + public void register( Transformation<T, R> transformation, boolean enabled ) + { + LOGGER.debug( "Register transformation '" + transformation.getName() + "' and " + + ( enabled ? "enable" : "do not enable" ) + " it" ); + + transformation.setEnabled( enabled ); + this.transformations.add( transformation ); + } + + /** + * Registers a transformation function as a new transformation and enables the registered + * transformation. + * + * @param name comprehensible name for the transformation. + * @param function transformation operation for the transformation. + */ + public void register( String name, TransformationFunction<T, R> function ) + { + this.register( name, function, true ); + } + + /** + * Registers a transformation function as a new transformation and sets the state of the + * registered transformation. + * + * @param name comprehensible name for the transformation. + * @param function transformation operation for the transformation. + * @param enabled state for the transformation. + */ + public void register( String name, TransformationFunction<T, R> function, boolean enabled ) + { + this.register( new Transformation<T, R>( name ) { + @Override + public void transform( T document, R args ) throws TransformationException + { + function.apply( document, args ); + } + }, enabled ); + } + + /** + * Applies all registered transformations, whose state is set to <code>enabled</code>, to the + * referenced configuration and input arguments. + * + * @throws TransformationException transformation of the configuration failed. + */ + public void transform() throws TransformationException + { + for ( Transformation<T, R> transformation : this.transformations ) { + LOGGER.debug( "Apply transformation '" + transformation.getName() + "'" ); + try { + transformation.apply( this.config, this.args ); + } catch ( TransformationException e ) { + final String errorMsg = + "Error in configuration filter '" + transformation.getName() + "': " + e.getLocalizedMessage(); + throw new TransformationException( errorMsg ); + } + } + } + + /** + * Returns a human readable summary of all registered transformations. + * + * @return human readable summary of all registered transformations. + */ + private String showTransformations() + { + String transformationSummary = ""; + final int maxFilterNumCharacters = ( this.transformations.size() + 1 ) / 10; + + for ( int i = 0; i < this.transformations.size(); i++ ) { + final Transformation<T, R> transformation = this.transformations.get( i ); + final String paddedNumber = String.format( "%-" + maxFilterNumCharacters + "s", i + 1 ); + final String transformationState = transformation.isEnabled() ? "[ active ]" : "[inactive]"; + transformationSummary += paddedNumber + ": " + transformationState + " "; + transformationSummary += transformation.getName() + System.lineSeparator(); + } + + return transformationSummary; + } + + @Override + public String toString() + { + return this.showTransformations(); + } +} diff --git a/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationSpecific.java b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationSpecific.java new file mode 100644 index 0000000..7038abf --- /dev/null +++ b/src/main/java/org/openslx/virtualization/configuration/transformation/TransformationSpecific.java @@ -0,0 +1,44 @@ +package org.openslx.virtualization.configuration.transformation; + +/** + * Represents a specific transformation that transforms (alters) a given configuration with + * specified input arguments. The specific transformation depends on external states of a + * specified virtualizer. + * + * @author Manuel Bentele + * @version 1.0 + * + * @param <T> type of the configuration which will be transformed. + * @param <R> type of input arguments for the transformation. + * @param <H> type of the external input source. + */ +public abstract class TransformationSpecific<T, R, H> extends Transformation<T, R> +{ + /** + * Reference to virtualizer to query external states. + */ + private final H virtualizer; + + /** + * Create a specific transformation. + * + * @param name comprehensible name for the transformation. + * @param virtualizer initialized virtualizer. + */ + public TransformationSpecific( String name, H virtualizer ) + { + super( name ); + + this.virtualizer = virtualizer; + } + + /** + * Returns the referenced virtualizer of the transformation. + * + * @return referenced virtualizer of the transformation. + */ + public H getVirtualizer() + { + return this.virtualizer; + } +} diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImage.java b/src/main/java/org/openslx/virtualization/disk/DiskImage.java new file mode 100644 index 0000000..dfb242a --- /dev/null +++ b/src/main/java/org/openslx/virtualization/disk/DiskImage.java @@ -0,0 +1,253 @@ +package org.openslx.virtualization.disk; + +import java.io.Closeable; +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; +import org.openslx.util.Util; +import org.openslx.virtualization.Version; + +/** + * 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 implements Closeable +{ + /** + * 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. + * + * @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. + */ + protected 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 Version 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 diskImagePath 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 diskImagePath ) + throws FileNotFoundException, IOException, DiskImageException + { + // Make sure this doesn't escape the scope, in case instantiation fails - we can't know when the GC + // would come along and close this file, which is problematic on Windows (blocking rename/delete) + final RandomAccessFile fileHandle = new RandomAccessFile( diskImagePath, "r" ); + + try { + if ( DiskImageQcow2.probe( fileHandle ) ) { + return new DiskImageQcow2( fileHandle ); + } else if ( DiskImageVdi.probe( fileHandle ) ) { + return new DiskImageVdi( fileHandle ); + } else if ( DiskImageVmdk.probe( fileHandle ) ) { + return new DiskImageVmdk( fileHandle ); + } + } catch ( Exception e ) { + Util.safeClose( fileHandle ); + throw e; + } + Util.safeClose( fileHandle ); + final String errorMsg = "File '" + diskImagePath.getAbsolutePath() + "' is not a valid disk image!"; + throw new DiskImageException( errorMsg ); + } + + @Override + public void close() throws IOException + { + Util.safeClose( diskImage ); + } + + @Override + protected void finalize() throws Throwable + { + close(); + } + + /** + * 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 supportedImageFormats 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/virtualization/disk/DiskImageException.java b/src/main/java/org/openslx/virtualization/disk/DiskImageException.java new file mode 100644 index 0000000..db62917 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/disk/DiskImageException.java @@ -0,0 +1,25 @@ +package org.openslx.virtualization.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/virtualization/disk/DiskImageQcow2.java b/src/main/java/org/openslx/virtualization/disk/DiskImageQcow2.java new file mode 100644 index 0000000..a6b3b73 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/disk/DiskImageQcow2.java @@ -0,0 +1,232 @@ +package org.openslx.virtualization.disk; + +import java.io.RandomAccessFile; + +import org.openslx.virtualization.Version; + +/** + * 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. + */ + DiskImageQcow2( RandomAccessFile diskImage ) + { + super( diskImage ); + } + + /** + * 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().getMajor() >= Short.valueOf( "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 Version 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 = "Invalid QCOW2 version in header found!"; + throw new DiskImageException( errorMsg ); + } + + return new Version( 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/virtualization/disk/DiskImageUtils.java b/src/main/java/org/openslx/virtualization/disk/DiskImageUtils.java new file mode 100644 index 0000000..6bcef7f --- /dev/null +++ b/src/main/java/org/openslx/virtualization/disk/DiskImageUtils.java @@ -0,0 +1,144 @@ +package org.openslx.virtualization.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 a variable number of bytes (<code>numBytes</code>) 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 <code>numBytes</code> bytes. + * @param numBytes number of bytes to read at <code>offset</code>. + * @return read bytes from the disk image file as {@link byte[]}. + * + * @throws DiskImageException unable to read bytes from the disk image file. + */ + public static byte[] readBytesAsArray( 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 values; + } +} diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImageVdi.java b/src/main/java/org/openslx/virtualization/disk/DiskImageVdi.java new file mode 100644 index 0000000..1e5b20b --- /dev/null +++ b/src/main/java/org/openslx/virtualization/disk/DiskImageVdi.java @@ -0,0 +1,112 @@ +package org.openslx.virtualization.disk; + +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.openslx.virtualization.Version; + +/** + * 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; + /** + * Just 16 null-bytes + */ + private static final byte[] ZERO_UUID = new byte[16]; + + /** + * Creates a new VDI disk image from an existing VDI image file. + * + * @param diskImage file to a VDI disk storing the image content. + */ + 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 + byte[] parentUuid = DiskImageUtils.readBytesAsArray( diskFile, 440, 16 ); + + return !Arrays.equals( ZERO_UUID, parentUuid ); + } + + @Override + public Version 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 new Version( vdiVersionMajor, vdiVersionMinor ); + } + + @Override + public String getDescription() throws DiskImageException + { + final RandomAccessFile diskFile = this.getDiskImage(); + byte[] data = DiskImageUtils.readBytesAsArray( diskFile, 84, 256 ); + // This will replace invalid chars. Maybe use CharsetDecoder and fall back to latin1 on error + return new String( data, StandardCharsets.UTF_8 ); + } + + @Override + public ImageFormat getFormat() + { + return ImageFormat.VDI; + } +} diff --git a/src/main/java/org/openslx/virtualization/disk/DiskImageVmdk.java b/src/main/java/org/openslx/virtualization/disk/DiskImageVmdk.java new file mode 100644 index 0000000..77986ef --- /dev/null +++ b/src/main/java/org/openslx/virtualization/disk/DiskImageVmdk.java @@ -0,0 +1,282 @@ +package org.openslx.virtualization.disk; + +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; + +import org.openslx.util.Util; +import org.openslx.virtualization.configuration.VirtualizationConfigurationVmwareFileFormat; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.configuration.VirtualizationConfigurationException; + +/** + * 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 VirtualizationConfigurationVmwareFileFormat 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. + */ + 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 VirtualizationConfigurationVmwareFileFormat 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 VirtualizationConfigurationVmwareFileFormat parseVmdkConfig() throws DiskImageException + { + final RandomAccessFile diskFile = this.getDiskImage(); + final VirtualizationConfigurationVmwareFileFormat 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 = new String ( DiskImageUtils.readBytesAsArray( diskFile, vmdkDescriptorOffset, + Long.valueOf( vmdkDescriptorSizeMax ).intValue() ), StandardCharsets.US_ASCII ); + + // 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 = "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 VirtualizationConfigurationVmwareFileFormat( configStr.getBytes(), vmdkDescriptorSize ); + } catch ( VirtualizationConfigurationException 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 VirtualizationConfigurationVmwareFileFormat 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 unable to obtain the VMDK's hardware version of the disk image + * format. + */ + public Version getHwVersion() throws DiskImageException + { + final VirtualizationConfigurationVmwareFileFormat vmdkConfig = this.getVmdkConfig(); + final Version 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 = new Version( 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 = new Version( 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 VirtualizationConfigurationVmwareFileFormat 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 Version getVersion() throws DiskImageException + { + final RandomAccessFile diskFile = this.getDiskImage(); + final int vmdkVersion = Integer.reverseBytes( DiskImageUtils.readInt( diskFile, 4 ) ); + + return new Version( Integer.valueOf( vmdkVersion ).shortValue() ); + } + + @Override + public String getDescription() throws DiskImageException + { + return null; + } + + @Override + public ImageFormat getFormat() + { + return ImageFormat.VMDK; + } +} diff --git a/src/main/java/org/openslx/virtualization/hardware/ConfigurationGroups.java b/src/main/java/org/openslx/virtualization/hardware/ConfigurationGroups.java new file mode 100644 index 0000000..8a0ab13 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/hardware/ConfigurationGroups.java @@ -0,0 +1,21 @@ +package org.openslx.virtualization.hardware; + +public enum ConfigurationGroups +{ + + NIC_MODEL( "E0VirtDev" ), + USB_SPEED( "maxUSBSpeed" ), + SOUND_CARD_MODEL( "sound" ), + GFX_MODEL( "graphics" ), + GFX_TYPE( "3DAcceleration" ), + HW_VERSION( "HWVersion" ); + + /** Identifier to use when looking up translation for this group. Should never change */ + public final String i18n; + + private ConfigurationGroups( String i18n ) + { + this.i18n = i18n; + } + +} diff --git a/src/main/java/org/openslx/virtualization/hardware/Ethernet.java b/src/main/java/org/openslx/virtualization/hardware/Ethernet.java new file mode 100644 index 0000000..744a71d --- /dev/null +++ b/src/main/java/org/openslx/virtualization/hardware/Ethernet.java @@ -0,0 +1,20 @@ +package org.openslx.virtualization.hardware; + +public class Ethernet +{ + + public static final String AUTO = "(default)"; + public static final String PCNET32 = "AMD PCnet32"; + public static final String E1000 = "Intel E1000 (PCI)"; + public static final String E1000E = "Intel E1000e (PCI-Express)"; + public static final String VMXNET = "VMXnet"; + public static final String VMXNET3 = "VMXnet 3"; + public static final String PCNETPCI2 = "PCnet-PCI II"; + public static final String PCNETFAST3 = "PCnet-FAST III"; + public static final String PRO1000MTD = "Intel PRO/1000 MT Desktop"; + public static final String PRO1000TS = "Intel PRO/1000 T Server"; + public static final String PRO1000MTS = "Intel PRO/1000 MT Server"; + public static final String PARAVIRT = "Paravirtualized Network"; + public static final String NONE = "No Network Card"; + +} diff --git a/src/main/java/org/openslx/virtualization/hardware/SoundCard.java b/src/main/java/org/openslx/virtualization/hardware/SoundCard.java new file mode 100644 index 0000000..e4065e4 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/hardware/SoundCard.java @@ -0,0 +1,13 @@ +package org.openslx.virtualization.hardware; + +public class SoundCard +{ + + public static final String NONE = "None"; + public static final String DEFAULT = "(default)"; + public static final String SOUND_BLASTER = "Sound Blaster 16"; + public static final String ES = "ES 1371"; + public static final String HD_AUDIO = "Intel Integrated HD Audio"; + public static final String AC = "Intel ICH Audio Codec 97"; + +} diff --git a/src/main/java/org/openslx/virtualization/hardware/Usb.java b/src/main/java/org/openslx/virtualization/hardware/Usb.java new file mode 100644 index 0000000..200bf8e --- /dev/null +++ b/src/main/java/org/openslx/virtualization/hardware/Usb.java @@ -0,0 +1,11 @@ +package org.openslx.virtualization.hardware; + +public class Usb +{ + + public static final String NONE = "None"; + public static final String USB1_1 = "USB 1.1"; + public static final String USB2_0 = "USB 2.0"; + public static final String USB3_0 = "USB 3.0"; + +} diff --git a/src/main/java/org/openslx/virtualization/hardware/VirtOptionValue.java b/src/main/java/org/openslx/virtualization/hardware/VirtOptionValue.java new file mode 100644 index 0000000..effc51f --- /dev/null +++ b/src/main/java/org/openslx/virtualization/hardware/VirtOptionValue.java @@ -0,0 +1,42 @@ +package org.openslx.virtualization.hardware; + +public abstract class VirtOptionValue +{ + + protected final String id; + + protected final String displayName; + + public VirtOptionValue( String id, String displayName ) + { + this.id = id; + this.displayName = displayName; + } + + public String getId() + { + return this.id; + } + + public String getDisplayName() + { + return this.displayName; + } + + public abstract void apply(); + + public abstract boolean isActive(); + + @Override + public boolean equals( Object obj ) + { + if ( this == obj ) + return true; + if ( obj instanceof VirtOptionValue ) { + VirtOptionValue other = ( (VirtOptionValue)obj ); + return other.id == this.id || ( other.id != null && other.id.equals( this.id ) ); + } + return false; + } + +} diff --git a/src/main/java/org/openslx/virtualization/virtualizer/Virtualizer.java b/src/main/java/org/openslx/virtualization/virtualizer/Virtualizer.java new file mode 100644 index 0000000..ff24862 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/virtualizer/Virtualizer.java @@ -0,0 +1,68 @@ +package org.openslx.virtualization.virtualizer; + +import java.util.List; + +import org.openslx.virtualization.Version; +import org.openslx.virtualization.disk.DiskImage.ImageFormat; + +/** + * Representation of a virtualization system. + * + * The virtualization system can be for example a virtual machine hypervisor (like VirtualBox), a + * container for operating systems or applications (like LXC or Docker), or a runtime environment + * (like Wine). + * + * @author Manuel Bentele + * @version 1.0 + */ +public abstract class Virtualizer +{ + /** + * Internal data representation for the virtualizer. + */ + protected final org.openslx.bwlp.thrift.iface.Virtualizer internalVirtualizer; + + /** + * Creates a new virtualizer. + * + * @param internalVirtualizer internal data representation for the new virtualizer. + */ + public Virtualizer( org.openslx.bwlp.thrift.iface.Virtualizer internalVirtualizer ) + { + this.internalVirtualizer = internalVirtualizer; + } + + /** + * Returns the identifier of the virtualizer. + * + * @return identifier of the virtualizer. + */ + public String getId() + { + return this.internalVirtualizer.getVirtId(); + } + + /** + * Returns the name of the virtualizer. + * + * @return name of the virtualizer. + */ + public String getName() + { + return this.internalVirtualizer.getVirtName(); + } + + /** + * Returns a list of supported disk image formats by the virtualizer. + * + * @return list of supported disk image formats by the virtualizer. + */ + public abstract List<ImageFormat> getSupportedImageFormats(); + + /** + * Returns a list of supported versions of the virtualizer. + * + * @return list of supported versions of the virtualizer. + */ + public abstract List<Version> getSupportedVersions(); +} diff --git a/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerDocker.java b/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerDocker.java new file mode 100644 index 0000000..06ac24e --- /dev/null +++ b/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerDocker.java @@ -0,0 +1,55 @@ +package org.openslx.virtualization.virtualizer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.openslx.thrifthelper.TConst; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.disk.DiskImage; +import org.openslx.virtualization.disk.DiskImage.ImageFormat; + +/** + * Representation of the Docker virtualizer for application containers. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class VirtualizerDocker extends Virtualizer +{ + /** + * Name of the Docker virtualizer. + */ + private static final String VIRTUALIZER_NAME = "Docker"; + + /** + * List of supported image formats by the Docker virtualizer. + */ + private static final List<DiskImage.ImageFormat> VIRTUALIZER_SUPPORTED_IMAGE_FORMATS = Collections + .unmodifiableList( Arrays.asList( ImageFormat.NONE ) ); + + /** + * List of supported versions of the Docker virtualizer. + */ + private static final List<Version> VIRTUALIZER_SUPPORTED_VERSIONS = null; + + /** + * Creates a new Docker virtualizer. + */ + public VirtualizerDocker() + { + super( new org.openslx.bwlp.thrift.iface.Virtualizer( TConst.VIRT_DOCKER, VirtualizerDocker.VIRTUALIZER_NAME ) ); + } + + @Override + public List<ImageFormat> getSupportedImageFormats() + { + return VirtualizerDocker.VIRTUALIZER_SUPPORTED_IMAGE_FORMATS; + } + + @Override + public List<Version> getSupportedVersions() + { + return VirtualizerDocker.VIRTUALIZER_SUPPORTED_VERSIONS; + } +} diff --git a/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerQemu.java b/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerQemu.java new file mode 100644 index 0000000..5d933bb --- /dev/null +++ b/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerQemu.java @@ -0,0 +1,72 @@ +package org.openslx.virtualization.virtualizer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.openslx.thrifthelper.TConst; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.disk.DiskImage; +import org.openslx.virtualization.disk.DiskImage.ImageFormat; + +/** + * Representation of the QEMU virtualizer for virtual machines. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class VirtualizerQemu extends Virtualizer +{ + /** + * Name of the QEMU virtualizer. + */ + private static final String VIRTUALIZER_NAME = "QEMU"; + + /** + * List of supported image formats by the QEMU virtualizer. + */ + private static final List<DiskImage.ImageFormat> VIRTUALIZER_SUPPORTED_IMAGE_FORMATS = Collections + .unmodifiableList( Arrays.asList( ImageFormat.QCOW2, ImageFormat.VMDK, ImageFormat.VDI ) ); + + /** + * List of supported versions of the QEMU virtualizer. + */ + private static final List<Version> VIRTUALIZER_SUPPORTED_VERSIONS = Collections.unmodifiableList( + Arrays.asList( + new Version( Short.valueOf( "2" ), Short.valueOf( "4" ), "QEMU 2.4" ), + new Version( Short.valueOf( "2" ), Short.valueOf( "5" ), "QEMU 2.5" ), + new Version( Short.valueOf( "2" ), Short.valueOf( "6" ), "QEMU 2.6" ), + new Version( Short.valueOf( "2" ), Short.valueOf( "7" ), "QEMU 2.7" ), + new Version( Short.valueOf( "2" ), Short.valueOf( "8" ), "QEMU 2.8" ), + new Version( Short.valueOf( "2" ), Short.valueOf( "9" ), "QEMU 2.9" ), + new Version( Short.valueOf( "3" ), Short.valueOf( "0" ), "QEMU 3.0" ), + new Version( Short.valueOf( "3" ), Short.valueOf( "1" ), "QEMU 3.1" ), + new Version( Short.valueOf( "4" ), Short.valueOf( "0" ), "QEMU 4.0" ), + new Version( Short.valueOf( "4" ), Short.valueOf( "1" ), "QEMU 4.1" ), + new Version( Short.valueOf( "4" ), Short.valueOf( "2" ), "QEMU 4.2" ), + new Version( Short.valueOf( "5" ), Short.valueOf( "0" ), "QEMU 5.0" ), + new Version( Short.valueOf( "5" ), Short.valueOf( "1" ), "QEMU 5.1" ), + new Version( Short.valueOf( "5" ), Short.valueOf( "2" ), "QEMU 5.2" ), + new Version( Short.valueOf( "6" ), Short.valueOf( "0" ), "QEMU 6.0" ), + new Version( Short.valueOf( "6" ), Short.valueOf( "1" ), "QEMU 6.1" ) ) ); + + /** + * Creates a new QEMU virtualizer. + */ + public VirtualizerQemu() + { + super( new org.openslx.bwlp.thrift.iface.Virtualizer( TConst.VIRT_QEMU, VirtualizerQemu.VIRTUALIZER_NAME ) ); + } + + @Override + public List<ImageFormat> getSupportedImageFormats() + { + return VirtualizerQemu.VIRTUALIZER_SUPPORTED_IMAGE_FORMATS; + } + + @Override + public List<Version> getSupportedVersions() + { + return VirtualizerQemu.VIRTUALIZER_SUPPORTED_VERSIONS; + } +} diff --git a/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerVirtualBox.java b/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerVirtualBox.java new file mode 100644 index 0000000..ebb3b3f --- /dev/null +++ b/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerVirtualBox.java @@ -0,0 +1,56 @@ +package org.openslx.virtualization.virtualizer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.openslx.thrifthelper.TConst; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.disk.DiskImage; +import org.openslx.virtualization.disk.DiskImage.ImageFormat; + +/** + * Representation of the VirtualBox virtualizer for virtual machines. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class VirtualizerVirtualBox extends Virtualizer +{ + /** + * Name of the VirtualBox virtualizer. + */ + private static final String VIRTUALIZER_NAME = "VirtualBox"; + + /** + * List of supported image formats by the VirtualBox virtualizer. + */ + private static final List<DiskImage.ImageFormat> VIRTUALIZER_SUPPORTED_IMAGE_FORMATS = Collections + .unmodifiableList( Arrays.asList( ImageFormat.VDI ) ); + + /** + * List of supported version of the VirtualBox virtualizer. + */ + private static final List<Version> VIRTUALIZER_SUPPORTED_VERSIONS = null; + + /** + * Creates a new VirtualBox virtualizer. + */ + public VirtualizerVirtualBox() + { + super( new org.openslx.bwlp.thrift.iface.Virtualizer( TConst.VIRT_VIRTUALBOX, + VirtualizerVirtualBox.VIRTUALIZER_NAME ) ); + } + + @Override + public List<ImageFormat> getSupportedImageFormats() + { + return VirtualizerVirtualBox.VIRTUALIZER_SUPPORTED_IMAGE_FORMATS; + } + + @Override + public List<Version> getSupportedVersions() + { + return VirtualizerVirtualBox.VIRTUALIZER_SUPPORTED_VERSIONS; + } +} diff --git a/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerVmware.java b/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerVmware.java new file mode 100644 index 0000000..f94ef39 --- /dev/null +++ b/src/main/java/org/openslx/virtualization/virtualizer/VirtualizerVmware.java @@ -0,0 +1,73 @@ +package org.openslx.virtualization.virtualizer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.openslx.thrifthelper.TConst; +import org.openslx.virtualization.Version; +import org.openslx.virtualization.disk.DiskImage; +import org.openslx.virtualization.disk.DiskImage.ImageFormat; + +/** + * Representation of the VMware virtualizer for virtual machines. + * + * @author Manuel Bentele + * @version 1.0 + */ +public class VirtualizerVmware extends Virtualizer +{ + /** + * Name of the VMware virtualizer. + */ + private static final String VIRTUALIZER_NAME = "VMware"; + + /** + * List of supported image formats by the VMware virtualizer. + */ + private static final List<DiskImage.ImageFormat> VIRTUALIZER_SUPPORTED_IMAGE_FORMATS = Collections + .unmodifiableList( Arrays.asList( ImageFormat.VMDK ) ); + + /** + * List of supported versions of the VMware virtualizer. + */ + private static final List<Version> VIRTUALIZER_SUPPORTED_VERSIONS = Collections.unmodifiableList( + Arrays.asList( + new Version( Short.valueOf( "03" ), "Workstation 4/5, Player 1" ), + new Version( Short.valueOf( "04" ), "Workstation 4/5, Player 1/2, Fusion 1" ), + new Version( Short.valueOf( "06" ), "Workstation 6" ), + new Version( Short.valueOf( "07" ), "Workstation 6.5/7, Player 3, Fusion 2/3" ), + new Version( Short.valueOf( "08" ), "Workstation 8, Player/Fusion 4" ), + new Version( Short.valueOf( "09" ), "Workstation 9, Player/Fusion 5" ), + new Version( Short.valueOf( "10" ), "Workstation 10, Player/Fusion 6" ), + new Version( Short.valueOf( "11" ), "Workstation 11, Player/Fusion 7" ), + new Version( Short.valueOf( "12" ), "Workstation/Player 12, Fusion 8" ), + new Version( Short.valueOf( "14" ), "Workstation/Player 14, Fusion 10" ), + new Version( Short.valueOf( "15" ), "Workstation/Player 15, Fusion 11" ), + new Version( Short.valueOf( "16" ), "Workstation/Player 15.1, Fusion 11.1" ), + new Version( Short.valueOf( "17" ), "Workstation/Player 16, Fusion 12" ), + new Version( Short.valueOf( "18" ), "Workstation/Player 16.1, Fusion 12.1" ), + new Version( Short.valueOf( "19" ), "Workstation/Player 16.2, Fusion 12.2" ), + new Version( Short.valueOf( "20" ), "Workstation/Player 17, Fusion 13" ) ) ); + + /** + * Creates a new VMware virtualizer. + */ + public VirtualizerVmware() + { + super( new org.openslx.bwlp.thrift.iface.Virtualizer( TConst.VIRT_VMWARE, VirtualizerVmware.VIRTUALIZER_NAME ) ); + } + + @Override + public List<ImageFormat> getSupportedImageFormats() + { + return VirtualizerVmware.VIRTUALIZER_SUPPORTED_IMAGE_FORMATS; + } + + @Override + public List<Version> getSupportedVersions() + { + return VirtualizerVmware.VIRTUALIZER_SUPPORTED_VERSIONS; + } + +} |