All Downloads are FREE. Search and download functionalities are using the official Maven repository.

brooklyn.entity.container.docker.DockerHostSshDriver Maven / Gradle / Ivy

There is a newer version: 1.2.0
Show newest version
/*
 * Copyright 2014-2015 by Cloudsoft Corporation Limited
 *
 * Licensed 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.container.docker;

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.fail;
import static brooklyn.util.ssh.BashCommands.ifExecutableElse0;
import static brooklyn.util.ssh.BashCommands.ifExecutableElse1;
import static brooklyn.util.ssh.BashCommands.installPackage;
import static brooklyn.util.ssh.BashCommands.sudo;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import org.jclouds.net.domain.IpPermission;
import org.jclouds.net.domain.IpProtocol;

import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver;
import brooklyn.entity.basic.Entities;
import brooklyn.entity.basic.lifecycle.ScriptHelper;
import brooklyn.entity.container.DockerUtils;
import brooklyn.entity.software.SshEffectorTasks;
import brooklyn.location.OsDetails;
import brooklyn.location.basic.SshMachineLocation;
import brooklyn.location.geo.LocalhostExternalIpLoader;
import brooklyn.location.jclouds.JcloudsMachineLocation;
import brooklyn.location.jclouds.JcloudsSshMachineLocation;
import brooklyn.location.jclouds.networking.JcloudsLocationSecurityGroupCustomizer;
import brooklyn.management.Task;
import brooklyn.networking.sdn.SdnAttributes;
import brooklyn.networking.sdn.SdnProvider;
import brooklyn.util.collections.MutableList;
import brooklyn.util.collections.MutableMap;
import brooklyn.util.file.ArchiveUtils;
import brooklyn.util.file.ArchiveUtils.ArchiveType;
import brooklyn.util.net.Cidr;
import brooklyn.util.net.Urls;
import brooklyn.util.os.Os;
import brooklyn.util.repeat.Repeater;
import brooklyn.util.task.DynamicTasks;
import brooklyn.util.task.TaskBuilder;
import brooklyn.util.task.system.ProcessTaskWrapper;
import brooklyn.util.text.Identifiers;
import brooklyn.util.text.Strings;
import brooklyn.util.time.Duration;
import brooklyn.util.time.Time;

import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

public class DockerHostSshDriver extends AbstractSoftwareProcessSshDriver implements DockerHostDriver {

    public DockerHostSshDriver(DockerHostImpl entity, SshMachineLocation machine) {
        super(entity, machine);
    }

    @Override
    public Integer getDockerPort() {
        return getEntity().getAttribute(DockerHost.DOCKER_SSL_PORT);
    }

    /** {@inheritDoc} */
    @Override
    public String buildImage(String dockerFile, String name) {
        if (!ArchiveType.UNKNOWN.equals(ArchiveType.of(dockerFile)) || Urls.isDirectory(dockerFile)) {
            ArchiveUtils.deploy(dockerFile, getMachine(), Os.mergePaths(getRunDir(), name));
            String baseImageId = buildDockerfileDirectory(name);
            log.info("Created base Dockerfile image with ID {}", baseImageId);
        } else {
            ProcessTaskWrapper task = SshEffectorTasks.ssh(format("mkdir -p %s", Os.mergePaths(getRunDir(), name)))
                    .machine(getMachine())
                    .newTask();
            DynamicTasks.queueIfPossible(task).executionContext(getEntity()).orSubmitAndBlock();
            int result = task.get();
            if (result != 0) throw new IllegalStateException("Error creating image directory: " + name);

            // Build an image from the base Dockerfile
            copyTemplate(dockerFile, Os.mergePaths(name, "Base" + DockerUtils.DOCKERFILE),
                    false, getExtraTemplateSubstitutions(name));
            String baseImageId = buildDockerfile("Base" + DockerUtils.DOCKERFILE, name);
            log.info("Created base Dockerfile image with ID {}", baseImageId);
        }

        // Update the image with the Clocker sshd Dockerfile
        copyTemplate(DockerUtils.SSHD_DOCKERFILE, Os.mergePaths(name, "Sshd" + DockerUtils.DOCKERFILE),
                false, getExtraTemplateSubstitutions(name));
        String sshdImageId = buildDockerfile("Sshd" + DockerUtils.DOCKERFILE, name);
        log.info("Created SSHable Dockerfile image with ID {}", sshdImageId);

        return sshdImageId;
    }

    /** {@inheritDoc} */
    @Override
    public String layerSshableImageOn(String name, String tag) {
        checkNotNull(name, "name");
        checkNotNull(tag, "tag");
        copyTemplate(DockerUtils.SSHD_DOCKERFILE, Os.mergePaths(name, "Sshd" + DockerUtils.DOCKERFILE),
                true, ImmutableMap.of("fullyQualifiedImageName", name + ":" + tag));
        String sshdImageId = buildDockerfile("Sshd" + DockerUtils.DOCKERFILE, name);
        log.info("Created SSH-based image from {} with ID {}", name, sshdImageId);

        return sshdImageId;
    }

    private Map getExtraTemplateSubstitutions(String imageName) {
        Map templateSubstitutions = MutableMap.of("fullyQualifiedImageName", imageName);
        DockerHost host = (DockerHost) getEntity();
        templateSubstitutions.putAll(host.getInfrastructure().config().get(DockerInfrastructure.DOCKERFILE_SUBSTITUTIONS));
        return templateSubstitutions;
    }

    private String buildDockerfileDirectory(String name) {
        String build = format("build --rm -t %s %s",
                name, Os.mergePaths(getRunDir(), name));
        String stdout = ((DockerHost) getEntity()).runDockerCommandTimeout(build, Duration.minutes(20));
        String prefix = Strings.getFirstWordAfter(stdout, "Successfully built");

        return getImageId(prefix, name);
    }

    private String buildDockerfile(String dockerfile, String name) {
        String build = format("build --rm -t %s - < %s",
                name, Os.mergePaths(getRunDir(), name, dockerfile));
        String stdout = ((DockerHost) getEntity()).runDockerCommandTimeout(build, Duration.minutes(20));
        String prefix = Strings.getFirstWordAfter(stdout, "Successfully built");

        return getImageId(prefix, name);
    }

    // Inspect the Docker image with this prefix
    private String getImageId(String prefix, String name) {
        String inspect = format("inspect --format={{.Id}} %s", prefix);
        String imageId = ((DockerHost) getEntity()).runDockerCommand(inspect);
        return DockerUtils.checkId(imageId);
    }

    public String getEpelRelease() {
        return getEntity().config().get(DockerHost.EPEL_RELEASE);
    }

    public String getStorageOpts() {
        String driver = getEntity().config().get(DockerHost.DOCKER_STORAGE_DRIVER);
        if (Strings.isBlank(driver)) {
            return "";
        } else {
            return "-s " + Strings.toLowerCase(driver);
        }
    }

    @Override
    public String deployArchive(String url) {
        String volumeId = Identifiers.makeIdFromHash(url.hashCode());
        String path = Os.mergePaths(getRunDir(), volumeId);
        ArchiveUtils.deploy(url, getMachine(), path);
        return path;
    }

    @Override
    public void configureSecurityGroups() {
        String securityGroup = getEntity().config().get(DockerInfrastructure.SECURITY_GROUP);
        if (Strings.isBlank(securityGroup)) {
            if (!(getLocation() instanceof JcloudsSshMachineLocation)) {
                log.info("{} not running in a JcloudsSshMachineLocation, not configuring extra security groups", entity);
                return;
            }
            // TODO check GCE compatibility?
            JcloudsMachineLocation location = (JcloudsMachineLocation) getLocation();
            JcloudsLocationSecurityGroupCustomizer customizer = JcloudsLocationSecurityGroupCustomizer.getInstance(getEntity().getApplicationId());
            Collection permissions = getIpPermissions();
            log.debug("Applying custom security groups to {}: {}", location, permissions);
            customizer.addPermissionsToLocation(location, permissions);
        }
    }

    /**
     * @return Extra IP permissions to be configured on this entity's location.
     */
    protected Collection getIpPermissions() {
        String localhost = LocalhostExternalIpLoader.getLocalhostIpWithin(Duration.minutes(1)) + "/32";
        IpPermission dockerPort = IpPermission.builder()
                .ipProtocol(IpProtocol.TCP)
                .fromPort(getEntity().getAttribute(DockerHost.DOCKER_PORT))
                .toPort(getEntity().getAttribute(DockerHost.DOCKER_PORT))
                .cidrBlock(localhost)
                .build();
        IpPermission dockerSslPort = IpPermission.builder()
                .ipProtocol(IpProtocol.TCP)
                .fromPort(getEntity().getAttribute(DockerHost.DOCKER_SSL_PORT))
                .toPort(getEntity().getAttribute(DockerHost.DOCKER_SSL_PORT))
                .cidrBlock(localhost)
                .build();
        IpPermission dockerPortForwarding = IpPermission.builder()
                .ipProtocol(IpProtocol.TCP)
                .fromPort(32768)
                .toPort(65534)
                .cidrBlock(Cidr.UNIVERSAL.toString())
                .build();
        List permissions = MutableList.of(dockerPort, dockerSslPort, dockerPortForwarding);

        if (getEntity().config().get(SdnAttributes.SDN_ENABLE)) {
            SdnProvider provider = (SdnProvider) (entity.getAttribute(DockerHost.DOCKER_INFRASTRUCTURE).getAttribute(DockerInfrastructure.SDN_PROVIDER));
            Collection sdnPermissions = provider.getIpPermissions(localhost);
            permissions.addAll(sdnPermissions);
        }

        return permissions;
    }

    @Override
    public void preInstall() {
        resolver = Entities.newDownloader(this);
        setExpandedInstallDir(Os.mergePaths(getInstallDir(), resolver.getUnpackedDirectoryName(format("docker-%s", getVersion()))));
    }

    @Override
    public void install() {
        OsDetails osDetails = getMachine().getMachineDetails().getOsDetails();
        String osVersion = osDetails.getVersion();
        String arch = osDetails.getArch();
        if (!osDetails.is64bit()) throw new IllegalStateException("Docker supports only 64bit OS");
        if (osDetails.isWindows()) throw new IllegalStateException("Windows operating system not yet supported by Docker");
        log.debug("Installing Docker on {} version {}", osDetails.getName(), osVersion);

        // Generate Linux kernel upgrade commands
        if (osDetails.isLinux()) {
            String kernelVersion = Strings.getFirstWord(((DockerHost) getEntity()).execCommand("uname -r"));
            String storage = Strings.toLowerCase(entity.config().get(DockerHost.DOCKER_STORAGE_DRIVER));
            if (!"devicemapper".equals(storage)) { // No kernel changes needed for devicemapper sadness as a servive
                int present = ((DockerHost) getEntity()).execCommandStatus("modprobe " + storage);
                if (present != 0) {
                    List commands = MutableList.of();
                    if ("ubuntu".equalsIgnoreCase(osDetails.getName())) {
                        commands.add(installPackage("software-properties-common"));
                        commands.add(sudo("add-apt-repository -y ppa:canonical-kernel-team/ppa"));
                        commands.add("export UCF_FORCE_CONFFNEW=1");
                        if ("overlay".equals(storage) || "btrfs".equals(storage)) {
                            commands.add(installPackage("linux-{image,headers,image-extra}-3.19.\\*-generic"));
                        } else if ("aufs".equals(storage) || Strings.isBlank(storage)) { // aufs is default
                            commands.add(installPackage("linux-image-extra-" + kernelVersion));
                        } else {
                            commands.add(installPackage("linux-{image,headers,image-extra}-3.16.\\*-generic"));
                        }
                        executeKernelInstallation(commands);
                    }
                    if ("centos".equalsIgnoreCase(osDetails.getName())) {
                        // TODO differentiate between CentOS 6 and 7 and RHEL
                        commands.add(sudo("yum -y --nogpgcheck upgrade kernel"));
                        executeKernelInstallation(commands);
                    }
                }
            }
        }

        // Generate Docker install commands
        List commands = Lists.newArrayList();
        if (osDetails.isMac()) {
            commands.add(alternatives(
                    ifExecutableElse1("boot2docker", "boot2docker status || boot2docker init"),
                    fail("Mac OSX install requires Boot2Docker preinstalled", 1)));
        }
        if (osDetails.isLinux()) {
            commands.add(INSTALL_CURL);
            if ("ubuntu".equalsIgnoreCase(osDetails.getName())) {
                commands.add(installDockerOnUbuntu());
            } else if ("centos".equalsIgnoreCase(osDetails.getName())) { // should work for RHEL also?
                commands.add(ifExecutableElse1("yum", useYum(osVersion, arch, getEpelRelease())));
                commands.add(installPackage(ImmutableMap.of("yum", "docker-io"), null));
                commands.add(sudo(format("curl https://get.docker.com/builds/Linux/x86_64/docker-%s -o /usr/bin/docker", getVersion())));
            } else {
                commands.add(installDockerFallback());
            }
        }
        newScript(INSTALLING)
                .body.append(commands)
                .failOnNonZeroResultCode()
                .execute();
    }

    private void executeKernelInstallation(List commands) {
        newScript(INSTALLING + "kernel")
                .body.append(commands)
                .body.append(sudo("reboot"))
                .execute();

        // Wait until the Docker host is SSHable after the reboot
        // Don't check immediately; it could take a few seconds for rebooting to make the machine not ssh'able;
        // must not accidentally think it's rebooted before we've actually rebooted!
        Stopwatch stopwatchForReboot = Stopwatch.createStarted();
        Time.sleep(Duration.seconds(30));

        Task sshable = TaskBuilder. builder()
                .name("Waiting until host is SSHable")
                .body(new Callable() {
                    @Override
                    public Boolean call() throws Exception {
                        return Repeater.create()
                                .every(Duration.TEN_SECONDS)
                                .until(new Callable() {
                                    public Boolean call() {
                                        return getLocation().isSshable();
                                    }
                                })
                                .limitTimeTo(Duration.minutes(15)) // Because of the reboot
                                .run();
                    }
                })
                .build();
        Boolean result = DynamicTasks.queueIfPossible(sshable)
                .orSubmitAndBlock()
                .andWaitForSuccess();
        if (!result) {
            throw new IllegalStateException(String.format("The entity %s is not sshable after reboot (waited %s)", 
                    entity, Time.makeTimeStringRounded(stopwatchForReboot)));
        }
    }

    private String useYum(String osVersion, String arch, String epelRelease) {
        String osMajorVersion = osVersion.substring(0, osVersion.lastIndexOf("."));
        return chainGroup(
                alternatives(
                        sudo("rpm -qa | grep epel-release"),
                        sudo(format("rpm -Uvh http://dl.fedoraproject.org/pub/epel/%s/%s/epel-release-%s.noarch.rpm", osMajorVersion, arch, epelRelease))));
    }

    @Override
    public String getVersion() {
        String version = super.getVersion();
        if (version.matches("^[0-9]+\\.[0-9]+$")) {
            version += ".0"; // Append minor version
        }
        return version;
    }

    private String installDockerOnUbuntu() {
        String version = getVersion();
        log.debug("Installing Docker version {} on Ubuntu", version);
        return chainGroup(
                installPackage("apt-transport-https"),
                "echo 'deb https://get.docker.com/ubuntu docker main' | " + sudo("tee -a /etc/apt/sources.list.d/docker.list"),
                sudo("apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9"),
                installPackage("lxc-docker-" + version));
    }

    /**
     * Uses the curl-able install.sh script provided at {@code get.docker.com}.
     * This will install the latest version, which may be incompatible with the
     * jclouds driver.
     */
    private String installDockerFallback() {
        return "curl -s https://get.docker.com/ | " + sudo("sh");
    }

    @Override
    public void customize() {
        if (isRunning()) {
            log.info("Stopping running Docker instance at {} before customising", getMachine());
            stop();
        }

        newScript(CUSTOMIZING)
                .body.append(
                        ifExecutableElse0("apt-get", chainGroup(
                                format("echo 'DOCKER_OPTS=\"-H tcp://0.0.0.0:%d -H unix:///var/run/docker.sock %s --tls --tlscert=%s/cert.pem --tlskey=% mapping = MutableMap.of();
        Map volumes = getEntity().config().get(DockerHost.DOCKER_HOST_VOLUME_MAPPING);
        if (volumes != null) {
            for (String source : volumes.keySet()) {
                if (Urls.isUrlWithProtocol(source)) {
                    String path = deployArchive(source);
                    mapping.put(path, volumes.get(source));
                } else {
                    mapping.put(source, volumes.get(source));
                }
            }
        }
        getEntity().setAttribute(DockerHost.DOCKER_HOST_VOLUME_MAPPING, mapping);
    }

    @Override
    public boolean isRunning() {
        ScriptHelper helper = newScript(CHECK_RUNNING)
                .body.append(
                        alternatives(
                                ifExecutableElse1("boot2docker", "boot2docker status"),
                                ifExecutableElse1("service", sudo("service docker status"))))
                .noExtraOutput() // otherwise Brooklyn appends 'check-running' and the method always returns true.
                .gatherOutput();
        helper.execute();
        return helper.getResultStdout().contains("running");
    }

    @Override
    public void stop() {
        newScript(STOPPING)
                .body.append(
                        alternatives(
                                ifExecutableElse1("boot2docker", "boot2docker down"),
                                ifExecutableElse1("service", sudo("service docker stop"))))
                .failOnNonZeroResultCode()
                .execute();
    }

    @Override
    public void launch() {
        newScript(LAUNCHING)
                .body.append(
                        alternatives(
                                ifExecutableElse1("boot2docker", "boot2docker up"),
                                ifExecutableElse1("service", sudo("service docker start"))))
                .failOnNonZeroResultCode()
                .uniqueSshConnection()
                .execute();
    }

    @Override
    public Map getShellEnvironment() {
        ImmutableMap.Builder builder = ImmutableMap. builder()
                .putAll(super.getShellEnvironment());
        if (getMachine().getMachineDetails().getOsDetails().isMac()) {
            builder.put("DOCKER_HOST", format("tcp://%s:%d", getSubnetAddress(), getDockerPort()));
        }
        return builder.build();
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy