com.sun.enterprise.admin.cli.cluster.SynchronizeInstanceCommand Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of payara-micro Show documentation
Show all versions of payara-micro Show documentation
Micro Distribution of the Payara Project
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2013 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
// Portions Copyright [2019-2021] Payara Foundation and/or affiliates
package com.sun.enterprise.admin.cli.cluster;
import com.sun.enterprise.admin.cli.CLIConstants;
import com.sun.enterprise.admin.cli.remote.RemoteCLICommand;
import java.io.*;
import java.net.ConnectException;
import java.text.DateFormat;
import java.util.*;
import java.util.logging.*;
import java.util.zip.*;
import jakarta.xml.bind.*;
import org.jvnet.hk2.annotations.Service;
import org.glassfish.api.Param;
import org.glassfish.api.admin.*;
import com.sun.enterprise.util.cluster.SyncRequest;
import com.sun.enterprise.util.io.FileUtils;
import org.glassfish.common.util.admin.AuthTokenManager;
import org.glassfish.hk2.api.PerLookup;
/**
* Synchronize a local server instance.
*/
@Service(name = "_synchronize-instance")
@PerLookup
public class SynchronizeInstanceCommand extends LocalInstanceCommand {
@Param(name = "instance_name", primary = true, optional = true)
private String instanceName0;
@Param(name = "sync", optional = true, defaultValue = "normal",
acceptableValues = "none, normal, full")
protected String sync="normal";
private RemoteCLICommand syncCmd = null;
private enum SyncLevel { TOP, FILES, DIRECTORY, RECURSIVE }
// the name of the sync state file, relative to the instance directory
private static final String SYNC_STATE_FILE = ".syncstate";
@Override
protected void validate() throws CommandException {
if (ok(instanceName0))
instanceName = instanceName0;
super.validate();
}
/**
*/
@Override
protected int executeCommand() throws CommandException {
if (synchronizeInstance()) {
return SUCCESS;
} else {
logger.info(Strings.get("Sync.failed",
programOpts.getHost(),
Integer.toString(programOpts.getPort())));
return ERROR;
}
}
/**
* Synchronize this server instance. Return true if server is synchronized.
* Return false if synchronization failed, but no files were changed
* (meaning that it is ok to bring the server up).
* Throw a CommandException if synchronization failed in such a way that
* instance startup should not be attempted.
*/
protected boolean synchronizeInstance() throws CommandException {
File dasProperties = getServerDirs().getDasPropertiesFile();
if (logger.isLoggable(Level.FINER))
logger.finer("das.properties: " + dasProperties);
if (!dasProperties.exists()) {
logger.info(
Strings.get("Sync.noDASConfigured", dasProperties.toString()));
return false;
}
setDasDefaults(dasProperties);
/*
* Create the remote command object that we'll reuse for each request.
*/
/*
* Because we reuse the command, we also need to reuse the auth token
* (if one is present).
*/
final String origAuthToken = programOpts.getAuthToken();
if (origAuthToken != null) {
programOpts.setAuthToken(AuthTokenManager.markTokenForReuse(origAuthToken));
}
syncCmd = new RemoteCLICommand("_synchronize-files", programOpts, env);
syncCmd.setFileOutputDirectory(instanceDir);
/*
* The sync state file records the fact that we're in the middle
* of a synchronization attempt. When we're done, we remove it.
* If we crash, it will be left behind, and the next sync attempt
* will notice it and force a full sync.
*/
File syncState = new File(instanceDir, SYNC_STATE_FILE);
boolean doFullSync = false;
if (sync.equals("normal") && syncState.exists()) {
String lastSync = DateFormat.getDateTimeInstance().format(
new Date(syncState.lastModified()));
logger.info(Strings.get("Sync.fullRequired", lastSync));
doFullSync = true;
}
/*
* Create the sync state file to indicate that
* we've started synchronization. If the file
* already exists (e.g., from a previous failed
* synchronization attempt), that's fine.
*/
try {
syncState.createNewFile();
} catch (IOException ex) {
logger.warning(
Strings.get("Sync.cantCreateSyncState", syncState));
}
/*
* If --sync full, we remove all local state related to the instance,
* then do a sync. We only remove the local directories that are
* synchronized from the DAS; any other local directories (logs,
* instance-private state) are left alone.
*/
if (sync.equals("full") || doFullSync) {
if (logger.isLoggable(Level.FINE))
logger.fine(Strings.get("Instance.fullsync", instanceName));
removeSubdirectory("config");
removeSubdirectory("applications");
removeSubdirectory("generated");
removeSubdirectory("lib");
removeSubdirectory("docroot");
removeSubdirectory("endpoints");
}
File domainXml =
new File(new File(instanceDir, "config"), "domain.xml");
long dtime = domainXml.exists() ? domainXml.lastModified() : -1;
File docroot = new File(instanceDir, "docroot");
File endpoints = new File(instanceDir, "endpoints");
CommandException exc = null;
try {
/*
* First, synchronize the config directory.
*/
SyncRequest sr = getModTimes("config", SyncLevel.FILES);
synchronizeFiles(sr);
/*
* Was domain.xml updated?
* If not, we're all done.
*/
if (domainXml.lastModified() == dtime) {
if (logger.isLoggable(Level.FINE))
logger.fine(Strings.get("Sync.alreadySynced"));
if (!syncState.delete())
logger.warning(Strings.get("Sync.cantDeleteSyncState", syncState));
/*
* Note that we earlier marked the token for reuse. It's OK
* to return immediately here with the DAS still willing to
* accept the same token again. The token will expire and be
* cleaned up in a little while and it was never exposed in a
* way that could be intercepted and used illicitly.
*/
return true;
}
/*
* Now synchronize the applications.
*/
sr = getModTimes("applications", SyncLevel.DIRECTORY);
synchronizeFiles(sr);
/*
* Did we get any archive files? If so,
* have to unzip them in the applications
* directory.
*/
File appsDir = new File(instanceDir, "applications");
File archiveDir = new File(appsDir, "__internal");
for (File adir : FileUtils.listFiles(archiveDir)) {
File[] af = FileUtils.listFiles(adir);
if (af.length != 1) {
if (logger.isLoggable(Level.FINER))
logger.log(Level.FINER, "IGNORING {0}, # files {1}", new Object[]{adir, af.length});
continue;
}
File archive = af[0];
File appDir = new File(appsDir, adir.getName());
if (logger.isLoggable(Level.FINER))
logger.log(Level.FINER, "UNZIP {0} TO {1}", new Object[]{archive, appDir});
try {
expand(appDir, archive);
} catch (Exception ex) { }
}
FileUtils.whack(archiveDir);
/*
* Next, the libraries.
* We assume there's usually very few files in the
* "lib" directory so we check them all individually.
*/
sr = getModTimes("lib", SyncLevel.RECURSIVE);
synchronizeFiles(sr);
/*
* Next, the docroot.
* The docroot could be full of files, so we only check
* one level.
*/
sr = getModTimes("docroot", SyncLevel.DIRECTORY);
synchronizeFiles(sr);
/*
* Next, the endpoints.
* The endpoints too could be full of files, so we only check
* one level.
*/
sr = getModTimes("endpoints", SyncLevel.DIRECTORY);
synchronizeFiles(sr);
/*
* Check any subdirectories of the instance config directory.
* We only expect one - the config-specific directory,
* but since we don't have an easy way of knowing the
* name of that directory, we include them all. The
* DAS will tell us to remove anything that shouldn't
* be there.
*/
sr = new SyncRequest();
sr.instance = instanceName;
sr.dir = "config-specific";
File configDir = new File(instanceDir, "config");
for (File f : configDir.listFiles()) {
if (!f.isDirectory())
continue;
getFileModTimes(f, configDir, sr, SyncLevel.DIRECTORY);
}
/*
* Before sending the last sync request revert to using the original
* auth token, if one is present. The token would be retired
* later when it expires anyway, but this is just a little cleaner.
*/
if (origAuthToken != null) {
syncCmd.getProgramOptions().setAuthToken(origAuthToken);
}
synchronizeFiles(sr);
} catch (ConnectException cex) {
if (logger.isLoggable(Level.FINER))
logger.finer("Couldn't connect to DAS: " + cex);
/*
* Don't chain the exception, otherwise asadmin will think it
* it was a connect failure and will list the closest matching
* local command. Not what we want here.
*/
exc = new CommandException(Strings.get("Sync.connectFailed", cex.getMessage()));
} catch (CommandException ex) {
if (logger.isLoggable(Level.FINER))
logger.finer("Exception during synchronization: " + ex);
exc = ex;
}
if (exc != null) {
/*
* Some unexpected failure. If the domain.xml hasn't
* changed, assume no local state has changed and it's safe
* to remove the sync state file. Otherwise, something has
* changed, and we don't know how much has changed, so leave
* the sync state file so we'll do a full sync the next time.
* If nothing has changed, allow the server to come up.
*/
if (domainXml.exists() && domainXml.lastModified() == dtime &&
docroot.isDirectory() && endpoints.isDirectory()) {
// nothing changed and sync has completed at least once
if (!syncState.delete())
logger.warning(
Strings.get("Sync.cantDeleteSyncState", syncState));
return false;
}
throw exc;
}
/*
* Success! Remove sync state file.
*/
if (!syncState.delete())
logger.warning(Strings.get("Sync.cantDeleteSyncState", syncState));
return true;
}
/**
* Return a SyncRequest with the mod times for all the
* files in the specified directory.
*/
private SyncRequest getModTimes(String dir, SyncLevel level) {
SyncRequest sr = new SyncRequest();
sr.instance = instanceName;
sr.dir = dir;
File fdir = new File(instanceDir, dir);
if (!fdir.exists())
return sr;
getFileModTimes(fdir, fdir, sr, level);
return sr;
}
/**
* Get the mod times for the entries in dir and add them to the
* SyncRequest, using names relative to baseDir. If level is
* RECURSIVE, check subdirectories and only include times for files,
* not directories.
*/
private void getFileModTimes(File dir, File baseDir, SyncRequest sr,
SyncLevel level) {
if (level == SyncLevel.TOP) {
long time = dir.lastModified();
SyncRequest.ModTime mt = new SyncRequest.ModTime(".", time);
sr.files.add(mt);
return;
}
for (String file : dir.list()) {
File f = new File(dir, file);
long time = f.lastModified();
if (time == 0)
continue;
if (f.isDirectory()) {
if (level == SyncLevel.RECURSIVE) {
getFileModTimes(f, baseDir, sr, level);
continue;
} else if (level == SyncLevel.FILES)
continue;
}
String name = baseDir.toURI().relativize(f.toURI()).getPath();
// if name is a directory, it will end with "/"
if (name.endsWith("/"))
name = name.substring(0, name.length() - 1);
SyncRequest.ModTime mt = new SyncRequest.ModTime(name, time);
sr.files.add(mt);
if (logger.isLoggable(Level.FINER))
logger.finer(f + ": mod time " + mt.time);
}
}
/**
* Ask the server to synchronize the files in the SyncRequest.
*/
private void synchronizeFiles(SyncRequest sr)
throws CommandException, ConnectException {
File tempFile = null;
try {
tempFile = File.createTempFile("mt.", ".xml");
FileUtils.deleteOnExit(tempFile);
JAXBContext context = JAXBContext.newInstance(SyncRequest.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty("jaxb.formatted.output", Boolean.TRUE);
marshaller.marshal(sr, tempFile);
if (logger.isLoggable(Level.FINER))
marshaller.marshal(sr, System.out);
File syncdir = new File(instanceDir, sr.dir);
if (logger.isLoggable(Level.FINER))
logger.finer("Sync directory: " + syncdir);
// Determine if this is a docker node
boolean dockerNode = false;
File nodePropertiesFile = getServerDirs().getNodePropertiesFile();
if (nodePropertiesFile.exists()) {
Properties nodeProperties = getNodeProperties(nodePropertiesFile);
dockerNode = Boolean.valueOf(nodeProperties.getProperty(CLIConstants.K_DOCKER_NODE, "false"));
}
// _synchronize-files takes a single operand of type File, though when working with files a hidden
// "upload" parameter option gets added
// Note: we throw the output away to avoid printing a blank line
if (dockerNode) {
syncCmd.executeAndReturnOutput("_synchronize-files", "--upload=true",
tempFile.getPath());
} else {
syncCmd.executeAndReturnOutput("_synchronize-files",
tempFile.getPath());
}
// the returned files are automatically saved by the command
} catch (IOException ex) {
if (logger.isLoggable(Level.FINER))
logger.finer("Got exception: " + ex);
throw new CommandException(
Strings.get("Sync.dirFailed", sr.dir, ex.toString()), ex);
} catch (JAXBException jex) {
if (logger.isLoggable(Level.FINER))
logger.finer("Got exception: " + jex);
throw new CommandException(
Strings.get("Sync.dirFailed", sr.dir, jex.toString()), jex);
} catch (CommandException cex) {
Throwable cause = cex.getCause();
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Got exception: {0}", cex);
logger.log(Level.FINER, " cause: {0}", cause);
}
if (cause instanceof ConnectException)
throw (ConnectException)cause;
throw new CommandException(
Strings.get("Sync.dirFailed", sr.dir, cex.getMessage()), cex);
} finally {
// remove tempFile
if (tempFile != null) {
if (!tempFile.delete())
logger.warning(
Strings.get("Sync.cantDeleteTempFile", tempFile));
}
}
}
/**
* Remove the named subdirectory of the instance directory.
*/
private void removeSubdirectory(String name) {
File subdir = new File(instanceDir, name);
if (logger.isLoggable(Level.FINER))
logger.finer("Removing: " + subdir);
FileUtils.whack(subdir);
}
/**
* Expand the archive to the specified directory.
* XXX - this doesn't handle all the cases required for a Java EE app,
* but it's good enough for now for some performance testing
*/
private static void expand(File dir, File archive) throws Exception {
if (!dir.mkdir()) {
logger.warning(Strings.get("Sync.cantCreateDirectory", dir));
}
long modtime = archive.lastModified();
try (ZipFile zf = new ZipFile(archive)) {
Enumeration extends ZipEntry> e = zf.entries();
while (e.hasMoreElements()) {
ZipEntry ze = e.nextElement();
File entry = new File(dir, ze.getName());
if (ze.isDirectory()) {
if (!entry.mkdir())
logger.warning(Strings.get("Sync.cantCreateDirectory", dir));
} else {
FileUtils.copy(zf.getInputStream(ze), new FileOutputStream(entry), 0);
}
}
}
if (!dir.setLastModified(modtime))
logger.warning(Strings.get("Sync.cantSetModTime", dir));
}
}