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

Java tutorial

Introduction

Here is the source code for be.ac.ua.comp.scarletnebula.core.CloudProvider.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.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import org.apache.commons.codec.binary.Base64;
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.ComputeServices;
import org.dasein.cloud.compute.MachineImage;
import org.dasein.cloud.compute.Platform;
import org.dasein.cloud.compute.VirtualMachine;
import org.dasein.cloud.compute.VirtualMachineProduct;
import org.dasein.cloud.compute.VirtualMachineSupport;
import org.dasein.cloud.identity.ShellKeySupport;
import org.dasein.cloud.network.Firewall;
import org.dasein.cloud.network.FirewallRule;
import org.dasein.cloud.network.FirewallSupport;
import org.dasein.cloud.network.NetworkServices;
import org.dasein.cloud.network.Protocol;

import be.ac.ua.comp.scarletnebula.misc.Utils;

/**
 * Class representing a cloud provider in Scarlet Nebula. This will contain a
 * Dasein cloudprovider object but also implement other stuff like making a
 * cloudprovider specific configuration.
 * 
 * @author ives
 */
public class CloudProvider {
    private static Log log = LogFactory.getLog(CloudProvider.class);

    private org.dasein.cloud.CloudProvider providerImpl;
    private ComputeServices computeServices = null;
    private VirtualMachineSupport virtualMachineServices = null;
    private FirewallSupport firewallSupport = null;

    private String name;
    private String providerClassName;
    private String apiSecret;
    private String apiKey;
    private String endpoint;
    private String defaultKeypair;

    private final Collection<Server> servers = new ArrayList<Server>();
    private Collection<MachineImage> favoriteImages = new LinkedList<MachineImage>();
    private final Collection<ServerLinkUnlinkObserver> linkUnlinkObservers = new ArrayList<ServerLinkUnlinkObserver>();

    /**
     * Constructor for constructing a cloudprovider from file.
     * 
     * Don't forget to update the other ctor.
     * 
     * @param name
     *            Name of the provider. Used to search for a savefile.
     */
    public CloudProvider(final String name) {
        load(name);

        connect();

    }

    /**
     * Sets up the provider implementation.
     */
    private void connect() {
        try {
            providerImpl = (org.dasein.cloud.CloudProvider) Class.forName(providerClassName).newInstance();
        } catch (final Exception e) {
            log.error("Underlying cloud provider class " + providerClassName + " failed creation.", e);
        }

        providerImpl.connect(getCurrentContext());

        computeServices = providerImpl.getComputeServices();
        if (computeServices == null) {
            log.error(providerImpl.getCloudName() + " does not support compute instances.");
            return;
        }
        virtualMachineServices = computeServices.getVirtualMachineSupport();
        if (computeServices == null) {
            log.error(providerImpl.getCloudName() + " does not support Virtual Machines.");
            return;
        }

        final NetworkServices networkServices = providerImpl.getNetworkServices();
        if (networkServices == null) {
            log.warn(providerImpl.getCloudName() + " does not support network services.");
        } else {
            firewallSupport = networkServices.getFirewallSupport();
            if (firewallSupport == null) {
                log.warn(providerImpl.getCloudName() + " does not support firewalls.");
                return;
            }
        }
    }

    /**
     * Call when a new server is linked and observers need to be notified.
     * 
     * @param srv
     *            The server that's linked.
     */
    private void notifyObserversBecauseServerLinked(final Server srv) {

        for (final ServerLinkUnlinkObserver obs : linkUnlinkObservers) {
            obs.serverLinked(this, srv);
            log.info("Cloudprovider is updating his observers");
        }
    }

    /**
     * Call when a new server is unlinked and observers need to be notified.
     * 
     * @param srv
     *            The server that is unlinked.
     */
    private void notifyObserversBecauseServerUnlinked(final Server srv) {
        for (final ServerLinkUnlinkObserver obs : linkUnlinkObservers) {
            obs.serverUnlinked(this, srv);
        }
    }

    /**
     * Loads a CloudProvider from file based on his name.
     * 
     * @param name
     *            Name of the provider to load.
     */
    private void load(final String name) {
        Properties properties = null;

        try {
            properties = new Properties();
            properties.load(new FileInputStream(getConfigfileName(name)));
        } catch (final IOException e) {
            log.error("IOException while loading provider from file.", e);
        }

        this.name = name;
        this.providerClassName = properties.getProperty("class");
        this.apiKey = properties.getProperty("apikey");
        this.apiSecret = properties.getProperty("apisecret");
        this.endpoint = properties.getProperty("endpoint");
        this.defaultKeypair = properties.getProperty("defaultKeypair", "");
        this.favoriteImages = deserialiseFavoriteImages(properties.getProperty("favoriteImages", null));

    }

    /**
     * Basic constructor for use when *not* loading from file. A provider made
     * in this fashion will probably not be linked with the cloudManager.
     * 
     * Don't forget to update the other constructor!
     * 
     * @param name
     *            CloudProvider's name
     * @param classname
     *            Name of the dasein class this provider is based on
     * @param endpoint
     *            Endpoint to use
     * @param apikey
     *            ID or username to login
     * @param apisecret
     *            Key or password to login
     * @param defaultKeypair
     *            Default keypair to use
     */
    public CloudProvider(final String name, final String classname, final String endpoint, final String apikey,
            final String apisecret, final String defaultKeypair) {
        this.name = name;
        this.providerClassName = classname;
        this.apiKey = apikey;
        this.apiSecret = apisecret;
        this.endpoint = endpoint;
        this.defaultKeypair = defaultKeypair;
        connect();
    }

    /**
     * Loads a server (from file!) and returns it.
     * 
     * @param unfriendlyName
     *            Provider implementation's name for the server to load.
     * @return The server that was just started.
     * @throws InternalException
     * @throws CloudException
     * @throws IOException
     */
    public Server loadServer(final String unfriendlyName) throws InternalException, CloudException, IOException {
        log.warn("Getting for name " + unfriendlyName);
        final VirtualMachine server = getServerImpl(unfriendlyName);

        if (server == null) {
            return null;
        }

        final Server rv = Server.load(server, this);
        return rv;

    }

    /**
     * Loads all servers that have a saved representation.
     * 
     * @return Collection of all the servers that were loaded.
     * @throws InternalException
     * @throws CloudException
     * @throws IOException
     */
    public Collection<Server> loadLinkedServers() throws InternalException, CloudException, IOException {
        final File dir = new File(getSaveFileDir());

        final String[] files = dir.list();

        if (files == null) {
            return servers;
        }

        for (final String file : files) {
            final Server server = loadServer(file);

            // If the server cannot be made it was deleted and the file
            // referencing it
            // should also be removed.
            if (server == null) {
                log.warn("Server from file " + file + " cannot be loaded. Discarting save file.");
                deleteServerSaveFile(file);
            } else {
                linkServer(server);
            }
        }

        return servers;
    }

    /**
     * Links a server to this cloudprovider.
     * 
     * @param server
     *            The server to link.
     */
    public void linkServer(final Server server) {
        notifyObserversBecauseServerLinked(server);
        servers.add(server);
    }

    /**
     * Delete's the server whose CloudProvider specific name is
     * "unfriendlyName".
     * 
     * @param unfriendlyName
     *            Unfriendly name of the server whose savefile to remove.
     */
    private void deleteServerSaveFile(final String unfriendlyName) {
        final File toBeRemoved = new File(getSaveFileDir() + unfriendlyName);
        final boolean result = toBeRemoved.delete();

        if (!result) {
            log.error("Could not remove savefile for server " + unfriendlyName);
        }
    }

    /**
     * Creates a new key with name "keyname", changes the default key if it has
     * to and stores the server to file.
     * 
     * @param keyname
     *            Name of the key to create
     * @throws InternalException
     * @throws CloudException
     */
    public void createKey(final String keyname, final boolean makeDefault)
            throws InternalException, CloudException {
        final ShellKeySupport shellKeySupport = providerImpl.getIdentityServices().getShellKeySupport();

        KeyManager.addKey(getName(), keyname, shellKeySupport.createKeypair(keyname));

        if (makeDefault) {
            setDefaultKeypair(keyname);
            store();
        }
    }

    /**
     * Adds a new firewall rule to an already existing firewall on this
     * provider.
     * 
     * @param firewall
     *            The firewall to add the rule to
     * @param beginPort
     *            The lower edge of the port range that should be allowed
     * @param endPort
     *            The upper edge of the port range that should be allowed
     * @param protocol
     *            The protocol, UDP or TCP which should be allowed
     * @param cidr
     *            The IP CIDR that should be allowed.
     * @throws CloudException
     * @throws InternalException
     */
    public void addFirewallRule(final Firewall firewall, final int beginPort, final int endPort,
            final Protocol protocol, final String cidr) throws CloudException, InternalException {
        firewallSupport.authorize(firewall.getProviderFirewallId(), cidr, protocol, beginPort, endPort);
    }

    /**
     * Deletes a firewall rule on the cloudprovider's side.
     * 
     * @param firewall
     *            The firewall whose rule to revoke.
     * @param beginPort
     *            The lower end of the port range.
     * @param endPort
     *            The higher end of the port range.
     * @param protocol
     *            The protocol.
     * @param cidr
     *            Ip address descriptor
     * @throws CloudException
     * @throws InternalException
     */
    public void deleteFirewallRule(final Firewall firewall, final int beginPort, final int endPort,
            final Protocol protocol, final String cidr) throws CloudException, InternalException {
        firewallSupport.revoke(firewall.getProviderFirewallId(), cidr, protocol, beginPort, endPort);
    }

    /**
     * @return All firewalls for this CloudProvider.
     * @throws InternalException
     * @throws CloudException
     */
    public Collection<Firewall> getFirewalls() throws InternalException, CloudException {
        final FirewallSupport fws = providerImpl.getNetworkServices().getFirewallSupport();

        if (fws == null) {
            return new ArrayList<Firewall>();
        }

        return fws.list();
    }

    /**
     * Returns a collection of all rules for a certain firewall.
     * 
     * @param firewall
     *            The firewall whose rules to return.
     * @return A collection of all rules for a certain firewall.
     * @throws InternalException
     * @throws CloudException
     */
    public Collection<FirewallRule> getFirewallRules(final String firewall)
            throws InternalException, CloudException {
        return firewallSupport.getRules(firewall);
    }

    /**
     * Lists all servers the underlying Cloud Provider manages, but that are
     * *not* by listLinkedServers() i.e. the ones that aren't managed by Scarlet
     * Nebula at this moment.
     * 
     * @return The servers that exist in the cloud but are not linked with this
     *         provider.
     * @throws CloudException
     * @throws InternalException
     */
    public ArrayList<Server> listUnlinkedServers() throws InternalException, CloudException {
        final ArrayList<Server> rv = new ArrayList<Server>();
        // List all servers
        for (final VirtualMachine testServer : virtualMachineServices.listVirtualMachines()) {
            // For each server, check if this server is already linked. Do
            // this based on his unfriendly id
            boolean found = false;
            for (final Iterator<Server> linkedServerIterator = servers.iterator(); linkedServerIterator.hasNext()
                    && !found;) {
                if (linkedServerIterator.next().getUnfriendlyName().equals(testServer.getName())) {
                    found = true;
                }
            }

            if (!found) {
                final List<String> daseinTags = new ArrayList<String>();
                for (final String key : testServer.getTags().keySet()) {
                    daseinTags.add(key + ":" + testServer.getTags().get(key));
                }

                rv.add(new Server(testServer, // dasein server
                        this, // cloud provider
                        "", // keypair
                        testServer.getName() + " (" + getName() + ")", // friendly
                        // name
                        daseinTags, // tags
                        true, // use password
                        nullToEmpty(testServer.getRootUser()), // root user
                        nullToEmpty(testServer.getRootPassword()), // root
                        // password
                        nullToEmpty(testServer.getRootPassword()), // VNC passwd
                        getDefaultStatisticsCommand(), // statistics command
                        "CPU"));
            }
        }

        return rv;
    }

    /**
     * Utility method that returns the string if it is not null, or the empty
     * string if the string is null.
     * 
     * @param input
     *            The input string.
     * @return the input string if it is not null, or the empty string if the
     *         string is null.
     */
    private String nullToEmpty(final String input) {
        return (input == null) ? "" : input;
    }

    /**
     * Returns all servers that are currently being managed by this
     * CloudProvider.
     * 
     * @return A collection of all servers that are linked with this
     *         cloudprovider.
     */
    public Collection<Server> listLinkedServers() {
        return servers;
    }

    /**
     * Terminates the server with unfriendlyName "unfriendlyName". This method
     * will and should only be called by Server.terminate().
     * 
     * @param unfriendlyName
     *            Unfriendly name of the server to terminate.
     * @throws InternalException
     * @throws CloudException
     */
    public void terminateServer(final String unfriendlyName) throws InternalException, CloudException {
        virtualMachineServices.terminate(unfriendlyName);
    }

    /**
     * Starts a new server.
     * 
     * @param serverName
     *            Friendly name of the server to start
     * @param product
     *            Product (size ed) for the new server
     * @param image
     *            Image to use for the new server
     * @param tags
     *            Tags this server will have
     * @param keypairOrPassword
     *            Keypair or password to use for the server
     * @param firewalls
     *            The firewalls that will protect this server
     * @return The server that's either started or starting.
     * @throws InternalException
     * @throws CloudException
     */
    public Server startServer(final String serverName, final VirtualMachineProduct product,
            final MachineImage image, final Collection<String> tags, final String keypairOrPassword,
            final Collection<String> firewalls) throws InternalException, CloudException {
        final String dataCenterId = "eu-west-1b";
        final String vlan = null;

        final Collection<org.dasein.cloud.Tag> daseinTags = new ArrayList<org.dasein.cloud.Tag>();
        int i = 0;

        for (final String tag : tags) {
            daseinTags.add(new org.dasein.cloud.Tag("tag" + (++i), tag));
        }

        final VirtualMachine daseinServer = virtualMachineServices.launch(image.getProviderMachineImageId(), // image id
                product, // vm product (size)
                dataCenterId, // data center id
                serverName, // friendly server name
                "", // server description
                keypairOrPassword, // keypair
                vlan, // vlan
                false, false, (firewalls != null ? firewalls.toArray(new String[0]) : null), // firewalls
                daseinTags.toArray(new org.dasein.cloud.Tag[0])); // tags

        String rootUser = daseinServer.getRootUser();
        if (rootUser == null || rootUser.isEmpty()) {
            if (image.getName().toLowerCase().contains("ubuntu")
                    || image.getDescription().toLowerCase().contains("ubuntu")) {
                rootUser = "ubuntu";
            } else if (image.getPlatform() != Platform.WINDOWS) {
                rootUser = "root";
            } else {
                rootUser = "";
            }
        }

        final Server server = new Server(daseinServer, // Dasein server
                // implementation
                this, // Cloud provider
                keypairOrPassword, // Keypair used
                serverName, // Server's friendly name
                tags, // Tags this server was given
                !supportsSSHKeys(), // server uses password to SSH
                rootUser, // SSH login
                daseinServer.getRootPassword(), // SSH Password
                keypairOrPassword, // VNC password
                getDefaultStatisticsCommand(), // Statistics
                // command
                "CPU"); // preferred datastream

        server.store();
        linkServer(server);
        return server;
    }

    /**
     * @return The name of the default keypair for this provider. If no default
     *         keypair is set (or the default keypair is invalid) but there are
     *         keys available, one of these will be chosen.
     */
    public String getDefaultKeypair() {
        // If no default keypair is entered in the cloudprovider, but there is a
        // key for this provider, make that the default.

        // Also do something similar when the default key is not in the set of
        // keys.
        final Collection<String> keys = KeyManager.getKeyNames(getName());

        if (defaultKeypair.isEmpty() || !keys.contains(defaultKeypair)) {
            if (!keys.isEmpty()) {
                final String newDefaultKey = keys.iterator().next();
                setDefaultKeypair(newDefaultKey);
                store();
            }
        }

        return defaultKeypair;
    }

    /**
     * Sets the default keypair (without saving) on the condition that a keypair
     * by this name exists.
     * 
     * @param newDefaultKeypair
     *            The name of the new keypair that will become default.
     */
    public void setDefaultKeypair(final String newDefaultKeypair) {
        if (KeyManager.getKeyNames(getName()).contains(newDefaultKeypair)) {
            defaultKeypair = newDefaultKeypair;
        }
    }

    /**
     * @return The default statistics command for this cloudprovider.
     */
    public String getDefaultStatisticsCommand() {
        final StringBuffer sb = new StringBuffer();

        try {
            BufferedReader br;

            br = new BufferedReader(new FileReader(Utils.internalFile("statistics.sh")));

            String nextLine = "";
            while ((nextLine = br.readLine()) != null) {
                sb.append(nextLine);
                sb.append("\n");
            }
        } catch (final Exception e) {
            log.error("Could not read default statistics command, continuing with empty string", e);
        }
        return sb.toString();
    }

    /**
     * Returns a VirtualMachineProduct representation of a certain unfriendly
     * name for that product.
     * 
     * @param name
     *            Unfriendly name for the vm product.
     * @return The product with name name or null if no such product exists.
     */
    public VirtualMachineProduct getVMProductWithName(final String name) {
        try {
            final Iterable<VirtualMachineProduct> products = virtualMachineServices.listProducts(Architecture.I32);

            for (final VirtualMachineProduct product : products) {
                if (name == product.getName()) {
                    return product;
                }
            }
            return null;
        } catch (final Exception e) {
            log.error("Error while getting VM Product with name", e);
        }
        return null;
    }

    /**
     * Returns the endpoint this CloudProvider uses.
     * 
     * @return The endpoint this CloudProvider uses.
     */
    private String getEndpoint() {
        return endpoint;
    }

    /**
     * Closes this CloudProvider (call it before the program ends).
     */
    void close() {
        providerImpl.close();
    }

    /**
     * @return A dasein provider context for this cloudprovider.
     */
    private org.dasein.cloud.ProviderContext getCurrentContext() {
        final org.dasein.cloud.ProviderContext context = new org.dasein.cloud.ProviderContext();

        context.setAccountNumber("000000000000");
        context.setAccessPublic(apiKey.getBytes());
        context.setAccessPrivate(apiSecret.getBytes());
        context.setEndpoint(getEndpoint());

        return context;
    }

    /**
     * Returns a collection of instance sizes that are possible.
     * 
     * @param architecture
     *            The architecture for which all instance sizes should be
     *            listed.
     * @return An iterator over all possible instance sizes for that
     *         architecture.
     */
    public Iterable<VirtualMachineProduct> getPossibleInstanceSizes(final Architecture architecture) {
        Iterable<VirtualMachineProduct> products = null;
        try {
            products = virtualMachineServices.listProducts(architecture);
        } catch (final Exception e) {
            log.error("Error while querying the list of VM products for architecture " + architecture, e);
        }

        return products;
    }

    /**
     * Returns the underlying class' name...
     * 
     * @return The name of the underlying dasein class.
     */
    public String getUnderlyingClassname() {
        return providerClassName;
    }

    /**
     * 
     * @param unfriendlyName
     *            The unfriendly name for which a dasein server is needed.
     * @return The dasein server implementation from an unfriendly name.
     * @throws InternalException
     * @throws CloudException
     */
    public VirtualMachine getServerImpl(final String unfriendlyName) throws InternalException, CloudException {
        return virtualMachineServices.getVirtualMachine(unfriendlyName);
    }

    /**
     * 
     * @param platform
     *            Platform to filter on
     * @param architecture
     *            Architecture to filter on.
     * @return An iterator over all possible machine images for a certain
     *         architecture & platform.
     */
    public Iterable<MachineImage> getAvailableMachineImages(final Platform platform,
            final Architecture architecture) {
        Iterable<MachineImage> images = null;
        try {
            images = computeServices.getImageSupport().searchMachineImages("", platform, architecture);
        } catch (final Exception e) {
            log.error("Error while searching for a machine image with platform " + platform + " and architecture "
                    + architecture);
        }
        return images;
    }

    /**
     * Pauses the server in parameter. This method is only supposed to be called
     * by Server.pause()
     * 
     * @param server
     *            The server to pause.
     * @throws InternalException
     * @throws CloudException
     */
    public void pause(final Server server) throws InternalException, CloudException {
        virtualMachineServices.pause(server.getUnfriendlyName());
    }

    /**
     * Resumes a previously paused server.
     * 
     * @param server
     *            The server to resume.
     * @throws InternalException
     * @throws CloudException
     */
    public void resume(final Server server) throws InternalException, CloudException {
        log.info("Resuming " + server);
        virtualMachineServices.boot(server.getUnfriendlyName());
    }

    /**
     * Reboots the server in parameter. This method is only supposed to be
     * called by Server.reboot()
     * 
     * @param server
     *            The server to reboot.
     * @throws CloudException
     * @throws InternalException
     */
    public void reboot(final Server server) throws CloudException, InternalException {
        virtualMachineServices.reboot(server.getUnfriendlyName());
    }

    /**
     * Unlinks the instance in parameter. After this call, this instance will no
     * longer be linked to this CloudProvider
     * 
     * @param selectedServer
     *            The server to unlink.
     */
    public void unlink(final Server selectedServer) {
        servers.remove(selectedServer);
        deleteServerSaveFile(selectedServer.getUnfriendlyName());
        notifyObserversBecauseServerUnlinked(selectedServer);
    }

    /**
     * Returns true if this cloudprovider owns an instance named "friendlyName"
     * 
     * @param friendlyName
     *            The name of the instance
     * @return True if a linked server with name "friendlyName" exists,
     *         otherwise false
     */
    public boolean hasServer(final String friendlyName) {
        for (final Server s : servers) {
            if (s.getFriendlyName().equals(friendlyName)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the identifier that uniquely identifies this CloudProvider
     * instance.
     * 
     * @return Friendly name for this cloudprovider.
     */
    public String getName() {
        return name;
    }

    /**
     * @return The directory (ending in "/") all servers with this CloudProvider
     *         should be saved in.
     */
    String getSaveFileDir() {
        return "servers/" + getName() + "/";
    }

    /**
     * Saves the file describing this CloudProvider. If a cloudprovider by the
     * name "providername" already exists, the savefile will be overwritten.
     * 
     */
    public void store() {
        // First assure the providers/ directory exists
        final File dir = new File("providers");

        if (!dir.exists() || !dir.isDirectory()) {
            if (!dir.mkdir()) {
                log.error("Could not create providers/ directory.");
            }
        }

        // Now write to the file properties file
        final Properties prop = new Properties();

        prop.setProperty("class", providerClassName);
        prop.setProperty("apikey", apiKey);
        prop.setProperty("apisecret", apiSecret);
        prop.setProperty("endpoint", endpoint);
        prop.setProperty("defaultKeypair", defaultKeypair);
        prop.setProperty("favoriteImages", getSerialisedFavoriteImages());

        try {
            prop.store(new FileOutputStream(getConfigfileName(name)), null);
        } catch (final Exception e) {
            log.error("Properties file describing cloud provider could not be created.");
        }
    }

    /**
     * @return A serialised base64-drepresentation of the favorite images. Ready
     *         to be debase64d and deserialised.
     */
    private String getSerialisedFavoriteImages() {
        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream oout = null;
        String rv = "";
        try {
            oout = new ObjectOutputStream(outputStream);
            log.info("size of serialised images collection = " + favoriteImages.size());
            oout.writeObject(favoriteImages);
            final byte[] bytes = outputStream.toByteArray();

            final byte[] encodedBytes = Base64.encodeBase64(bytes, false);
            rv = new String(encodedBytes);
        } catch (final IOException e) {
            log.error("Exception while serialising favorite images.", e);
        } finally {
            try {
                oout.close();
                outputStream.close();
            } catch (final IOException ignore) {
            }
        }

        return rv;
    }

    /**
     * Converts a string containing base64 encoded favorite image data back to a
     * collection of machine images. The suppress warnings needs to be here
     * because of type erasure -- the runtime has no clue what's inside the
     * collection...
     * 
     * @param input
     *            String to be deserialised
     * @return The collection of machine images contained in the input string.
     */
    @SuppressWarnings("unchecked")
    private Collection<MachineImage> deserialiseFavoriteImages(final String input) {
        if (input == null || input.isEmpty()) {
            return new ArrayList<MachineImage>();
        }

        final byte[] decodedBytes = Base64.decodeBase64(input.getBytes());

        if (decodedBytes != null) {
            try {
                final ObjectInputStream objectIn = new ObjectInputStream(new ByteArrayInputStream(decodedBytes));

                final Object readObject = objectIn.readObject();

                return (Collection<MachineImage>) readObject;

            } catch (final Exception e) {
                log.error("Exception while deserialising favorite images.", e);
            }
        }
        return new ArrayList<MachineImage>();
    }

    /**
     * @param providername
     *            The friendly name for the provider whose file we're asking
     *            for.
     * @return The filename of this provider's savefile (or at least where it
     *         would be if one existed).
     */
    public static String getConfigfileName(final String providername) {
        return "providers/" + providername + ".properties";
    }

    /**
     * @return Returns a collection of all providernames that exist on disk.
     */
    static Collection<String> getProviderNames() {
        final File dir = new File("providers");

        if (!dir.exists() || !dir.isDirectory()) {
            return new ArrayList<String>();
        }

        final Collection<String> files = Arrays.asList(dir.list());
        final Collection<String> rv = new ArrayList<String>(files.size());

        for (final String file : files) {
            rv.add(file.replaceFirst(".properties$", ""));
        }
        return rv;
    }

    /**
     * @param server
     *            The server to check.
     * @return True if this server is linked, false otherwise.
     */
    public boolean isLinked(final Server server) {
        // Not using servers.contains because this seems to use Server.equals
        // which isn't implemented...
        for (final Server linkedServer : servers) {
            if (server == linkedServer) {
                return true;
            }
        }

        return false;
    }

    /**
     * Adds an observer that will be notified when a new server is linked or
     * unlinked.
     * 
     * @param obs
     *            The observer that will be notified.
     */
    public void addServerLinkUnlinkObserver(final ServerLinkUnlinkObserver obs) {
        linkUnlinkObservers.add(obs);
    }

    /**
     * Tests the current CloudProvider for connectivity.
     * 
     * @return True if a connection can be made, false otherwise.
     */
    public boolean test() {
        return providerImpl.testContext() != null;
    }

    /**
     * @return A collection of all keys that exist on the provider side but not
     *         locally for this provider.
     * @throws InternalException
     * @throws CloudException
     */
    public Collection<String> getUnknownKeys() throws InternalException, CloudException {
        final ShellKeySupport shellKeySupport = providerImpl.getIdentityServices().getShellKeySupport();

        final Collection<String> keys = shellKeySupport.list();
        final Collection<String> knownKeys = KeyManager.getKeyNames(getName());
        keys.removeAll(knownKeys);
        return keys;
    }

    /**
     * @return True if this provider supports SSH keys, false otherwise.
     */
    public boolean supportsSSHKeys() {
        return providerImpl.getIdentityServices() != null
                && providerImpl.getIdentityServices().getShellKeySupport() != null;
    }

    /**
     * 
     * @param checkKeyname
     *            The name of the key to check.
     * @return Returns true if a key by the name given exists on the
     *         cloudprovider's side.
     */
    public boolean linkedUnlinkedKeyExists(final String checkKeyname) {
        final ShellKeySupport shellKeySupport = providerImpl.getIdentityServices().getShellKeySupport();

        boolean exists = false;
        try {
            exists = shellKeySupport.list().contains(checkKeyname);
        } catch (final Exception e) {
            log.error("Could not list keys.", e);
        }
        return exists;
    }

    /**
     * Imports a key to the Scarlet Nebula system, and optionally makes it
     * default and saves the server.
     * 
     * @param keyname
     *            The key's name
     * @param keyFile
     *            The file to copy from
     * @param makeDefault
     *            True if it should become default, false otherwise.
     */
    public void importKey(final String keyname, final File keyFile, final boolean makeDefault) {
        KeyManager.addKey(getName(), keyname, keyFile);

        if (makeDefault) {
            setDefaultKeypair(keyname);
            store();
        }
    }

    /**
     * Deletes and SSH key, both locally and, if it exists, remotely.
     * 
     * @param key
     *            The key to delete remotely & locally.
     * @throws CloudException
     * @throws InternalException
     */
    public void deleteKey(final String key) throws InternalException, CloudException {
        KeyManager.deleteKey(getName(), key);

        final ShellKeySupport shellKeySupport = providerImpl.getIdentityServices().getShellKeySupport();
        shellKeySupport.deleteKeypair(key);
    }

    /**
     * Creates a firewall in the cloudprovider and returns a pseudo-firewall
     * object. This object only contains a user friendly name and a provider id.
     * 
     * @param firewallName
     *            User friendly name of the firewall
     * @return A pseudo-firewall object. This object only contains a user
     *         friendly name and a provider id.
     * @throws InternalException
     * @throws CloudException
     */
    public Firewall createFirewall(final String firewallName) throws InternalException, CloudException {
        final String id = firewallSupport.create(firewallName, firewallName);
        final Firewall firewall = new Firewall();
        firewall.setName(firewallName);
        firewall.setProviderFirewallId(id);
        return firewall;
    }

    /**
     * Deletes a firewall remotely.
     * 
     * @param firewall
     *            The firewall to delete (if possible)
     * @throws InternalException
     * @throws CloudException
     */
    public void deleteFirewall(final Firewall firewall) throws InternalException, CloudException {
        firewallSupport.delete(firewall.getProviderFirewallId());
    }

    /**
     * @return True if this cloudprivder supports firewalls, false otherwise.
     */
    public boolean supportsFirewalls() {
        return firewallSupport != null;
    }

    /**
     * Adds an image to the list of images that are favorite for this provider
     * (without saving).
     * 
     * @param image
     *            The image to add.
     */
    public void addToFavorites(final MachineImage image) {
        if (!imageInFavorites(image)) {
            favoriteImages.add(image);
        }
    }

    /**
     * @return A copy of all favorite images for this cloudprovider.
     */
    public Collection<MachineImage> getFavoriteImages() {
        return new ArrayList<MachineImage>(favoriteImages);
    }

    /**
     * Removes a MachineImage from the list of favorites (without storing).
     * 
     * @param image
     *            The image to remove from favorites.
     */
    public void removeFromFavorites(final MachineImage image) {
        favoriteImages.remove(image);
    }

    /**
     * Method that works around a nullexception when comparing MachineImages.
     * 
     * @param testImage
     *            Image that is or is not in the favoriteImages
     * @return True if testImage is a favorite image for this cloudprovider,
     *         false otherwise.
     */
    public boolean imageInFavorites(final MachineImage testImage) {
        boolean found = false;

        for (final MachineImage image : favoriteImages) {
            if (image.getProviderMachineImageId() == testImage.getProviderMachineImageId()) {
                found = true;
                break;
            }
        }

        return found;
    }
}