package betsy.bpel.virtual.host.virtualbox; import java.util.Objects; import java.util.Set; import betsy.bpel.virtual.host.VirtualBoxException; import betsy.bpel.virtual.host.VirtualBoxMachine; import betsy.bpel.virtual.host.exceptions.vm.VBoxExceptionCode; import betsy.common.timeouts.timeout.TimeoutRepository; import org.apache.log4j.Logger; import org.virtualbox_4_2.IConsole; import org.virtualbox_4_2.IMachine; import org.virtualbox_4_2.IProgress; import org.virtualbox_4_2.ISession; import org.virtualbox_4_2.ISnapshot; import org.virtualbox_4_2.LockType; import org.virtualbox_4_2.MachineState; import org.virtualbox_4_2.VBoxException; import org.virtualbox_4_2.VirtualBoxManager; /** * The {@link VirtualBoxMachineImpl} represents a virtual machine running on * VirtualBox. It can be started, stopped and snapshots may be created of its * current state. * * @author Cedric Roeck * @version 1.0 */ public class VirtualBoxMachineImpl implements VirtualBoxMachine { private static final Logger log = Logger.getLogger(VirtualBoxMachineImpl.class); private final IMachine machine; private final VirtualBoxManager vbManager; private final ISession session; /** * if true, the {@link VirtualBoxMachineImpl} will be running in the * background and no GUI window will appear. */ private boolean headless; public VirtualBoxMachineImpl(VirtualBoxManager vbManager, IMachine machine) { this.machine = Objects.requireNonNull(machine); this.vbManager = Objects.requireNonNull(vbManager); this.session = vbManager.getSessionObject(); } /** * Start the {@link IMachine}. VirtualBox currently supports three different * states. Two of them are showing the GUI, one is saving the resources, * hides the GUI and therefore is 'headless'.<br/> * <br> * The {@link IMachine} should not be running before. */ public void start() { log.trace("Starting VM"); if (isActive()) { log.warn("Can't start VM, is already active. Aborting."); return; } startVirtualMachine(); } private void startVirtualMachine() { IProgress startProgress; if (headless) { startProgress = machine.launchVMProcess(session, "headless", null); } else { startProgress = machine.launchVMProcess(session, "gui", null); } waitForCompletion(startProgress, TimeoutRepository.getTimeout("VirtualBoxMachineImpl.startVirtualMachine").getTimeoutInMs()); } private void waitForCompletion(IProgress progress, int timeout) { if (!progress.getCompleted()) { progress.waitForCompletion(timeout); } } /** * Stopping the {@link IMachine} and causing the VirtualBox's VM to be in * 'PoweredOff' state.<br> * If the VM is still running after the timeout, for instance of the stop * caused a lock, it will be killed using VirtualBox's emergency kill * switch.<br> * <br> * Should only be used of the {@link IMachine} is in a running state. */ public void stop() { log.trace("Stopping VM"); try { if (isRunning()) { waitForCompletion(session.getConsole().powerDown(), TimeoutRepository.getTimeout("VirtualBoxMachineImpl.stop").getTimeoutInMs()); } } catch (VBoxException e) { if (VBoxExceptionCode.valueOf(e).equals(VBoxExceptionCode.VBOX_E_INVALID_VM_STATE)) { log.warn("Could not power off, VM was in invalid state:", e); } else { log.fatal("Unexpected VBoxException while stopping VM:", e); } } finally { // verify if stopped, else kill killAndCleanup(); } log.trace("finished stop method"); } /** * Save the current running state of the {@link IMachine}. After saving the * state the {@link IMachine} won't be running anymore.<br> * It can be used as an alternative to {@link #stop()}. Nevertheless is also takes * significantly longer. <br> * <br> * Should only be used of the {@link IMachine} is in a running state. */ public void saveState() { log.trace("Saving VM state"); try { if (isRunning()) { waitForCompletion(session.getConsole().saveState(), TimeoutRepository.getTimeout("VirtualBoxMachineImpl.saveState").getTimeoutInMs()); } } catch (VBoxException e) { if (VBoxExceptionCode.valueOf(e).equals(VBoxExceptionCode.VBOX_E_INVALID_VM_STATE)) { log.warn("Could not save VM state, was in invalid state:", e); // ignore } else { throw e; // unknown } } finally { killAndCleanup(); } } private void killAndCleanup() { // verify if stopped, else kill if (isActive()) { kill(); } try { session.unlockMachine(); } catch (VBoxException exception) { // ignore if was not locked } } /** * Killing the {@link IMachine} using the 'emergencystop'.<br> * Stops the VM in nearly every situation but might also cause data loss or * corrupted states. <br> * <br> * Should only be used of the {@link IMachine} is in a running state. */ public void kill() { log.debug("killing machine"); machine.launchVMProcess(session, "emergencystop", null); try { session.unlockMachine(); } catch (VBoxException exception) { // ignore if was not locked log.trace("Unlocking session after kill failed:", exception); } } /** * Check whether the {@link IMachine} is in an active running state. * Therefore the State must be either Running, Paused or Stuck (after an * severe error).<br> * If the VM is running it can be stopped. * * @return true if the VM is running, paused or stuck */ public boolean isRunning() { try { MachineState state = this.machine.getState(); if (state.equals(MachineState.Running)) { return true; } if (state.equals(MachineState.Paused)) { return true; } if (state.equals(MachineState.Stuck)) { return true; } } catch (VBoxException exception) { // in error cases the state can't be always read. log.error("Couldn't determine running state:", exception); } return false; } /** * Check whether the {@link IMachine} is in a active state.<br> * Even if this method is very similar to {@link #isRunning()}, the * difference is that every mid-state such as saving, powering-off, etc. is * still an active state, even if the VM is currently not responding to the * User's input. * * @return true if the VM is active */ public boolean isActive() { try { return !isInactive(); } catch (VBoxException exception) { // in error cases the state can't be always read. log.warn("Couldn't determine active state:", exception); } return false; } private boolean isInactive() { MachineState state = this.machine.getState(); log.debug("Current VM State: " + state.toString()); return MachineState.Aborted.equals(state) || MachineState.PoweredOff.equals(state) || MachineState.Saved.equals(state); } /** * Resetting the {@link IMachine} to the latest {@link ISnapshot}.<br> * <br> * Requires the {@link IMachine} to have at least one {@link ISnapshot}. */ public void restore() { ISession subSession = null; try { log.debug("Resetting " + machine.getName() + " to latest snapshot"); subSession = vbManager.getSessionObject(); machine.lockMachine(subSession, LockType.Write); ISnapshot snapshot = machine.getCurrentSnapshot(); if (snapshot == null) { throw new IllegalStateException("Can't reset the VM to the " + "latest snapshot if the VM does not have any " + "snapshot."); } IConsole console = subSession.getConsole(); waitForCompletion(console.restoreSnapshot(snapshot), TimeoutRepository.getTimeout("VirtualBoxMachineImpl.restore").getTimeoutInMs()); log.trace("State after restoration:" + machine.getState()); log.trace("restored VM to latest snapshot with name [" + snapshot.getName() + "]"); } finally { log.trace("trigger finally block after reset snapshot"); if (subSession != null) { try { subSession.unlockMachine(); } catch (VBoxException exception) { // ignore if was not locked log.warn("Unlocking subSession after restoration failed:", exception); } } } } @Override public void setHeadlessMode(boolean headless) { this.headless = headless; } @Override public void applyPortForwarding(Set<Integer> forwardingPorts) throws VirtualBoxException { new PortForwardingConfigurator(this.machine).applyPortForwarding(forwardingPorts); } /** * Taking a snapshot of this {@link betsy.bpel.virtual.host.virtualbox.VirtualBoxMachineImpl} based on the current * state.<br> * A bug in VirtualBox (https://www.virtualbox.org/ticket/9255) forces us to * pause the VM before taking the snapshot. Afterwards we will always resume * the VM, even if it was already paused before. * * @param name the name of the snapshot to be created * @param desc the description of the snapshot to be created */ public void takeSnapshot(final String name, final String desc) { log.debug("Taking VM snapshot"); IConsole console = session.getConsole(); log.debug("Pausing VM before taking a snapshot"); console.pause(); waitForCompletion(console.takeSnapshot(name, desc), TimeoutRepository.getTimeout("VirtualBoxMachineImpl.takeSnapshot").getTimeoutInMs()); // before resuming make sure the snapshot has been saved while (this.machine.getState().equals(MachineState.Saving)) { try { Thread.sleep(TimeoutRepository.getTimeout("VirtualBoxMachineImpl.takeSnapshot.sleep").getTimeoutInMs()); } catch (InterruptedException e) { // ignore } } log.debug("Resuming VM after taking a snapshot"); console.resume(); } /** * Check whether the VM has at least one {@link ISnapshot} that is marked as * 'online' and therefore contains an already started system. * * @return true if VM contains at least one 'online' snapshot */ public boolean hasRunningSnapshot() { ISnapshot snapshot = machine.getCurrentSnapshot(); if (snapshot != null) { // online snapshot? return snapshot.getOnline(); } return false; } }