package io.airlift.airship.agent;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import io.airlift.airship.shared.Assignment;
import io.airlift.airship.shared.ConfigUtils;
import io.airlift.airship.shared.Installation;
import io.airlift.command.CommandFailedException;
import io.airlift.json.JsonCodec;
import io.airlift.log.Logger;
import io.airlift.units.Duration;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.UUID;
import static com.google.common.base.Charsets.UTF_8;
import static io.airlift.airship.shared.FileUtils.createTempDir;
import static io.airlift.airship.shared.FileUtils.deleteRecursively;
import static io.airlift.airship.shared.FileUtils.extractTar;
import static io.airlift.airship.shared.FileUtils.listFiles;
import static io.airlift.json.JsonCodec.jsonCodec;
public class DirectoryDeploymentManager
implements DeploymentManager
{
private static final Logger log = Logger.get(DirectoryDeploymentManager.class);
private final JsonCodec<DeploymentRepresentation> jsonCodec = jsonCodec(DeploymentRepresentation.class);
private final UUID slotId;
private final String location;
private final Duration tarTimeout;
private final File baseDir;
private final File deploymentFile;
private Deployment deployment;
public DirectoryDeploymentManager(File baseDir, String location, Duration tarTimeout)
{
Preconditions.checkNotNull(location, "location is null");
Preconditions.checkArgument(location.startsWith("/"), "location must start with /");
this.location = location;
this.tarTimeout = tarTimeout;
Preconditions.checkNotNull(baseDir, "baseDir is null");
baseDir.mkdirs();
Preconditions.checkArgument(baseDir.isDirectory(), "baseDir is not a directory: " + baseDir.getAbsolutePath());
this.baseDir = baseDir;
// verify deployment file is readable and writable
deploymentFile = new File(baseDir, "airship-deployment.json");
if (deploymentFile.exists()) {
Preconditions.checkArgument(deploymentFile.canRead(), "Can not read slot-id file %s", deploymentFile.getAbsolutePath());
Preconditions.checkArgument(deploymentFile.canWrite(), "Can not write slot-id file %s", deploymentFile.getAbsolutePath());
}
// load deployments
if (deploymentFile.exists()) {
try {
Deployment deployment = load(deploymentFile);
Preconditions.checkArgument(deployment.getDeploymentDir().isDirectory(), "Deployment directory is not a directory: %s", deployment.getDeploymentDir());
this.deployment = deployment;
}
catch (IOException e) {
throw new IllegalArgumentException("Invalid deployment file: " + deploymentFile.getAbsolutePath(), e);
}
}
// load slot-id
File slotIdFile = new File(baseDir, "airship-slot-id.txt");
UUID uuid = null;
if (slotIdFile.exists()) {
Preconditions.checkArgument(slotIdFile.canRead(), "can not read " + slotIdFile.getAbsolutePath());
try {
String slotIdString = Files.toString(slotIdFile, UTF_8).trim();
try {
uuid = UUID.fromString(slotIdString);
}
catch (IllegalArgumentException e) {
}
if (uuid == null) {
log.warn("Invalid slot id [" + slotIdString + "]: attempting to delete airship-slot-id.txt file and recreating a new one");
slotIdFile.delete();
}
}
catch (IOException e) {
Preconditions.checkArgument(slotIdFile.canRead(), "can not read " + slotIdFile.getAbsolutePath());
}
}
if (uuid == null) {
uuid = UUID.randomUUID();
try {
Files.write(uuid.toString(), slotIdFile, UTF_8);
}
catch (IOException e) {
Preconditions.checkArgument(slotIdFile.canRead(), "can not write " + slotIdFile.getAbsolutePath());
}
}
slotId = uuid;
}
@Override
public UUID getSlotId()
{
return slotId;
}
public String getLocation()
{
return location;
}
@Override
public Deployment install(Installation installation)
{
Preconditions.checkNotNull(installation, "installation is null");
File deploymentDir = new File(baseDir, "installation");
Assignment assignment = installation.getAssignment();
Deployment newDeployment = new Deployment(slotId, location, deploymentDir, getDataDir(), assignment, installation.getResources());
File tempDir = createTempDir(baseDir, "tmp-install");
try {
// download the binary
File binary = new File(tempDir, "airship-binary.tar.gz");
try {
Resources.asByteSource(installation.getBinaryFile().toURL()).copyTo(Files.asByteSink(binary));
}
catch (IOException e) {
throw new RuntimeException("Unable to download binary " + assignment.getBinary() + " from " + installation.getBinaryFile(), e);
}
// unpack the binary into a temp unpack dir
File unpackDir = new File(tempDir, "unpack");
unpackDir.mkdirs();
try {
extractTar(binary, unpackDir, tarTimeout);
}
catch (CommandFailedException e) {
throw new RuntimeException("Unable to extract tar file " + assignment.getBinary() + ": " + e.getMessage());
}
// find the archive root dir (it should be the only file in the temp unpack dir)
List<File> files = listFiles(unpackDir);
if (files.size() != 1) {
throw new RuntimeException("Invalid tar file: file does not have a root directory " + assignment.getBinary());
}
File binaryRootDir = files.get(0);
// unpack config bundle
try {
URL url = installation.getConfigFile().toURL();
ConfigUtils.unpackConfig(Resources.asByteSource(url), binaryRootDir);
}
catch (Exception e) {
throw new RuntimeException("Unable to extract config bundle " + assignment.getConfig() + ": " + e.getMessage());
}
// installation is good, clear the current deployment
if (this.deployment != null) {
this.deploymentFile.delete();
deleteRecursively(this.deployment.getDeploymentDir());
this.deployment = null;
}
// save deployment versions file
try {
save(newDeployment);
}
catch (IOException e) {
throw new RuntimeException("Unable to save deployment file", e);
}
// move the binary root directory to the final target
try {
Files.move(binaryRootDir, deploymentDir);
}
catch (IOException e) {
throw new RuntimeException("Unable to move deployment to final location", e);
}
}
finally {
if (!deleteRecursively(tempDir)) {
log.warn("Unable to delete temp directory: %s", tempDir.getAbsolutePath());
}
}
this.deployment = newDeployment;
return newDeployment;
}
@Override
public Deployment getDeployment()
{
return deployment;
}
@Override
public void terminate()
{
deleteRecursively(baseDir);
deployment = null;
}
@Override
public File hackGetDataDir()
{
return getDataDir();
}
public void save(Deployment deployment)
throws IOException
{
String json = jsonCodec.toJson(DeploymentRepresentation.from(deployment));
Files.write(json, deploymentFile, UTF_8);
}
public Deployment load(File deploymentFile)
throws IOException
{
String json = Files.toString(deploymentFile, UTF_8);
DeploymentRepresentation data = jsonCodec.fromJson(json);
File deploymentDir = new File(baseDir, "installation");
if (!deploymentDir.isDirectory()) {
deploymentDir = new File(baseDir, "deployment");
}
Deployment deployment = data.toDeployment(deploymentDir, getDataDir(), location);
return deployment;
}
private File getDataDir()
{
File dataDir = new File(baseDir, "data");
dataDir.mkdirs();
if (!dataDir.isDirectory()) {
throw new RuntimeException(String.format("Unable to create data dir %s", dataDir.getAbsolutePath()));
}
return dataDir;
}
}