/*******************************************************************************
* 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.installer;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import net.jini.discovery.Constants;
import org.apache.commons.lang.StringUtils;
import org.cloudifysource.dsl.utils.IPUtils;
import org.cloudifysource.shell.AdminFacade;
import org.cloudifysource.shell.ConditionLatch;
import org.cloudifysource.shell.ShellUtils;
import org.cloudifysource.shell.exceptions.CLIException;
import org.openspaces.admin.Admin;
import org.openspaces.admin.gsa.GridServiceAgent;
import org.openspaces.admin.gsc.GridServiceContainer;
import org.openspaces.admin.pu.ProcessingUnit;
import org.openspaces.admin.pu.ProcessingUnitAlreadyDeployedException;
import org.openspaces.admin.pu.ProcessingUnitDeployment;
import org.openspaces.admin.pu.ProcessingUnitInstance;
import org.openspaces.admin.pu.dependency.ProcessingUnitDeploymentDependenciesConfigurer;
import org.openspaces.admin.pu.elastic.ElasticStatelessProcessingUnitDeployment;
import org.openspaces.admin.pu.elastic.config.DiscoveredMachineProvisioningConfigurer;
import org.openspaces.admin.pu.elastic.config.EagerScaleConfigurer;
import org.openspaces.core.util.MemoryUnit;
import org.openspaces.pu.service.ServiceDetails;
import com.j_spaces.kernel.Environment;
/**
* @author rafi, barakm
* @since 2.0.0
*
* Handles the installation of a management web service
*
*/
public class ManagementWebServiceInstaller extends AbstractManagementServiceInstaller {
private int port;
private String username;
private String password;
private File warFile;
private boolean waitForConnection;
private final List<LocalhostBootstrapperListener> eventsListenersList =
new ArrayList<LocalhostBootstrapperListener>();
private boolean isLocalcloud;
private boolean isSecureConnection; // Indicates whether the connection to this web server is secure (SSL)
private String lrmiCommandLineArgument = "";
private String cloudName;
private int memoryInMB;
/**
* Sets the service's port.
*
* @param port
* The port to be used by the service
*/
public void setPort(final int port) {
this.port = port;
}
public void setUsername(final String username) {
this.username = username;
}
public void setPassword(final String password) {
this.password = password;
}
/**
* Sets the service's war file.
*
* @param warFile
* The service's war file, required for deployment.
*/
public void setWarFile(final File warFile) {
this.warFile = warFile;
}
/*********
* Installs the web service.
*
* @throws CLIException .
*/
public void installWebService() throws CLIException {
if (isLocalcloud) {
installLocalCloud();
} else {
install();
}
}
/**
* Installs the management web service with the configured settings (e.g. memory, scale). If a dependency on another
* PU is set, the deployment will wait until at least 1 instance of that PU is available.
*
* @throws CLIException
* Reporting a failure to get the Grid Service Manager (GSM) to install the service
*/
@Override
public void install() throws CLIException {
if (agentZone == null) {
throw new IllegalStateException("Management services must be installed on management zone");
}
final ElasticStatelessProcessingUnitDeployment deployment = new ElasticStatelessProcessingUnitDeployment(
getGSFile(warFile))
.memoryCapacityPerContainer(memoryInMB, MemoryUnit.MEGABYTES)
.name(serviceName)
// All PUs on this role share the same machine. Machines
// are identified by zone.
.publicMachineProvisioning(
new DiscoveredMachineProvisioningConfigurer().addGridServiceAgentZone(agentZone)
.reservedMemoryCapacityPerMachine(RESERVED_MEMORY_IN_MB, MemoryUnit.MEGABYTES)
.create())
// Eager scale (1 container per machine per PU)
.scale(new EagerScaleConfigurer().atMostOneContainerPerMachine().create())
.addCommandLineArgument(this.lrmiCommandLineArgument);
for (final Entry<Object, Object> prop : getContextProperties().entrySet()) {
deployment.addContextProperty(prop.getKey().toString(), prop.getValue().toString());
}
for (final String requiredPUName : dependencies) {
deployment.addDependencies(new ProcessingUnitDeploymentDependenciesConfigurer()
.dependsOnMinimumNumberOfDeployedInstancesPerPartition(requiredPUName, 1).create());
}
// The gsc java options define the lrmi port range and memory size if not defined.
try {
getGridServiceManager().deploy(deployment);
} catch (final ProcessingUnitAlreadyDeployedException e) {
// this is possible in a re-bootstrap scenario
logger.warning("Deployment of " + serviceName + " failed because a Processing unit with the "
+ "same name already exists. If this error occured during recovery of management machines, "
+ "this error is normal and can be ignored.");
}
}
/**
* Installs the management web service with the configured settings inside the localcloud dedicated management
* service container. If a dependency on another PU is set, the deployment will wait until at least 1 instance of
* that PU is available.
*
* @throws CLIException
* Reporting a failure to get the Grid Service Manager (GSM) to install the service
*/
public void installLocalCloud() throws CLIException {
if (agentZone == null) {
throw new IllegalStateException("Management services must be installed on management zone");
}
final ProcessingUnitDeployment deployment = new ProcessingUnitDeployment(
getGSFile(warFile))
.addZone(serviceName)
.name(serviceName);
for (final Entry<Object, Object> prop : getContextProperties().entrySet()) {
deployment.setContextProperty(prop.getKey().toString(), prop.getValue().toString());
}
for (final String requiredPUName : dependencies) {
deployment.addDependencies(new ProcessingUnitDeploymentDependenciesConfigurer()
.dependsOnMinimumNumberOfDeployedInstancesPerPartition(requiredPUName, 1).create());
}
getGridServiceManager().deploy(deployment);
}
/**
* {@inheritDoc}
*/
@Override
public void waitForInstallation(final AdminFacade adminFacade, final GridServiceAgent agent, final long timeout,
final TimeUnit timeunit) throws InterruptedException, TimeoutException, CLIException {
final long startTime = System.currentTimeMillis();
waitForProcessingUnitInstance(agent, timeout, timeunit);
final URL url = getProcessingUnitUrl(agent);
final long remainingTime = timeunit.toMillis(timeout) - (System.currentTimeMillis() - startTime);
if (waitForConnection) {
waitForConnection(adminFacade, username, password, url, remainingTime, TimeUnit.MILLISECONDS);
}
}
/**
* Indicates it is required to wait for a successful connection with the service after the service installation
* completes.
*/
public void setWaitForConnection() {
waitForConnection = true;
}
@Override
public void validateManagementService(final Admin admin, final GridServiceAgent agent, final long timeout,
final TimeUnit timeunit) throws InterruptedException, TimeoutException, CLIException {
createConditionLatch(timeout, timeunit).waitFor(new ConditionLatch.Predicate() {
private boolean messagePublished = false;
/**
* {@inheritDoc}
*/
@Override
public boolean isDone() throws CLIException, InterruptedException {
logger.fine("Waiting for " + serviceName + " service.");
if (!messagePublished) {
messagePublished = true;
}
final ProcessingUnit pu = admin.getProcessingUnits().getProcessingUnit(serviceName);
boolean isDone = false;
if (pu != null) {
for (final ProcessingUnitInstance instance : pu) {
final GridServiceContainer gsc = instance.getGridServiceContainer();
if (gsc != null) {
final GridServiceAgent gsa = gsc.getGridServiceAgent();
if (gsa != null && agent.equals(gsa)) {
isDone = true;
break;
}
}
}
}
if (!isDone) {
publishEvent(null);
}
return isDone;
}
});
return;
}
/**
* Waits for a PU instance to be available, indicating the service is installed and running. If the timeout is
* reached before a connection could be established, a {@link TimeoutException} is thrown.
*
* @param agent
* The grid service agent to use
* @param timeout
* number of {@link TimeUnit}s to wait
* @param timeunit
* The {@link TimeUnit} to use
* @throws InterruptedException
* Reporting the thread is interrupted while waiting
* @throws TimeoutException
* Reporting the time out was reached
* @throws CLIException
* Reporting different errors while creating the connection to the PU
*/
public void waitForProcessingUnitInstance(final GridServiceAgent agent, final long timeout, final TimeUnit timeunit)
throws InterruptedException, TimeoutException, CLIException {
createConditionLatch(timeout, timeunit).waitFor(new ConditionLatch.Predicate() {
private boolean messagePublished = false;
/**
* {@inheritDoc}
*/
@Override
public boolean isDone() throws CLIException, InterruptedException {
logger.fine("Waiting for " + serviceName + " service.");
if (!messagePublished) {
final String message = ShellUtils.getMessageBundle().getString("starting_management_web_service");
publishEvent(MessageFormat.format(message, serviceName.toUpperCase()));
messagePublished = true;
}
final ProcessingUnit pu = getProcessingUnit();
boolean isDone = false;
if (pu != null) {
for (final ProcessingUnitInstance instance : pu) {
final GridServiceContainer gsc = instance.getGridServiceContainer();
if (gsc != null) {
final GridServiceAgent gsa = gsc.getGridServiceAgent();
if (gsa != null && agent.equals(gsa)) {
isDone = true;
break;
}
}
}
}
if (!isDone) {
publishEvent(null);
}
return isDone;
}
});
}
/**
* Gets the URL of the service.
*
* @param agent
* The grid service agent to use
* @return the URL of the service
*/
private URL getProcessingUnitUrl(final GridServiceAgent agent) {
// TODO [noak]: verify this always the correct port (SSL-wise) ?
final URL url = getWebProcessingUnitURL(agent, getProcessingUnit(), isSecureConnection);
final String serviceNameCapital = StringUtils.capitalize(serviceName);
final String returnMessage = ShellUtils.getMessageBundle().getString("web_service_available_at");
final String formattedMessage = MessageFormat.format(returnMessage, serviceNameCapital, url);
logger.fine(formattedMessage);
publishEvent(formattedMessage);
return url;
}
/**
* Waits for a connection to be established with the service. If the timeout is reached before a connection could be
* established, a {@link TimeoutException} is thrown.
*
* @param adminFacade
* The admin facade used to connect and disconnect from the REST server
* @param username
* The username for a secure connection to the rest server
* @param password
* The password for a secure connection to the rest server
* @param url
* The URL of the service
* @param timeout
* number of {@link TimeUnit}s to wait
* @param timeunit
* The {@link TimeUnit} to use
* @throws InterruptedException
* Reporting the thread is interrupted while waiting
* @throws TimeoutException
* Reporting the time out was reached
* @throws CLIException
* Reporting different errors while creating the connection to the service
*/
private void waitForConnection(final AdminFacade adminFacade, final String username, final String password,
final URL url, final long timeout, final TimeUnit timeunit)
throws InterruptedException, TimeoutException,
CLIException {
adminFacade.disconnect();
createConditionLatch(timeout, timeunit).waitFor(new ConditionLatch.Predicate() {
/**
* {@inheritDoc}
*/
@Override
public boolean isDone() throws CLIException, InterruptedException {
try {
adminFacade.connect(username, password, url.toString(), isSecureConnection);
return true;
} catch (final Exception e) {
if (verbose) {
logger.log(Level.INFO, "Error connecting to web service [" + serviceName + "].", e);
}
}
logger.log(Level.INFO, "Connecting to web service [" + serviceName + "].");
return false;
}
});
}
/**
* Writes the URL of the service to the log.
*
* @throws CLIException
* Reporting a failure to get the host address
*/
public void logServiceLocation() throws CLIException {
try {
final String serviceNameCapital = StringUtils.capitalize(serviceName);
final String localhost = Constants.getHostAddress();
final String protocol = isSecureConnection ? "https" : "http";
logger.info(serviceNameCapital + " service will be available at: " + protocol + "://"
+ IPUtils.getSafeIpAddress(localhost) + ":" + port);
} catch (final UnknownHostException e) {
throw new CLIException("Failed getting host address", e);
}
}
/**
* {@inheritDoc}
*/
@Override
protected Properties getContextProperties() {
final Properties props = super.getContextProperties();
final String portAsString = String.valueOf(port);
props.setProperty("web.port", portAsString);
props.setProperty("web.sslPort", portAsString);
props.setProperty("web.context", "/");
props.setProperty("web.context.unique", "true");
return props;
}
// TODO:consider delete.
/**
* Waits for the management processes (GSM and ESM) to be available. If the timeout is reached before a connection
* could be established, a {@link TimeoutException} is thrown.
*
* @param timeout
* number of {@link TimeUnit}s to wait
* @param timeunit
* The {@link TimeUnit} to use
* @throws InterruptedException
* Reporting the thread is interrupted while waiting
* @throws TimeoutException
* Reporting the time out was reached
* @throws CLIException
* Reporting different errors while sampling the active management processes
*/
public void waitForManagers(final long timeout, final TimeUnit timeunit) throws InterruptedException,
TimeoutException, CLIException {
createConditionLatch(timeout, timeunit).waitFor(new ConditionLatch.Predicate() {
/**
* {@inheritDoc}
*/
@Override
public boolean isDone() throws CLIException, InterruptedException {
boolean isDone = true;
if (0 == admin.getGridServiceManagers().getSize()) {
isDone = false;
if (verbose) {
logger.fine("Waiting for Grid Service Manager");
publishEvent("Waiting for Grid Service Manager");
}
}
if (admin.getElasticServiceManagers().getSize() == 0) {
isDone = false;
if (verbose) {
logger.fine("Waiting for Elastic Service Manager");
publishEvent("Waiting for Elastic Service Manager");
}
}
if (!isDone && !verbose) {
logger.fine("Waiting for Cloudify management processes");
publishEvent("Waiting for Cloudify management processes");
}
return isDone;
}
});
admin.getGridServiceManagers().waitForAtLeastOne();
}
private ProcessingUnit getProcessingUnit() {
return admin.getProcessingUnits().getProcessingUnit(serviceName);
}
/**
* Constructs and returns the URL of a processing unit instance deployed with the given agent.
*
* @param agent
* The agent of the desired processing unit instance
* @param pu
* The processing unit to find an instance of, for construction of the requested URL
*
* @param isSecureConnection
* .
* @return URL to a specific processing unit instance
*/
public static URL getWebProcessingUnitURL(final GridServiceAgent agent, final ProcessingUnit pu,
final boolean isSecureConnection) {
ProcessingUnitInstance pui = null;
for (final ProcessingUnitInstance instance : pu.getInstances()) {
if (instance.getGridServiceContainer() != null
&& instance.getGridServiceContainer().getGridServiceAgent() != null
&& instance.getGridServiceContainer().getGridServiceAgent().equals(agent)) {
pui = instance;
}
}
if (pui == null) {
throw new IllegalStateException("Failed finding " + pu.getName() + " on "
+ agent.getMachine().getHostAddress());
}
final Map<String, ServiceDetails> alldetails = pui.getServiceDetailsByServiceId();
final ServiceDetails details = alldetails.get("jee-container");
final String host = details.getAttributes().get("host").toString();
final int port = Integer.parseInt(details.getAttributes().get("port").toString());
final String ctx = details.getAttributes().get("context-path").toString();
try {
return new URL(ShellUtils.getRestProtocol(isSecureConnection), host, port, ctx);
} catch (final MalformedURLException e) {
// this is a bug since we formed the URL correctly
throw new IllegalStateException(e);
}
}
/**
* Gets the service's war file. If the war file's path is not an absolute path, it is considered relative to the
* home directory.
*
* @param warFile
* The service's war file object (possibly with a relative path)
* @return The service's war file object
*/
public static File getGSFile(final File warFile) {
File absWarFile = warFile;
if (!absWarFile.isAbsolute()) {
absWarFile = new File(Environment.getHomeDirectory(), warFile.getPath());
}
return absWarFile;
}
/*******
* Adds an event listener.
*
* @param listener
* the listener.
*/
public void addListener(final LocalhostBootstrapperListener listener) {
this.eventsListenersList.add(listener);
}
/********
* Adds events listeners.
*
* @param listeners
* the listeners.
*/
public void addListeners(final List<LocalhostBootstrapperListener> listeners) {
for (final LocalhostBootstrapperListener listener : listeners) {
this.eventsListenersList.add(listener);
}
}
private void publishEvent(final String event) {
for (final LocalhostBootstrapperListener listner : this.eventsListenersList) {
listner.onLocalhostBootstrapEvent(event);
}
}
public void setIsLocalCloud(final boolean isLocalCloud) {
this.isLocalcloud = isLocalCloud;
}
public void setIsSecureConnection(final boolean isSecureConnection) {
this.isSecureConnection = isSecureConnection;
}
public void setLrmiCommandLineArgument(final String lrmiCommandLineArgument) {
this.lrmiCommandLineArgument = lrmiCommandLineArgument;
}
public String getCloudName() {
return cloudName;
}
/**
* Set the memory in various memory units.
*
* @param memory
* number of memory units
* @param unit
* Memory unit to use
*/
public void setMemory(final long memory, final MemoryUnit unit) {
this.memoryInMB = (int) unit.toMegaBytes(memory);
}
}