/******************************************************************************* * Copyright (c) 2011 GigaSpaces Technologies Ltd. All rights reserved * * Licensed 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.cloudifysource.shell.commands; import java.io.File; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.felix.gogo.commands.Argument; import org.apache.felix.gogo.commands.Command; import org.apache.felix.gogo.commands.Option; import org.cloudifysource.domain.Application; import org.cloudifysource.domain.Service; import org.cloudifysource.dsl.internal.CloudifyConstants; import org.cloudifysource.dsl.internal.CloudifyErrorMessages; import org.cloudifysource.dsl.internal.DSLErrorMessageException; import org.cloudifysource.dsl.internal.DSLReader; import org.cloudifysource.dsl.internal.DSLUtils; import org.cloudifysource.dsl.internal.debug.DebugModes; import org.cloudifysource.dsl.internal.debug.DebugUtils; import org.cloudifysource.dsl.internal.packaging.Packager; import org.cloudifysource.dsl.internal.packaging.ZipUtils; import org.cloudifysource.dsl.rest.request.InstallApplicationRequest; import org.cloudifysource.dsl.rest.response.InstallApplicationResponse; import org.cloudifysource.dsl.utils.RecipePathResolver; import org.cloudifysource.restclient.RestClient; import org.cloudifysource.shell.Constants; import org.cloudifysource.shell.GigaShellMain; import org.cloudifysource.shell.ShellUtils; import org.cloudifysource.shell.exceptions.CLIStatusException; import org.cloudifysource.shell.installer.CLIEventsDisplayer; import org.cloudifysource.shell.rest.RestAdminFacade; import org.cloudifysource.shell.rest.RestLifecycleEventsLatch; import org.cloudifysource.shell.rest.inspect.CLIApplicationInstaller; import org.cloudifysource.shell.util.ApplicationResolver; import org.cloudifysource.shell.util.NameAndPackedFileResolver; import org.cloudifysource.shell.util.PreparedApplicationPackageResolver; import org.fusesource.jansi.Ansi.Color; /** * @author rafi, barakm, adaml * @since 2.0.0 * * Installs an application, including it's contained services ordered according to their dependencies. * * Required arguments: application-file - The application recipe file path, folder or archive (zip/jar) * * Optional arguments: name - The name of the application timeout - The number of minutes to wait until the * operation is completed (default: 10 minutes) * * Command syntax: install-application [-name name] [-timeout timeout] application-file */ @Command(scope = "cloudify", name = "install-application", description = "Installs an application. If you specify" + " a folder path it will be packed and deployed. If you sepcify an application archive, the shell will deploy" + " that file.") public class InstallApplication extends AdminAwareCommand implements NewRestClientCommand { private static final int DEFAULT_TIMEOUT_MINUTES = 10; private static final String TIMEOUT_ERROR_MESSAGE = "Application installation timed out." + " Configure the timeout using the -timeout flag."; private static final long TEN_K = 10 * FileUtils.ONE_KB; @Argument(required = true, name = "application-file", description = "The application recipe file path, folder " + "or archive") private File applicationFile; @Option(required = false, name = "-authGroups", description = "The groups authorized to access this application " + "(multiple values can be comma-separated)") private String authGroups; @Option(required = false, name = "-name", description = "The name of the application") private String applicationName; @Option(required = false, name = "-timeout", description = "The number of minutes to wait until the operation" + " is done.") private int timeoutInMinutes = DEFAULT_TIMEOUT_MINUTES; @Option(required = false, name = "-disableSelfHealing", description = "Disables service self healing") private boolean disableSelfHealing = false; @Option(required = false, name = "-cloudConfiguration", description = "File or directory containing configuration information to be used by the cloud driver " + "for this application") private File cloudConfiguration; @Option(required = false, name = "-overrides", description = "File containing properties to be used to override the current " + "properties of the application and its services") private File overrides; @Option(required = false, name = "-cloud-overrides", description = "File containing properties to be used to override the current cloud " + "configuration for this application and its services.") private File cloudOverrides; @Option(required = false, name = "-debug-all", description = "Debug all supported lifecycle events") private boolean debugAll; @Option(required = false, name = "-debug-events", description = "Debug the specified events") private String debugEvents; @Option(required = false, name = "-debug-mode", description = "Debug mode. One of: instead, after or onError") private String debugModeString = DebugModes.INSTEAD.getName(); private CLIEventsDisplayer displayer = new CLIEventsDisplayer(); /** * {@inheritDoc} */ @SuppressWarnings("boxing") @Override protected Object doExecute() throws Exception { try { DebugUtils.validateDebugSettings(debugAll, debugEvents, getDebugModeString()); } catch (final DSLErrorMessageException e) { throw new CLIStatusException(e.getErrorMessage().getName(), (Object[]) e.getArgs()); } if (cloudOverrides != null) { if (cloudOverrides.length() >= TEN_K) { throw new CLIStatusException(CloudifyErrorMessages.CLOUD_OVERRIDES_TO_LONG.getName()); } } final RecipePathResolver pathResolver = new RecipePathResolver(); if (pathResolver.resolveApplication(applicationFile)) { applicationFile = pathResolver.getResolved(); } else { throw new CLIStatusException("application_not_found", StringUtils.join(pathResolver.getPathsLooked().toArray(), ", ")); } logger.info("Validating file " + applicationFile.getName()); final DSLReader dslReader = createDslReader(); final Application application = dslReader.readDslEntity(Application.class); if (StringUtils.isBlank(applicationName)) { applicationName = application.getName(); } if (!org.cloudifysource.restclient.StringUtils.isValidRecipeName(applicationName)) { throw new CLIStatusException(CloudifyErrorMessages.APPLICATION_NAME_INVALID_CHARS.getName(), applicationName); } if (adminFacade.getApplicationNamesList().contains(applicationName)) { throw new CLIStatusException("application_already_deployed", application.getName()); } final File cloudConfigurationZipFile = createCloudConfigurationZipFile(); File zipFile; if (applicationFile.isFile()) { if (applicationFile.getName().endsWith(".zip") || applicationFile.getName().endsWith(".jar")) { zipFile = applicationFile; } else { throw new CLIStatusException("application_file_format_mismatch", applicationFile.getPath()); } } else { // pack an application folder final List<File> additionalServiceFiles = new LinkedList<File>(); if (cloudConfigurationZipFile != null) { additionalServiceFiles.add(cloudConfigurationZipFile); } zipFile = Packager.packApplication(application, applicationFile, additionalServiceFiles); } // toString of string list (i.e. [service1, service2]) logger.info("Uploading application " + applicationName); final Map<String, String> result = adminFacade.installApplication(zipFile, applicationName, authGroups, getTimeoutInMinutes(), !isDisableSelfHealing(), overrides, cloudOverrides, debugAll, debugEvents, getDebugModeString()); final String serviceOrder = result.get(CloudifyConstants.SERVICE_ORDER); // If temp file was created, Delete it. if (!applicationFile.isFile()) { final boolean delete = zipFile.delete(); if (!delete) { logger.info("Failed to delete application file: " + zipFile.getAbsolutePath()); } } if (serviceOrder.charAt(0) != '[' && serviceOrder.charAt(serviceOrder.length() - 1) != ']') { throw new IllegalStateException("Cannot parse service order response: " + serviceOrder); } printApplicationInfo(application); session.put(Constants.ACTIVE_APP, applicationName); GigaShellMain.getInstance().setCurrentApplicationName(applicationName); final String pollingID = result.get(CloudifyConstants.LIFECYCLE_EVENT_CONTAINER_ID); final RestLifecycleEventsLatch lifecycleEventsPollingLatch = this.adminFacade.getLifecycleEventsPollingLatch(pollingID, TIMEOUT_ERROR_MESSAGE); boolean isDone = false; boolean continuous = false; while (!isDone) { try { if (!continuous) { lifecycleEventsPollingLatch.waitForLifecycleEvents(getTimeoutInMinutes(), TimeUnit.MINUTES); } else { lifecycleEventsPollingLatch.continueWaitForLifecycleEvents(getTimeoutInMinutes(), TimeUnit.MINUTES); } isDone = true; } catch (final TimeoutException e) { if (!(Boolean) session.get(Constants.INTERACTIVE_MODE)) { throw e; } final boolean continueInstallation = promptWouldYouLikeToContinueQuestion(); if (!continueInstallation) { throw new CLIStatusException(e, "application_installation_timed_out_on_client", applicationName); } continuous = true; } } return this.getFormattedMessage("application_installed_successfully", Color.GREEN, applicationName); } private DSLReader createDslReader() { final DSLReader dslReader = new DSLReader(); final File dslFile = DSLReader.findDefaultDSLFile(DSLUtils.APPLICATION_DSL_FILE_NAME_SUFFIX, applicationFile); dslReader.setDslFile(dslFile); dslReader.setCreateServiceContext(false); dslReader.addProperty(DSLUtils.APPLICATION_DIR, dslFile.getParentFile().getAbsolutePath()); dslReader.setOverridesFile(overrides); return dslReader; } private File createCloudConfigurationZipFile() throws CLIStatusException, IOException { if (this.cloudConfiguration == null) { return null; } if (!this.cloudConfiguration.exists()) { throw new CLIStatusException("cloud_configuration_file_not_found", this.cloudConfiguration.getAbsolutePath()); } // create a temp file in a temp directory final File tempDir = File.createTempFile("__Cloudify_Cloud_configuration", ".tmp"); FileUtils.forceDelete(tempDir); final boolean mkdirs = tempDir.mkdirs(); if (!mkdirs) { logger.info("Field to create temporary directory " + tempDir.getAbsolutePath()); } final File tempFile = new File(tempDir, CloudifyConstants.SERVICE_CLOUD_CONFIGURATION_FILE_NAME); logger.info("Created temporary file " + tempFile.getAbsolutePath() + " in temporary directory" + tempDir.getAbsolutePath()); // mark files for deletion on JVM exit tempFile.deleteOnExit(); tempDir.deleteOnExit(); if (this.cloudConfiguration.isDirectory()) { ZipUtils.zip(this.cloudConfiguration, tempFile); } else if (this.cloudConfiguration.isFile()) { ZipUtils.zipSingleFile(this.cloudConfiguration, tempFile); } else { throw new IOException(this.cloudConfiguration + " is neither a file nor a directory"); } return tempFile; } private boolean promptWouldYouLikeToContinueQuestion() throws IOException { return ShellUtils.promptUser(session, "would_you_like_to_continue_application_installation", this.applicationName); } /** * Prints Application data - the application name and it's services name, dependencies and number of instances. * * @param application * Application object to analyze */ private void printApplicationInfo(final Application application) { final List<Service> services = application.getServices(); logger.info("Application [" + applicationName + "] with " + services.size() + " services"); for (final Service service : services) { if (service.getDependsOn().isEmpty()) { logger.info("Service [" + service.getName() + "] " + service.getNumInstances() + " planned instances"); } else { // Service has dependencies logger.info("Service [" + service.getName() + "] depends on " + service.getDependsOn().toString() + " " + service.getNumInstances() + " planned instances"); } } } public File getCloudConfiguration() { return cloudConfiguration; } public void setCloudConfiguration(final File cloudConfiguration) { this.cloudConfiguration = cloudConfiguration; } public int getTimeoutInMinutes() { return timeoutInMinutes; } public void setTimeoutInMinutes(final int timeoutInMinutes) { this.timeoutInMinutes = timeoutInMinutes; } public String getDebugModeString() { return debugModeString; } public void setDebugModeString(final String debugModeString) { this.debugModeString = debugModeString; } public boolean isDisableSelfHealing() { return disableSelfHealing; } public void setDisableSelfHealing(final boolean disableSelfHealing) { this.disableSelfHealing = disableSelfHealing; } @Override public Object doExecuteNewRestClient() throws Exception { //resolve the path for the given app input final RecipePathResolver pathResolver = new RecipePathResolver(); if (pathResolver.resolveApplication(applicationFile)) { applicationFile = pathResolver.getResolved(); } else { throw new CLIStatusException("application_not_found", StringUtils.join(pathResolver.getPathsLooked().toArray(), ", ")); } //resolve packed file and application name final NameAndPackedFileResolver nameAndPackedFileResolver = getResolver(); if (StringUtils.isBlank(applicationName)) { applicationName = nameAndPackedFileResolver.getName(); } final File packedFile = nameAndPackedFileResolver.getPackedFile(); //upload relevant application deployment files RestClient newRestClient = ((RestAdminFacade) getRestAdminFacade()).getNewRestClient(); final String packedFileKey = ShellUtils.uploadToRepo(newRestClient, packedFile, displayer); final String overridesFileKey = ShellUtils.uploadToRepo(newRestClient, overrides, displayer); final String cloudOverridesFileKey = ShellUtils.uploadToRepo(newRestClient, cloudOverrides, displayer); final String cloudConfigurationFileKey = ShellUtils.uploadToRepo(newRestClient, createCloudConfigurationZipFile(), displayer); //create the install request InstallApplicationRequest request = new InstallApplicationRequest(); request.setApplcationFileUploadKey(packedFileKey); request.setApplicationOverridesUploadKey(overridesFileKey); request.setCloudOverridesUploadKey(cloudOverridesFileKey); request.setCloudConfigurationUploadKey(cloudConfigurationFileKey); request.setApplicationName(applicationName); request.setAuthGroups(authGroups); request.setDebugAll(debugAll); request.setDebugEvents(debugEvents); request.setDebugMode(debugModeString); request.setSelfHealing(!disableSelfHealing); //install application final InstallApplicationResponse installApplicationResponse = newRestClient.installApplication(applicationName, request); Application application = ((Application) nameAndPackedFileResolver.getDSLObject()); //print application info. printApplicationInfo(application); Map<String, Integer> plannedNumberOfInstancesPerService = nameAndPackedFileResolver .getPlannedNumberOfInstancesPerService(); CLIApplicationInstaller installer = new CLIApplicationInstaller(); installer.setApplicationName(applicationName); installer.setAskOnTimeout(true); installer.setDeploymentId(installApplicationResponse.getDeploymentID()); installer.setPlannedNumberOfInstancesPerService(plannedNumberOfInstancesPerService); installer.setInitialTimeout(timeoutInMinutes); installer.setRestClient(newRestClient); installer.setSession(session); try { installer.install(); } finally { // drop one line displayer.printEvent(""); if (!applicationFile.isFile()) { final boolean delete = FileUtils.deleteQuietly(packedFile); if (!delete) { logger.info("Failed to delete application file: " + packedFile.getAbsolutePath()); } } } //set the active application in the CLI. session.put(Constants.ACTIVE_APP, applicationName); GigaShellMain.getInstance().setCurrentApplicationName(applicationName); // drop one line before printing the last message displayer.printEvent(""); return this.getFormattedMessage("application_installed_successfully", Color.GREEN, applicationName); } private NameAndPackedFileResolver getResolver() throws CLIStatusException { // this is a prepared package we can just use. if (applicationFile.isFile()) { if (applicationFile.getName().endsWith("zip") || applicationFile.getName().endsWith("jar")) { return new PreparedApplicationPackageResolver(applicationFile, overrides); } throw new CLIStatusException("application_file_format_mismatch", applicationFile.getPath()); } // this is an actual application directory return new ApplicationResolver(applicationFile, overrides); } }