/******************************************************************************* * 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.esc.shell.commands; import java.io.File; import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; 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.cloud.Cloud; import org.cloudifysource.dsl.internal.CloudifyConstants; import org.cloudifysource.dsl.internal.CloudifyErrorMessages; import org.cloudifysource.dsl.internal.DSLUtils; import org.cloudifysource.dsl.internal.ServiceReader; import org.cloudifysource.dsl.internal.packaging.FileAppender; import org.cloudifysource.dsl.utils.RecipePathResolver; import org.cloudifysource.esc.driver.provisioning.jclouds.DefaultProvisioningDriver; import org.cloudifysource.esc.installer.AgentlessInstaller; import org.cloudifysource.esc.shell.installer.CloudGridAgentBootstrapper; import org.cloudifysource.shell.AdminFacade; import org.cloudifysource.shell.Constants; import org.cloudifysource.shell.KeystoreFileVerifier; import org.cloudifysource.shell.ShellUtils; import org.cloudifysource.shell.commands.AbstractGSCommand; import org.cloudifysource.shell.exceptions.CLIStatusException; import com.j_spaces.kernel.Environment; /************ * CLI Command to bootstrap a cloud. * * @author barakme * @since 2.0.0 * */ @Command( scope = "cloudify", name = "bootstrap-cloud", description = "Starts Cloudify Agent without any zone, and the Cloudify management processes on the provided " + "cloud.") public class BootstrapCloud extends AbstractGSCommand { private static final int PROGRESS_INTERVAL_SECONDS = 10; private static final int DEFAULT_TIMEOUT_MINUTES = 60; private static final String PATH_SEPARATOR = System.getProperty("file.separator"); @Argument(required = true, name = "provider", description = "The cloud provider to use") private String cloudProvider; @Option(required = false, description = "Server security mode (on/off)", name = "-secured") private boolean secured; @Option(required = false, description = "Path to a custom spring security configuration file", name = "-security-file") private String securityFilePath; @Option(required = false, description = "The username when connecting to a secure admin server", name = "-user") private String username; @Option(required = false, description = "The password when connecting to a secure admin server", name = "-password") private String password; @Option(required = false, description = "The path to the keystore used for SSL connections", name = "-keystore") private String keystore; @Option(required = false, description = "The password to the keystore", name = "-keystore-password") private String keystorePassword; @Option(required = false, description = "Path to a file containing override properties", name = "-cloud-overrides") private File cloudOverrides; @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 = "-no-web-services", description = "if set, no attempt to deploy the rest admin and web-ui will be made. Used for testing purposes.") private boolean noWebServices; @Option(required = false, name = "-no-management-space", description = "if set, no attempt to deploy the cloudifyManagementSpace will be made. Used for testing purposes.") private boolean noManagementSpace; @Option(required = false, name = "-use-existing", description = "if set, will attempt to find existing management servers. " + "Management should already have been shut-down with stop-management.") private boolean useExistingManagers = false; @Option(required = false, name = "-use-existing-from-file", description = "if set, will attempt to find existing management servers based on server " + "details supplied in file. " + "Management should already have been shut-down with stop-management.") private File existingManagersFile = null; @Option(required = false, name = "-skip-validation", description = "if set, will not validate the configuration before executing bootstrap") private boolean skipValidation = false; private String securityProfile = CloudifyConstants.SPRING_PROFILE_NON_SECURE; // flags to indicate if bootstrap operation created a backup file that // should be reverted private static final String CLOUDIFY_HOME = Environment.getHomeDirectory(); private static final String DEFAULT_SECURITY_FILE_PATH = CLOUDIFY_HOME + "/config/security/spring-security.xml"; private static final String[] NON_VERBOSE_LOGGERS = { DefaultProvisioningDriver.class.getName(), AgentlessInstaller.class.getName() }; private final Map<String, Level> loggerStates = new HashMap<String, Level>(); private static final long TEN_K = 10 * FileUtils.ONE_KB; private File defaultSecurityTargetFile; @Override protected Object doExecute() throws Exception { if (this.existingManagersFile != null) { if (!this.existingManagersFile.exists() || !this.existingManagersFile.isFile()) { throw new CLIStatusException(CloudifyErrorMessages.FILE_NOT_EXISTS.getName(), this.existingManagersFile.getAbsolutePath()); } } if (cloudOverrides != null) { if (cloudOverrides.length() >= TEN_K) { throw new CLIStatusException(CloudifyErrorMessages.CLOUD_OVERRIDES_TO_LONG.getName()); } } final RecipePathResolver pathResolver = new RecipePathResolver(); File providerDirectory = null; if (pathResolver.resolveCloud(new File(getCloudProvider()))) { providerDirectory = pathResolver.getResolved(); } else { throw new CLIStatusException("cloud_driver_file_doesnt_exist", StringUtils.join(pathResolver.getPathsLooked().toArray(), ", ")); } final File tempFolder = createTempFolder(); FileUtils.copyDirectoryToDirectory(providerDirectory, tempFolder); providerDirectory = new File(tempFolder, providerDirectory.getName()); defaultSecurityTargetFile = new File(providerDirectory + PATH_SEPARATOR + CloudifyConstants.SECURITY_FILE_NAME); setSecurityMode(); copySecurityFiles(providerDirectory.getAbsolutePath()); // load the cloud file final File cloudFile = findCloudFile(providerDirectory); // load properties file final File cloudPropertiesFile = new File(providerDirectory, cloudFile.getName().split("\\.")[0] + DSLUtils.PROPERTIES_FILE_SUFFIX); // check for overrides file Cloud cloud = null; if (cloudOverrides == null) { cloud = ServiceReader.readCloud(cloudFile); } else { // read cloud with overrides properties so they reflect during bootstrap. cloud = ServiceReader. readCloudFromDirectory(providerDirectory.getAbsolutePath(), FileUtils.readFileToString(cloudOverrides)); // append the overrides file to the existing properties file final FileAppender appender = new FileAppender(cloudPropertiesFile); appender.append("Overrides File Properties", cloudOverrides); appender.flush(); } // start the installer final CloudGridAgentBootstrapper installer = new CloudGridAgentBootstrapper(); installer.setProviderDirectory(providerDirectory); if (this.adminFacade != null) { installer.setAdminFacade(this.adminFacade); } else { installer.setAdminFacade((AdminFacade) session.get(Constants.ADMIN_FACADE)); } installer.setProgressInSeconds(PROGRESS_INTERVAL_SECONDS); installer.setVerbose(verbose); installer.setCloud(cloud); installer.setCloudFile(cloudFile); installer.setNoWebServices(noWebServices); installer.setNoManagementSpace(noManagementSpace); installer.setUseExisting(this.useExistingManagers); installer.setExistingManagersFile(this.existingManagersFile); // Bootstrap! // Note: The cloud driver may be very verbose. This is EXTEREMELY useful // when debugging ESM // issues, but can also clutter up the CLI display. It makes more sense to temporarily raise the log level here, // so that all of these // messages will not be displayed on the console. limitLoggingLevel(); logger.info(getFormattedMessage("bootstrapping_cloud", getCloudProvider())); try { // TODO: Create the event listeners here and pass them to the installer. installer.bootstrapCloudAndWait(securityProfile, username, password, keystorePassword, !skipValidation, getTimeoutInMinutes(), TimeUnit.MINUTES); return getFormattedMessage("cloud_started_successfully", getCloudProvider()); } finally { // if an overrides file was passed, then the properties file is dirty. delete it. if (cloudOverrides != null) { cloudPropertiesFile.delete(); } FileUtils.deleteQuietly(tempFolder); installer.close(); restoreLoggingLevel(); } } private File createTempFolder() throws IOException { final File tempFile = File.createTempFile("cloud-", ""); tempFile.delete(); tempFile.mkdir(); tempFile.deleteOnExit(); return tempFile; } private void limitLoggingLevel() { if (!this.verbose) { loggerStates.clear(); for (final String loggerName : NON_VERBOSE_LOGGERS) { final Logger provisioningLogger = Logger.getLogger(loggerName); final Level logLevelBefore = provisioningLogger.getLevel(); provisioningLogger.setLevel(Level.WARNING); loggerStates.put(loggerName, logLevelBefore); } } } private void restoreLoggingLevel() { if (!verbose) { final Set<Entry<String, Level>> entries = loggerStates.entrySet(); for (final Entry<String, Level> entry : entries) { final Logger provisioningLogger = Logger.getLogger(entry.getKey()); provisioningLogger.setLevel(entry.getValue()); } } } private File findCloudFile(final File providerDirectory) throws FileNotFoundException { if (!providerDirectory.exists() || !providerDirectory.isDirectory()) { throw new FileNotFoundException("Could not find cloud provider directory: " + providerDirectory); } final File[] cloudFiles = providerDirectory.listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String name) { return name.endsWith("-cloud.groovy"); } }); if (cloudFiles.length == 0) { throw new FileNotFoundException("Could not find a cloud definition file in: " + providerDirectory + ". Definitions file must end with the suffix '-cloud.groovy'"); } else if (cloudFiles.length > 1) { throw new IllegalArgumentException("Found multiple cloud definition files in: " + providerDirectory + ". Only one file may end with the suffix '-cloud.groovy'"); } return cloudFiles[0]; } private void setSecurityMode() throws CLIStatusException { if (secured) { // enable security if (keystore == null || keystorePassword == null || securityFilePath == null) { throw new IllegalArgumentException(ShellUtils.getFormattedMessage( CloudifyErrorMessages.SECURED_BOOTSTRAP_IS_PARTIAL.getName())); } securityProfile = CloudifyConstants.SPRING_PROFILE_SECURE; logger.info(getFormattedMessage(CloudifyErrorMessages.SETTING_SERVER_SECURITY_PROFILE.getName(), CloudifyConstants.SPRING_PROFILE_SECURE)); } else { // disable security logger.info(getFormattedMessage(CloudifyErrorMessages.SETTING_SERVER_SECURITY_PROFILE.getName(), CloudifyConstants.SPRING_PROFILE_NON_SECURE)); securityProfile = CloudifyConstants.SPRING_PROFILE_NON_SECURE; } if (securityProfile.equalsIgnoreCase(CloudifyConstants.SPRING_PROFILE_NON_SECURE)) { if (StringUtils.isNotBlank(username)) { throw new IllegalArgumentException("'-user' is only valid when '-secured' is set"); } if (StringUtils.isNotBlank(password)) { throw new IllegalArgumentException("'-password' is only valid when '-secured' is set"); } if (StringUtils.isNotBlank(securityFilePath)) { throw new IllegalArgumentException("'-securityfile' is only valid when '-secured' is set"); } if (StringUtils.isNotBlank(keystore)) { throw new IllegalArgumentException("'-keystore' is only valid when '-secured' is set"); } if (StringUtils.isNotBlank(keystorePassword)) { throw new IllegalArgumentException("'-keystore-password' is only valid when '-secured' is set"); } } else { if (StringUtils.isBlank(securityFilePath)) { throw new IllegalArgumentException("Security file is missing or empty"); } if (StringUtils.isNotBlank(username) && StringUtils.isBlank(password)) { throw new IllegalArgumentException("Password is missing or empty"); } if (StringUtils.isBlank(username) && StringUtils.isNotBlank(password)) { throw new IllegalArgumentException("Username is missing or empty"); } if (StringUtils.isBlank(keystore)) { throw new IllegalArgumentException("Keystore is missing or empty"); } if (StringUtils.isBlank(keystorePassword)) { throw new IllegalArgumentException("Keystore password is missing or empty"); } if (StringUtils.isNotBlank(keystore)) { new KeystoreFileVerifier().verifyKeystoreFile(new File(keystore), keystorePassword); } } } private void copySecurityFiles(final String providerDirectory) throws Exception { final File defaultSecuritySourceFile = new File(DEFAULT_SECURITY_FILE_PATH); if (securityProfile.equalsIgnoreCase(CloudifyConstants.SPRING_PROFILE_NON_SECURE)) { // copy the default security file (defines no security) to the upload folder FileUtils.copyFile(defaultSecuritySourceFile, defaultSecurityTargetFile); } else { // handle the configuration file if (StringUtils.isNotBlank(securityFilePath)) { final File securitySourceFile = new File(securityFilePath); if (!securitySourceFile.isFile()) { throw new Exception("Security configuration file not found: " + securityFilePath); } // copy to the cloud provider's folder, to be copied to all management servers remote directory if (!securitySourceFile.getCanonicalFile().equals(defaultSecurityTargetFile.getCanonicalFile())) { FileUtils.copyFile(securitySourceFile, defaultSecurityTargetFile); } } else { // TODO : should we use the default security location and assume it was edited by the user? // securityFilePath = CLOUDIFY_HOME + "/config/security/spring-security.xml"; throw new IllegalArgumentException("-security-file is missing or empty"); } // handle the keystore file if (StringUtils.isNotBlank(keystore)) { final File keystoreSourceFile = new File(keystore); if (!keystoreSourceFile.isFile()) { throw new Exception("Keystore file not found: " + keystore); } // copy to the override folder, to be copied to all management servers as well final File defaultKeystoreTargetFile = new File(providerDirectory + PATH_SEPARATOR + CloudifyConstants.KEYSTORE_FILE_NAME); if (!keystoreSourceFile.getCanonicalFile().equals(defaultKeystoreTargetFile.getCanonicalFile())) { FileUtils.copyFile(keystoreSourceFile, defaultKeystoreTargetFile); } } } } public int getTimeoutInMinutes() { return timeoutInMinutes; } public void setTimeoutInMinutes(final int timeoutInMinutes) { this.timeoutInMinutes = timeoutInMinutes; } public String getCloudProvider() { return cloudProvider; } public void setCloudProvider(final String cloudProvider) { this.cloudProvider = cloudProvider; } public boolean isUseExistingManagers() { return useExistingManagers; } public void setUseExistingManagers(final boolean useExistingManagers) { this.useExistingManagers = useExistingManagers; } public File getExistingManagersFile() { return existingManagersFile; } public void setExistingManagersFile(final File existingManagersFile) { this.existingManagersFile = existingManagersFile; } }