Java tutorial
/* * 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(); } }