/* * JBoss, Home of Professional Open Source. * Copyright 2014, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.domain.controller.operations; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.INCLUDES; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.LOCAL_DESTINATION_OUTBOUND_SOCKET_BINDING; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.PROFILE; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REMOTE_DESTINATION_OUTBOUND_SOCKET_BINDING; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SOCKET_BINDING; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SOCKET_BINDING_GROUP; import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationContext.AttachmentKey; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.OperationStepHandler; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.registry.Resource; import org.jboss.as.controller.registry.Resource.ResourceEntry; import org.jboss.as.host.controller.logging.HostControllerLogger; import org.jboss.dmr.ModelNode; /** * Handler validating that "including" resources don't involve cycles * and that including resources don't involve children that override the included resources. * * @author Emanuel Muckenhuber * @author Kabir Khan */ public class DomainModelIncludesValidator implements OperationStepHandler { private static DomainModelIncludesValidator INSTANCE = new DomainModelIncludesValidator(); private static final AttachmentKey<DomainModelIncludesValidator> KEY = AttachmentKey.create(DomainModelIncludesValidator.class); private DomainModelIncludesValidator() { } public static void addValidationStep(OperationContext context, ModelNode operation) { assert !context.getProcessType().isServer() : "Not a host controller"; if (!context.isBooting()) { // This does not need to get executed on boot the domain controller service does that once booted // by calling validateAtBoot(). Otherwise we get issues with the testsuite, which only partially sets up the model if (context.attachIfAbsent(KEY, DomainModelIncludesValidator.INSTANCE) == null) { context.addStep(DomainModelIncludesValidator.INSTANCE, OperationContext.Stage.MODEL); } } } public static void validateAtBoot(OperationContext context, ModelNode operation) { assert !context.getProcessType().isServer() : "Not a host controller"; assert context.isBooting() : "Should only be called at boot"; assert operation.require(OP).asString().equals("validate"); //Should only be called by the domain controller service //Only validate once if (context.attachIfAbsent(KEY, DomainModelIncludesValidator.INSTANCE) == null) { context.addStep(DomainModelIncludesValidator.INSTANCE, OperationContext.Stage.MODEL); } } @Override public void execute(final OperationContext context, final ModelNode operation) throws OperationFailedException { // Validate validate(context); } public void validate(final OperationContext context) throws OperationFailedException { final Resource domain = context.readResourceFromRoot(PathAddress.EMPTY_ADDRESS); final Set<String> missingProfiles = new HashSet<>(); final Set<String> missingSocketBindingGroups = new HashSet<>(); checkProfileIncludes(domain, missingProfiles); checkSocketBindingGroupIncludes(domain, missingSocketBindingGroups); } private Set<String> checkProfileIncludes(Resource domain, Set<String> missingProfiles) throws OperationFailedException { ProfileIncludeValidator validator = new ProfileIncludeValidator(); for (ResourceEntry entry : domain.getChildren(PROFILE)) { validator.processResource(entry); } validator.validate(missingProfiles); return validator.resourceIncludes.keySet(); } private Set<String> checkSocketBindingGroupIncludes(Resource domain, Set<String> missingSocketBindingGroups) throws OperationFailedException { SocketBindingGroupIncludeValidator validator = new SocketBindingGroupIncludeValidator(); for (ResourceEntry entry : domain.getChildren(SOCKET_BINDING_GROUP)) { validator.processResource(entry); } validator.validate(missingSocketBindingGroups); return new HashSet<>(validator.resourceIncludes.keySet()); } private abstract static class AbstractIncludeValidator { protected final Set<String> seen = new HashSet<>(); protected final Set<String> onStack = new HashSet<>(); protected final Map<String, String> linkTo = new HashMap<>(); protected final Map<String, Set<String>> resourceIncludes = new HashMap<>(); protected final Map<String, Set<String>> resourceChildren = new HashMap<>(); protected final List<String> post = new ArrayList<>(); void processResource(ResourceEntry resourceEntry) throws OperationFailedException{ ModelNode model = resourceEntry.getModel(); final Set<String> includes; if (model.hasDefined(INCLUDES)) { includes = new HashSet<>(); for (ModelNode include : model.get(INCLUDES).asList()) { includes.add(include.asString()); } } else { includes = Collections.emptySet(); } resourceIncludes.put(resourceEntry.getName(), includes); } void validate(Set<String> missingEntries) throws OperationFailedException { //Look for cycles for (String resourceName : resourceIncludes.keySet()) { if (!seen.contains(resourceName)) { dfsForMissingOrCyclicIncludes(resourceName, missingEntries); } } if (missingEntries.size() > 0) { //We are missing some entries, don't continue with the validation since it has failed return; } //Check that children are not overridden, by traversing them in the order child->parent //using the reverse post-order of the dfs seen.clear(); for (ListIterator<String> it = post.listIterator(post.size()) ; it.hasPrevious() ; ) { String resourceName = it.previous(); if (seen.contains(resourceName)) { continue; } List<String> stack = new ArrayList<>(); Map<String, List<String>> reachableChildren = new HashMap<>(); validateChildrenNotOverridden(resourceName, reachableChildren, stack); } } void validateChildrenNotOverridden(String resourceName, Map<String, List<String>> reachableChildren, List<String> stack) throws OperationFailedException { stack.add(resourceName); try { seen.add(resourceName); Set<String> includes = resourceIncludes.get(resourceName); Set<String> children = resourceChildren.get(resourceName); if (includes.size() == 0 && children.size() == 0) { return; } for (String child : resourceChildren.get(resourceName)) { List<String> existingChildParentStack = reachableChildren.get(child); if (existingChildParentStack != null) { logError(resourceName, stack, child, existingChildParentStack); } reachableChildren.put(child, new ArrayList<>(stack)); } for (String include : includes) { validateChildrenNotOverridden(include, reachableChildren, stack); } } finally { stack.remove(stack.size() - 1); } } private void logError(String resourceName, List<String> stack, String child, List<String> existingChildParentStack) throws OperationFailedException { //Now figure out if this is a direct override, or no override but including two parents //with the same child for (ListIterator<String> it = stack.listIterator(stack.size()) ; it.hasPrevious() ; ) { String commonParent = it.previous(); if (existingChildParentStack.contains(commonParent)) { if (!getLastElement(existingChildParentStack).equals(commonParent)) { //This is not an override but 'commonParent' includes two parents with the same child throw twoParentsWithSameChild(commonParent, getLastElement(stack), getLastElement(existingChildParentStack), child); } } } //It is a direct override //Alternatively, something went wrong when trying to determine the cause, in which case this message //will not be 100% correct, but it is better to get an error than not. throw attemptingToOverride(getLastElement(existingChildParentStack), child, resourceName); } private String getLastElement(List<String> list) { return list.get(list.size() - 1); } protected abstract OperationFailedException twoParentsWithSameChild(String commonParent, String include1, String include2, String child); void dfsForMissingOrCyclicIncludes(String resourceName, Set<String> missingEntries) throws OperationFailedException { onStack.add(resourceName); try { seen.add(resourceName); Set<String> includes = resourceIncludes.get(resourceName); if (includes == null) { missingEntries.add(resourceName); return; } for (String include : includes) { if (!seen.contains(include)) { linkTo.put(include, resourceName); dfsForMissingOrCyclicIncludes(include, missingEntries); } else if (onStack.contains(include)) { throw involvedInACycle(include); } } } finally { onStack.remove(resourceName); } post.add(resourceName); } abstract OperationFailedException attemptingToOverride(String parentOfExistingChild, String child, String resourceName); abstract OperationFailedException involvedInACycle(String profile); } private static class ProfileIncludeValidator extends AbstractIncludeValidator { void processResource(ResourceEntry profileEntry) throws OperationFailedException { super.processResource(profileEntry); final Set<String> subsystems; if (profileEntry.hasChildren(SUBSYSTEM)) { subsystems = new HashSet<>(); subsystems.addAll(profileEntry.getChildrenNames(SUBSYSTEM)); } else { subsystems = Collections.emptySet(); } resourceChildren.put(profileEntry.getName(), subsystems); } @Override OperationFailedException attemptingToOverride(String parentOfExistingChild, String child, String resourceName) { return HostControllerLogger.ROOT_LOGGER.profileAttemptingToOverrideSubsystem(parentOfExistingChild, child, resourceName); } @Override OperationFailedException involvedInACycle(String include) { return HostControllerLogger.ROOT_LOGGER.profileInvolvedInACycle(include); } @Override protected OperationFailedException twoParentsWithSameChild(String commonParent, String include1, String include2, String child) { return HostControllerLogger.ROOT_LOGGER.profileIncludesSameSubsystem(commonParent, include1, include2, child); } } private static class SocketBindingGroupIncludeValidator extends AbstractIncludeValidator { void processResource(ResourceEntry groupEntry) throws OperationFailedException{ //Remote and local outbound socket binding names must be unique or we get a DuplicateServiceException //Tighten this up to also make the 'normal' ones unique, to make the validation a bit easier. super.processResource(groupEntry); final Set<String> bindings; if (groupEntry.hasChildren(SOCKET_BINDING) || groupEntry.hasChildren(LOCAL_DESTINATION_OUTBOUND_SOCKET_BINDING) || groupEntry.hasChildren(REMOTE_DESTINATION_OUTBOUND_SOCKET_BINDING)) { bindings = new HashSet<>(); addBindings(groupEntry, bindings, SOCKET_BINDING); addBindings(groupEntry, bindings, LOCAL_DESTINATION_OUTBOUND_SOCKET_BINDING); addBindings(groupEntry, bindings, REMOTE_DESTINATION_OUTBOUND_SOCKET_BINDING); bindings.addAll(groupEntry.getChildrenNames(SUBSYSTEM)); } else { bindings = Collections.emptySet(); } resourceChildren.put(groupEntry.getName(), bindings); } private void addBindings(ResourceEntry groupEntry, Set<String> bindings, String bindingType) throws OperationFailedException{ if (groupEntry.hasChildren(bindingType)) { for (String name : groupEntry.getChildrenNames(bindingType)) { if (!bindings.add(name)) { throw HostControllerLogger.ROOT_LOGGER.bindingNameNotUnique(name, groupEntry.getName()); } } } } @Override OperationFailedException attemptingToOverride(String parentOfExistingChild, String child, String resourceName) { return HostControllerLogger.ROOT_LOGGER.socketBindingGroupAttemptingToOverrideSocketBinding(parentOfExistingChild, child, resourceName); } @Override OperationFailedException involvedInACycle(String include) { return HostControllerLogger.ROOT_LOGGER.socketBindingGroupInvolvedInACycle(include); } @Override protected OperationFailedException twoParentsWithSameChild(String commonParent, String include1, String include2, String child) { return HostControllerLogger.ROOT_LOGGER.socketBindingGroupIncludesSameSocketBinding(commonParent, include1, include2, child); } } }