/******************************************************************************* * 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.Properties; 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.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.ServiceReader; 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.PackagingException; import org.cloudifysource.dsl.internal.packaging.ZipUtils; import org.cloudifysource.dsl.rest.request.InstallServiceRequest; import org.cloudifysource.dsl.rest.response.InstallServiceResponse; import org.cloudifysource.dsl.utils.RecipePathResolver; import org.cloudifysource.restclient.RestClient; import org.cloudifysource.shell.Constants; import org.cloudifysource.shell.ShellUtils; import org.cloudifysource.shell.exceptions.CLIException; 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.CLIServiceInstaller; import org.cloudifysource.shell.util.NameAndPackedFileResolver; import org.cloudifysource.shell.util.PreparedPackageResolver; import org.cloudifysource.shell.util.ServiceResolver; import org.fusesource.jansi.Ansi.Color; /** * @author rafi, adaml, barakm * @since 2.0.0 * * Installs a service by deploying the service files as one packed file (zip, war or jar). Service files can also * be supplied as a folder containing multiple files. * * Required arguments: service-file - Path to the service's packed file or folder * * Optional arguments: zone - The machines zone in which to install the service name - The name of the service * timeout - The number of minutes to wait until the operation is completed (default: 5 minutes) * * Command syntax: install-service [-zone zone] [-name name] [-timeout timeout] service-file */ @Command(scope = "cloudify", name = "install-service", description = "Installs a service. If you specify a folder" + " path it will be packed and deployed. If you specify a service archive, the shell will deploy that file.") public class InstallService extends AdminAwareCommand implements NewRestClientCommand { private static final int DEFAULT_TIMEOUT_MINUTES = 5; private static final String TIMEOUT_ERROR_MESSAGE = "Service installation timed out." + " Configure the timeout using the -timeout flag."; private static final long TEN_K = 10 * FileUtils.ONE_KB; @Argument(required = true, name = "recipe", description = "The service recipe folder or archive") private File recipe; @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 = "-zone", description = "The machines zone in which to install the service") private String zone; @Option(required = false, name = "-name", description = "The name of the service") private String serviceName = null; @Option(required = false, name = "-timeout", description = "The number of minutes to wait until the operation is " + "done. Defaults to 5 minutes.") private int timeoutInMinutes = DEFAULT_TIMEOUT_MINUTES; @Deprecated @Option(required = false, name = "-service-file-name", description = "Name of the service file in the " + "recipe folder. If not specified, uses the default file name") private String serviceFileName = null; @Option(required = false, name = "-cloudConfiguration", description = "File of directory containing configuration information to be used by the cloud driver " + "for this application") private File cloudConfiguration; @Option(required = false, name = "-disableSelfHealing", description = "Disables service self healing") private boolean disableSelfHealing = false; @Option(required = false, name = "-overrides", description = "File containing properties to be used to overrides the current service's properties.") private File overrides; @Option(required = false, name = "-cloud-overrides", description = "File containing properties to be used to override the current cloud " + "configuration for this service.") 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} */ @Override protected Object doExecute() throws Exception { logger.fine("install-service using the old rest client"); try { DebugUtils.validateDebugSettings(debugAll, debugEvents, debugModeString); } catch (final DSLErrorMessageException e) { throw new CLIStatusException(e, 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.resolveService(recipe)) { recipe = pathResolver.getResolved(); } else { throw new CLIStatusException("service_file_doesnt_exist", StringUtils.join(pathResolver.getPathsLooked().toArray(), ", ")); } File packedFile; final File cloudConfigurationZipFile = createCloudConfigurationZipFile(); // TODO: this logics should not be done twice. should be done directly // in the rest server. // also figure out how to treat war/jar files that have no .groovy file. // create default? Service service = null; try { if (recipe.getName().endsWith(".jar") || recipe.getName().endsWith(".war")) { // legacy XAP Processing Unit packedFile = recipe; } else if (recipe.isDirectory()) { // Assume that a folder will contain a DSL file? final List<File> additionFiles = new LinkedList<File>(); if (cloudConfigurationZipFile != null) { additionFiles.add(cloudConfigurationZipFile); } File recipeFile = recipe; if (getServiceFileName() != null) { final File fullPathToRecipe = new File( recipe.getAbsolutePath() + "/" + getServiceFileName()); if (!fullPathToRecipe.exists()) { throw new CLIStatusException( "service_file_doesnt_exist", fullPathToRecipe.getPath()); } // locate recipe file recipeFile = fullPathToRecipe.isDirectory() ? DSLReader.findDefaultDSLFile(DSLUtils.SERVICE_DSL_FILE_NAME_SUFFIX, fullPathToRecipe) : fullPathToRecipe; } else { recipeFile = DSLReader.findDefaultDSLFile(DSLUtils.SERVICE_DSL_FILE_NAME_SUFFIX, recipe); } service = ServiceReader.readService(recipeFile, recipe, null, false, overrides); packedFile = Packager.pack(recipeFile, false, service, additionFiles); packedFile.deleteOnExit(); } else { // serviceFile is a zip file packedFile = recipe; service = ServiceReader.readServiceFromZip(packedFile); } } catch (final IOException e) { throw new CLIException(e); } catch (final PackagingException e) { throw new CLIException(e); } final String currentApplicationName = getCurrentApplicationName(); Properties props = null; if (service != null) { props = createServiceContextProperties(service); if (getServiceFileName() != null) { props.setProperty(CloudifyConstants.CONTEXT_PROPERTY_SERVICE_FILE_NAME, getServiceFileName()); } if (serviceName == null || serviceName.isEmpty()) { serviceName = service.getName(); } if (!org.cloudifysource.restclient.StringUtils.isValidRecipeName(serviceName)) { throw new CLIStatusException(CloudifyErrorMessages.SERVICE_NAME_INVALID_CHARS.getName(), serviceName); } } else { if (serviceName == null || serviceName.isEmpty()) { serviceName = recipe.getName(); final int endIndex = serviceName.lastIndexOf('.'); if (endIndex > 0) { serviceName = serviceName.substring(0, endIndex); } } } if (zone == null || zone.isEmpty()) { zone = serviceName; } String templateName; // service is null when a simple deploying war for example if (service == null || service.getCompute() == null) { templateName = ""; } else { templateName = service.getCompute().getTemplate(); if (templateName == null) { templateName = ""; } } try { final String lifecycleEventContainerPollingID = adminFacade .installElastic(packedFile, currentApplicationName, serviceName, zone, props, templateName, authGroups, getTimeoutInMinutes(), !disableSelfHealing, cloudOverrides, overrides); pollForLifecycleEvents(lifecycleEventContainerPollingID); } finally { // if a zip file was created, delete it at the end of use. if (recipe.isDirectory()) { FileUtils.deleteQuietly(packedFile.getParentFile()); } } // TODO - server may have failed! We should check the service state and // decide accordingly // which message to display. return getFormattedMessage("service_install_ended", Color.GREEN, serviceName); } private void pollForLifecycleEvents(final String lifecycleEventContainerPollingID) throws InterruptedException, CLIException, TimeoutException, IOException { final RestLifecycleEventsLatch lifecycleEventsPollingLatch = this.adminFacade .getLifecycleEventsPollingLatch( lifecycleEventContainerPollingID, 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, "service_installation_timed_out_on_client", serviceName); } else { continuous = true; } } } } private boolean promptWouldYouLikeToContinueQuestion() throws IOException { return ShellUtils.promptUser(session, "would_you_like_to_continue_service_installation", serviceName); } // TODO: THIS CODE IS COPIED AS IS FROM THE REST PROJECT // It is used originally in ApplicationInstallerRunnable // This copy is a bad idea, and should be moved out of here as soon as // possible. /** * Create Properties object with settings from the service object, if found on the given service. The supported * settings are: com.gs.application.dependsOn com.gs.service.type com.gs.service.icon * com.gs.service.network.protocolDescription * * @param service * The service object the read the settings from * @return Properties object populated with the above properties, if found on the given service. */ private Properties createServiceContextProperties(final Service service) { final Properties contextProperties = new Properties(); // contextProperties.setProperty("com.gs.application.services", // serviceNamesString); if (service.getDependsOn() != null) { contextProperties.setProperty( CloudifyConstants.CONTEXT_PROPERTY_DEPENDS_ON, service .getDependsOn().toString()); } if (service.getType() != null) { contextProperties.setProperty( CloudifyConstants.CONTEXT_PROPERTY_SERVICE_TYPE, service.getType()); } if (service.getIcon() != null) { contextProperties.setProperty( CloudifyConstants.CONTEXT_PROPERTY_SERVICE_ICON, CloudifyConstants.SERVICE_EXTERNAL_FOLDER + service.getIcon()); } if (service.getNetwork() != null) { if (service.getNetwork().getProtocolDescription() != null) { contextProperties .setProperty( CloudifyConstants.CONTEXT_PROPERTY_NETWORK_PROTOCOL_DESCRIPTION, service.getNetwork().getProtocolDescription()); } } contextProperties.setProperty( CloudifyConstants.CONTEXT_PROPERTY_ELASTIC, Boolean.toString(service.isElastic())); if (this.debugAll) { contextProperties.setProperty(CloudifyConstants.CONTEXT_PROPERTY_DEBUG_ALL, Boolean.TRUE.toString()); contextProperties.setProperty(CloudifyConstants.CONTEXT_PROPERTY_DEBUG_MODE, this.getDebugModeString()); } else if (this.debugEvents != null) { contextProperties.setProperty(CloudifyConstants.CONTEXT_PROPERTY_DEBUG_EVENTS, this.debugEvents); contextProperties.setProperty(CloudifyConstants.CONTEXT_PROPERTY_DEBUG_MODE, this.getDebugModeString()); } return contextProperties; } 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); tempDir.mkdirs(); final File tempFile = new File(tempDir, CloudifyConstants.SERVICE_CLOUD_CONFIGURATION_FILE_NAME); // 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; } 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 boolean isDisableSelfHealing() { return disableSelfHealing; } public void setDisableSelfHealing(final boolean disableSelfHealing) { this.disableSelfHealing = disableSelfHealing; } public boolean isDebugAll() { return debugAll; } public void setDebugAll(final boolean debugAll) { this.debugAll = debugAll; } public String getDebugEvents() { return debugEvents; } public void setDebugEvents(final String debugEvents) { this.debugEvents = debugEvents; } public String getDebugModeString() { return debugModeString; } public void setDebugModeString(final String debugModeString) { this.debugModeString = debugModeString; } public String getServiceFileName() { return serviceFileName; } public void setServiceFileName(final String serviceFileName) { this.serviceFileName = serviceFileName; } @Override public Object doExecuteNewRestClient() throws Exception { logger.fine("Installing service " + serviceName + " using the new rest client"); RestClient newRestClient = ((RestAdminFacade) getRestAdminFacade()).getNewRestClient(); NameAndPackedFileResolver nameAndPackedFileResolver = getResolver(recipe); serviceName = nameAndPackedFileResolver.getName(); File packedFile = nameAndPackedFileResolver.getPackedFile(); // upload the files if necessary final String cloudConfigurationFileKey = ShellUtils.uploadToRepo(newRestClient, cloudConfiguration, displayer); final String cloudOverridesFileKey = ShellUtils.uploadToRepo(newRestClient, cloudOverrides, displayer); final String overridesFileKey = ShellUtils.uploadToRepo(newRestClient, overrides, displayer); final String recipeFileKey = ShellUtils.uploadToRepo(newRestClient, packedFile, displayer); InstallServiceRequest request = new InstallServiceRequest(); request.setAuthGroups(authGroups); request.setCloudConfigurationUploadKey(cloudConfigurationFileKey); request.setDebugAll(debugAll); request.setCloudOverridesUploadKey(cloudOverridesFileKey); request.setDebugEvents(debugEvents); request.setServiceOverridesUploadKey(overridesFileKey); request.setServiceFolderUploadKey(recipeFileKey); request.setSelfHealing(!disableSelfHealing); // execute the request InstallServiceResponse installServiceResponse = newRestClient.installService(getCurrentApplicationName(), serviceName, request); CLIServiceInstaller installer = new CLIServiceInstaller(); installer.setApplicationName(getCurrentApplicationName()); installer.setAskOnTimeout(true); installer.setDeploymentId(installServiceResponse.getDeploymentID()); installer.setInitialTimeout(timeoutInMinutes); installer.setRestClient(newRestClient); installer.setServiceName(serviceName); installer.setSession(session); installer.setPlannedNumberOfInstances( nameAndPackedFileResolver.getPlannedNumberOfInstancesPerService().get(serviceName)); try { installer.install(); } finally { // drop one line displayer.printEvent(""); } return getFormattedMessage("service_install_ended", Color.GREEN, serviceName); } private NameAndPackedFileResolver getResolver(final File recipe) throws CLIStatusException { if (recipe.isFile()) { // this is a prepared package we can just use. return new PreparedPackageResolver(recipe); } // this is an actual service directory return new ServiceResolver(resolve(recipe), overrides, serviceName); } private File resolve(final File recipe) throws CLIStatusException { final RecipePathResolver pathResolver = new RecipePathResolver(); if (pathResolver.resolveService(recipe)) { return pathResolver.getResolved(); } throw new CLIStatusException("service_file_doesnt_exist", StringUtils.join(pathResolver.getPathsLooked().toArray(), ", ")); } }