org.hawkular.wildfly.agent.installer.AgentInstaller.java Source code

Java tutorial

Introduction

Here is the source code for org.hawkular.wildfly.agent.installer.AgentInstaller.java

Source

/*
 * Copyright 2015-2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * 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.hawkular.wildfly.agent.installer;

import java.io.Console;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;
import org.hawkular.wildfly.module.installer.DeploymentConfiguration;
import org.hawkular.wildfly.module.installer.DeploymentConfiguration.Builder;
import org.hawkular.wildfly.module.installer.ExtensionDeployer;
import org.hawkular.wildfly.module.installer.XmlEdit;
import org.jboss.aesh.cl.CommandLine;
import org.jboss.aesh.cl.internal.ProcessedCommand;
import org.jboss.aesh.cl.parser.CommandLineParser;
import org.jboss.aesh.cl.parser.CommandLineParserBuilder;
import org.jboss.aesh.cl.parser.CommandLineParserException;
import org.jboss.logging.Logger;

public class AgentInstaller {

    private static final Logger log = Logger.getLogger(AgentInstaller.class);
    private static final String SECURITY_REALM_NAME = "HawkularRealm";

    public static void main(String[] args) throws Exception {
        ProcessedCommand<?> options = null;

        ArrayList<File> filesToDelete = new ArrayList<>();

        try {
            options = InstallerConfiguration.buildCommandLineOptions();
            CommandLineParser<?> parser = new CommandLineParserBuilder().processedCommand(options).create();

            StringBuilder argLine = new StringBuilder(InstallerConfiguration.COMMAND_NAME);
            for (String str : args) {
                argLine.append(' ').append(str);
            }

            CommandLine<?> commandLine = parser.parse(argLine.toString());
            InstallerConfiguration installerConfig = new InstallerConfiguration(commandLine);

            // IF we were told the passwords were encrypted THEN
            //   IF we were given the key on the command line THEN
            //      Use the key given on the command line for decoding
            //   ELSE
            //      Use the key the user gives us over stdin for decoding
            //
            //   IF we were given the salt on the command line THEN
            //      Use the salt given on the command line for decoding
            //   ELSE
            //      Use the salt the user gives us over stdin for decoding
            //
            // Decode using the key and salt.
            boolean passwordsEncrypted = commandLine.hasOption(InstallerConfiguration.OPTION_ENCRYPTION_KEY);
            if (passwordsEncrypted) {
                String key = commandLine.getOptionValue(InstallerConfiguration.OPTION_ENCRYPTION_KEY, null);
                String saltAsString = commandLine.getOptionValue(InstallerConfiguration.OPTION_ENCRYPTION_SALT,
                        null);
                if (key == null || key.isEmpty()) {
                    key = readPasswordFromStdin("Encryption key:");
                }

                boolean saltSpecified = commandLine.hasOption(InstallerConfiguration.OPTION_ENCRYPTION_SALT);
                if (!saltSpecified) {
                    saltAsString = key;
                }

                if (saltAsString == null || saltAsString.isEmpty()) {
                    saltAsString = readPasswordFromStdin("Salt:");
                }

                assert saltAsString != null;
                assert key != null;

                byte[] salt = saltAsString.getBytes("UTF-8");
                installerConfig.decodeProperties(key, salt);
            }

            String jbossHome = installerConfig.getTargetLocation();
            if (jbossHome == null) {
                // user did not provide us with a wildfly home - let's see if we are sitting in a wildfly home already
                File jbossHomeFile = new File(".").getCanonicalFile();
                if (!(jbossHomeFile.exists() && jbossHomeFile.isDirectory() && jbossHomeFile.canRead()
                        && new File(jbossHomeFile, "modules").isDirectory())) {
                    throw new Exception(InstallerConfiguration.OPTION_TARGET_LOCATION + " must be specified");
                }
                // looks like our current working directory is a WildFly home - use that
                jbossHome = jbossHomeFile.getCanonicalPath();
            }

            if ((installerConfig.getUsername() == null || installerConfig.getPassword() == null)
                    && (installerConfig.getSecurityKey() == null || installerConfig.getSecuritySecret() == null)) {
                throw new Exception(
                        "You must provide credentials (username/password or key/secret) in installer configuration");
            }

            String hawkularServerProtocol;
            String hawkularServerHost;
            String hawkularServerPort;

            if (installerConfig.getServerUrl() == null) {
                throw new Exception("You must provide the Hawkular Server URL");
            }

            try {
                URL hawkularServerUrl = new URL(installerConfig.getServerUrl());
                hawkularServerProtocol = hawkularServerUrl.getProtocol();
                hawkularServerHost = hawkularServerUrl.getHost();
                hawkularServerPort = String.valueOf(hawkularServerUrl.getPort());
                if ("-1".equals(hawkularServerPort)) {
                    hawkularServerPort = "80";
                }
            } catch (MalformedURLException mue) {
                // its possible the user passed a URL with a WildFly expression like
                // "http://${jboss.bind.address:localhost}:8080". Try to parse something like that.
                Matcher m = Pattern.compile("(https?)://(.*):(\\d+)").matcher(installerConfig.getServerUrl());
                if (!m.matches()) {
                    throw mue;
                }
                try {
                    hawkularServerProtocol = m.group(1);
                    hawkularServerHost = m.group(2);
                    hawkularServerPort = m.group(3);
                } catch (Exception e) {
                    throw mue;
                }
            }

            String moduleZip = installerConfig.getModuleDistribution();

            URL moduleZipUrl;

            if (moduleZip == null) {
                // --module is not supplied so try to download agent module from server
                File moduleTempFile = downloadModuleZip(getHawkularServerAgentDownloadUrl(installerConfig));
                if (moduleTempFile == null) {
                    throw new IOException("Failed to retrieve module dist from server, You can use option ["
                            + InstallerConfiguration.OPTION_MODULE_DISTRIBUTION + "] to supply your own");
                }
                filesToDelete.add(moduleTempFile);
                moduleZipUrl = moduleTempFile.toURI().toURL();
            } else if (moduleZip.startsWith("classpath:")) {
                // This special protocol tells us to read module zip as resource from classpath.
                // This is in case the module zip is bundled directly in the installer.
                String resourceUrl = moduleZip.substring(10);
                if (!resourceUrl.startsWith("/")) {
                    resourceUrl = "/" + resourceUrl;
                }
                moduleZipUrl = AgentInstaller.class.getResource(resourceUrl);
                if (moduleZipUrl == null) {
                    throw new IOException("Unable to load module.zip from classpath [" + resourceUrl + "]");
                }
            } else if (moduleZip.matches("(http|https|file):.*")) {
                // the module is specified as a URL - we'll download it
                File moduleTempFile = downloadModuleZip(new URL(moduleZip));
                if (moduleTempFile == null) {
                    throw new IOException("Failed to retrieve agent module from server, option ["
                            + InstallerConfiguration.OPTION_MODULE_DISTRIBUTION
                            + "] is now required but it was not supplied");
                }
                filesToDelete.add(moduleTempFile);
                moduleZipUrl = moduleTempFile.toURI().toURL();
            } else {
                // the module is specified as a file path
                moduleZipUrl = new File(moduleZip).toURI().toURL();
            }

            // deploy given module into given app server home directory and
            // set it up the way it talks to hawkular server on hawkularServerUrl
            // TODO support domain mode
            File socketBindingSnippetFile = createSocketBindingSnippet(hawkularServerHost, hawkularServerPort);
            filesToDelete.add(socketBindingSnippetFile);
            Builder configurationBldr = DeploymentConfiguration.builder().jbossHome(new File(jbossHome))
                    .module(moduleZipUrl).socketBinding(socketBindingSnippetFile.toURI().toURL());

            // let the user override the default subsystem snippet that is found in the module zip
            // can be specified as a URL or a file path
            if (installerConfig.getSubsystemSnippet() != null) {
                try {
                    configurationBldr.subsystem(new URL(installerConfig.getSubsystemSnippet()));
                } catch (MalformedURLException mue) {
                    File file = new File(installerConfig.getSubsystemSnippet());
                    if (file.exists()) {
                        configurationBldr.subsystem(file.getAbsoluteFile().toURI().toURL());
                    } else {
                        throw new FileNotFoundException(
                                "Subsystem snippet not found at [" + installerConfig.getSubsystemSnippet() + "]");
                    }
                }
            }

            String targetConfig = installerConfig.getTargetConfig();
            if (targetConfig != null) {
                configurationBldr.serverConfig(targetConfig);
            } else {
                targetConfig = DeploymentConfiguration.DEFAULT_SERVER_CONFIG;
                // we'll use this in case of https to resolve server configuration directory
            }

            // If we are to talk to the Hawkular Server over HTTPS, we need to set up some additional things
            if (hawkularServerProtocol.equals("https")) {
                String keystorePath = installerConfig.getKeystorePath();
                String keystorePass = installerConfig.getKeystorePassword();
                String keyPass = installerConfig.getKeyPassword();
                String keyAlias = installerConfig.getKeyAlias();
                if (keystorePath == null || keyAlias == null) {
                    throw new Exception(String.format(
                            "When using https protocol, the following keystore "
                                    + "command line options are required: %s, %s",
                            InstallerConfiguration.OPTION_KEYSTORE_PATH, InstallerConfiguration.OPTION_KEY_ALIAS));
                }

                // password fields are not required, but if not supplied we'll ask user
                if (keystorePass == null) {
                    keystorePass = readPasswordFromStdin("Keystore password:");
                    if (keystorePass == null || keystorePass.isEmpty()) {
                        keystorePass = "";
                        log.warn(InstallerConfiguration.OPTION_KEYSTORE_PASSWORD
                                + " was not provided; using empty password");
                    }
                }
                if (keyPass == null) {
                    keyPass = readPasswordFromStdin("Key password:");
                    if (keyPass == null || keyPass.isEmpty()) {
                        keyPass = "";
                        log.warn(InstallerConfiguration.OPTION_KEY_PASSWORD
                                + " was not provided; using empty password");
                    }
                }

                // if given keystore path is not already present within server-config directory, copy it
                File keystoreSrcFile = new File(keystorePath);
                if (!(keystoreSrcFile.isFile() && keystoreSrcFile.canRead())) {
                    throw new FileNotFoundException("Cannot read " + keystoreSrcFile.getAbsolutePath());
                }
                File targetConfigDir;
                if (new File(targetConfig).isAbsolute()) {
                    targetConfigDir = new File(targetConfig).getParentFile();
                } else {
                    targetConfigDir = new File(jbossHome, targetConfig).getParentFile();
                }
                Path keystoreDst = Paths.get(targetConfigDir.getAbsolutePath()).resolve(keystoreSrcFile.getName());
                // never overwrite target keystore
                if (!keystoreDst.toFile().exists()) {
                    log.info(
                            "Copy [" + keystoreSrcFile.getAbsolutePath() + "] to [" + keystoreDst.toString() + "]");
                    Files.copy(Paths.get(keystoreSrcFile.getAbsolutePath()), keystoreDst);
                }

                // setup security-realm and storage-adapter (within hawkular-wildfly-agent subsystem)
                String securityRealm = createSecurityRealm(keystoreSrcFile.getName(), keystorePass, keyPass,
                        keyAlias);
                configurationBldr.addXmlEdit(new XmlEdit("/server/management/security-realms", securityRealm));
                configurationBldr.addXmlEdit(createStorageAdapter(true, installerConfig));
            } else {
                // just going over non-secure HTTP
                configurationBldr.addXmlEdit(createStorageAdapter(false, installerConfig));
            }

            configurationBldr.addXmlEdit(createManagedServers(installerConfig));
            configurationBldr.addXmlEdit(setEnableFlag(installerConfig));
            configurationBldr.modulesHome("modules");

            new ExtensionDeployer().install(configurationBldr.build());

        } catch (CommandLineParserException pe) {
            log.error(pe);
            printHelp(options);
            if (Boolean.getBoolean("org.hawkular.wildfly.agent.installer.throw-exception-on-error")) {
                throw pe;
            }
        } catch (Exception ex) {
            log.error(ex);
            if (Boolean.getBoolean("org.hawkular.wildfly.agent.installer.throw-exception-on-error")) {
                throw ex;
            }
        } finally {
            for (File fileToDelete : filesToDelete) {
                if (!fileToDelete.delete()) {
                    log.warn("Failed to delete temporary file: " + fileToDelete);
                }
            }
        }
    }

    /**
     * Reads password from the console (stdin).
     *
     * @param message to present before reading
     * @return password or null if console is not available
     */
    private static String readPasswordFromStdin(String message) {
        Console console = System.console();
        if (console == null) {
            return null;
        }
        console.writer().write(message);
        console.writer().flush();
        return String.valueOf(console.readPassword());
    }

    /**
     * Creates xml snippet which sets up security-realm.
     *
     * @param keystoreFile location of the keystore file
     * @param keystorePass the password to access the keystore file
     * @param keyPass the password to access the data for the given alias
     * @param keyAlias the alias specifying the identifying security information
     * @return XML snippet
     */
    private static String createSecurityRealm(String keystoreFile, String keystorePass, String keyPass,
            String keyAlias) {
        return new StringBuilder("<security-realm name=\"" + SECURITY_REALM_NAME + "\">")
                .append("<server-identities><ssl>").append("<keystore path=\"" + keystoreFile + "\"")
                .append(" relative-to=\"jboss.server.config.dir\"")
                .append(" keystore-password=\"" + keystorePass + "\"").append(" key-password=\"" + keyPass + "\"")
                .append(" alias=\"" + keyAlias + "\"").append(" /></ssl></server-identities></security-realm>")
                .toString();
    }

    /**
     * Creates XML edit which sets up storage-adapter configuration, creates a reference
     * to the security-realm and enables SSL, if appropriate.
     *
     * @param withHttps if the storage adapter will be accessed via HTTPS
     * @return object that can be used to edit some xml content
     */
    private static XmlEdit createStorageAdapter(boolean withHttps, InstallerConfiguration installerConfig) {
        String select = "/server/profile/*[namespace-uri()='urn:org.hawkular.agent:agent:1.0']/";
        StringBuilder xml = new StringBuilder("<storage-adapter");

        String tenantId = installerConfig.getTenantId();
        if (installerConfig.isMetricsOnlyMode()) {
            xml.append(" type=\"METRICS\"");
            if (tenantId == null || tenantId.isEmpty()) {
                throw new IllegalArgumentException("You must specify tenant-id when in metrics-only mode");
            }
        } else {
            xml.append(" type=\"HAWKULAR\"");
        }

        if (tenantId != null && !tenantId.isEmpty()) {
            xml.append(" tenantId=\"" + tenantId + "\"");
        }

        if (withHttps) {
            xml.append(" securityRealm=\"" + SECURITY_REALM_NAME + "\"").append(" useSSL=\"true\"");
        }

        if (installerConfig.getFeedId() != null && !installerConfig.getFeedId().isEmpty()) {
            xml.append(" feedId=\"" + installerConfig.getFeedId() + "\"");
        }

        if (installerConfig.getUsername() != null && !installerConfig.getUsername().isEmpty()) {
            xml.append(" username=\"" + installerConfig.getUsername() + "\"");
        }
        if (installerConfig.getPassword() != null && !installerConfig.getPassword().isEmpty()) {
            xml.append(" password=\"" + installerConfig.getPassword() + "\"");
        }

        if (installerConfig.getSecurityKey() != null && !installerConfig.getSecurityKey().isEmpty()) {
            xml.append(" securityKey=\"" + installerConfig.getSecurityKey() + "\"");
        }
        if (installerConfig.getSecuritySecret() != null && !installerConfig.getSecuritySecret().isEmpty()) {
            xml.append(" securitySecret=\"" + installerConfig.getSecuritySecret() + "\"");
        }

        xml.append(" serverOutboundSocketBindingRef=\"hawkular\"");
        xml.append("/>");

        // replaces <storage-adapter> under urn:org.hawkular.agent:agent:1.0 subsystem with above content
        // we ignore whether the original storage-adapter has type of HAWKULAR or METRICS
        return new XmlEdit(select, xml.toString()).withAttribute("type").withIsIgnoreAttributeValue(true);
    }

    /**
     * Creates a (outbound) socket-binding snippet XML file
     *
     * @param host the host where the hawkular server is running
     * @param port the port where the hawkular server is listening
     * @return file to the temporary socket binding snippet file (this should be cleaned up by the caller)
     * @throws IOException on error
     */
    private static File createSocketBindingSnippet(String host, String port) throws IOException {
        StringBuilder xml = new StringBuilder("<outbound-socket-binding name=\"hawkular\">\n")
                .append("  <remote-destination host=\"" + host + "\" port=\"" + port + "\" />\n")
                .append("</outbound-socket-binding>");
        Path tempFile = Files.createTempFile("hawkular-wildfly-module-installer-outbound-socket-binding", ".xml");
        Files.write(tempFile, xml.toString().getBytes());
        return tempFile.toFile();
    }

    private static XmlEdit createManagedServers(InstallerConfiguration config) {
        String select = "/server/profile/" + "*[namespace-uri()='urn:org.hawkular.agent:agent:1.0']/";
        String managedServerName = config.getManagedServerName();
        if (managedServerName == null || managedServerName.trim().isEmpty()) {
            managedServerName = "Local"; // just make sure its something
        }
        StringBuilder xml = new StringBuilder("<managed-servers>")
                .append("<local-dmr name=\"" + managedServerName + "\" enabled=\"true\" "
                        + "resourceTypeSets=\"Main,Deployment,Web Component,EJB,Datasource,"
                        + "XA Datasource,JDBC Driver,Transaction Manager,Messaging,Hawkular\" />")
                .append("</managed-servers>");

        // replaces <managed-servers> under urn:org.hawkular.agent:agent:1.0 subsystem with above content
        return new XmlEdit(select, xml.toString());
    }

    private static XmlEdit setEnableFlag(InstallerConfiguration config) {
        String select = "/server/profile/*[namespace-uri()='urn:org.hawkular.agent:agent:1.0'][@enabled]";
        String isEnabled = String.valueOf(config.isEnabled());
        return new XmlEdit(select, isEnabled).withIsAttributeContent(true).withAttribute("enabled");
    }

    private static URL getHawkularServerAgentDownloadUrl(InstallerConfiguration config)
            throws MalformedURLException {
        String serverUrl = String.format("%s/hawkular-wildfly-agent/download", config.getServerUrl());
        return new URL(serverUrl);
    }

    /**
     * Downloads the Hawkular WildFly Agent ZIP file from a URL
     *
     * @param url where the agent zip is
     * @return absolute path to module downloaded locally or null if it could not be retrieved;
     *         this is a temporary file that should be cleaned once it is used
     */
    private static File downloadModuleZip(URL url) {
        File tempFile;

        try {
            tempFile = File.createTempFile("hawkular-wildfly-agent", ".zip");
        } catch (Exception e) {
            throw new RuntimeException("Cannot create temp file to hold module zip", e);
        }

        try (FileOutputStream fos = new FileOutputStream(tempFile); InputStream ios = url.openStream()) {
            IOUtils.copyLarge(ios, fos);
            return tempFile;
        } catch (Exception e) {
            log.warn("Unable to download hawkular wildfly agent ZIP: " + url, e);
            tempFile.delete();
        }
        return null;
    }

    private static void printHelp(ProcessedCommand<?> options) {
        if (options == null) {
            throw new RuntimeException("Cannot print help - options is null");
        }
        System.out.println(options.printHelp());
    }
}