
com.sun.enterprise.v3.admin.cluster.SecureAdminBootstrapHelper Maven / Gradle / Ivy
/*
* Copyright (c) 2022, 2023 Contributors to the Eclipse Foundation
* Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package com.sun.enterprise.v3.admin.cluster;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpException;
import com.sun.enterprise.config.serverbeans.Node;
import com.sun.enterprise.util.cluster.RemoteType;
import com.sun.enterprise.util.io.FileUtils;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.glassfish.cluster.ssh.launcher.SSHLauncher;
import org.glassfish.cluster.ssh.sftp.SFTPClient;
import org.glassfish.hk2.api.ServiceLocator;
/**
* Bootstraps the secure admin-related files, either over ssh (copying files from the
* current runtime environment to the remote system via secure ftp) or locally
* (using more straightforward file-copying).
*
* @author Tim Quinn
*/
public abstract class SecureAdminBootstrapHelper {
private static final String DOMAIN_XML_PATH = "config/domain.xml";
private static final String[] SECURE_ADMIN_FILE_REL_URIS_TO_COPY = new String[]{
DOMAIN_XML_PATH,
"config/keystore.jks",
"config/cacerts.jks"
};
private static final String[] SECURE_ADMIN_FILE_DIRS_TO_CREATE = new String[]{
"config"
};
/**
* Creates a new helper for delivering files needed for secure admin to
* the remote instance.
*
* @param habitat hk2 habitat
* @param DASInstanceDir directory of the local instance - source for the required files
* @param remoteNodeDir directory of the remote node on the remote system
* @param instance name of the instance on the remote node to bootstrap
* @param node Node from the domain configuration for the target node
* @param logger
* @return the remote helper
* @throws BootstrapException
*/
public static SecureAdminBootstrapHelper getRemoteHelper(
final ServiceLocator habitat,
final File DASInstanceDir,
final String remoteNodeDir,
final String instance,
final Node node,
final Logger logger) throws BootstrapException {
RemoteType type = null;
try {
// this also handles the case where node is null
type = RemoteType.valueOf(node.getType());
}
catch (Exception e) {
throw new IllegalArgumentException(
Strings.get("internal.error", "unknown type"));
}
switch (type) {
case SSH:
return new SSHHelper(
habitat,
DASInstanceDir,
remoteNodeDir,
instance,
node,
logger);
default:
throw new IllegalArgumentException(
Strings.get("internal.error", "A new type must have "
+ "been added --> unknown type: " + type.toString()));
}
}
/**
* Creates a new helper for delivering files needed for secure admin to
* the local instance (local meaning on the same node as the DAS).
*
* @param existingInstanceDir directory of an existing instance (typically the DAS) from where the files can be copied
* @param newInstanceDir directory of the new instance to where the files will be copied
*
* @return the local helper
*/
public static SecureAdminBootstrapHelper getLocalHelper(
final File existingInstanceDir,
final File newInstanceDir) {
return new LocalHelper(existingInstanceDir, newInstanceDir);
}
/**
* Cleans up any allocated resources.
*/
protected abstract void mkdirs(String dirURI) throws IOException;
protected abstract void close();
/**
* Copies the bootstrap files from their origin to their destination.
*
* Concrete subclasses implement this differently, depending on exactly
* how they actually transfer the files.
*
* @throws FileNotFoundException
* @throws IOException
*/
protected abstract void copyBootstrapFiles() throws FileNotFoundException, IOException;
/**
* Adjusts the date on the new instance's domain.xml so it looks older
* than the original one on the DAS.
*
* We have copied the domain.xml and a small number of other files to the
* instance, but not all the files needed for the instance to be fully
* sync-ed. The sync logic decides if an instance is up-to-date by comparing
* the timestamp of the instance's domain.xml with that of the DAS. If
* those timestamps match then the sync logic judges the
* instance to be up-to-date. When we copy the DAS domain.xml to the instance
* to deliver the secure admin configuration (so the start-local-instance
* command and the instance will know how to connect to the DAS) it is left
* with the same timestamp as the DAS copy. To make sure sync works when
* start-local-instance runs, we backdate the instance's copy of domain.xml.
*
* @throws com.sun.enterprise.v3.admin.cluster.SecureAdminBootstrapHelper.BootstrapException
*/
protected abstract void backdateInstanceDomainXML() throws BootstrapException;
/**
* Bootstraps the instance for remote admin.
*
* @throws IOException
*/
public void bootstrapInstance() throws BootstrapException {
try {
mkdirs();
copyBootstrapFiles();
backdateInstanceDomainXML();
}
catch (Exception ex) {
throw new BootstrapException(ex);
}
}
private void mkdirs() throws IOException {
for (String dirPath : SECURE_ADMIN_FILE_DIRS_TO_CREATE) {
mkdirs(dirPath);
}
}
/**
* Implements the helper functionality for a remote instance.
*/
private static abstract class RemoteHelper extends SecureAdminBootstrapHelper {
final Logger logger;
final File dasInstanceDir;
final String instance;
final String remoteNodeDir;
final String remoteInstanceDir;
RemoteHelper(
final ServiceLocator habitat,
final File dasInstanceDir,
String remoteNodeDir,
final String instance,
final Node node,
final Logger logger) throws BootstrapException {
this.dasInstanceDir = dasInstanceDir;
this.instance = instance;
this.logger = logger;
this.remoteNodeDir = remoteNodeDirUnixStyle(node, remoteNodeDir);
remoteInstanceDir = remoteInstanceDir(this.remoteNodeDir);
}
// private long dasDomainXMLTimestamp(final File dasInstanceDir) {
// return new File(dasInstanceDir.toURI().resolve(DOMAIN_XML_PATH)).lastModified();
// }
abstract void writeToFile(final String path, final InputStream content) throws IOException;
abstract void setLastModified(final String path, final long when) throws IOException;
String ensureTrailingSlash(final String path) {
if (!path.endsWith("/")) {
return path + "/";
}
else {
return path;
}
}
String remoteNodeDirUnixStyle(final Node node, final String remoteNodeDir) {
/*
* Use the node dir if it was specified when the node was created.
* Otherwise derive it: ${remote-install-dir}/glassfish/${node-name}
*/
String result;
if (remoteNodeDir != null) {
result = remoteNodeDir;
}
else {
result = new StringBuilder(ensureTrailingSlash(node.getInstallDirUnixStyle())).append("glassfish/nodes/").append(node.getName()).toString();
}
return ensureTrailingSlash(result.replaceAll("\\\\", "/"));
}
String remoteInstanceDir(final String remoteNodeDirPath) {
final StringBuilder remoteInstancePath = new StringBuilder(remoteNodeDirPath);
if (!remoteNodeDirPath.endsWith("/")) {
remoteInstancePath.append("/");
}
remoteInstancePath.append(instance).append("/");
return remoteInstancePath.toString().replaceAll("\\\\", "/");
}
@Override
protected void copyBootstrapFiles() throws FileNotFoundException, IOException {
for (String fileRelativePath : SECURE_ADMIN_FILE_REL_URIS_TO_COPY) {
InputStream is = null;
String remoteFilePath = null;
try {
is = new BufferedInputStream(
new FileInputStream(
new File(dasInstanceDir.toURI().resolve(fileRelativePath))));
remoteFilePath = remoteInstanceDir + fileRelativePath;
writeToFile(remoteFilePath, is);
logger.log(Level.FINE, "Copied bootstrap file to {0}", remoteFilePath);
}
catch (Exception ex) {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "Error copying bootstrap file to " + remoteFilePath, ex);
}
throw new IOException(ex);
}
finally {
if (is != null) {
is.close();
}
}
}
}
/**
* Returns the specified system time in seconds since 01 Jan 1970.
*
* @param milliseconds normal Java time (in milliseconds)
* @return
*/
int secondsSince_01_Jan_1970(final long milliseconds) {
return (int) (milliseconds) / 1000;
}
}
private static class SSHHelper extends RemoteHelper {
final SFTPClient ftpClient;
final SSHLauncher launcher;
private SSHHelper(
final ServiceLocator habitat,
final File dasInstanceDir,
String remoteNodeDir,
final String instance,
final Node node,
final Logger logger) throws BootstrapException {
super(habitat, dasInstanceDir, remoteNodeDir, instance, node, logger);
launcher = habitat.getService(SSHLauncher.class);
launcher.init(node, logger);
try {
ftpClient = launcher.getSFTPClient();
}
catch (JSchException ex) {
throw new BootstrapException(launcher, ex);
}
}
@Override
protected void mkdirs(String dir) throws IOException {
String remoteDir = remoteInstanceDir + dir;
logger.log(Level.FINE, "Trying to create directories for remote path {0}",
remoteDir);
Integer instanceDirPermissions;
try {
instanceDirPermissions = ftpClient.getSftpChannel().lstat(remoteNodeDir).getPermissions();
}
catch (SftpException ex) {
throw new IOException(remoteNodeDir, ex);
}
logger.log(Level.FINE, "Creating remote bootstrap directory "
+ remoteDir + " with permissions "
+ instanceDirPermissions.toString());
try {
ftpClient.mkdirs(remoteDir, instanceDirPermissions);
}
catch (SftpException ex) {
throw new IOException(remoteDir, ex);
}
}
@Override
protected void close() {
if (ftpClient != null) {
ftpClient.close();
}
}
@Override
void writeToFile(final String path, final InputStream content) throws IOException {
try {
ftpClient.getSftpChannel().put(content, path);
}
catch (SftpException ex) {
throw new IOException(ex);
}
}
/* bnevins -- this method had to be made abstract ONLY because of the
* annoying special exception constructor that is SSH-specific.
*/
@Override
protected void backdateInstanceDomainXML() throws BootstrapException {
final String remoteDomainXML = remoteInstanceDir + DOMAIN_XML_PATH;
try {
setLastModified(remoteDomainXML, 0);
}
catch (IOException ex) {
throw new BootstrapException(launcher, ex);
}
logger.log(Level.FINE, "Backdated the instance's copy of domain.xml");
}
@Override
void setLastModified(final String path, final long when) throws IOException {
/*
* Times over ssh are expressed as seconds since 01 Jan 1970.
*/
try {
ftpClient.getSftpChannel().setMtime(path, secondsSince_01_Jan_1970(when));
} catch (SftpException e) {
throw new IOException(e);
}
}
}
/**
* Implements the helper for a local instance (one co-located with the DAS).
*/
private static class LocalHelper extends SecureAdminBootstrapHelper {
private final URI existingInstanceDirURI;
private final URI newInstanceDirURI;
private LocalHelper(final File existingInstanceDir, final File newInstanceDir) {
this.existingInstanceDirURI = existingInstanceDir.toURI();
this.newInstanceDirURI = newInstanceDir.toURI();
}
@Override
protected void mkdirs(String dir) {
final File newDir = new File(newInstanceDirURI.resolve(dir));
if (!newDir.exists() && !newDir.mkdirs()) {
throw new RuntimeException(Strings.get("secure.admin.boot.errCreDir", newDir.getAbsolutePath()));
}
}
@Override
public void copyBootstrapFiles() throws IOException {
for (String relativePathToFile : SECURE_ADMIN_FILE_REL_URIS_TO_COPY) {
final File origin = new File(existingInstanceDirURI.resolve(relativePathToFile));
final File dest = new File(newInstanceDirURI.resolve(relativePathToFile));
FileUtils.copy(origin, dest);
}
}
@Override
protected void backdateInstanceDomainXML() throws BootstrapException {
final File newDomainXMLFile = new File(newInstanceDirURI.resolve(DOMAIN_XML_PATH));
if (!newDomainXMLFile.setLastModified(0)) {
throw new RuntimeException(Strings.get("secure.admin.boot.errSetLastMod", newDomainXMLFile.getAbsolutePath()));
}
}
@Override
protected void close() {
// Nothing to do for local provider
return;
}
}
public static class BootstrapException extends Exception {
private transient final SSHLauncher launcher;
public BootstrapException(final SSHLauncher launcher, final Exception ex) {
super(ex);
this.launcher = launcher;
}
public BootstrapException(final Exception ex) {
super(ex);
launcher = null;
}
public BootstrapException(final String msg) {
super(msg);
launcher = null;
}
public String sshSettings() {
return (launcher != null ? launcher.toString() : "");
}
}
}