/**
* 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.jboss.loom;
import java.io.File;
import java.io.IOException;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.*;
import javax.xml.parsers.DocumentBuilder;
import org.eclipse.persistence.exceptions.JAXBException;
import org.jboss.as.cli.batch.BatchedCommand;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.loom.actions.ActionDependencySorter;
import org.jboss.loom.actions.CliCommandAction;
import org.jboss.loom.actions.IMigrationAction;
import org.jboss.loom.actions.ManualAction;
import org.jboss.loom.actions.review.IActionReview;
import org.jboss.loom.conf.AS7Config;
import org.jboss.loom.conf.Configuration;
import org.jboss.loom.ctx.DeploymentInfo;
import org.jboss.loom.ctx.MigrationContext;
import org.jboss.loom.ex.ActionException;
import org.jboss.loom.ex.CliBatchException;
import org.jboss.loom.ex.LoadMigrationException;
import org.jboss.loom.ex.MigrationException;
import org.jboss.loom.migrators.IMigratorFilter;
import org.jboss.loom.migrators._ext.DefinitionBasedMigrator;
import org.jboss.loom.migrators._ext.ExternalMigratorsLoader;
import org.jboss.loom.migrators.windup.WindUpMigrator;
import org.jboss.loom.recog.ServerInfo;
import org.jboss.loom.recog.ServerRecognizer;
import org.jboss.loom.spi.IMigrator;
import org.jboss.loom.tools.report.Reporter;
import org.jboss.loom.utils.Utils;
import org.jboss.loom.utils.XmlUtils;
import org.jboss.loom.utils.as7.AS7CliUtils;
import org.jboss.loom.utils.as7.BatchFailure;
import org.jboss.loom.utils.as7.BatchedCommandWithAction;
import org.jboss.loom.utils.compar.FileHashComparer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* Controls the core migration processes.
*
* TODO: Perhaps leave init() and doMigration() in here
* and separate the other methods to a MigrationService{} ?
*
* @author Ondrej Zizka
*/
public class MigrationEngine {
private static final Logger log = LoggerFactory.getLogger(MigrationEngine.class);
private Configuration config;
private MigrationContext ctx;
private List<IMigrator> migrators;
public MigrationEngine( Configuration config ) throws MigrationException {
this.config = config;
this.init();
this.resetContext( config );
}
public MigrationContext getContext() { return ctx; }
public Configuration getConfig() { return config; }
/** Creates a brand new fresh clear context. */
private void resetContext( Configuration config ) {
this.ctx = new MigrationContext( config );
}
/**
* Initializes this Migrator, especially instantiates the IMigrators.
* No time to make it neatly so it's a bit procedural.
*/
private void init() throws MigrationException {
log.debug("======== init() ========");
// Migrators filter.
final List<String> onlyMig = this.config.getGlobal().getOnlyMigrators();
IMigratorFilter filter = onlyMig == null || onlyMig.isEmpty()
? new IMigratorFilter.All()
: new IMigratorFilter.ByNames( onlyMig );
// Initialize the static java migrators.
Map<Class<? extends IMigrator>, IMigrator> migratorsMap = MigratorsInstantiator.findAndInstantiateStaticMigratorClasses( filter, this.config );
// Initialize the externalized migrators.
String extMigDir = config.getGlobal().getExternalMigratorsDir();
if( extMigDir != null ){
final Map<Class<? extends DefinitionBasedMigrator>, DefinitionBasedMigrator> migs
= new ExternalMigratorsLoader().loadMigrators( new File(extMigDir), filter, this.config.getGlobal() );
log.debug("Loaded " + migs.size() + " external migrators from " + extMigDir);
migratorsMap.putAll( migs );
}
this.migrators = new ArrayList(migratorsMap.values());
// For each migrator...
for( IMigrator mig : this.migrators ){
// Supply some references.
mig.setGlobalConfig( this.config.getGlobal() );
// Let migrators process module-specific args.
for( Configuration.ModuleSpecificProperty moduleOption : config.getModuleConfigs() ){
mig.examineConfigProperty( moduleOption );
}
}
}// init()
/**
* Performs the migration.
*
* 1) Parse AS 7 config into context.
2) Let the migrators gather the data into the context.
3) Let them prepare the actions.
An action should include what caused it to be created. IMigrationAction.getOriginMessage()
==== From now on, don't use the scanned data, only actions. ===
4) reviewActions
5) preValidate
6) backup
7) perform
8) postValidate
9] rollback
*/
public void doMigration() throws MigrationException {
log.info("Commencing migration.");
boolean dryRun = config.getGlobal().isDryRun();
this.resetContext( config );
// Recognize version of the source server.
this.recognizeSourceServer();
// Parse AS 7 config. Not needed anymore - we use CLI.
this.parseAS7Config();
// Unzip the deployments.
this.unzipDeployments();
// MIGR-31 - The new way.
String message = null;
try {
// Load the source server config.
message = "Failed loading source server config.";
this.loadASourceServerConfig();
// Open an AS 7 management client connection.
message = "Failed opening target server management client.";
this.openManagementClient();
// Ask all the migrators to create the actions to be performed.
message = "Failed preparing the migration actions.";
this.prepareActions();
message = "Actions review failed.";
this.reviewActions();
message = "Migration actions validation failed.";
this.preValidateActions();
message = "Failed creating backups for the migration actions.";
this.backupActions();
// Perform
message = "Failed performing the migration actions.";
this.performActions();
if( ! dryRun ){
message = "Verification of migration actions results failed.";
this.postValidateActions();
}
// Inform the user about necessary manual actions
this.announceManualActions();
}
catch( MigrationException ex ) {
// Rollback.
this.rollbackActionsWhichWerePerformed();
// Build up a description of what happened.
String description = "";
if( ex instanceof ActionException ){
description = ((ActionException) ex).formatDescription();
}
this.ctx.setFinalException( new MigrationException( message
+ "\n " + ex.getMessage()
+ description, ex ) );
// Clean backups - only if rollback went fine.
try {
this.cleanBackupsIfAny();
} catch ( Exception ex2 ){
log.error("Cleaning backups of migration actions failed: " + ex2.getMessage(), ex2 );
}
}
finally {
// Close the AS 7 management client connection.
this.closeManagementClient();
}
// Report
this.createReport();
if( this.ctx.getFinalException() != null)
throw this.ctx.getFinalException();
}// migrate()
/**
* Ask all the migrators to create the actions to be performed; stores them in the context.
*/
private void prepareActions() throws MigrationException {
log.debug("====== prepareActions() ========");
// Call all migrators to create their actions.
try {
for( IMigrator mig : this.migrators ) {
log.debug(" Preparing actions with " + mig.getClass().getSimpleName());
mig.createActions(this.ctx);
}
} catch( Exception ex ){
throw new MigrationException(ex);
}
finally {
// Set migration context to all actions (don't rely on migrators to do that).
for( IMigrationAction action : this.ctx.getActions() ) {
action.setMigrationContext( this.ctx );
}
}
}
/*
* -------------- Actions methods. --------------
*/
/**
* TODO: Additional logic to filter out duplicated file copying etc.
*/
private void reviewActions() throws MigrationException {
log.debug("======== reviewActions() ========");
List<IMigrationAction> actions = ctx.getActions();
for( Class<? extends IActionReview> arClass : MigratorsInstantiator.findActionReviewers() ){
IActionReview ar;
try {
ar = arClass.newInstance();
} catch( InstantiationException | IllegalAccessException ex ) {
throw new MigrationException("Can't instantiate action reviewer " + arClass.getSimpleName() + ": " + ex, ex);
}
ar.setContext(ctx);
ar.setConfig(config);
for( IMigrationAction action : actions ) {
ar.review( action );
}
}
}
private void preValidateActions() throws MigrationException {
log.debug("======== preValidateActions() ========");
List<IMigrationAction> actions = ctx.getActions();
for( IMigrationAction action : actions ) {
action.setMigrationContext(ctx);
action.preValidate();
}
}
private void backupActions() throws MigrationException {
log.debug("======== backupActions() ========");
List<IMigrationAction> actions = ctx.getActions();
for( IMigrationAction action : actions ) {
action.backup();
}
}
/**
* Performs the actions.
* Should do all the active steps: File manipulation, AS CLI commands etc.
*/
private void performActions() throws MigrationException {
log.debug("======== performActions() ========");
if( ctx.getActions().isEmpty() ){
log.info("No actions to run.");
return;
}
boolean dryRun = config.getGlobal().isDryRun();
if(dryRun)
log.info("\n** This is a DRY RUN, operations are not really performed, only prepared and listed. **\n");
String dryPrefix = dryRun ? "(DRY RUN) " : "";
// Clear CLI commands, should there be any.
ctx.getBatch().clear();
// Sort the actions according to dependencies. MIGR-104
List<IMigrationAction> actions = ctx.getActions();
List<IMigrationAction> sorted = ActionDependencySorter.sort( actions );
// Store CLI actions into an ordered list.
// In perform(), they are just put into a batch. Using this, we can tell which one failed.
List<CliCommandAction> cliActions = new LinkedList();
// Perform the actions.
log.info(dryPrefix + "Performing actions:");
for( IMigrationAction action : sorted ) {
if( action instanceof CliCommandAction )
cliActions.add((CliCommandAction) action);
log.info(" " + action.toDescription());
action.setMigrationContext(ctx); // Again. To be sure.
// On dry run, CliCommandActions can still be performed as they only add to the batch.
if( ! dryRun || (action instanceof CliCommandAction) )
try {
action.perform();
} catch( ActionException ex ){
throw ex;
} catch( Throwable ex ){
throw new ActionException( action, "Failed to perform action:\n"+action.toDescription()+"\n " + ex.getMessage(), ex);
}
}
/// DEBUG: Dump created CLI operations
if( ctx.getBatch().getCommands().isEmpty() ){
log.info("No CLI operations to perform.");
return;
}
log.debug(dryPrefix + "Management operations in batch:");
int i = 1;
for( BatchedCommand command : ctx.getBatch().getCommands() ){
log.debug(" " + i++ + ": " + command.getCommand());
}
// CLI batch execution.
log.debug(dryPrefix + "Executing CLI batch:");
try {
if( ! dryRun )
AS7CliUtils.executeRequest( ctx.getBatch().toRequest(), config.getGlobal().getAS7Config() );
}
catch( CliBatchException ex ){
//Integer index = AS7CliUtils.parseFailedOperationIndex( ex.getResponseNode() );
BatchFailure failure = AS7CliUtils.extractFailedOperationNode( ex.getResponseNode() );
if( null == failure ){
log.warn("Unable to parse CLI batch operation index: " + ex.getResponseNode());
throw new MigrationException("Executing a CLI batch failed: " + ex, ex);
}
IMigrationAction causeAction;
// First, try if it's a BatchedCommandWithAction, and get the action if so.
BatchedCommand cmd = ctx.getBatch().getCommands().get( failure.getIndex() - 1 );
if( cmd instanceof BatchedCommandWithAction )
causeAction = ((BatchedCommandWithAction)cmd).getAction();
// Then shoot blindly into cliActions. May be wrong offset - some actions create multiple CLI commands! TODO.
else
causeAction = cliActions.get( failure.getIndex() - 1 );
throw new ActionException( causeAction, "Executing a CLI batch failed: " + failure.getMessage());
}
catch( Exception ex ) {
throw new MigrationException("Executing a CLI batch failed: " + ex, ex);
}
}// performActions()
private void postValidateActions() throws MigrationException {
log.debug("======== postValidateActions() ========");
List<IMigrationAction> actions = ctx.getActions();
for( IMigrationAction action : actions ) {
action.postValidate();
}
}
private void cleanBackupsIfAny() throws MigrationException {
log.debug("======== cleanBackupsIfAny() ========");
List<IMigrationAction> actions = ctx.getActions();
for( IMigrationAction action : actions ) {
//if( action.isAfterBackup()) // Checked in cleanBackup() itself.
action.cleanBackup();
}
}
private void rollbackActionsWhichWerePerformed() throws MigrationException {
log.debug("======== rollbackActionsWhichWerePerformed() ========");
List<IMigrationAction> actions = ctx.getActions();
for( IMigrationAction action : actions ) {
//if( action.isAfterPerform()) // Checked in rollback() itself.
try {
action.rollback();
} catch ( ActionException ex ){
throw new MigrationException( "Rollback failed: " + ex.formatDescription(), ex );
}
}
}
/**
* Creates a migration report.
* Can't throw.
*/
private void createReport() {
try {
Reporter.createReport( ctx, new File(config.getGlobal().getReportDir()) );
}
catch( Throwable ex ){
log.error("Failed creating migration report:\n " + ex.getMessage(), ex);
// Only throw if it's a test run; Only log on normal run.
if( config.getGlobal().isTestRun() )
throw new RuntimeException(ex);
}
}
private void announceManualActions(){
log.debug("======== announceManualActions() ========");
boolean bannerShown = false;
List<IMigrationAction> actions = ctx.getActions();
for( IMigrationAction action : actions ) {
if( ! ( action instanceof ManualAction ) )
continue;
List<String> warns = ((ManualAction)action).getWarnings();
for( String warn : warns ) {
if( ! bannerShown ) bannerShown = showBanner();
log.warn( warn );
}
}
if( bannerShown ){
log.warn("\n"
+ "\n===================================================================="
+ "\n End of manual actions."
+ "\n====================================================================\n");
}
}
private boolean showBanner(){
log.warn("\n"
+ "\n===================================================================="
+ "\n Some parts of the source server configuration are not supported "
+ "\n and need to be done manually. See the messages bellow."
+ "\n====================================================================\n");
return true;
}
/**
* Calls all migrators' callback for loading configuration data from the source server.
*
* @throws LoadMigrationException
*/
private void loadASourceServerConfig() throws MigrationException {
log.debug("======== loadASourceServerConfig() ========");
try {
for (IMigrator mig : this.migrators) {
log.debug(" Scanning with " + mig.getClass().getSimpleName());
mig.loadSourceServerConfig(this.ctx);
}
} catch (JAXBException e) {
throw new LoadMigrationException(e);
}
}
/**
* Recognize the source server version (and type, in the future).
*/
private void recognizeSourceServer() throws MigrationException {
log.debug("======== recognizeSourceServer() ========");
File serverDir = new File(config.getGlobal().getAS5Config().getDir());
try {
// Recognize
ServerInfo serverInfo = ServerRecognizer.recognize( serverDir );
log.info("Source server recognized as " + serverInfo.format());
// Compute files hashes
try {
serverInfo.compareHashes();
log.info("Hash comparison against distribution files: " + serverInfo.getHashesComparisonResult().formatStats());
announceHashComparisonResults( serverInfo, config.getGlobal().isTestRun() );
} catch( Exception ex ){
log.error("Failed comparing files hashes for " + serverInfo.format() + ":\n " + ex.getMessage(), ex);
}
this.ctx.setSourceServer( serverInfo );
}
catch( Exception ex ) {
throw new MigrationException("Failed recognizing the source server in " + serverDir + ":\n " + ex.getMessage(), ex);
}
}
// Helper for the above method.
private static void announceHashComparisonResults( ServerInfo serverInfo, boolean noMissOrEmpty ) {
Map<Path, FileHashComparer.MatchResult> matches = serverInfo.getHashesComparisonResult().getMatches();
for( Map.Entry<Path, FileHashComparer.MatchResult> entry : matches.entrySet() ) {
if( entry.getValue() == FileHashComparer.MatchResult.MATCH ) continue;
if( entry.getValue() == FileHashComparer.MatchResult.MISSING && noMissOrEmpty ) continue;
if( entry.getValue() == FileHashComparer.MatchResult.EMPTY && noMissOrEmpty ) continue;
log.info(" " + entry.getValue().rightPad() + ": " + entry.getKey());
}
if( noMissOrEmpty ) log.info("This is a test run, MISSING and EMPTY files aren't printed.");
}
/**
* Unzips the apps specified in config to temp dirs, to be deleted at the end.
*/
private void unzipDeployments() throws MigrationException {
Set<String> deplPaths = this.config.getGlobal().getDeploymentsPaths();
List<DeploymentInfo> depls = new ArrayList( deplPaths.size() );
for( String path : deplPaths ) {
File deplZip = new File( path );
if( !deplZip.exists() ){
log.warn( "Application not found: " + path );
continue;
}
DeploymentInfo depl = new DeploymentInfo( path );
// It's a dir - no need to unzip.
if( deplZip.isDirectory() ){
depls.add( depl );
continue;
}
// It's a file - try to unzip.
//AppConfigUtils.unzipDeployment( deplZip )
depl.unzipToTmpDir();
depls.add( depl );
}
ctx.setDeployments( depls );
}
// AS 7 management client connection.
private void openManagementClient() throws MigrationException {
ModelControllerClient as7Client = null;
AS7Config as7Config = this.config.getGlobal().getAS7Config();
try {
as7Client = ModelControllerClient.Factory.create( as7Config.getHost(), as7Config.getManagementPort() );
}
catch( UnknownHostException ex ){
throw new MigrationException("Unknown AS 7 host: " + as7Config.getHost(), ex);
}
try {
as7Client.execute( AS7CliUtils.parseCommand("/core-service=platform-mbean/type=runtime:read-attribute(name=system-properties)") );
}
catch( IOException ex ){
String rootMsg = Utils.getRootCause( ex ).getMessage();
throw new MigrationException("Failed connecting to AS 7 host, is it running? " + as7Config.getHost() + "\n " + rootMsg); //, ex
}
this.ctx.setAS7ManagementClient( as7Client );
}
private void closeManagementClient(){
AS7CliUtils.safeClose( ctx.getAS7Client() );
ctx.setAS7ManagementClient( null );
}
/**
* Parses AS 7 config.
* @deprecated Not needed anymore - we use CLI.
*/
private void parseAS7Config() throws MigrationException {
File as7configFile = new File( this.config.getGlobal().getAS7Config().getConfigFilePath() );
try {
DocumentBuilder db = XmlUtils.createXmlDocumentBuilder();
Document doc = db.parse(as7configFile);
ctx.setAS7ConfigXmlDoc(doc);
// TODO: Do backup at file level, instead of parsing and writing back.
// And rework it in general. MIGR-23.
doc = db.parse(as7configFile);
ctx.setAs7ConfigXmlDocOriginal(doc);
}
catch ( SAXException | IOException ex ) {
throw new MigrationException("Failed loading AS 7 config from " + as7configFile, ex );
}
}
}// class