be.iminds.aiolos.cloud.jclouds.CloudManagerImplJClouds.java Source code

Java tutorial

Introduction

Here is the source code for be.iminds.aiolos.cloud.jclouds.CloudManagerImplJClouds.java

Source

/*******************************************************************************
 * AIOLOS  - Framework for dynamic distribution of software components at runtime.
 * Copyright (C) 2014-2016  iMinds - IBCN - UGent
 *
 * This file is part of AIOLOS.
 *
 * AIOLOS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Contributors:
 *     Tim Verbelen, Steven Bohez, Elias Deconinck
 *******************************************************************************/
package be.iminds.aiolos.cloud.jclouds;

import static org.jclouds.scriptbuilder.domain.Statements.exec;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeoutException;

import org.jclouds.ContextBuilder;
import org.jclouds.compute.ComputeService;
import org.jclouds.compute.ComputeServiceContext;
import org.jclouds.compute.RunNodesException;
import org.jclouds.compute.domain.ExecResponse;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.NodeMetadataBuilder;
import org.jclouds.compute.domain.OperatingSystem;
import org.jclouds.compute.domain.Template;
import org.jclouds.compute.predicates.NodePredicates;
import org.jclouds.compute.predicates.OperatingSystemPredicates;
import org.jclouds.io.Payload;
import org.jclouds.io.payloads.ByteSourcePayload;
import org.jclouds.scriptbuilder.ScriptBuilder;
import org.jclouds.scriptbuilder.domain.Statement;
import org.jclouds.ssh.SshClient;
import org.jclouds.sshj.config.SshjSshClientModule;
import org.osgi.service.log.LogService;

import be.iminds.aiolos.cloud.AbstractCloudManager;
import be.iminds.aiolos.cloud.Activator;
import be.iminds.aiolos.cloud.api.CloudException;
import be.iminds.aiolos.cloud.api.CloudManager;
import be.iminds.aiolos.cloud.api.VMInstance;

import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import com.google.inject.Module;

/**
 * Implementation of the {@link CloudManager} interface 
 * using jClouds
 *
 */
public abstract class CloudManagerImplJClouds extends AbstractCloudManager implements Closeable {

    protected static final String PROPERTY_GROUP = "vmgroup";
    protected static final String PROPERTY_IMAGE_ID = "imageid";
    protected static final String PROPERTY_NETWORK_IDS = "network.ids";
    protected static final String PROPERTY_MIN_RAM = "image.ram.min";
    protected static final String PROPERTY_MIN_DISK = "vm.disk.min";
    protected static final String PROPERTY_MIN_VCPU = "vm.vcpu.min";
    protected static final String PROPERTY_PUBLIC_KEY = "publickey.file";
    protected static final String PROPERTY_ADMIN_ACCESS = "admin.access";
    protected static final String PROPERTY_SSH_TIMEOUT = "vm.ssh.timeout";
    protected static final String PROPERTY_PROXY = "vm.http.proxy";
    protected static final String PROPERTY_NOPROXY = "vm.http.noproxy";
    protected static final String PROPERTY_INSTALL_PKGS = "vm.install.packages";
    protected static final String PROPERTY_PORTS_INBOUND = "ports.inbound";
    protected static final String PROPERTY_PORTS_INBOUND_EXTRA = "ports.inbound.extra";

    protected static final String APT_GET_INSTALL = "sudo -E apt-get update -qq && sudo -E apt-get install -f -y -qq --force-yes";
    protected static final String YUM_INSTALL = "sudo -E yum --quiet --nogpgcheck -y install";
    protected static final String ZYPPER_INSTALL = "sudo -E zypper install";

    protected ComputeService computeService;
    protected Map<String, VMInstance> instances = Collections.synchronizedMap(new HashMap<String, VMInstance>());

    protected String vmgroup = "aiolos";

    protected File publicKeyFile;
    protected boolean adminAccess = true;
    protected String proxy;
    protected String noproxy;
    protected String installPkgs;
    protected int sshTimeout = 300;
    protected int[] inboundPorts = new int[] { 22, 80, 8080, 8081, 443, 9278 };

    // provide the correct ComputeServiceContext builder
    protected abstract ContextBuilder getContextBuilder();

    // provide the correct VM template
    protected abstract Template getTemplate();

    // configure and instantiate the computeService
    @SuppressWarnings("unchecked")
    public void configure(Dictionary<String, ?> properties) {
        proxy = (String) properties.get(PROPERTY_PROXY);
        noproxy = (String) properties.get(PROPERTY_NOPROXY);
        installPkgs = (String) properties.get(PROPERTY_INSTALL_PKGS);
        adminAccess = (properties.get(PROPERTY_ADMIN_ACCESS) == null ? adminAccess
                : Boolean.parseBoolean((String) properties.get(PROPERTY_ADMIN_ACCESS)));
        vmgroup = (properties.get(PROPERTY_GROUP) == null ? vmgroup : (String) properties.get(PROPERTY_GROUP));
        sshTimeout = (properties.get(PROPERTY_SSH_TIMEOUT) == null ? sshTimeout
                : Integer.parseInt((String) properties.get(PROPERTY_SSH_TIMEOUT)));

        String inports = (String) properties.get(PROPERTY_PORTS_INBOUND);
        if (inports != null) {
            String[] split = inports.split(",");
            inboundPorts = new int[split.length];
            for (int i = 0; i < split.length; i++) {
                inboundPorts[i] = Integer.parseInt(split[i]);
            }
        }
        String inportsExtra = (String) properties.get(PROPERTY_PORTS_INBOUND_EXTRA);
        if (inportsExtra != null) {
            String[] split = inportsExtra.split(",");
            int o = inboundPorts.length;
            inboundPorts = Arrays.copyOf(inboundPorts, o + split.length);
            for (int i = 0; i < split.length; i++) {
                inboundPorts[o + i] = Integer.parseInt(split[i]);
            }
        }

        String fileName = (String) properties.get(PROPERTY_PUBLIC_KEY);
        if (fileName != null) {
            publicKeyFile = new File(fileName);
            if (!publicKeyFile.exists())
                publicKeyFile = new File(System.getProperty("user.home") + "/.ssh/" + fileName);
            if (!publicKeyFile.exists())
                publicKeyFile = new File("/tmp/AIOLOS/tools/resources/shared.pub"); // extra for vms created by cloudmanager (hack)
        }

        this.computeService = initComputeService();
    }

    private ComputeService initComputeService() {
        // injecting a ssh implementation
        Iterable<Module> modules = null;
        try {
            Module loggingModule = (Module) Class.forName("org.jclouds.logging.slf4j.config.SLF4JLoggingModule")
                    .newInstance();
            modules = ImmutableSet.<Module>of(new SshjSshClientModule(), loggingModule);
        } catch (Exception e) {
            Activator.logger.log(LogService.LOG_DEBUG, "No slf4j logging module available");
        }

        if (modules == null)
            modules = ImmutableSet.<Module>of(new SshjSshClientModule());

        ContextBuilder builder = getContextBuilder();
        builder.modules(modules);

        Activator.logger.log(LogService.LOG_DEBUG, String.format(">> initializing %s%n", builder.getApiMetadata()));

        return builder.buildView(ComputeServiceContext.class).getComputeService();
    }

    @Override
    public void close() throws IOException {
        this.computeService.getContext().close();
    }

    private String getInstallCommand(OperatingSystem os) {
        if (os == null || OperatingSystemPredicates.supportsApt().apply(os))
            return APT_GET_INSTALL;
        else if (OperatingSystemPredicates.supportsYum().apply(os))
            return YUM_INSTALL;
        else if (OperatingSystemPredicates.supportsZypper().apply(os))
            return ZYPPER_INSTALL;
        else
            throw new IllegalArgumentException("don't know how to handle" + os.toString());
    }

    private Statement installJavaIfNotPresent(OperatingSystem os) {
        if (os == null || OperatingSystemPredicates.supportsApt().apply(os))
            return installIfNotPresent("java", "openjdk-7-jre", os);
        else if (OperatingSystemPredicates.supportsYum().apply(os)
                || OperatingSystemPredicates.supportsZypper().apply(os))
            return installIfNotPresent("java", "java-1_7_0-openjdk", os);
        else
            throw new IllegalArgumentException("don't know how to handle" + os.toString());
    }

    private Statement installIfNotPresent(String cmd, String pkgs, OperatingSystem os) {
        if (cmd != null)
            return exec(String.format("hash %s 2> /dev/null || (%s %s)", cmd, getInstallCommand(os), pkgs));
        else
            return exec(String.format("%s %s", getInstallCommand(os), pkgs));
    }

    private VMInstance provisionOSGiRuntime(String bndrun, List<String> resources, NodeMetadata node)
            throws Exception {

        int osgiPort = 9278;
        int httpPort = 8080;

        Activator.logger.log(LogService.LOG_INFO, "Waiting for node " + node.getHostname() + " to come online...");
        SshClient sshClient = computeService.getContext().utils().sshForNode().apply(NodeMetadataBuilder
                .fromNodeMetadata(node).privateAddresses(Collections.<String>emptyList()).build());
        Activator.logger.log(LogService.LOG_INFO, "Node " + node.getHostname() + " online, provision OSGi");

        synchronized (this) {
            try {
                sshClient.connect();

                Activator.logger.log(LogService.LOG_INFO, "Connected to node " + node.getHostname() + " at ip "
                        + sshClient.getHostAddress() + ", uploading necessary files...");

                // set build.bnd and copy ext folder
                sshClient.exec("mkdir -p /tmp/AIOLOS/cnf/ext");
                sshClient.exec("touch /tmp/AIOLOS/cnf/build.bnd");

                File ext = new File("../cnf/ext");
                for (String name : ext.list()) {
                    uploadFile(sshClient, "../cnf/ext/" + name, "/tmp/AIOLOS/cnf/ext/" + name);
                }

                // upload bnd
                String bnd = null;
                File bndDir = new File("../cnf/plugins/biz.aQute.bnd");
                for (String name : bndDir.list()) {
                    bnd = name;
                    break;
                }
                if (bnd == null)
                    throw new Exception("No bnd present...");

                uploadFile(sshClient, "../cnf/plugins/biz.aQute.bnd/" + bnd,
                        "/tmp/AIOLOS/cnf/plugins/biz.aQute.bnd/" + bnd);

                // upload bnd repository plugin TODO avoid hard coded url?
                uploadFile(sshClient, "../cnf/plugins/biz.aQute.repository/biz.aQute.repository-2.1.0.jar",
                        "/tmp/AIOLOS/cnf/plugins/biz.aQute.repository/biz.aQute.repository-2.1.0.jar");

                // copy all bndrun files present (can be used for cloudmanagers on the new machine)
                sshClient.exec("mkdir -p /tmp/AIOLOS/tools");
                for (File file : getFilteredBndRunFiles()) {
                    uploadFile(sshClient, file.getAbsolutePath(), "/tmp/AIOLOS/tools/" + file.getName());
                }

                // Set the rsa.ip property to the public ip of the VM
                try {
                    Properties run = new Properties();
                    run.load(new FileInputStream(new File(bndrun)));
                    String runproperties = run.getProperty("-runproperties");

                    // check for port properties
                    try {
                        Properties props = new Properties();
                        props.load(new ByteArrayInputStream(runproperties.replaceAll(",", "\n").getBytes("UTF-8")));
                        String osgiPortString = props.getProperty("rsa.port");
                        if (osgiPortString != null) {
                            osgiPort = Integer.parseInt(osgiPortString);
                        }
                        String httpPortString = props.getProperty("org.osgi.service.http.port");
                        if (httpPortString != null) {
                            httpPort = Integer.parseInt(httpPortString);
                        }
                    } catch (Exception e) {
                    }

                    String publicIP = node.getPublicAddresses().iterator().next();
                    String privateIP = node.getPrivateAddresses().iterator().next();
                    if (runproperties == null)
                        runproperties = "rsa.ip=" + publicIP + ",private.ip=" + privateIP + ",public.ip="
                                + publicIP;
                    else
                        runproperties += ",rsa.ip=" + publicIP + ",private.ip=" + privateIP + ",public.ip="
                                + publicIP;
                    run.put("-runproperties", runproperties);
                    ByteArrayOutputStream bao = new ByteArrayOutputStream();
                    run.store(bao, "bnd run configuration");
                    bao.flush();
                    ByteSource byteSource = ByteSource.wrap(bao.toByteArray());
                    Payload payload = new ByteSourcePayload(byteSource);
                    payload.getContentMetadata().setContentLength(byteSource.size());
                    bao.close();
                    sshClient.put("/tmp/AIOLOS/tools/" + bndrun, payload);
                } catch (Exception e) {
                    throw new Exception("Invalid bndrun configuration provided " + bndrun);
                }

                // copy resources
                if (resources != null) {
                    sshClient.exec("mkdir -p /tmp/AIOLOS/tools/resources");
                    for (String r : resources) {
                        File f = new File(r);
                        ByteSource byteSource = Files.asByteSource(f);
                        Payload payload = new ByteSourcePayload(byteSource);
                        payload.getContentMetadata().setContentLength(byteSource.size());
                        sshClient.put("/tmp/AIOLOS/tools/resources/" + f.getName(), payload);
                    }
                }
                if (publicKeyFile != null && publicKeyFile.exists()) {
                    try {
                        uploadFile(sshClient, publicKeyFile.getAbsolutePath(),
                                "/tmp/AIOLOS/tools/resources/shared.pub");
                    } catch (IOException e) {
                    }
                }

                // upload repositories
                // TODO don't hard code this?
                uploadDir(sshClient, "../cnf/localrepo", "/tmp/AIOLOS/cnf/localrepo");
                uploadDir(sshClient, "../cnf/releaserepo", "/tmp/AIOLOS/cnf/releaserepo");
                uploadDir(sshClient, "../tools/generated/workspacerepo",
                        "/tmp/AIOLOS/tools/generated/workspacerepo");

                // start 
                ByteSource byteSource = ByteSource
                        .wrap(buildBndRunScript(bnd, bndrun, node.getOperatingSystem()).getBytes());
                Payload payload = new ByteSourcePayload(byteSource);
                payload.getContentMetadata().setContentLength(byteSource.size());
                sshClient.put("/tmp/AIOLOS/init.sh", payload);
                sshClient.exec("chmod a+x /tmp/AIOLOS/init.sh");

                Activator.logger.log(LogService.LOG_INFO, "Start OSGi on node " + node.getHostname() + "  ...");
                ExecResponse response = sshClient
                        .exec("nohup /tmp/AIOLOS/init.sh >> /tmp/AIOLOS/init.log 2>&1 < /dev/null");
                if (response.getExitStatus() != 0) {
                    Activator.logger.log(LogService.LOG_ERROR,
                            "Execution of script failed: " + response.getError());
                }
            } finally {
                if (sshClient != null)
                    sshClient.disconnect();
            }
        }
        return new VMInstance(node.getId(), node.getUri(), node.getName(), node.getGroup(), node.getImageId(),
                node.getStatus().name(), node.getHostname(), node.getPrivateAddresses(), node.getPublicAddresses(),
                node.getHardware().getName(), osgiPort, httpPort);
    }

    private void uploadDir(SshClient ssh, String src, String dest) throws IOException {
        File dir = new File(src);
        if (dir.isDirectory()) {
            ssh.exec("mkdir -p " + dest);
            for (File f : dir.listFiles()) {
                if (f.isDirectory()) {
                    uploadDir(ssh, f.getAbsolutePath(), dest + "/" + f.getName());
                } else {
                    uploadFile(ssh, f.getAbsolutePath(), dest + "/" + f.getName());
                }
            }
        }
    }

    private void uploadFile(SshClient ssh, String src, String dest) throws IOException {
        String dir = dest.substring(0, dest.lastIndexOf("/"));
        ssh.exec("mkdir -p " + dir);
        File srcFile = new File(src);
        ByteSource byteSource = Files.asByteSource(srcFile);
        Payload payload = new ByteSourcePayload(byteSource);
        payload.getContentMetadata().setContentLength(byteSource.size());
        ssh.put(dest, payload);
    }

    private String buildBndRunScript(String bnd, String bndrun, OperatingSystem os) {
        ScriptBuilder scriptBuilder = new ScriptBuilder();

        if (proxy != null)
            scriptBuilder.addStatement(exec(String.format("export http_proxy=%s", proxy)))
                    .addStatement(exec(String.format("export https_proxy=%s", proxy)))
                    .addStatement(exec(String.format("export ftp_proxy=%s", proxy)));
        if (noproxy != null)
            scriptBuilder.addStatement(exec(String.format("export no_proxy='%s'", noproxy)));

        if (installPkgs != null)
            scriptBuilder.addStatement(installIfNotPresent(null, installPkgs, os));

        // construct bnd run statement
        String run = "java -jar";
        if (proxy != null) {
            try {
                URI proxyURL = new URI(proxy);
                run += " -Dhttp.proxyHost=" + proxyURL.getHost();
                run += " -Dhttp.proxyPort=" + proxyURL.getPort();
            } catch (URISyntaxException e) {
            }
        }
        if (noproxy != null) {
            run += " -Dhttp.nonProxyHosts=\"" + noproxy + "\"";
        }
        // TODO also handle http.proxyUser and http.proxyPassword
        run += " /tmp/AIOLOS/cnf/plugins/biz.aQute.bnd/" + bnd + " " + bndrun
                + " >> /tmp/AIOLOS/aiolos.log 2>&1 < /dev/null &";

        // Check curl and java. Install packages if if missing.
        scriptBuilder.addStatement(installIfNotPresent("curl", "curl", os))
                .addStatement(installJavaIfNotPresent(os))

                // run AIOLOS
                .addStatement(exec("cd /tmp/AIOLOS/tools")) //change work directory
                .addStatement(exec(run));

        return scriptBuilder.render(org.jclouds.scriptbuilder.domain.OsFamily.UNIX);
    }

    @Override
    public VMInstance startVM(String bndrun, List<String> resources) throws CloudException, TimeoutException {
        List<VMInstance> vms = startVMs(bndrun, resources, 1);
        if (vms.size() > 0)
            return vms.iterator().next();
        else
            throw new CloudException("Failed to start instance");
    }

    @Override
    public List<VMInstance> startVMs(String bndrun, List<String> resources, int count)
            throws CloudException, TimeoutException {
        List<VMInstance> vms = new ArrayList<VMInstance>();
        int retry = 0;
        int noVMs = 0;
        Activator.logger.log(LogService.LOG_INFO,
                String.format("Start VM(s), this could take a while (Admin Install = %s).", adminAccess));
        long t1 = System.currentTimeMillis();
        while (noVMs < count && retry < 3) {
            Set<? extends NodeMetadata> nodes = null;
            try {
                nodes = this.computeService.createNodesInGroup(vmgroup, count - noVMs, getTemplate());
            } catch (RunNodesException e) {
                Activator.logger.log(LogService.LOG_ERROR, "createNodesInGroup failed", e);
                nodes = e.getSuccessfulNodes();
                Activator.logger.log(LogService.LOG_ERROR,
                        String.format("%d of %d nodes successfully started.", nodes.size() + noVMs, count));
            }
            for (NodeMetadata node : nodes) {
                try {
                    Activator.logger.log(LogService.LOG_INFO,
                            String.format("Node %s with credentials %s. Provision OSGi runtime on node %s.",
                                    node.getHostname(), node.getCredentials(), node.getHostname()));
                    VMInstance vm = provisionOSGiRuntime(bndrun, resources, node);
                    noVMs++;
                    vms.add(vm);
                    instances.put(vm.getId(), vm);
                } catch (Exception e) {
                    Activator.logger.log(LogService.LOG_INFO, e.getLocalizedMessage()
                            + "\nProvisioning OSGi on node " + node.getHostname() + " failed, retry.");
                    this.computeService.destroyNode(node.getId());
                }
            }
            retry++;
        }
        long t2 = System.currentTimeMillis();
        Activator.logger.log(LogService.LOG_INFO,
                "Started " + noVMs + " VM(s), which took " + (t2 - t1) + " ms. (" + (retry - 1) + " retries)");

        return vms;
    }

    @Override
    public VMInstance stopVM(String id) {
        VMInstance vm = instances.remove(id);
        if (vm != null) {
            this.computeService.destroyNodesMatching(NodePredicates.<NodeMetadata>withIds(id));
        }
        return vm;
    }

    @Override
    public List<VMInstance> stopVMs(String[] ids) {
        List<VMInstance> vms = new ArrayList<VMInstance>();
        for (String id : ids) {
            VMInstance vm = stopVM(id);
            if (vm != null)
                vms.add(vm);
        }
        return vms;
    }

    @Override
    public List<VMInstance> stopVMs() {
        // this will stop all VMs running ... is used to clean up cloud vms ...
        // TODO is probably not the correct behavior?
        this.computeService.destroyNodesMatching(NodePredicates.RUNNING);
        List<VMInstance> vms = new ArrayList<VMInstance>(instances.values());
        instances.clear();
        return vms;
    }

    @Override
    public List<VMInstance> listVMs() {
        List<VMInstance> vms = new ArrayList<VMInstance>(instances.values());
        return vms;
    }
}