package org.jboss.as.domain.controller.operations; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CONTENT; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DEPLOYMENT; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.DEPLOYMENT_OVERLAY; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.GROUP; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HASH; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MANAGEMENT_CLIENT_CONTENT; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.PROFILE; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ROLLOUT_PLAN; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ROLLOUT_PLANS; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SERVER; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SERVER_CONFIG; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SERVER_GROUP; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.UNDEFINE_ATTRIBUTE_OPERATION; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.WRITE_ATTRIBUTE_OPERATION; import static org.jboss.as.domain.management.ModelDescriptionConstants.NAME; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.OperationStepHandler; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.operations.common.Util; import org.jboss.as.controller.registry.AttributeAccess; import org.jboss.as.controller.registry.ImmutableManagementResourceRegistration; import org.jboss.as.controller.registry.OperationEntry; import org.jboss.as.controller.registry.Resource; import org.jboss.as.domain.controller.ServerIdentity; import org.jboss.as.domain.controller.operations.coordination.ServerOperationResolver; import org.jboss.as.domain.controller.operations.deployment.SyncModelParameters; import org.jboss.as.host.controller.ManagedServerBootCmdFactory; import org.jboss.as.host.controller.ManagedServerBootConfiguration; import org.jboss.as.management.client.content.ManagedDMRContentTypeResource; import org.jboss.as.repository.ContentReference; import org.jboss.as.server.deployment.ModelContentReference; import org.jboss.as.server.operations.ServerProcessStateHandler; import org.jboss.dmr.ModelNode; import org.jboss.dmr.Property; /** * @author <a href="mailto:kabir.khan@jboss.com">Kabir Khan</a> */ class SyncServerStateOperationHandler implements OperationStepHandler { private final SyncModelParameters parameters; private final List<ModelNode> operations; private enum SyncServerResultAction {RESTART_REQUIRED, RELOAD_REQUIRED}; public SyncServerStateOperationHandler(SyncModelParameters parameters, List<ModelNode> operations) { this.parameters = parameters; this.operations = operations; } @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { assert !context.isBooting() : "Should not be used when the context is booting"; assert parameters.isFullModelTransfer() : "Should only be used during a full model transfer"; final Resource startResource = context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS, true); final ModelNode startRoot = Resource.Tools.readModel(startResource); final String localHostName = startResource.getChildrenNames(HOST).iterator().next(); final ModelNode startHostModel = startRoot.require(HOST).asPropertyList().iterator().next().getValue(); if (!startHostModel.hasDefined(SERVER_CONFIG)) { return; } final ServerOperationResolver resolver = new ServerOperationResolver(localHostName, parameters.getServerProxies()); context.addStep(operation, new OperationStepHandler() { @Override public void execute(OperationContext context, ModelNode operation) throws OperationFailedException { final Resource domainRootResource = context.readResourceForUpdate(PathAddress.EMPTY_ADDRESS); final ModelNode endRoot = Resource.Tools.readModel(domainRootResource); final ModelNode endHostModel = endRoot.require(HOST).asPropertyList().iterator().next().getValue(); //Get the affected servers for each op. ContentDownloader contentDownloader = new ContentDownloader(startRoot, endRoot, endHostModel); final Map<String, SyncServerResultAction> servers = determineServerStateChanges(context, domainRootResource, resolver, contentDownloader); for (String serverName : endHostModel.get(SERVER_CONFIG).keys()) { // Compare boot cmd (requires restart) SyncServerResultAction restart = servers.get(serverName); if (restart == null || restart == SyncServerResultAction.RELOAD_REQUIRED) { //In some unit tests the start config may be null ManagedServerBootConfiguration startConfig = new ManagedServerBootCmdFactory(serverName, startRoot, startHostModel, parameters.getHostControllerEnvironment(), parameters.getDomainController().getExpressionResolver(), false).createConfiguration(); ManagedServerBootConfiguration endConfig = new ManagedServerBootCmdFactory(serverName, endRoot, endHostModel, parameters.getHostControllerEnvironment(), parameters.getDomainController().getExpressionResolver(), false).createConfiguration(); if (startConfig == null || !startConfig.getServerLaunchCommand().equals(endConfig.getServerLaunchCommand())) { servers.put(serverName, SyncServerResultAction.RESTART_REQUIRED); } } } for (Map.Entry<String, SyncServerResultAction> entry : servers.entrySet()) { final PathAddress serverAddress = PathAddress.pathAddress(HOST, localHostName).append(SERVER, entry.getKey()); final String opName = entry.getValue() == SyncServerResultAction.RESTART_REQUIRED ? ServerProcessStateHandler.REQUIRE_RESTART_OPERATION : ServerProcessStateHandler.REQUIRE_RELOAD_OPERATION; final OperationStepHandler handler = context.getResourceRegistration().getOperationHandler(serverAddress, opName); final ModelNode op = Util.createEmptyOperation(opName, serverAddress); context.addStep(op, handler, OperationContext.Stage.MODEL); } context.completeStep(new OperationContext.ResultHandler() { @Override public void handleResult(OperationContext.ResultAction resultAction, OperationContext context, ModelNode operation) { if (resultAction == OperationContext.ResultAction.KEEP) { for (ContentReference ref : contentDownloader.removedContent) { parameters.getContentRepository().removeContent(ref); } } } }); } }, OperationContext.Stage.MODEL); } private Map<String, SyncServerResultAction> determineServerStateChanges(OperationContext context, Resource domainRootResource, ServerOperationResolver resolver, ContentDownloader contentDownloader) { final Map<String, SyncServerResultAction> serverStateChanges = new HashMap<>(); for (ModelNode operation : operations) { PathAddress addr = PathAddress.pathAddress(operation.get(OP_ADDR)); contentDownloader.checkContent(operation, addr); Map<Set<ServerIdentity>, ModelNode> serverMap = resolver.getServerOperations(context, operation, addr); for (Map.Entry<Set<ServerIdentity>, ModelNode> entry : serverMap.entrySet()) { ModelNode op = entry.getValue(); String opName = op.get(OP).asString(); assert opName != null; boolean restart = false; // XXX debug operation.get(OP) vs opName // if we got REQUIRE_RESTART_OPERATION back, or the op.get(OP) doesn't match the operation.get(OP), flag for a restart. if (opName.equals(ServerProcessStateHandler.REQUIRE_RESTART_OPERATION) || (!operation.get(OP).asString().equals(opName))) { restart = true; } // if we got back REQUIRE_RELOAD_OPERATION, trust that. if (!restart && !opName.equals(ServerProcessStateHandler.REQUIRE_RELOAD_OPERATION)) { // otherwise we do an additional check, but only for changes within /profile=* with length > 1 // i.e. not the root profile. if (addr.size() > 1 && addr.getElement(0).getKey().equals(PROFILE)) { restart = checkOperationForRestartRequired(context, operation); } } for (ServerIdentity id : entry.getKey()) { String serverName = id.getServerName(); SyncServerResultAction existing = serverStateChanges.get(serverName); if (existing == null || (existing == SyncServerResultAction.RELOAD_REQUIRED && restart)) { serverStateChanges.put(serverName, restart ? SyncServerResultAction.RESTART_REQUIRED : SyncServerResultAction.RELOAD_REQUIRED); } } } } Set<String> affectedServers = contentDownloader.pullDownContent(domainRootResource); for (String server : affectedServers) { if (!serverStateChanges.containsKey(server)) { serverStateChanges.put(server, SyncServerResultAction.RELOAD_REQUIRED); } } return serverStateChanges; } private boolean checkOperationForRestartRequired(final OperationContext context, final ModelNode operation) { boolean restart = false; final ImmutableManagementResourceRegistration registration = context.getResourceRegistration(); final PathAddress address = PathAddress.pathAddress(operation.get(OP_ADDR)); // an example of this is toggling jts: // /profile=full/subsystem=transactions:write-attribute(name=jts, value=true) // previously on a reconnect to the DC the slave check would flag reload-required on a WRITE_ATTRIBUTE / UNDEFINE_ATTRIBUTE, // so would get flagged into reload-required, but it really should be restart-required to match what would happen doing it with the DC // online, and the registration of the jts attribute. final String opName = operation.get(OP).asString(); if (WRITE_ATTRIBUTE_OPERATION.equals(opName) || UNDEFINE_ATTRIBUTE_OPERATION.equals(opName)) { // look up the attribute name we're writing, and check the flags to see if we need restart. final String attributeName = operation.get(NAME).asString(); // look up if the attribute requires restart / reload final EnumSet<AttributeAccess.Flag> flags = registration.getAttributeAccess(address, attributeName).getAttributeDefinition().getFlags(); if (flags.contains(AttributeAccess.Flag.RESTART_JVM)) { restart = true; } } else { // all other ops final Set<OperationEntry.Flag> flags = registration.getOperationFlags(address, opName); if (flags.contains(OperationEntry.Flag.RESTART_JVM)) { restart = true; } } return restart; } private class ContentDownloader { private final ModelNode startRoot; private final ModelNode endRoot; private final Map<String, Set<String>> serversByGroup; private final Set<String> affectedGroups = new HashSet<>(); private final Map<String, Set<ContentReference>> deploymentHashes = new HashMap<>(); private final Set<String> relevantDeployments = new HashSet<String>(); private final Set<ContentReference> requiredContent = new HashSet<>(); private boolean updateRolloutPlans; private byte[] rolloutPlansHash; private List<ContentReference> removedContent = new ArrayList<>(); ContentDownloader(ModelNode startRoot, ModelNode endRoot, ModelNode hostModel) { this.startRoot = startRoot; this.endRoot = endRoot; serversByGroup = getOurServerGroups(hostModel); } void checkContent(ModelNode operation, PathAddress operationAddress) { if (!operation.get(OP).asString().equals(ADD) || operationAddress.size() == 0) { return; } final PathElement firstElement = operationAddress.getElement(0); final String contentType = firstElement.getKey(); if (contentType == null) { return; } if (operationAddress.size() == 1) { switch (contentType) { case DEPLOYMENT: final String deployment = firstElement.getValue(); Set<ContentReference> hashes = deploymentHashes.get(deployment); if (hashes == null) { hashes = new HashSet<>(); deploymentHashes.put(deployment, hashes); for (ModelNode contentItem : operation.get(CONTENT).asList()) { hashes.add(ModelContentReference.fromModelAddress(operationAddress, contentItem.get(HASH).asBytes())); if (parameters.getHostControllerEnvironment().isBackupDomainFiles()) { relevantDeployments.add(firstElement.getValue()); } } } makeExistingDeploymentUpdatedAffected(firstElement, operation); break; case DEPLOYMENT_OVERLAY: break; case MANAGEMENT_CLIENT_CONTENT: if (firstElement.getValue().equals(ROLLOUT_PLANS)) { updateRolloutPlans = true; //This needs special handling. Drop the existing resource and add a new one if (operation.hasDefined(HASH)) { rolloutPlansHash = operation.get(HASH).asBytes(); requiredContent.add(ModelContentReference.fromModelAddress(operationAddress, rolloutPlansHash)); } } break; default: return; } } else if (operationAddress.size() == 2) { if( firstElement.getKey().equals(SERVER_GROUP) && serversByGroup.containsKey(firstElement.getValue())) { PathElement secondElement = operationAddress.getElement(1); if (secondElement.getKey().equals(DEPLOYMENT)) { relevantDeployments.add(secondElement.getValue()); affectedGroups.add(firstElement.getValue()); } } else if (firstElement.getKey().equals(DEPLOYMENT_OVERLAY)) { requiredContent.add(ModelContentReference.fromModelAddress(operationAddress, operation.get(CONTENT).asBytes())); } } return; } private void makeExistingDeploymentUpdatedAffected(final PathElement deploymentElement, final ModelNode operation) { //Check if this is an existing deployment being updated //If so, check the hashes are different if (!startRoot.hasDefined(deploymentElement.getKey(), deploymentElement.getValue())) { return; } final ModelNode deployment = startRoot.get(deploymentElement.getKey(), deploymentElement.getValue()); final List<ModelNode> currentContents = deployment.get(CONTENT).asList(); final List<ModelNode> newContents = operation.get(CONTENT).asList(); boolean changes = currentContents.size() != newContents.size(); if (!changes) { final Set<byte[]> currentHashes = new HashSet<>(); for (final ModelNode contentItem : currentContents) { currentHashes.add(contentItem.get(HASH).asBytes()); } for (ModelNode contentItem : newContents) { if (!currentHashes.contains(contentItem.get(HASH).asBytes())) { changes = true; break; } } } if (changes) { //There are changes, add all server groups using this deployment to the affectedGroups if (endRoot.hasDefined(SERVER_GROUP)) { for (final Property serverGroup : endRoot.get(SERVER_GROUP).asPropertyList()) { if (serverGroup.getValue().hasDefined(deploymentElement.getKey(), deploymentElement.getValue())) { affectedGroups.add(serverGroup.getName()); relevantDeployments.add(deploymentElement.getValue()); } } } } } Set<String> pullDownContent(final Resource domainRootResource) { // Make sure we have all needed deployment and management client content for (final String id : relevantDeployments) { final Set<ContentReference> hashes = deploymentHashes.remove(id); if (hashes != null) { requiredContent.addAll(hashes); } } for (final ContentReference reference : requiredContent) { parameters.getFileRepository().getDeploymentFiles(reference); parameters.getContentRepository().addContentReference(reference); } if (updateRolloutPlans) { final PathElement rolloutPlansElement = PathElement.pathElement(MANAGEMENT_CLIENT_CONTENT, ROLLOUT_PLANS); final Resource existing = domainRootResource.removeChild(rolloutPlansElement); if (existing != null) { final ModelNode hashNode = existing.getModel().get(HASH); if (hashNode.isDefined()) { removedContent.add( new ContentReference(PathAddress.pathAddress(rolloutPlansElement).toCLIStyleString(), hashNode.asBytes())); } } ManagedDMRContentTypeResource rolloutPlansResource = new ManagedDMRContentTypeResource(PathAddress.pathAddress(rolloutPlansElement), ROLLOUT_PLAN, rolloutPlansHash, parameters.getContentRepository()); domainRootResource.registerChild(rolloutPlansElement, rolloutPlansResource); } final Set<String> servers = new HashSet<>(); for (String group : affectedGroups) { servers.addAll(serversByGroup.get(group)); } return servers; } private Map<String, Set<String>> getOurServerGroups(final ModelNode hostModel) { final Map<String, Set<String>> result = new HashMap<>(); if (hostModel.hasDefined(SERVER_CONFIG)) { for (final Property config : hostModel.get(SERVER_CONFIG).asPropertyList()) { final String group = config.getValue().get(GROUP).asString(); Set<String> servers = result.get(group); if (servers == null) { servers = new HashSet<>(); result.put(group, servers); } servers.add(config.getName()); } } return result; } } }