brooklyn.entity.nosql.couchbase.CouchbaseNodeSshDriver.java Source code

Java tutorial

Introduction

Here is the source code for brooklyn.entity.nosql.couchbase.CouchbaseNodeSshDriver.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package brooklyn.entity.nosql.couchbase;

import static brooklyn.util.ssh.BashCommands.INSTALL_CURL;
import static brooklyn.util.ssh.BashCommands.alternatives;
import static brooklyn.util.ssh.BashCommands.chainGroup;
import static brooklyn.util.ssh.BashCommands.ok;
import static brooklyn.util.ssh.BashCommands.sudo;
import static java.lang.String.format;

import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.http.HttpHeaders;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.entity.ContentType;

import brooklyn.entity.Entity;
import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver;
import brooklyn.entity.basic.Attributes;
import brooklyn.entity.basic.Entities;
import brooklyn.entity.basic.ServiceStateLogic;
import brooklyn.entity.drivers.downloads.BasicDownloadRequirement;
import brooklyn.entity.drivers.downloads.DownloadProducerFromUrlAttribute;
import brooklyn.entity.software.SshEffectorTasks;
import brooklyn.event.basic.DependentConfiguration;
import brooklyn.event.feed.http.HttpValueFunctions;
import brooklyn.event.feed.http.JsonFunctions;
import brooklyn.location.OsDetails;
import brooklyn.location.basic.SshMachineLocation;
import brooklyn.util.guava.Functionals;
import brooklyn.util.http.HttpTool;
import brooklyn.util.http.HttpToolResponse;
import brooklyn.util.net.Urls;
import brooklyn.util.repeat.Repeater;
import brooklyn.util.ssh.BashCommands;
import brooklyn.util.task.DynamicTasks;
import brooklyn.util.task.TaskTags;
import brooklyn.util.task.Tasks;
import brooklyn.util.text.NaturalOrderComparator;
import brooklyn.util.text.StringEscapes.BashStringEscapes;
import brooklyn.util.time.Duration;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;

public class CouchbaseNodeSshDriver extends AbstractSoftwareProcessSshDriver implements CouchbaseNodeDriver {

    public CouchbaseNodeSshDriver(final CouchbaseNodeImpl entity, final SshMachineLocation machine) {
        super(entity, machine);
    }

    public static String couchbaseCli(String cmd) {
        return "/opt/couchbase/bin/couchbase-cli " + cmd + " ";
    }

    @Override
    public void preInstall() {
        resolver = Entities.newDownloader(this);
        setExpandedInstallDir(getInstallDir());
    }

    @Override
    public void install() {
        //for reference https://github.com/urbandecoder/couchbase/blob/master/recipes/server.rb
        //installation instructions (http://docs.couchbase.com/couchbase-manual-2.5/cb-install/#preparing-to-install)

        List<String> urls = resolver.getTargets();
        String saveAs = resolver.getFilename();

        OsDetails osDetails = getMachine().getMachineDetails().getOsDetails();

        if (osDetails.isLinux()) {
            List<String> commands = installLinux(urls, saveAs);
            //FIXME installation return error but the server is up and running.
            newScript(INSTALLING).body.append(commands).execute();
        } else {
            Tasks.markInessential();
            throw new IllegalStateException(
                    "Unsupported OS for installing Couchbase. Will continue but may fail later.");
        }
    }

    private List<String> installLinux(List<String> urls, String saveAs) {

        log.info("Installing " + getEntity() + " using couchbase-server-{} {}", getCommunityOrEnterprise(),
                getVersion());

        String apt = chainGroup("export DEBIAN_FRONTEND=noninteractive", "which apt-get", sudo("apt-get update"),
                // The following line is required to run on Docker container
                sudo("apt-get install -y python-httplib2"), sudo("apt-get install -y libssl0.9.8"),
                sudo(format("dpkg -i %s", saveAs)));

        String yum = chainGroup("which yum",
                // The following prevents failure on RHEL AWS nodes:
                // https://forums.aws.amazon.com/thread.jspa?threadID=100509
                ok(sudo("sed -i.bk s/^enabled=1$/enabled=0/ /etc/yum/pluginconf.d/subscription-manager.conf")),
                ok(sudo("yum check-update")), sudo("yum install -y pkgconfig"),
                // RHEL requires openssl version 098
                sudo("[ -f /etc/redhat-release ] && (grep -i \"red hat\" /etc/redhat-release && sudo yum install -y openssl098e) || :"),
                sudo(format("rpm --install %s", saveAs)));

        String link = new DownloadProducerFromUrlAttribute().apply(new BasicDownloadRequirement(this))
                .getPrimaryLocations().iterator().next();
        return ImmutableList.<String>builder().add(INSTALL_CURL)
                .addAll(Arrays.asList(INSTALL_CURL, BashCommands.require(
                        BashCommands.alternatives(BashCommands.simpleDownloadUrlAs(urls, saveAs),
                                // Referer link is required for 3.0.0; note mis-spelling is correct, as per http://en.wikipedia.org/wiki/HTTP_referer
                                "curl -f -L -k " + BashStringEscapes.wrapBash(link)
                                        + " -H 'Referer: http://www.couchbase.com/downloads'" + " -o " + saveAs),
                        "Could not retrieve " + saveAs + " (from " + urls.size() + " sites)", 9)))
                .add(alternatives(apt, yum)).build();
    }

    @Override
    public void customize() {
        //TODO: add linux tweaks for couchbase
        //http://blog.couchbase.com/often-overlooked-linux-os-tweaks
        //http://blog.couchbase.com/kirk

        //turn off swappiness
        //vm.swappiness=0
        //sudo echo 0 > /proc/sys/vm/swappiness

        //os page cache = 20%

        //disable THP
        //sudo echo never > /sys/kernel/mm/transparent_hugepage/enabled
        //sudo echo never > /sys/kernel/mm/transparent_hugepage/defrag

        //turn off transparent huge pages
        //limit page cache disty bytes
        //control the rate page cache is flused ... vm.dirty_*
    }

    @Override
    public void launch() {
        String clusterPrefix = "--cluster-" + (isPreV3() ? "init-" : "");
        // in v30, the cluster arguments were changed, and it became mandatory to supply a url + password (if there is none, these are ignored)
        newScript(LAUNCHING).body.append(sudo("/etc/init.d/couchbase-server start"), "for i in {0..120}\n" + "do\n"
                + "    if [ $i -eq 120 ]; then echo REST API unavailable after 120 seconds, failing; exit 1; fi;\n"
                + "    curl -s " + String.format("http://localhost:%s", getWebPort())
                + " > /dev/null && echo REST API available after $i seconds && break\n" + "    sleep 1\n" + "done\n"
                + couchbaseCli("cluster-init")
                + (isPreV3() ? getCouchbaseHostnameAndPort() : getCouchbaseHostnameAndCredentials()) + " "
                + clusterPrefix + "username=" + getUsername() + " " + clusterPrefix + "password=" + getPassword()
                + " " + clusterPrefix + "port=" + getWebPort() + " " + clusterPrefix + "ramsize="
                + getClusterInitRamSize()).execute();
    }

    @Override
    public boolean isRunning() {
        //TODO add a better way to check if couchbase server is running
        return (newScript(CHECK_RUNNING).body.append(
                format("curl -u %s:%s http://localhost:%s/pools/nodes", getUsername(), getPassword(), getWebPort()))
                .execute() == 0);
    }

    @Override
    public void stop() {
        newScript(STOPPING).body.append(sudo("/etc/init.d/couchbase-server stop")).execute();
    }

    @Override
    public String getVersion() {
        return entity.getConfig(CouchbaseNode.SUGGESTED_VERSION);
    }

    @Override
    public String getOsTag() {
        return newDownloadLinkSegmentComputer().getOsTag();
    }

    protected DownloadLinkSegmentComputer newDownloadLinkSegmentComputer() {
        return new DownloadLinkSegmentComputer(getLocation().getOsDetails(), !isPreV3(), "" + getEntity());
    }

    public static class DownloadLinkSegmentComputer {

        // links are:

        // http://packages.couchbase.com/releases/2.2.0/couchbase-server-community_2.2.0_x86_64.rpm
        // http://packages.couchbase.com/releases/2.2.0/couchbase-server-community_2.2.0_x86_64.deb
        // ^^^ preV3 is _ everywhere
        // http://packages.couchbase.com/releases/3.0.0/couchbase-server-community_3.0.0-ubuntu12.04_amd64.deb
        // ^^^ most V3 is _${version}-
        // http://packages.couchbase.com/releases/3.0.0/couchbase-server-community-3.0.0-centos6.x86_64.rpm
        // ^^^ but RHEL is -${version}-

        @Nullable
        private final OsDetails os;
        @Nonnull
        private final boolean isV3OrLater;
        @Nonnull
        private final String context;
        @Nonnull
        private final String osName;
        @Nonnull
        private final boolean isRpm;
        @Nonnull
        private final boolean is64bit;

        public DownloadLinkSegmentComputer(@Nullable OsDetails os, boolean isV3OrLater, @Nonnull String context) {
            this.os = os;
            this.isV3OrLater = isV3OrLater;
            this.context = context;
            if (os == null) {
                // guess centos as RPM is sensible default
                log.warn("No details known for OS of " + context
                        + "; assuming 64-bit RPM distribution of Couchbase");
                osName = "centos";
                isRpm = true;
                is64bit = true;
                return;
            }
            osName = os.getName().toLowerCase();
            isRpm = !(osName.contains("deb") || osName.contains("ubuntu"));
            is64bit = os.is64bit();
        }

        /** separator after the version number used to be _ but is - in 3.0 and later */
        public String getPreVersionSeparator() {
            if (!isV3OrLater)
                return "_";
            if (isRpm)
                return "-";
            return "_";
        }

        public String getOsTag() {
            // couchbase only provide certain versions; if on other platforms let's suck-it-and-see
            String family;
            if (osName.contains("debian"))
                family = "debian7_";
            else if (osName.contains("ubuntu"))
                family = "ubuntu12.04_";
            else if (osName.contains("centos") || osName.contains("rhel")
                    || (osName.contains("red") && osName.contains("hat")))
                family = "centos6.";
            else {
                log.warn("Unrecognised OS " + os + " of " + context + "; assuming RPM distribution of Couchbase");
                family = "centos6.";
            }

            if (!is64bit && !isV3OrLater) {
                // NB: 32-bit binaries aren't (yet?) available for v30
                log.warn("32-bit binaries for Couchbase might not be available, when deploying " + context);
            }
            String arch = !is64bit ? "x86" : !isRpm && isV3OrLater ? "amd64" : "x86_64";
            String fileExtension = isRpm ? ".rpm" : ".deb";

            if (isV3OrLater)
                return family + arch + fileExtension;
            else
                return arch + fileExtension;
        }

        public String getOsTagWithPrefix() {
            return (!isV3OrLater ? "_" : "-") + getOsTag();
        }
    }

    @Override
    public String getDownloadLinkOsTagWithPrefix() {
        return newDownloadLinkSegmentComputer().getOsTagWithPrefix();
    }

    @Override
    public String getDownloadLinkPreVersionSeparator() {
        return newDownloadLinkSegmentComputer().getPreVersionSeparator();
    }

    private boolean isPreV3() {
        return NaturalOrderComparator.INSTANCE.compare(getEntity().getConfig(CouchbaseNode.SUGGESTED_VERSION),
                "3.0") < 0;
    }

    @Override
    public String getCommunityOrEnterprise() {
        Boolean isEnterprise = getEntity().getConfig(CouchbaseNode.USE_ENTERPRISE);
        return isEnterprise ? "enterprise" : "community";
    }

    private String getUsername() {
        return entity.getConfig(CouchbaseNode.COUCHBASE_ADMIN_USERNAME);
    }

    private String getPassword() {
        return entity.getConfig(CouchbaseNode.COUCHBASE_ADMIN_PASSWORD);
    }

    private String getWebPort() {
        return "" + entity.getAttribute(CouchbaseNode.COUCHBASE_WEB_ADMIN_PORT);
    }

    private String getCouchbaseHostnameAndCredentials() {
        return format("-c localhost:%s -u %s -p %s", getWebPort(), getUsername(), getPassword());
    }

    private String getCouchbaseHostnameAndPort() {
        return format("-c localhost:%s", getWebPort());
    }

    private String getClusterInitRamSize() {
        return entity.getConfig(CouchbaseNode.COUCHBASE_CLUSTER_INIT_RAM_SIZE).toString();
    }

    @Override
    public void rebalance() {
        entity.setAttribute(CouchbaseNode.REBALANCE_STATUS, "explicitly started");
        newScript("rebalance").body.append(couchbaseCli("rebalance") + getCouchbaseHostnameAndCredentials())
                .failOnNonZeroResultCode().execute();

        // wait until the re-balance is started
        // (if it's quick, this might miss it, but it will only block for 30s if so)
        Repeater.create().backoff(Duration.millis(10), 2, Duration.millis(500)).limitTimeTo(Duration.THIRTY_SECONDS)
                .until(new Callable<Boolean>() {
                    @Override
                    public Boolean call() throws Exception {
                        for (String nodeHostAndPort : CouchbaseNodeSshDriver.this.getNodesHostAndPort()) {
                            if (isNodeRebalancing(nodeHostAndPort)) {
                                return true;
                            }
                        }
                        return false;
                    }
                }).run();

        entity.setAttribute(CouchbaseNode.REBALANCE_STATUS, "waiting for completion");
        // NB: a sensor feed will also update this

        // then wait until the re-balance is complete
        boolean completed = Repeater.create().backoff(Duration.ONE_SECOND, 1.2, Duration.TEN_SECONDS)
                .limitTimeTo(Duration.FIVE_MINUTES).until(new Callable<Boolean>() {
                    @Override
                    public Boolean call() throws Exception {
                        for (String nodeHostAndPort : getNodesHostAndPort()) {
                            if (isNodeRebalancing(nodeHostAndPort)) {
                                return false;
                            }
                        }
                        return true;
                    }
                }).run();
        if (completed) {
            entity.setAttribute(CouchbaseNode.REBALANCE_STATUS, "completed");
            ServiceStateLogic.ServiceNotUpLogic.clearNotUpIndicator(getEntity(), "rebalancing");
            log.info("Rebalanced cluster via primary node {}", getEntity());
        } else {
            entity.setAttribute(CouchbaseNode.REBALANCE_STATUS, "timed out");
            ServiceStateLogic.ServiceNotUpLogic.updateNotUpIndicator(getEntity(), "rebalancing",
                    "rebalance did not complete within time limit");
            log.warn("Timeout rebalancing cluster via primary node {}", getEntity());
        }
    }

    private Iterable<String> getNodesHostAndPort() {
        Function<JsonElement, Iterable<String>> getNodesAsList = new Function<JsonElement, Iterable<String>>() {
            @Override
            public Iterable<String> apply(JsonElement input) {
                if (input == null) {
                    return Collections.emptyList();
                }
                Collection<String> names = Lists.newArrayList();
                JsonArray nodes = input.getAsJsonArray();
                for (JsonElement element : nodes) {
                    // NOTE: the 'hostname' element also includes the port
                    names.add(element.getAsJsonObject().get("hostname").toString().replace("\"", ""));
                }
                return names;
            }
        };
        HttpToolResponse nodesResponse = getApiResponse(
                String.format("http://%s:%s/pools/nodes", getHostname(), getWebPort()));
        return Functionals.chain(HttpValueFunctions.jsonContents(), JsonFunctions.walkN("nodes"), getNodesAsList)
                .apply(nodesResponse);
    }

    private boolean isNodeRebalancing(String nodeHostAndPort) {
        HttpToolResponse response = getApiResponse(
                "http://" + nodeHostAndPort + "/pools/default/rebalanceProgress");
        if (response.getResponseCode() != 200) {
            throw new IllegalStateException("failed retrieving rebalance status: " + response);
        }
        return !"none".equals(HttpValueFunctions.jsonContents("status", String.class).apply(response));
    }

    private HttpToolResponse getApiResponse(String uri) {
        return HttpTool.httpGet(HttpTool.httpClientBuilder()
                // the uri is required by the HttpClientBuilder in order to set the AuthScope of the credentials
                .uri(uri).credentials(new UsernamePasswordCredentials(getUsername(), getPassword())).build(),
                URI.create(uri), ImmutableMap.<String, String>of());
    }

    @Override
    public void serverAdd(String serverToAdd, String username, String password) {
        // TODO the POST is failing with SocketException: Connection reset
        // removing any data makes the problem go away; i suspect it is the combo of:
        // credentials, an explicit port, and content.
        // but i do not know how to fix it...
        ////      curl -u Administrator:password\
        ////      192.168.60.101:8091/controller/addNode \
        ////       -d "hostname=192.168.60.103&user=Administrator&password=password"
        //        String baseUrl = Preconditions.checkNotNull(getEntity().getAttribute(CouchbaseNode.COUCHBASE_WEB_ADMIN_URL), "web admin URL not available");
        //        String uri = Urls.mergePaths(baseUrl, "controller/addNode");
        //        URI uriU = URI.create(uri);
        //        
        //        HttpClient client = HttpTool.httpClientBuilder()
        //            // the uri is required by the HttpClientBuilder in order to set the AuthScope of the credentials
        //            .uri(uriU.getScheme()+"://"+uriU.getHost())
        //            .credentials(new UsernamePasswordCredentials(getUsername(), getPassword()))
        //            .build();
        //        client.getParams().setParameter("http.socket.timeout", new Integer(0)); 
        //        client.getParams().setParameter("http.connection.stalecheck", new Boolean(true));
        //        
        //        HttpToolResponse response = HttpTool.httpPost(client, 
        //            URI.create(uri), 
        ////            ImmutableMap.of(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()),
        //            // TODO do we need the above?
        //            ImmutableMap.<String,String>of(),
        //            
        //            ("hostname="+Urls.encode(serverToAdd)+
        //                "&user"+Urls.encode(username)+
        //                "&password"+Urls.encode(password)).getBytes()
        //            // the following two work
        ////            "".getBytes()
        ////            ImmutableMap.<String,String>of()
        //            );
        //        if (response.getResponseCode()==200) {
        //            log.debug("Completed addNode call for "+serverToAdd+" via REST to "+getEntity()+": "+response.getContentAsString());
        //        } else {
        //            log.warn("Failed addNode call for "+serverToAdd+" via REST to "+getEntity()+": "+response.getResponseCode()+" / "+response.getContentAsString());
        //            throw new IllegalStateException("Failed addNode call for "+serverToAdd+" via REST to "+getEntity()+": "+response.getResponseCode()+" / "+response.getContentAsString());
        //        }

        // TODO would like a WebTasks API such as this:
        //        DynamicTasks.queue(WebTasks.get(baseUrl).subpath("controller/addNode").credentials(getUsername(), getPassword())
        //            .queryParam("hostname", serverToAdd).queryParam("user", username).queryParam("password", password)
        //            .summary("REST addNode "+serverToAdd)).getUnchecked();

        // or, via CLI:
        newScript("serverAdd").body.append(couchbaseCli("server-add") + getCouchbaseHostnameAndCredentials()
                + " --server-add=" + BashStringEscapes.wrapBash(serverToAdd) + " --server-add-username="
                + BashStringEscapes.wrapBash(username) + " --server-add-password="
                + BashStringEscapes.wrapBash(password)).failOnNonZeroResultCode().execute();
    }

    @Override
    public void serverAddAndRebalance(String serverToAdd, String username, String password) {
        newScript("serverAddAndRebalance").body.append(couchbaseCli("rebalance")
                + getCouchbaseHostnameAndCredentials() + " --server-add=" + BashStringEscapes.wrapBash(serverToAdd)
                + " --server-add-username=" + BashStringEscapes.wrapBash(username) + " --server-add-password="
                + BashStringEscapes.wrapBash(password)).failOnNonZeroResultCode().execute();
        entity.setAttribute(CouchbaseNode.REBALANCE_STATUS, "triggered as part of server-add");
    }

    @Override
    public void bucketCreate(String bucketName, String bucketType, Integer bucketPort, Integer bucketRamSize,
            Integer bucketReplica) {
        log.info("Adding bucket: {} to cluster {} primary node: {}",
                new Object[] { bucketName, CouchbaseClusterImpl.getClusterOrNode(getEntity()), getEntity() });

        newScript("bucketCreate").body
                .append(couchbaseCli("bucket-create") + getCouchbaseHostnameAndCredentials() + " --bucket="
                        + BashStringEscapes.wrapBash(bucketName) + " --bucket-type="
                        + BashStringEscapes.wrapBash(bucketType) + " --bucket-port=" + bucketPort
                        + " --bucket-ramsize=" + bucketRamSize + " --bucket-replica=" + bucketReplica)
                .failOnNonZeroResultCode().execute();
    }

    @Override
    public void addReplicationRule(Entity toCluster, String fromBucket, String toBucket) {
        DynamicTasks.queue(DependentConfiguration.attributeWhenReady(toCluster, Attributes.SERVICE_UP))
                .getUnchecked();

        String destName = CouchbaseClusterImpl.getClusterName(toCluster);

        log.info("Setting up XDCR for " + fromBucket + " from " + CouchbaseClusterImpl.getClusterName(getEntity())
                + " (via " + getEntity() + ") " + "to " + destName + " (" + toCluster + ")");

        Entity destPrimaryNode = toCluster.getAttribute(CouchbaseCluster.COUCHBASE_PRIMARY_NODE);
        String destHostname = destPrimaryNode.getAttribute(Attributes.HOSTNAME);
        String destUsername = toCluster.getConfig(CouchbaseNode.COUCHBASE_ADMIN_USERNAME);
        String destPassword = toCluster.getConfig(CouchbaseNode.COUCHBASE_ADMIN_PASSWORD);

        // on the REST API there is mention of a 'type' 'continuous' but i don't see other refs to this

        // PROTOCOL   Select REST protocol or memcached for replication. xmem indicates memcached while capi indicates REST protocol.
        // looks like xmem is the default; leave off for now
        //        String replMode = "xmem";

        DynamicTasks.queue(TaskTags.markInessential(SshEffectorTasks
                .ssh(couchbaseCli("xdcr-setup") + getCouchbaseHostnameAndCredentials() + " --create"
                        + " --xdcr-cluster-name=" + BashStringEscapes.wrapBash(destName) + " --xdcr-hostname="
                        + BashStringEscapes.wrapBash(destHostname) + " --xdcr-username="
                        + BashStringEscapes.wrapBash(destUsername) + " --xdcr-password="
                        + BashStringEscapes.wrapBash(destPassword))
                .summary("create xdcr destination " + destName).newTask()));

        // would be nice to auto-create bucket, but we'll need to know the parameters; the port in particular is tedious
        //        ((CouchbaseNode)destPrimaryNode).bucketCreate(toBucket, "couchbase", null, 0, 0);

        DynamicTasks
                .queue(SshEffectorTasks.ssh(couchbaseCli("xdcr-replicate") + getCouchbaseHostnameAndCredentials()
                        + " --create" + " --xdcr-cluster-name=" + BashStringEscapes.wrapBash(destName)
                        + " --xdcr-from-bucket=" + BashStringEscapes.wrapBash(fromBucket) + " --xdcr-to-bucket="
                        + BashStringEscapes.wrapBash(toBucket)
        //            + " --xdcr-replication-mode="+replMode
        ).summary("configure replication for " + fromBucket + " to " + destName + ":" + toBucket).newTask());
    }
}