| /* |
| * 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 java.util.stream.Collectors.joining; |
| import static java.util.stream.Collectors.toSet; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.text.SimpleDateFormat; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Set; |
| |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.transform.TransformerException; |
| |
| import org.apache.commons.io.FileUtils; |
| import org.apache.logging.log4j.Logger; |
| import org.springframework.shell.core.annotation.CliCommand; |
| import org.springframework.shell.core.annotation.CliOption; |
| import org.xml.sax.SAXException; |
| |
| import org.apache.geode.cache.execute.ResultCollector; |
| import org.apache.geode.distributed.ConfigurationPersistenceService; |
| import org.apache.geode.distributed.DistributedMember; |
| import org.apache.geode.distributed.internal.InternalConfigurationPersistenceService; |
| import org.apache.geode.logging.internal.log4j.api.LogService; |
| import org.apache.geode.management.cli.CliMetaData; |
| import org.apache.geode.management.cli.GfshCommand; |
| import org.apache.geode.management.internal.cli.AbstractCliAroundInterceptor; |
| import org.apache.geode.management.internal.cli.GfshParseResult; |
| import org.apache.geode.management.internal.cli.functions.CliFunctionResult; |
| import org.apache.geode.management.internal.cli.i18n.CliStrings; |
| import org.apache.geode.management.internal.cli.remote.CommandExecutionContext; |
| import org.apache.geode.management.internal.cli.result.model.FileResultModel; |
| import org.apache.geode.management.internal.cli.result.model.InfoResultModel; |
| 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.configuration.domain.Configuration; |
| import org.apache.geode.management.internal.configuration.functions.GetRegionNamesFunction; |
| import org.apache.geode.management.internal.configuration.functions.RecreateCacheFunction; |
| import org.apache.geode.management.internal.configuration.utils.ZipUtils; |
| import org.apache.geode.management.internal.security.ResourceOperation; |
| import org.apache.geode.security.ResourcePermission.Operation; |
| import org.apache.geode.security.ResourcePermission.Resource; |
| |
| /** |
| * Commands for the cluster configuration |
| */ |
| @SuppressWarnings("unused") |
| public class ImportClusterConfigurationCommand extends GfshCommand { |
| public static final Logger logger = LogService.getLogger(); |
| public static final String XML_FILE = "xml-file"; |
| public static final String ACTION = "action"; |
| public static final String ACTION_HELP = |
| "What to do with the running servers if any. APPLY would try to apply the configuration to the empty servers. STAGE would leave the running servers alone."; |
| |
| public enum Action { |
| APPLY, STAGE |
| }; |
| |
| @CliCommand(value = {CliStrings.IMPORT_SHARED_CONFIG}, |
| help = CliStrings.IMPORT_SHARED_CONFIG__HELP) |
| @CliMetaData( |
| interceptor = "org.apache.geode.management.internal.cli.commands.ImportClusterConfigurationCommand$ImportInterceptor", |
| isFileUploaded = true, relatedTopic = {CliStrings.TOPIC_GEODE_CONFIG}) |
| @ResourceOperation(resource = Resource.CLUSTER, operation = Operation.MANAGE) |
| @SuppressWarnings("unchecked") |
| public ResultModel importSharedConfig( |
| @CliOption(key = CliStrings.GROUP, |
| specifiedDefaultValue = ConfigurationPersistenceService.CLUSTER_CONFIG, |
| unspecifiedDefaultValue = ConfigurationPersistenceService.CLUSTER_CONFIG) String group, |
| @CliOption(key = XML_FILE) String xmlFile, |
| @CliOption(key = ACTION, help = ACTION_HELP, unspecifiedDefaultValue = "APPLY") Action action, |
| @CliOption(key = {CliStrings.IMPORT_SHARED_CONFIG__ZIP}, |
| help = CliStrings.IMPORT_SHARED_CONFIG__ZIP__HELP) String zip) |
| throws IOException, TransformerException, SAXException, ParserConfigurationException { |
| |
| if (!isSharedConfigurationRunning()) { |
| return ResultModel.createError("Cluster configuration service is not running."); |
| } |
| |
| InternalConfigurationPersistenceService ccService = |
| (InternalConfigurationPersistenceService) getConfigurationPersistenceService(); |
| Set<DistributedMember> servers = findMembers(group); |
| File file = getUploadedFile(); |
| |
| ResultModel result = new ResultModel(); |
| InfoResultModel infoSection = result.addInfo(ResultModel.INFO_SECTION); |
| ccService.lockSharedConfiguration(); |
| try { |
| if (action == Action.APPLY && servers.size() > 0) { |
| // make sure the servers are vanilla servers, users hasn't done anything on them. |
| // server might belong to multiple group, so we can't just check one group's xml is null, |
| // has to make sure |
| // all group's xml are null |
| if (ccService.hasXmlConfiguration()) { |
| return ResultModel.createError("Can not configure servers that are already configured."); |
| } |
| // if no existing cluster configuration, to be safe, further check to see if running |
| // servers has regions already defined |
| Set<String> regionsOnServers = servers.stream().map(this::getRegionNamesOnServer) |
| .flatMap(Collection::stream).collect(toSet()); |
| |
| if (!regionsOnServers.isEmpty()) { |
| return ResultModel.createError("Can not configure servers with existing regions: " |
| + regionsOnServers.stream().collect(joining(","))); |
| } |
| } |
| |
| // backup the old config |
| backupTheOldConfig(ccService); |
| |
| if (zip != null) { |
| Path tempDir = Files.createTempDirectory("config"); |
| ZipUtils.unzip(file.getAbsolutePath(), tempDir.toAbsolutePath().toString()); |
| // load it from the disk |
| ccService.loadSharedConfigurationFromDir(tempDir.toFile()); |
| FileUtils.deleteQuietly(tempDir.toFile()); |
| infoSection.addLine("Cluster configuration successfully imported."); |
| } else { |
| // update the xml in the cluster configuration service |
| Configuration configuration = ccService.getConfiguration(group); |
| if (configuration == null) { |
| configuration = new Configuration(group); |
| } |
| configuration.setCacheXmlFile(file); |
| ccService.setConfiguration(group, configuration); |
| logger.info( |
| configuration.getConfigName() + "xml content: \n" + configuration.getCacheXmlContent()); |
| infoSection.addLine( |
| "Successfully set the '" + group + "' configuration to the content of " + xmlFile); |
| } |
| } finally { |
| FileUtils.deleteQuietly(file); |
| ccService.unlockSharedConfiguration(); |
| } |
| |
| if (servers.size() > 0) { |
| if (action == Action.APPLY) { |
| List<CliFunctionResult> functionResults = |
| executeAndGetFunctionResult(new RecreateCacheFunction(), null, servers); |
| TabularResultModel tableSection = |
| result.addTableAndSetStatus(ResultModel.MEMBER_STATUS_SECTION, functionResults, false, |
| true); |
| tableSection.setHeader("Configure the servers in '" + group + "' group: "); |
| } else { |
| infoSection.addLine("Existing servers are not affected with this configuration change."); |
| } |
| } |
| return result; |
| } |
| |
| void backupTheOldConfig(InternalConfigurationPersistenceService ccService) throws IOException { |
| String backupDir = "cluster_config_" + new SimpleDateFormat("yyyyMMddhhmm").format(new Date()) |
| + '.' + System.nanoTime(); |
| File backDirFile = ccService.getClusterConfigDirPath().getParent().resolve(backupDir).toFile(); |
| for (Configuration config : ccService.getEntireConfiguration().values()) { |
| ccService.writeConfigToFile(config, backDirFile); |
| } |
| } |
| |
| File getUploadedFile() { |
| List<String> filePathFromShell = CommandExecutionContext.getFilePathFromShell(); |
| File file = new File(filePathFromShell.get(0)); |
| return file; |
| } |
| |
| Set<DistributedMember> findMembers(String group) { |
| Set<DistributedMember> serversInGroup; |
| if (ConfigurationPersistenceService.CLUSTER_CONFIG.equals(group)) { |
| serversInGroup = getAllNormalMembers(); |
| } else { |
| serversInGroup = findMembers(new String[] {group}, null); |
| } |
| return serversInGroup; |
| } |
| |
| private Set<String> getRegionNamesOnServer(DistributedMember server) { |
| ResultCollector rc = executeFunction(new GetRegionNamesFunction(), null, server); |
| List<Set<String>> results = (List<Set<String>>) rc.getResult(); |
| |
| return results.get(0); |
| } |
| |
| public static class ImportInterceptor extends AbstractCliAroundInterceptor { |
| @Override |
| public ResultModel preExecution(GfshParseResult parseResult) { |
| String zip = parseResult.getParamValueAsString(CliStrings.IMPORT_SHARED_CONFIG__ZIP); |
| String xmlFile = parseResult.getParamValueAsString(XML_FILE); |
| String group = parseResult.getParamValueAsString(CliStrings.GROUP); |
| |
| if (group != null && group.contains(",")) { |
| return ResultModel.createError("Only a single group name is supported."); |
| } |
| |
| if (zip == null && xmlFile == null) { |
| return ResultModel.createError("Either a zip file or a xml file is required."); |
| } |
| |
| if (zip != null && xmlFile != null) { |
| return ResultModel.createError("Zip file and xml File can't both be specified."); |
| } |
| |
| |
| if (zip != null) { |
| if (!group.equals(ConfigurationPersistenceService.CLUSTER_CONFIG)) { |
| return ResultModel.createError("zip file can not be imported with a specific group."); |
| } |
| |
| if (!zip.endsWith(CliStrings.ZIP_FILE_EXTENSION)) { |
| return ResultModel.createError("Invalid file type. The file extension must be .zip"); |
| } |
| } |
| |
| if (xmlFile != null) { |
| if (!xmlFile.endsWith(".xml")) { |
| return ResultModel.createError("Invalid file type. The file extension must be .xml."); |
| } |
| } |
| |
| String file = (zip != null) ? zip : xmlFile; |
| File importedFile = new File(file).getAbsoluteFile(); |
| if (!importedFile.exists()) { |
| return ResultModel.createError("'" + file + "' not found."); |
| } |
| |
| String message = "This command will replace the existing cluster configuration, if any, " |
| + "The old configuration will be backed up in the working directory.\n\n" + "Continue? "; |
| |
| if (readYesNo(message, Response.YES) == Response.NO) { |
| return ResultModel.createError("Aborted import of " + file + "."); |
| } |
| |
| Action action = (Action) parseResult.getParamValue(ACTION); |
| if (action == Action.STAGE) { |
| message = |
| "The configuration you are trying to import should NOT have any conflict with the configuration" |
| + "of existing running servers if any, otherwise you may not be able to start new servers. " |
| + "\nIt is also expected that you would restart the servers with the old configuration after new servers have come up." |
| + "\n\nContinue? "; |
| if (readYesNo(message, Response.YES) == Response.NO) { |
| return ResultModel.createError("Aborted import of " + xmlFile + "."); |
| } |
| } |
| |
| ResultModel result = new ResultModel(); |
| result.addFile(importedFile, FileResultModel.FILE_TYPE_FILE); |
| return result; |
| } |
| } |
| |
| } |