be.ac.ua.comp.scarletnebula.core.Server.java Source code

Java tutorial

Introduction

Here is the source code for be.ac.ua.comp.scarletnebula.core.Server.java

Source

/*
 * Copyright (C) 2011  Ives van der Flaas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package be.ac.ua.comp.scarletnebula.core;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dasein.cloud.CloudException;
import org.dasein.cloud.InternalException;
import org.dasein.cloud.compute.Architecture;
import org.dasein.cloud.compute.Platform;
import org.dasein.cloud.compute.VirtualMachine;
import org.dasein.cloud.compute.VmState;

import be.ac.ua.comp.scarletnebula.gui.GraphPanelCache;
import be.ac.ua.comp.scarletnebula.misc.SearchHelper;
import be.ac.ua.comp.scarletnebula.misc.Utils;

import com.jcraft.jsch.UserInfo;

/**
 * Representation of a Server that is somehow connected to a CloudProvider.
 * 
 * @author ives
 * 
 */
public class Server {
    private static Log log = LogFactory.getLog(Server.class);

    private VirtualMachine serverImpl;
    private final Collection<ServerChangedObserver> serverChangedObservers = new ArrayList<ServerChangedObserver>();
    private final CloudProvider provider;
    private String friendlyName;
    private String keypair;
    private String sshLogin;
    private String sshPassword;
    private String vncPassword;
    private Collection<String> tags;
    private ServerStatisticsManager serverStatisticsManager;
    private String statisticsCommand;
    private final String preferredDatastream;
    private boolean useSshPassword;
    private boolean noConnection = false;

    /**
     * Constructor.
     * 
     * @param server
     *            Dasein server implementation
     * @param inputProvider
     *            CloudProvider the server is running on.
     * @param inputKeypair
     *            Keypair the server is using.
     * @param inputFriendlyName
     *            Friendly name the server is known by.
     * @param tags
     *            Tags the server is tagged with.
     * @param useSshPassword
     *            True if server uses a password to connect to SSH
     * @param sshLogin
     *            Login the server can be ssh'ed to.
     * @param sshPassword
     *            Password the server can be reached at.
     * @param vncPassword
     *            Password the server can be vnc'ed with.
     * @param statisticsCommand
     *            Statistics command that will be executed to retrieve
     *            statistics.
     * @param preferredDatastream
     *            Preferred datastream to display in baregraph.
     */
    public Server(final VirtualMachine server, final CloudProvider inputProvider, final String inputKeypair,
            final String inputFriendlyName, final Collection<String> tags, final boolean useSshPassword,
            final String sshLogin, final String sshPassword, final String vncPassword,
            final String statisticsCommand, final String preferredDatastream) {
        provider = inputProvider;
        keypair = inputKeypair;
        serverImpl = server;
        this.useSshPassword = useSshPassword;
        this.sshLogin = (sshLogin != null ? sshLogin : "");
        this.sshPassword = (sshPassword != null ? sshPassword : "");
        this.vncPassword = (vncPassword != null ? vncPassword : "");
        this.statisticsCommand = statisticsCommand;
        this.preferredDatastream = preferredDatastream;
        this.tags = tags;
        setFriendlyName(inputFriendlyName);
    }

    /**
     * @return A ServerStatisticsManager or null if one cannot be created.
     */
    public ServerStatisticsManager getServerStatistics() {
        if (sshWillFail() || noConnection || getStatisticsCommand() == null || getStatisticsCommand().isEmpty()) {
            // Do nothing -- return null
            serverStatisticsManager = null;
        } else if (serverStatisticsManager == null) {
            serverStatisticsManager = getNewServerStatistics(true);
        }
        return serverStatisticsManager;
    }

    private ServerStatisticsManager getNewServerStatistics(final boolean retry) {
        serverStatisticsManager = new ServerStatisticsManager(this);
        serverStatisticsManager.addNoStatisticsListener(new ServerStatisticsManager.NoStatisticsListener() {

            @Override
            public void connectionFailed(final ServerStatisticsManager manager) {
                log.info("Being notified of server statistics failure.");
                noConnection = true;
                serverChanged();

                if (!retry)
                    return;

                log.info("Starting timer that will retry statistics in 120 sec");
                final java.util.Timer twoMin = new java.util.Timer();
                twoMin.schedule(new java.util.TimerTask() {
                    @Override
                    public void run() {
                        if (getServerStatistics() != null
                                && getServerStatistics().getAvailableDatastreams().size() > 0)
                            return;

                        log.info("Retrying statistics (after 120 sec)");
                        noConnection = false;
                        serverStatisticsManager = getNewServerStatistics(false);
                        serverChanged();
                        cancel();
                    }
                }, (120 * 1000));

                log.info("Starting timer that will retry statistics in 30 sec");
                final java.util.Timer thirtySecs = new java.util.Timer();
                thirtySecs.schedule(new java.util.TimerTask() {
                    @Override
                    public void run() {
                        log.info("Retrying statistics (after 30 sec)");
                        noConnection = false;
                        serverStatisticsManager = getNewServerStatistics(false);
                        serverChanged();
                        cancel();
                    }
                }, (30 * 1000));

            }
        });
        return serverStatisticsManager;
    }

    /**
     * Does basic sanity checks to see if it's even remotely possible to
     * establish an SSH connection.
     * 
     * @return True if the SSH connection will fail, false if it might succeed.
     */
    public boolean sshWillFail() {
        return getStatus() != VmState.RUNNING || sshLogin.isEmpty()
                || ((useSshPassword && sshPassword.isEmpty() || (!useSshPassword && keypair.isEmpty())));
    }

    /**
     * @param ui
     *            *sigh* General suckage of the SSH package I'm using. Don't
     *            even look. It's that bad. I tried others but although this
     *            one's code and architecture sucks majorly, it's the only SSH
     *            client I could find for java that had at least some terminal
     *            emulation support (and worked).
     * @return A new CommandConnection to this server
     * @throws Exception
     * @throws FileNotFoundException
     */
    public CommandConnection newCommandConnection(final UserInfo ui) throws Exception {
        SSHCommandConnection rv = null;
        String address;

        if (serverImpl.getPublicDnsAddress() != null) {
            address = serverImpl.getPublicDnsAddress();
        } else if (serverImpl.getPublicIpAddresses().length >= 1) {
            address = serverImpl.getPublicIpAddresses()[0];
        } else {
            log.warn("Cannot make SSH connection -- no address to connect to.");
            return null;
        }

        if (usesSshPassword()) {
            rv = SSHCommandConnection.newConnectionWithPassword(address, sshLogin, sshPassword, ui);
        } else {
            rv = SSHCommandConnection.newConnectionWithKey(address, sshLogin,
                    KeyManager.getKeyFilename(provider.getName(), keypair), ui);
        }

        return rv;
    }

    /**
     * Factory method that will returns a server object. If we've seen this
     * server before and there's saved data for him, this saved data will be
     * loaded.
     * 
     * @param server
     *            Dasein server implementation.
     * @param provider
     *            The provider this server is running with .
     * @return The server.
     */
    static Server load(final VirtualMachine server, final CloudProvider provider) {
        final String propertiesfilename = getSaveFilename(provider, server);
        final Properties props = new Properties();
        try {
            props.load(new FileInputStream(propertiesfilename));
        } catch (final Exception e) {
            log.error("Save file for server " + server + " not found");
        }
        final String keypair = props.getProperty("keypair");
        final String friendlyName = props.getProperty("friendlyName");
        final boolean useSshPassword = Boolean.valueOf(props.getProperty("useSshPassword", "false"));
        final String sshLogin = props.getProperty("sshLogin");
        final String sshPassword = props.getProperty("sshPassword");
        final String vncPassword = props.getProperty("vncPassword");
        final String statisticsCommand = props.getProperty("statisticsCommand");
        final String tagString = props.getProperty("tags");
        final String preferredDatastream = props.getProperty("preferredDatastream");

        return new Server(server, // dasein server implementation
                provider, // cloud provider
                keypair, // ssh keypair chosen
                friendlyName, // the servers friendly name
                Arrays.asList(tagString.split(",")), // tags given to
                // the server
                useSshPassword, // true if an ssh password instead of keypair
                // is used
                sshLogin, // Login for SSH'ing
                sshPassword, // Password for ssh'ing (if any)
                vncPassword, // Password for VNC'ing
                statisticsCommand, // Command to be executed for statistics
                preferredDatastream); // Datastream to show in small server
    }

    /**
     * @return True if this server should be connected to with a password, false
     *         if it should be connected to with a key.
     */
    public boolean usesSshPassword() {
        return useSshPassword || keypair == null;
    }

    /**
     * @return A string containing the server's preferred datastream.
     */
    public String getPreferredDatastream() {
        return preferredDatastream;
    }

    /**
     * Returns the filename (with directory) a new instance with name
     * "instanceName" for CloudProvider "provider" should get.
     * 
     * @param provider
     *            CloudProvider this server is with
     * @param server
     *            Dasein server implementation
     * @return Filename for this server.
     */
    static String getSaveFilename(final CloudProvider provider, final VirtualMachine server) {
        return provider.getSaveFileDir() + server.getProviderVirtualMachineId();
    }

    /**
     * Saves this server to its savefile.
     */
    public void store() {
        // Write key to file
        final String dir = provider.getSaveFileDir();
        final File dirFile = new File(dir);

        // Check if the key dir already exists
        if (!dirFile.exists()) {
            // If it does not exist, create the directory
            if (!dirFile.mkdirs()) {
                log.fatal("Cannot make server directory!");
                return;
            }
        }

        // Write properties file.
        try {
            final Properties properties = new Properties();
            properties.setProperty("friendlyName", getFriendlyName());
            properties.setProperty("keypair", keypair);
            properties.setProperty("providerClassName", provider.getUnderlyingClassname());
            properties.setProperty("sshLogin", sshLogin);
            properties.setProperty("statisticsCommand", statisticsCommand);
            properties.setProperty("sshPassword", sshPassword);
            properties.setProperty("vncPassword", vncPassword);
            properties.setProperty("useSshPassword", new Boolean(useSshPassword).toString());
            properties.setProperty("tags", Utils.implode(new ArrayList<String>(tags), ","));
            properties.setProperty("preferredDatastream", preferredDatastream);

            final FileOutputStream outputstream = new FileOutputStream(getSaveFilename(provider, serverImpl));
            properties.store(outputstream, null);
            outputstream.close();
        } catch (final Exception e) {
            log.error("Properties for " + this + " could not be stored.", e);
        }

    }

    /**
     * Returns the server implementation's cloud specific (unfriendly) name.
     * 
     * @return The server's unfriendly (CloudProvider specific) name.
     */
    public String getUnfriendlyName() {
        return serverImpl.getProviderVirtualMachineId();
    }

    /**
     * @return The server's architecture.
     */
    public Architecture getArchitecture() {
        return serverImpl.getArchitecture();
    }

    /**
     * @return The server's platform (OS).
     */
    public Platform getPlatform() {
        return serverImpl.getPlatform();
    }

    @Override
    public String toString() {
        final String rv = serverImpl.getProviderVirtualMachineId() + " (" + serverImpl.getCurrentState() + ") @ "
                + serverImpl.getPublicDnsAddress();
        return rv;
    }

    /**
     * @return A public DNS address for this server, null if none is available.
     */
    public String getPublicDnsAddress() {
        return serverImpl.getPublicDnsAddress();
    }

    /**
     * @return A list of public IP address for this server, empty array if none
     *         is available.
     */
    public String[] getPublicIpAddresses() {
        final String[] addresses = serverImpl.getPublicIpAddresses();

        if (addresses == null) {
            return new String[0];
        } else {
            return addresses;
        }
    }

    /**
     * @return The statistics command for this server.
     */
    public String getStatisticsCommand() {
        return statisticsCommand;
    }

    /**
     * Sets the friendly name for this server
     * 
     * @param friendlyName
     */
    final public void setFriendlyName(final String friendlyName) {
        this.friendlyName = friendlyName;
    }

    /**
     * @return This server's friendly name
     */
    final public String getFriendlyName() {
        return friendlyName;
    }

    /**
     * Terminates this server
     * 
     * @throws InternalException
     * @throws CloudException
     */
    public void terminate() throws InternalException, CloudException {
        provider.terminateServer(getUnfriendlyName());
    }

    /**
     * @return This server's status
     */
    public VmState getStatus() {
        return serverImpl.getCurrentState();
    }

    /**
     * Pulls current information from the cloud this server is located in. This
     * just replaces the the dasein server object stored in this server. I don't
     * see any better way to do this in the Dasein API.
     * 
     * @throws CloudException
     * @throws InternalException
     * @throws ServerDisappearedException
     */
    public void refresh() throws InternalException, CloudException, ServerDisappearedException {
        final VirtualMachine refreshedServer = provider.getServerImpl(getUnfriendlyName());

        // If the sever disappeared in the mean while, throw an Exception
        if (refreshedServer == null) {
            throw new ServerDisappearedException(this);
        }

        serverImpl = refreshedServer;

        // if(!serverImpl.equals(refreshedServer))
        serverChanged();
    }

    /**
     * @return The CloudProvider this server was started on
     */
    public CloudProvider getCloud() {
        return provider;
    }

    /**
     * @return This server's size string
     */
    public String getSize() {
        return serverImpl.getProduct().getName();
    }

    /**
     * @return This server's image id
     */
    public String getImage() {
        return serverImpl.getProviderMachineImageId();
    }

    /**
     * Pauses this server if it can be paused
     * 
     * @throws InternalException
     * @throws CloudException
     */
    public void pause() throws InternalException, CloudException {
        if (serverImpl.isPausable()) {
            provider.pause(this);
        }

        return;
    }

    /**
     * @see CloudProvider
     * @throws InternalException
     * @throws CloudException
     */
    public void resume() throws InternalException, CloudException {
        provider.resume(this);

        return;
    }

    /**
     * Reboots this server
     * 
     * @throws CloudException
     * @throws InternalException
     */
    public void reboot() throws CloudException, InternalException {
        provider.reboot(this);
    }

    /**
     * Add a ServerChangedObserver that will be notified when the server
     * changes.
     * 
     * @param sco
     *            The observer that will be notified when the server changes.
     */
    public void addServerChangedObserver(final ServerChangedObserver sco) {
        serverChangedObservers.add(sco);
    }

    /**
     * Removes an observer from the list of observers
     * 
     * @param sco
     *            The observer that will be deleted.
     */
    public void removeServerChangedObserver(final ServerChangedObserver sco) {
        serverChangedObservers.remove(sco);
    }

    /**
     * Notify all observers the server has changed.
     */
    public void serverChanged() {
        for (final ServerChangedObserver obs : serverChangedObservers) {
            obs.serverChanged(this);
        }
    }

    /**
     * Unlinks this server. This will remove the save file for this server and
     * will remove it from the list of linked servers the cloudprovider
     * maintains. This obviously does not affect the server's running state in
     * any way.
     */
    public void unlink() {
        getCloud().unlink(this);
        stopConnections();
    }

    /**
     * Checks with the CloudManager if a server by this name is linked in *any*
     * CloudProvider
     * 
     * @param name
     * @return
     */
    public static boolean exists(final String name) {
        return CloudManager.get().serverExists(name);
    }

    public Collection<String> getTags() {
        return tags;
    }

    public boolean match(final Collection<String> filterTerms) {
        for (String token : filterTerms) {
            final boolean negated = token.startsWith("-");

            if (negated) {
                token = token.substring(1);
            }

            if (token.length() == 0) {
                continue;
            }

            final int colonPosition = token.indexOf(':');

            // Prefix-based search term
            if (colonPosition > 0) {
                final String prefix = token.substring(0, colonPosition);
                final String term = token.substring(colonPosition + 1);

                if ("tag".equals(prefix)) {
                    return SearchHelper.matchTags(term, getTags(), negated);
                } else if ("name".equals(prefix) || "inname".equals(prefix)) {
                    return SearchHelper.matchName(term, getFriendlyName(), negated);
                } else if ("size".equals(prefix)) {
                    return SearchHelper.matchSize(term, getSize(), negated);
                } else if ("status".equals(prefix) || "state".equals(prefix)) {
                    return SearchHelper.matchStatus(term, getStatus(), negated);
                } else if ("provider".equals(prefix) || "inprovider".equals(prefix)) {
                    return SearchHelper.matchCloudProvider(term, getCloud().getName(), negated);
                } else {
                    return false;
                }
            } else {
                return SearchHelper.matchName(token.toLowerCase(), getFriendlyName().toLowerCase(), negated)
                        || SearchHelper.matchTags(token, getTags(), negated)
                        || SearchHelper.matchSize(token.toLowerCase(), getSize(), negated)
                        || SearchHelper.matchStatus(token, getStatus(), negated)
                        || SearchHelper.matchCloudProvider(token, getCloud().getName(), negated);
            }

        }
        return true;
    }

    /**
     * The method you should call when you want to keep refreshing until Server
     * "server" has state "state".
     * 
     * TODO Keep some kind of a map for each state, which can be checked whem
     * manually refreshing. Suppose a server is refreshed and it's state is
     * PAUSED. The user can then resume. Later, the server's timer that checks
     * server state will fire, and the state will show as RUNNING. The timer
     * will keep on firing until the count is high enough and it gives up, which
     * sucks. Therefore, when manually refreshing a server S, all timers for
     * that server should be checked. If there's a timer waiting for S's current
     * state, that timer should be cancelled.
     * 
     * @param server
     * @param state
     */
    public void refreshUntilServerHasState(final VmState state) {
        refreshUntilServerHasState(state, 1);
    }

    private void refreshUntilServerHasState(final VmState state, final int attempt) {
        if (getStatus() == state || attempt > 20) {
            return;
        }

        try {
            refresh();
        } catch (final ServerDisappearedException e) {
            getCloud().unlink(this);
            return;
        } catch (final Exception e) {
            log.error("Something happened while refreshing server " + this, e);
        }

        if (getStatus() == state) {
            return;
        }

        // If the server's state still isn't the one we want it to be, try
        // again, but only after waiting
        // a logarithmic amount of time.
        final double wait = 15.0 * (Math.log10(attempt) + 1.0);

        final java.util.Timer timer = new java.util.Timer();
        timer.schedule(new java.util.TimerTask() {
            @Override
            public void run() {
                refreshUntilServerHasState(state, attempt + 1);
                log.debug("Refreshing state for server " + getFriendlyName()
                        + " because timer fired, waiting for state " + state.toString());
                cancel();
            }
        }, (long) (wait * 1000));

    }

    public void setTags(final Collection<String> newTags) {
        tags = newTags;
        serverChanged();
    }

    public String getKeypair() {
        return keypair;
    }

    public void assureKeypairLogin(final String username, final String keyname) {
        sshLogin = username;
        keypair = (keyname != null ? keyname : "");
        useSshPassword = false;

        resetConnections();
        serverChanged();
    }

    private void stopConnections() {
        if (serverStatisticsManager != null) {
            serverStatisticsManager.stop();
        }
        serverStatisticsManager = null;
    }

    private void resetConnections() {
        GraphPanelCache.get().clearBareServerCache(this);

        stopConnections();
        noConnection = false;
    }

    public void assurePasswordLogin(final String username, final String password) {
        sshLogin = username;
        sshPassword = password;
        useSshPassword = true;

        resetConnections();
        serverChanged();
    }

    public void setStatisticsCommand(final String command) {
        statisticsCommand = command;

        final ServerStatisticsManager manager = getServerStatistics();
        if (manager != null) {
            manager.reset();
        }

        serverChanged();
    }

    public String getSshUsername() {
        return sshLogin;
    }

    public String getSshPassword() {
        return sshPassword;
    }

    public String getVNCPassword() {
        return vncPassword;
    }

    public void setVNCPassword(final String password) {
        vncPassword = password;
    }

    public boolean isPausable() {
        return serverImpl.isPausable();
    }

    public boolean isRebootable() {
        return serverImpl.isRebootable();
    }
}