Skip to content

Commit

Permalink
Added basic migration capability to org.apache.brooklyn.entity.webapp.*
Browse files Browse the repository at this point in the history
This effector is pluggable via brooklyn.initializers,

Example YAML:

```
name: My Migrable WebApp
location: aws-ireland
services:
- type: org.apache.brooklyn.entity.webapp.tomcat.TomcatServer
  name: AppServer HelloWorld
  brooklyn.config:
    wars.root: http://search.maven.org/remotecontent?filepath=io/brooklyn/example/brooklyn-example-hello-world-sql-webapp/0.6.0/brooklyn-example-hello-world-sql-webapp-0.6.0.war
  brooklyn.initializers:
  - type: org.apache.brooklyn.entity.webapp.JavaWebAppMigrateEffector
```
  • Loading branch information
Adrian Nieto committed Nov 23, 2015
1 parent 15faad3 commit 4594914
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.core.effector;

import com.google.common.annotations.Beta;
import org.apache.brooklyn.core.annotation.Effector;
import org.apache.brooklyn.core.annotation.EffectorParam;

@Beta
public interface MigrateEffector {

MethodEffector<Void> MIGRATE = new MethodEffector<Void>(MigrateEffector.class, "migrate");
String MIGRATE_LOCATION_SPEC = "locationSpec";

/**
* Starts a migration process.
* It calls stop() on the original locations and start() on the new one.
* <p/>
* After this process finishes it refreshes all the sibling entities dependent data (ConfigKeys, Env variables...)
*/
@Beta
@Effector(description = "Migrates the current entity to another location. It will free the provisioned resources" +
" used by the former location")
void migrate(@EffectorParam(name = MIGRATE_LOCATION_SPEC, description = "Location Spec", nullable = false) String locationSpec);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.core.initializer;

import com.google.common.annotations.Beta;
import com.google.common.collect.Lists;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntityInitializer;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.core.effector.MigrateEffector;
import org.apache.brooklyn.core.effector.EffectorBody;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Beta
public class AddMigrateEffectorEntityInitializer implements EntityInitializer {

private static final Logger LOG = LoggerFactory.getLogger(AddMigrateEffectorEntityInitializer.class);

@Override
public void apply(final EntityLocal entity) {
if (LOG.isDebugEnabled()) {
LOG.debug("Adding Migrate effector to {}", entity);
}

((EntityInternal) entity).getMutableEntityType().addEffector(MigrateEffector.MIGRATE, new EffectorBody<Void>() {
@Override
public Void call(ConfigBag parameters) {
migrate((EntityInternal) entity, (String) parameters.getStringKey(MigrateEffector.MIGRATE_LOCATION_SPEC));
return null;
}
});
}

private static void migrate(EntityInternal entity, String locationSpec) {
if (entity.sensors().get(Attributes.SERVICE_STATE_ACTUAL) != Lifecycle.RUNNING) {
// Is it necessary to check if the whole application is healthy?
throw new RuntimeException("The entity needs to be healthy before the migration starts");
}

if (entity.getParent() != null && !entity.getParent().equals(entity.getApplication())) {
/*
* TODO: Allow nested entites to be migrated
* If the entity has a parent different to the application root the migration cannot be done right now,
* as it could lead into problems to deal with hierarchies like SameServerEntity -> Entity
*/

throw new RuntimeException("Nested entities cannot be migrated right now");
}

// Retrieving the location from the catalog.
Location newLocation = Entities.getManagementContext(entity).getLocationRegistry().resolve(locationSpec);

// TODO: Find a better way to check if you're migrating an entity to the exactly same VM. This not always works.
for (Location oldLocation : entity.getLocations()) {
if (oldLocation.containsLocation(newLocation)) {
LOG.warn("You cannot migrate an entity to the same location, the migration process will stop right now");
return;
}
}

LOG.info("Migration process of " + entity.getId() + " started.");

// When we have the new location, we free the resources of the current instance
entity.invoke(Startable.STOP, MutableMap.<String, Object>of()).blockUntilEnded();

// Clearing old locations to remove the relationship with the previous instance
entity.clearLocations();
entity.addLocations(Lists.newArrayList(newLocation));

// Starting the new instance
entity.invoke(Startable.START, MutableMap.<String, Object>of()).blockUntilEnded();

// Refresh all the dependent entities

for (Entity applicationChild : entity.getApplication().getChildren()) {
// TODO: Find a better way to refresh the application configuration.
// TODO: Refresh nested entities or find a way to propagate the restart properly.

// Restart any entity but the migrated one.
if (applicationChild instanceof Startable) {
if (entity.equals(applicationChild)) {
// The entity is sensors should rewired automatically on stop() + restart()
} else {
// Restart the entity to fetch again all the dependencies (ie. attributeWhenReady ones)
((Startable) applicationChild).restart();
}
}
}

LOG.info("Migration process of " + entity.getId() + " finished.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,24 @@
*/
package org.apache.brooklyn.entity.webapp;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.File;
import java.net.URI;
import java.util.Set;

import com.google.common.collect.ImmutableList;
import com.google.common.net.HostAndPort;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.location.access.BrooklynAccessUtils;
import org.apache.brooklyn.entity.java.JavaSoftwareProcessSshDriver;
import org.apache.brooklyn.entity.java.UsesJmx;
import org.apache.brooklyn.feed.jmx.JmxHelper;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.core.task.ssh.SshTasks;
import org.apache.brooklyn.util.text.Strings;

import com.google.common.collect.ImmutableList;
import java.io.File;
import java.net.URI;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;

public abstract class JavaWebAppSshDriver extends JavaSoftwareProcessSshDriver implements JavaWebAppDriver {

Expand Down Expand Up @@ -112,6 +113,10 @@ public void postLaunch() {
String rootUrl = inferRootUrl();
entity.sensors().set(Attributes.MAIN_URI, URI.create(rootUrl));
entity.sensors().set(WebAppService.ROOT_URL, rootUrl);

// JMX_SERVICE_URL is not updated properly when the locations changes so we update in postlaunch to avoid
// isRunning to fail finding the right location to check.
entity.sensors().set(UsesJmx.JMX_URL, JmxHelper.toJmxmpUrl(getHostname(), entity.getAttribute(UsesJmx.JMX_PORT)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.entity.webapp;

import com.google.common.collect.ImmutableList;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.ProvisioningLocation;
import org.apache.brooklyn.core.effector.Effectors;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.initializer.AddMigrateEffectorEntityInitializer;
import org.apache.brooklyn.core.internal.BrooklynProperties;
import org.apache.brooklyn.core.test.BrooklynAppLiveTestSupport;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.core.effector.MigrateEffector;
import org.apache.brooklyn.entity.webapp.jboss.JBoss6Server;
import org.apache.brooklyn.entity.webapp.jboss.JBoss7Server;
import org.apache.brooklyn.entity.webapp.jetty.Jetty6Server;
import org.apache.brooklyn.entity.webapp.tomcat.Tomcat8Server;
import org.apache.brooklyn.entity.webapp.tomcat.TomcatServer;
import org.apache.brooklyn.test.EntityTestUtils;
import org.apache.brooklyn.test.HttpTestUtils;
import org.apache.brooklyn.test.support.TestResourceUnavailableException;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.time.Duration;
import org.reflections.Reflections;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

// This test could extend AbstractEc2LiveTest but then it will not be able to be parametrized properly.
public class WebAppMigrationLiveTest extends BrooklynAppLiveTestSupport {
private static final String PROVIDER = "aws-ec2";
private static final String REGION_NAME = "us-west-2";
private static final String LOCATION_SPEC = PROVIDER + (REGION_NAME == null ? "" : ":" + REGION_NAME);

private BrooklynProperties brooklynProperties;
private ProvisioningLocation amazonProvisioningLocation;

@Override
@BeforeMethod(alwaysRun = true)
public void setUp() throws Exception {
brooklynProperties = BrooklynProperties.Factory.newDefault();

// EC2 setup obtained from AbstractEc2LiveTest
brooklynProperties.remove("brooklyn.jclouds." + PROVIDER + ".image-description-regex");
brooklynProperties.remove("brooklyn.jclouds." + PROVIDER + ".image-name-regex");
brooklynProperties.remove("brooklyn.jclouds." + PROVIDER + ".image-id");
brooklynProperties.remove("brooklyn.jclouds." + PROVIDER + ".inboundPorts");
brooklynProperties.remove("brooklyn.jclouds." + PROVIDER + ".hardware-id");

// Also removes scriptHeader (e.g. if doing `. ~/.bashrc` and `. ~/.profile`, then that can cause "stdin: is not a tty")
brooklynProperties.remove("brooklyn.ssh.config.scriptHeader");
mgmt = new LocalManagementContextForTests(brooklynProperties);
amazonProvisioningLocation = (ProvisioningLocation) mgmt.getLocationRegistry().resolve(LOCATION_SPEC);
super.setUp();
}

@DataProvider(name = "webAppEntities")
public Object[][] webAppEntities() {
Object[][] dataProviderResult = new Object[][] {
{EntitySpec.create(JBoss6Server.class)},
{EntitySpec.create(JBoss7Server.class)},
{EntitySpec.create(Jetty6Server.class)},
{EntitySpec.create(TomcatServer.class)},
{EntitySpec.create(Tomcat8Server.class)}
};

return dataProviderResult;
}


private List<Object[]> getEntitiesFromClasses(Set<Class<? extends JavaWebAppSoftwareProcess>> clazzSet) {
ArrayList<Object[]> entitiyList = new ArrayList<>();

for (Class clazz : clazzSet) {
if (clazz.isInterface() && !clazz.getName().contains("JavaWebAppSoftwareProcess")) {
EntitySpec entity = EntitySpec.create(clazz);
entitiyList.add(new Object[]{entity});
}

}


return entitiyList;
}

private String getTestWar() {
TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/hello-world.war");
return "classpath://hello-world.war";
}

@Test(groups = "Live", dataProvider = "webAppEntities")
public void testMigration(EntitySpec<? extends JavaWebAppSoftwareProcess> webServerSpec) throws Exception {
final JavaWebAppSoftwareProcess server = app.createAndManageChild(webServerSpec
.addInitializer(new AddMigrateEffectorEntityInitializer())
.configure(JavaWebAppSoftwareProcess.OPEN_IPTABLES, true)
.configure("war", getTestWar()));

// Run the webserver in the premigration location
app.start(ImmutableList.of(amazonProvisioningLocation));
EntityTestUtils.assertAttributeEqualsEventually(server, Attributes.SERVICE_UP, Boolean.TRUE);

// Check if the WebServer is reachable in the premigration location
String preMigrationUrl = server.getAttribute(JavaWebAppSoftwareProcess.ROOT_URL);
HttpTestUtils.assertUrlReachable(preMigrationUrl);

// Start the migration process
Effectors.invocation(MigrateEffector.MIGRATE, MutableMap.of("locationSpec", LOCATION_SPEC), server).asTask().blockUntilEnded(Duration.FIVE_MINUTES);
EntityTestUtils.assertAttributeEqualsEventually(server, Attributes.SERVICE_UP, Boolean.TRUE);

// Check if the WebServer is reachable in the postmigration location
String afterMigrationUrl = server.getAttribute(JavaWebAppSoftwareProcess.ROOT_URL);
HttpTestUtils.assertUrlReachable(afterMigrationUrl);

// Check if the old location was unprovisioned succesfully
HttpTestUtils.assertUrlUnreachable(preMigrationUrl);

}


}

0 comments on commit 4594914

Please sign in to comment.