/*
* Copyright 2016 Fizzed, Inc.
*
* 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 com.fizzed.stork.deploy;
import static com.fizzed.blaze.Contexts.fail;
import com.fizzed.blaze.core.Actions;
import com.fizzed.blaze.core.UnexpectedExitValueException;
import com.fizzed.blaze.ssh.SshExec;
import com.fizzed.blaze.ssh.SshSession;
import com.fizzed.blaze.util.Streamables;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fizzed.blaze.ssh.SshFile;
import com.fizzed.blaze.ssh.SshFileAttributes;
import com.fizzed.blaze.ssh.SshSftpNoSuchFileException;
import com.fizzed.blaze.ssh.SshSftpSession;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class UnixTarget extends SshTarget {
static private final Logger log = LoggerFactory.getLogger(UnixTarget.class);
public UnixTarget(SshSession ssh, SshSftpSession sftp, String uname, InitType initType, String tempDir, Map<String,String> commands) {
super(ssh, sftp, uname, initType, tempDir, commands);
}
@Override
public void put(Path source, String target) {
sftp.put()
.source(source)
.target(target)
.run();
}
@Override
public List<BasicFile> listFiles(Object path) {
try {
List<SshFile> files = sftp.ls(path.toString());
List<BasicFile> basicFiles = new ArrayList<>(files.size());
files.stream().forEach((file) -> {
SshFileAttributes attrs = file.attributes();
long createdAt = attrs.creationTime().toMillis();
basicFiles.add(new BasicFile(file.path(), createdAt, attrs.uid(), attrs.gid()));
});
return basicFiles;
} catch (SshSftpNoSuchFileException e) {
return null;
}
}
@Override
public Path readlink(Object path) {
try {
return sftp.readlink(path.toString());
} catch (SshSftpNoSuchFileException e) {
return null;
}
}
@Override
public Path realpath(Object path) {
try {
return sftp.realpath(path.toString());
} catch (SshSftpNoSuchFileException e) {
return null;
}
}
@Override
public void chown(boolean sudo, boolean recursive, String owner, String target) {
String options = "";
if (recursive) {
options = "-R";
}
sshExec(sudo, false, "chown", options, owner, target).run();
log.info("Set owner to {} for {}", owner, target);
}
@Override
public void chmod(boolean sudo, boolean recursive, String permissions, String target) {
String options = "";
if (recursive) {
options = "-R";
}
sshExec(sudo, false, "chmod", options, permissions, target).run();
log.info("Set perms to {} for {}", permissions, target);
}
@Override
public void symlink(boolean sudo, String target, String link) {
remove(sudo, link);
sshExec(sudo, false, "ln", "-s", target, link).run();
log.info("Symlink " + link + " -> " + target);
}
@Override
public void copyFiles(boolean sudo, String source, String target) {
sshExec(sudo, true, "if [ -e " + source + " ]; then cp -R " + source + " " + target + "; fi").run();
log.info("Copied file(s) from " + source + " to " + target);
}
@Override
public void moveFiles(boolean sudo, String source, String target) {
sshExec(sudo, true, "if [ -d " + source + " ]; then mv " + source + " " + target + "; fi").run();
log.info("Moved file(s) from " + source + " to " + target);
}
@Override
public void createDirectories(boolean sudo, Object path) {
sshExec(sudo, false, "mkdir", "-p", path).run();
log.info("Created dir(s) {}", path);
}
@Override
public void remove(boolean sudo, Object... paths) {
// remove any old assembly or unpacked version
SshExec sshExec = sshExec(sudo, false, "rm", "-Rf");
for (Object path : paths) {
sshExec.arg(path);
}
sshExec.run();
log.info("Removed {}", (Object) paths);
}
@Override
public void unpack(String path, String targetDir) {
if (path.endsWith(".tar.gz")) {
sshExec(false, false, "tar", "xzf", path, "-C", targetDir).run();
} else if (path.endsWith(".zip")) {
sshExec(false, false, "unzip", "-q", "-d", targetDir, path).run();
} else {
fail("Unable to support file extension for '" + path + "'");
}
log.info("Unpacked {}", path);
}
public Integer getUserId(String user) {
try {
String userId
= sshExec(false, false, "id", "-u", user)
.pipeOutput(Streamables.captureOutput())
.pipeError(Streamables.nullOutput())
.runResult()
.map(Actions::toCaptureOutput)
.asString()
.trim();
return Integer.valueOf(userId);
} catch (UnexpectedExitValueException e) {
return null;
}
}
@Override
public boolean hasUser(String user) {
return getUserId(user) != null;
}
public Integer getGroupId(String group) {
try {
String groupId
= sshExec(false, false, "id", "-g", group)
.pipeOutput(Streamables.captureOutput())
.pipeError(Streamables.nullOutput())
.runResult()
.map(Actions::toCaptureOutput)
.asString()
.trim();
return Integer.valueOf(groupId);
} catch (UnexpectedExitValueException e) {
return null;
}
}
@Override
public boolean hasGroup(String group) {
return getGroupId(group) != null;
}
@Override
public void stopDaemon(Daemon daemon) throws DeployerException {
switch (daemon.getInitType()) {
case SYSV:
case UPSTART:
try {
log.info("Trying to stop daemon {}...", daemon.getName());
sshExec(true, false, "service", daemon.getName(), "stop").run();
} catch (UnexpectedExitValueException e) {
throw new DeployerException(
"Unable to stop service " + daemon.getName()
+ ". Exit value " + e.getActual() + ". Output from failed command is above.");
}
break;
case SYSTEMD:
try {
// 0 = success, 5 = service not loaded
log.info("Trying to stop daemon {}...", daemon.getName());
sshExec(true, false, "systemctl", "stop", daemon.getName())
.exitValues(0, 5)
.run();
// TODO: should we do our own loop to confirm it stopped?
} catch (UnexpectedExitValueException e) {
throw new DeployerException(
"Unable to stop service " + daemon.getName()
+ ". Exit value " + e.getActual() + ". Output from failed command is above.");
}
break;
default:
throw new DeployerException("Unable to support init type " + daemon.getInitType());
}
}
@Override
public void startDaemon(Daemon daemon) throws DeployerException {
switch (daemon.getInitType()) {
case SYSV:
case UPSTART:
try {
log.info("Trying to start daemon {}...", daemon.getName());
sshExec(true, false, "service", daemon.getName(), "start").run();
log.info("Daemon {} started!", daemon.getName());
} catch (UnexpectedExitValueException e) {
throw new DeployerException(
"Unable to start service " + daemon.getName()
+ ". Exit value " + e.getActual() + ". Output from failed command is above.");
}
break;
case SYSTEMD:
try {
log.info("Reloading systemd daemon...", daemon.getName());
sshExec(true, false, "systemctl", "daemon-reload").run();
log.info("Trying to start daemon {}...", daemon.getName());
sshExec(true, false, "systemctl", "start", daemon.getName()).run();
// wait for service to truly start
long timeout = 15000L;
long now = System.currentTimeMillis();
boolean confirmed = false;
while (System.currentTimeMillis() < (now+timeout)) {
log.info("Querying systemctl to verify {} started...", daemon.getName());
// run a status command so user can see what's up
String output
= sshExec(true, false, "systemctl", "status", daemon.getName())
.pipeOutput(Streamables.captureOutput())
.runResult()
.map(Actions::toCaptureOutput)
.asString();
// TODO: this needs to be configurable!
if (output.contains("OK")) {
confirmed = true;
break;
}
Thread.sleep(1000L);
}
if (!confirmed) {
sshExec(true, false, "systemctl", "status", daemon.getName()).run();
throw new DeployerException("Unable to confirm service started within " + timeout + " ms");
} else {
log.info("Daemon {} started!", daemon.getName());
}
} catch (UnexpectedExitValueException e) {
// run a status command so user can see what's up
sshExec(true, false, "systemctl", "status", daemon.getName()).run();
throw new DeployerException(
"Unable to start service " + daemon.getName()
+ ". Exit value " + e.getActual() + ". Output from failed command is above.");
} catch (InterruptedException e) {
throw new DeployerException(e.getMessage());
}
break;
default:
throw new DeployerException("Unable to support init type " + daemon.getInitType());
}
}
@Override
public void installDaemon(Deployment install, Daemon daemon, boolean onBoot) throws DeployerException {
switch (daemon.getInitType()) {
case SYSV:
case UPSTART:
installSysvDaemon(install, daemon, onBoot);
break;
case SYSTEMD:
installSystemdDaemon(install, daemon, onBoot);
break;
default:
throw new DeployerException("Unable to support init type " + daemon.getInitType());
}
}
private void installDaemonDefaults(Deployment install, Daemon daemon) {
// configure service config files
for (String dir : Arrays.asList("default", "sysconfig")) {
String defaultsFile = "/etc/" + dir + "/" + daemon.getName();
String cmd
= "if [ -d /etc/" + dir + " ]; then "
+ " if [ ! -f " + defaultsFile + " ]; then "
+ " echo \"APP_HOME=\\\"" + install.getCurrentDir() + "\\\"\" > " + defaultsFile + "; "
+ " echo \"APP_USER=\\\"" + install.getUser().orElse("") + "\\\"\" >> " + defaultsFile + "; "
+ " echo \"APP_GROUP=\\\"" + install.getGroup().orElse("") + "\\\"\" >> " + defaultsFile + "; "
+ " exit 20; "
+ " else "
+ " exit 30; "
+ " fi "
+ "else "
+ " exit 10; "
+ "fi";
Integer exitValue
= sshExec(true, true, cmd)
.exitValues(10, 20, 30)
.run();
if (exitValue == 20) {
log.info("Created {}", defaultsFile);
break; // no need to keep running loop
} else if (exitValue == 30) {
log.info("Defaults {} already exists (will not overwrite)", defaultsFile);
break; // no need to keep running loop
}
}
}
private void installSysvDaemon(Deployment install, Daemon daemon, boolean onBoot) {
// symlink to init.d
String initdFile = "/etc/init.d/" + daemon.getName();
String initFile = install.getCurrentDir() + "/share/init.d/" + daemon.getName() + ".init";
symlink(true, initFile, initdFile);
installDaemonDefaults(install, daemon);
// auto start?
if (onBoot) {
String cmd
= "if type \"chkconfig\" > /dev/null; then "
+ " chkconfig --add " + daemon.getName() + "; "
+ " exit 1; "
+ "elif type \"update-rc.d\" > /dev/null; then "
+ " update-rc.d " + daemon.getName() + " defaults; "
+ " exit 2; "
+ "else "
+ " exit 3; "
+ "fi";
Integer exitValue
= sshExec(true, true, cmd)
.exitValues(1, 2, 3)
.run();
switch (exitValue) {
case 1:
log.info("Daemon {} will start at boot (via chkconfig)", daemon.getName());
break;
case 2:
log.info("Daemon {} will start at boot (via update-rc.d)", daemon.getName());
break;
case 3:
log.error("Daemon {} will be unable to start at boot (neither chkconfig or update-rc.d found)", daemon.getName());
break;
}
}
}
private void installSystemdDaemon(Deployment install, Daemon daemon, boolean onBoot) {
// upload modified file to target, then copy it over
String sourceServiceFile = install.getCurrentDir() + "/share/systemd/" + daemon.getName() + ".service";
String serviceFile = "/etc/systemd/system/" + daemon.getName() + ".service";
copyFiles(true, sourceServiceFile, serviceFile);
installDaemonDefaults(install, daemon);
}
}