blob: 18676df7e1821c5d12b2ec7862f861a570c40d5a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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 org.apache.geode.management.internal.cli.commands;
import static org.apache.commons.io.FileUtils.ONE_MB;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import com.healthmarketscience.rmiio.RemoteInputStream;
import com.healthmarketscience.rmiio.SimpleRemoteInputStream;
import com.healthmarketscience.rmiio.exporter.RemoteStreamExporter;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.InternalConfigurationPersistenceService;
import org.apache.geode.management.cli.CliMetaData;
import org.apache.geode.management.cli.ConverterHint;
import org.apache.geode.management.cli.GfshCommand;
import org.apache.geode.management.cli.Result;
import org.apache.geode.management.internal.ManagementAgent;
import org.apache.geode.management.internal.SystemManagementService;
import org.apache.geode.management.internal.cli.AbstractCliAroundInterceptor;
import org.apache.geode.management.internal.cli.GfshParseResult;
import org.apache.geode.management.internal.cli.domain.DeploymentInfo;
import org.apache.geode.management.internal.cli.functions.DeployFunction;
import org.apache.geode.management.internal.cli.remote.CommandExecutionContext;
import org.apache.geode.management.internal.cli.remote.CommandExecutor;
import org.apache.geode.management.internal.cli.result.model.FileResultModel;
import org.apache.geode.management.internal.cli.result.model.ResultModel;
import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
import org.apache.geode.management.internal.cli.util.DeploymentInfoTableUtil;
import org.apache.geode.management.internal.functions.CliFunctionResult;
import org.apache.geode.management.internal.i18n.CliStrings;
import org.apache.geode.management.internal.security.ResourceOperation;
import org.apache.geode.management.internal.util.ManagementUtils;
import org.apache.geode.management.internal.utils.JarFileUtils;
import org.apache.geode.security.ResourcePermission;
public class DeployCommand extends GfshCommand {
private final DeployFunction deployFunction = new DeployFunction();
/**
* Deploy one or more JAR files to members of a group or all members.
*
* @param groups Group(s) to deploy the JAR to or null for all members
* @param jars JAR file to deploy
* @param dir Directory of JAR files to deploy
* @return The result of the attempt to deploy
*/
@CliCommand(value = {CliStrings.DEPLOY}, help = CliStrings.DEPLOY__HELP)
@CliMetaData(
interceptor = "org.apache.geode.management.internal.cli.commands.DeployCommand$Interceptor",
isFileUploaded = true, relatedTopic = {CliStrings.TOPIC_GEODE_CONFIG})
@ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
operation = ResourcePermission.Operation.MANAGE, target = ResourcePermission.Target.DEPLOY)
public ResultModel deploy(
@CliOption(key = {CliStrings.GROUP, CliStrings.GROUPS}, help = CliStrings.DEPLOY__GROUP__HELP,
optionContext = ConverterHint.MEMBERGROUP) String[] groups,
@CliOption(key = {CliStrings.JAR, CliStrings.JARS}, optionContext = ConverterHint.JARFILES,
help = CliStrings.DEPLOY__JAR__HELP) String[] jars,
@CliOption(key = {CliStrings.DEPLOY__DIR}, optionContext = ConverterHint.JARDIR,
help = CliStrings.DEPLOY__DIR__HELP) String dir)
throws IOException {
ResultModel result = new ResultModel();
TabularResultModel deployResult = result.addTable("deployResult");
List<String> jarFullPaths = CommandExecutionContext.getFilePathFromShell();
verifyJarContent(jarFullPaths);
Set<DistributedMember> targetMembers;
targetMembers = findMembers(groups, null);
List<List<Object>> results = new LinkedList<>();
ManagementAgent agent = ((SystemManagementService) getManagementService()).getManagementAgent();
RemoteStreamExporter exporter = agent.getRemoteStreamExporter();
results = deployJars(jarFullPaths, targetMembers, results, exporter);
List<CliFunctionResult> cleanedResults = CliFunctionResult.cleanResults(results);
List<DeploymentInfo> deploymentInfos =
DeploymentInfoTableUtil.getDeploymentInfoFromFunctionResults(cleanedResults);
DeploymentInfoTableUtil.writeDeploymentInfoToTable(
new String[] {"Member", "JAR", "JAR Location"}, deployResult,
deploymentInfos);
if (result.getStatus() == Result.Status.OK) {
InternalConfigurationPersistenceService sc = getConfigurationPersistenceService();
if (sc == null) {
result.addInfo().addLine(CommandExecutor.SERVICE_NOT_RUNNING_CHANGE_NOT_PERSISTED);
} else {
sc.addJarsToThisLocator(jarFullPaths, groups);
}
}
return result;
}
private List<List<Object>> deployJars(List<String> jarFullPaths,
Set<DistributedMember> targetMembers,
List<List<Object>> results,
RemoteStreamExporter exporter)
throws FileNotFoundException, java.rmi.RemoteException {
for (DistributedMember member : targetMembers) {
List<RemoteInputStream> remoteStreams = new ArrayList<>();
List<String> jarNames = new ArrayList<>();
try {
for (String jarFullPath : jarFullPaths) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(jarFullPath);
remoteStreams.add(exporter.export(new SimpleRemoteInputStream(fileInputStream)));
jarNames.add(FilenameUtils.getName(jarFullPath));
} catch (Exception ex) {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException ignore) {
}
}
throw ex;
}
}
// this deploys the jars to all the matching servers
ResultCollector<?, ?> resultCollector =
executeFunction(deployFunction,
new Object[] {jarNames, remoteStreams}, member);
@SuppressWarnings("unchecked")
final List<List<Object>> resultCollectorResult =
(List<List<Object>>) resultCollector.getResult();
results.add(resultCollectorResult.get(0));
} finally {
for (RemoteInputStream ris : remoteStreams) {
try {
ris.close(true);
} catch (IOException ex) {
// Ignored. the stream may have already been closed.
}
}
}
}
return results;
}
private void verifyJarContent(List<String> jarNames) {
for (String jarName : jarNames) {
File jar = new File(jarName);
if (!JarFileUtils.hasValidJarContent(jar)) {
throw new IllegalArgumentException(
"File does not contain valid JAR content: " + jar.getName());
}
}
}
@Override
public boolean affectsClusterConfiguration() {
return true;
}
/**
* Interceptor used by gfsh to intercept execution of deploy command at "shell".
*/
public static class Interceptor extends AbstractCliAroundInterceptor {
private final DecimalFormat numFormatter = new DecimalFormat("###,##0.00");
/**
*
* @return FileResult object or ResultModel in case of errors
*/
@Override
public ResultModel preExecution(GfshParseResult parseResult) {
String[] jars = (String[]) parseResult.getParamValue("jar");
String dir = (String) parseResult.getParamValue("dir");
if (ArrayUtils.isEmpty(jars) && StringUtils.isBlank(dir)) {
return ResultModel.createError(
"Parameter \"jar\" or \"dir\" is required. Use \"help <command name>\" for assistance.");
}
if (ArrayUtils.isNotEmpty(jars) && StringUtils.isNotBlank(dir)) {
return ResultModel.createError("Parameters \"jar\" and \"dir\" can not both be specified.");
}
ResultModel result = new ResultModel();
if (jars != null) {
for (String jar : jars) {
File jarFile = new File(jar);
if (!jarFile.exists()) {
return ResultModel.createError(jar + " not found.");
}
result.addFile(jarFile, FileResultModel.FILE_TYPE_FILE);
}
} else {
File fileDir = new File(dir);
if (!fileDir.isDirectory()) {
return ResultModel.createError(dir + " is not a directory");
}
File[] childJarFile = fileDir.listFiles(ManagementUtils.JAR_FILE_FILTER);
for (File file : childJarFile) {
result.addFile(file, FileResultModel.FILE_TYPE_FILE);
}
}
// check if user wants to upload with the computed file size
String message =
"\nDeploying files: " + result.getFormattedFileList() + "\nTotal file size is: "
+ numFormatter.format((double) result.computeFileSizeTotal() / ONE_MB)
+ "MB\n\nContinue? ";
if (readYesNo(message, Response.YES) == Response.NO) {
return ResultModel.createError(
"Aborted deploy of " + result.getFormattedFileList() + ".");
}
return result;
}
}
}