/* * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.controller.containermanager.internal; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.osgi.framework.console.CommandInterpreter; import org.eclipse.osgi.framework.console.CommandProvider; import org.opendaylight.controller.clustering.services.CacheConfigException; import org.opendaylight.controller.clustering.services.CacheExistException; import org.opendaylight.controller.clustering.services.ICacheUpdateAware; import org.opendaylight.controller.clustering.services.IClusterGlobalServices; import org.opendaylight.controller.clustering.services.IClusterServices; import org.opendaylight.controller.configuration.IConfigurationAware; import org.opendaylight.controller.configuration.IConfigurationService; import org.opendaylight.controller.containermanager.IContainerAuthorization; import org.opendaylight.controller.containermanager.IContainerManager; import org.opendaylight.controller.sal.authorization.AppRoleLevel; import org.opendaylight.controller.sal.authorization.Privilege; import org.opendaylight.controller.sal.authorization.Resource; import org.opendaylight.controller.sal.authorization.ResourceGroup; import org.opendaylight.controller.sal.authorization.UserLevel; import org.opendaylight.controller.sal.core.ContainerFlow; import org.opendaylight.controller.sal.core.IContainerAware; import org.opendaylight.controller.sal.core.IContainerListener; import org.opendaylight.controller.sal.core.Node; import org.opendaylight.controller.sal.core.NodeConnector; import org.opendaylight.controller.sal.core.UpdateType; import org.opendaylight.controller.sal.match.Match; import org.opendaylight.controller.sal.utils.GlobalConstants; import org.opendaylight.controller.sal.utils.IObjectReader; import org.opendaylight.controller.sal.utils.NodeConnectorCreator; import org.opendaylight.controller.sal.utils.NodeCreator; import org.opendaylight.controller.sal.utils.ObjectReader; import org.opendaylight.controller.sal.utils.ObjectWriter; import org.opendaylight.controller.sal.utils.ServiceHelper; import org.opendaylight.controller.sal.utils.Status; import org.opendaylight.controller.sal.utils.StatusCode; import org.opendaylight.controller.topologymanager.ITopologyManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.opendaylight.controller.appauth.authorization.Authorization; import org.opendaylight.controller.containermanager.ContainerFlowChangeEvent; import org.opendaylight.controller.containermanager.ContainerFlowConfig; import org.opendaylight.controller.containermanager.NodeConnectorsChangeEvent; import org.opendaylight.controller.containermanager.ContainerChangeEvent; import org.opendaylight.controller.containermanager.ContainerConfig; import org.opendaylight.controller.containermanager.ContainerData; public class ContainerManager extends Authorization implements IContainerManager, IObjectReader, CommandProvider, ICacheUpdateAware, IContainerInternal, IContainerAuthorization, IConfigurationAware { private static final Logger logger = LoggerFactory.getLogger(ContainerManager.class); private static String ROOT = GlobalConstants.STARTUPHOME.toString(); private static String containersFileName = ROOT + "containers.conf"; private static final String allContainersGroup = "allContainers"; private IClusterGlobalServices clusterServices; /* * Collection containing the configuration objects. This is configuration * world: container names (also the map key) are maintained as they were * configured by user, same case */ private ConcurrentMap containerConfigs; private ConcurrentMap containerData; private ConcurrentMap> nodeConnectorToContainers; private ConcurrentMap> nodeToContainers; private ConcurrentMap containerChangeEvents; private final Set iContainerAware = Collections.synchronizedSet(new HashSet()); private final Set iContainerListener = Collections .synchronizedSet(new HashSet()); void setIContainerListener(IContainerListener s) { if (this.iContainerListener != null) { this.iContainerListener.add(s); /* * At boot with startup, containers are created before listeners have * joined. Replaying here the first container creation notification for * the joining listener when containers are already present. Also * replaying all the node connectors and container flows additions * to the existing containers. */ if (!this.containerData.isEmpty()) { s.containerModeUpdated(UpdateType.ADDED); } for (ConcurrentMap.Entry> entry : nodeConnectorToContainers .entrySet()) { NodeConnector port = entry.getKey(); for (String container : entry.getValue()) { s.nodeConnectorUpdated(container, port, UpdateType.ADDED); } } for (Map.Entry container : containerData.entrySet()) { for (ContainerFlow cFlow : container.getValue().getContainerFlowSpecs()) { s.containerFlowUpdated(container.getKey(), cFlow, cFlow, UpdateType.ADDED); } } } } void unsetIContainerListener(IContainerListener s) { if (this.iContainerListener != null) { this.iContainerListener.remove(s); } } public void setIContainerAware(IContainerAware iContainerAware) { if (!this.iContainerAware.contains(iContainerAware)) { this.iContainerAware.add(iContainerAware); // Now call the container creation for all the known containers so far for (String container : getContainerNameList()) { iContainerAware.containerCreate(container.toLowerCase(Locale.ENGLISH)); } } } public void unsetIContainerAware(IContainerAware iContainerAware) { this.iContainerAware.remove(iContainerAware); // There is no need to do cleanup of the component when // unregister because it will be taken care by the Containerd // component itself } public void setClusterServices(IClusterGlobalServices i) { this.clusterServices = i; logger.debug("IClusterServices set"); } public void unsetClusterServices(IClusterGlobalServices i) { if (this.clusterServices == i) { this.clusterServices = null; logger.debug("IClusterServices Unset"); } } private void allocateCaches() { logger.debug("Container Manager allocating caches"); if (clusterServices == null) { logger.warn("un-initialized Cluster Services, can't allocate caches"); return; } try { clusterServices.createCache("containermgr.containerConfigs", EnumSet.of(IClusterServices.cacheMode.TRANSACTIONAL)); clusterServices.createCache("containermgr.event.containerChange", EnumSet.of(IClusterServices.cacheMode.TRANSACTIONAL)); clusterServices.createCache("containermgr.containerData", EnumSet.of(IClusterServices.cacheMode.TRANSACTIONAL)); clusterServices.createCache("containermgr.nodeConnectorToContainers", EnumSet.of(IClusterServices.cacheMode.TRANSACTIONAL)); clusterServices.createCache("containermgr.nodeToContainers", EnumSet.of(IClusterServices.cacheMode.TRANSACTIONAL)); clusterServices.createCache("containermgr.containerGroups", EnumSet.of(IClusterServices.cacheMode.TRANSACTIONAL)); clusterServices.createCache("containermgr.containerAuthorizations", EnumSet.of(IClusterServices.cacheMode.TRANSACTIONAL)); clusterServices.createCache("containermgr.roles", EnumSet.of(IClusterServices.cacheMode.TRANSACTIONAL)); } catch (CacheConfigException cce) { logger.error("Cache configuration invalid - check cache mode"); } catch (CacheExistException ce) { logger.error("Cache already exits - destroy and recreate if needed"); } } @SuppressWarnings({ "unchecked" }) private void retrieveCaches() { logger.debug("Container Manager retrieving caches"); if (clusterServices == null) { logger.warn("un-initialized Cluster Services, can't retrieve caches"); return; } containerConfigs = (ConcurrentMap) clusterServices.getCache("containermgr.containerConfigs"); containerChangeEvents = (ConcurrentMap) clusterServices.getCache("containermgr.event.containerChange"); containerData = (ConcurrentMap) clusterServices.getCache("containermgr.containerData"); nodeConnectorToContainers = (ConcurrentMap>) clusterServices .getCache("containermgr.nodeConnectorToContainers"); nodeToContainers = (ConcurrentMap>) clusterServices.getCache("containermgr.nodeToContainers"); resourceGroups = (ConcurrentMap>) clusterServices.getCache("containermgr.containerGroups"); groupsAuthorizations = (ConcurrentMap>) clusterServices .getCache("containermgr.containerAuthorizations"); roles = (ConcurrentMap) clusterServices.getCache("containermgr.roles"); if (containerConfigs.size() > 0) { for (Map.Entry entry : containerConfigs.entrySet()) { notifyContainerChangeInternal(entry.getValue(), UpdateType.ADDED); } } } @Override public void entryCreated(String containerName, String cacheName, boolean originLocal) { } @Override public void entryUpdated(String key, Object value, String cacheName, boolean originLocal) { if (!originLocal) { if (value instanceof NodeConnectorsChangeEvent) { NodeConnectorsChangeEvent event = (NodeConnectorsChangeEvent) value; List ncList = event.getNodeConnectors(); notifyContainerEntryChangeInternal(key, ncList, event.getUpdateType()); } else if (value instanceof ContainerFlowChangeEvent) { ContainerFlowChangeEvent event = (ContainerFlowChangeEvent) value; notifyCFlowChangeInternal(key, event.getConfigList(), event.getUpdateType()); } else if (value instanceof ContainerChangeEvent) { ContainerChangeEvent event = (ContainerChangeEvent) value; notifyContainerChangeInternal(event.getConfig(), event.getUpdateType()); } } } @Override public void entryDeleted(String containerName, String cacheName, boolean originLocal) { } public ContainerManager() { } public void init() { } public void start() { // Get caches from cluster manager allocateCaches(); retrieveCaches(); // Allocates default groups and association to default roles createDefaultAuthorizationGroups(); // Read startup configuration and create local database loadConfigurations(); } public void destroy() { // Clear local states this.iContainerAware.clear(); this.iContainerListener.clear(); } /** * Adds/Remove the list of flow specs to/from the specified container. This * function is supposed to be called after all the validation checks have * already been run on the proposed configuration. */ private Status updateContainerFlow(String containerName, List confList, boolean delete) { ContainerData container = getContainerByName(containerName); if (container == null) { return new Status(StatusCode.GONE, "Container not present"); } for (ContainerFlowConfig conf : confList) { // Validation was fine. Modify the database now. for (Match match : conf.getMatches()) { ContainerFlow cFlow = new ContainerFlow(match); if (delete) { logger.trace("Removing Flow Spec %s from Container {}", conf.getName(), containerName); container.deleteFlowSpec(cFlow); } else { logger.trace("Adding Flow Spec %s to Container {}", conf.getName(), containerName); container.addFlowSpec(cFlow); } // Update Database putContainerDataByName(containerName, container); } } return new Status(StatusCode.SUCCESS); } /** * Adds/Remove this container to/from the Container database, no updates are going * to be generated here other that the destroying and creation of the container. * This function is supposed to be called after all the validation checks * have already been run on the configuration object */ private Status updateContainerDatabase(ContainerConfig containerConf, boolean delete) { /* * Back-end world here, container names are all stored in lower case */ String containerName = containerConf.getContainerName(); ContainerData container = getContainerByName(containerName); if (delete && container == null) { return new Status(StatusCode.NOTFOUND, "Container is not present"); } if (!delete && container != null) { // A container with the same (lower case) name already exists return new Status(StatusCode.CONFLICT, "A container with the same name already exists"); } if (delete) { logger.debug("Removing container {}", containerName); removeNodeToContainersMapping(container); removeNodeConnectorToContainersMapping(container); removeContainerDataByName(containerName); } else { logger.debug("Adding container {}", containerName); container = new ContainerData(containerConf); putContainerDataByName(containerName, container); // If flow specs are specified, add them if (containerConf.hasFlowSpecs()) { updateContainerFlow(containerName, containerConf.getContainerFlowConfigs(), delete); } // If ports are specified, add them if (!containerConf.getPortList().isEmpty()) { updateContainerEntryDatabase(containerName, containerConf.getPortList(), delete); } } return new Status(StatusCode.SUCCESS); } private void removeNodeConnectorToContainersMapping(ContainerData container) { Iterator>> it = nodeConnectorToContainers.entrySet().iterator(); String containerName = container.getContainerName(); for (; it.hasNext();) { Entry> entry = it.next(); final NodeConnector nc = entry.getKey(); final CopyOnWriteArrayList slist = entry.getValue(); for (final String sdata : slist) { if (sdata.equalsIgnoreCase(containerName)) { logger.debug("Removing NodeConnector->Containers mapping, nodeConnector: {}", nc); slist.remove(containerName); if (slist.isEmpty()) { nodeConnectorToContainers.remove(nc); } else { nodeConnectorToContainers.put(nc, slist); } break; } } } } private void removeNodeToContainersMapping(ContainerData container) { for (Entry> entry : nodeToContainers.entrySet()) { Node node = entry.getKey(); for (String sdata : entry.getValue()) { if (sdata.equals(container.getContainerName())) { logger.debug("Removing Node->Containers mapping, node {} container {}", node, sdata); Set value = nodeToContainers.get(node); value.remove(sdata); nodeToContainers.put(node, value); break; } } } } /** * Adds/Remove container data to/from the container. This function is supposed to be * called after all the validation checks have already been run on the * configuration object */ private Status updateContainerEntryDatabase(String containerName, List nodeConnectors, boolean delete) { ContainerData container = getContainerByName(containerName); // Presence check if (container == null) { return new Status(StatusCode.NOTFOUND, "Container Not Found"); } // Check changes in the portlist for (NodeConnector port : nodeConnectors) { Node node = port.getNode(); if (delete) { container.removePortFromSwitch(port); putContainerDataByName(containerName, container); /* remove - container mapping */ if (nodeConnectorToContainers.containsKey(port)) { nodeConnectorToContainers.remove(port); } /* * If no more ports in the switch, remove switch from container * Generate switchRemoved Event */ if (container.portListEmpty(node)) { logger.debug("Port List empty for switch {}", node); putContainerDataByName(containerName, container); // remove node->containers mapping Set slist = nodeToContainers.get(node); if (slist != null) { logger.debug("Removing container from switch-container list. node{}, container{}", node, containerName); slist.remove(container.getContainerName()); nodeToContainers.put(node, slist); if (slist.isEmpty()) { logger.debug("Container list empty for switch {}. removing switch-container mapping", node); nodeToContainers.remove(node); } } } } else { if (container.isSwitchInContainer(node) == false) { Set value = nodeToContainers.get(node); // Add node->containers mapping if (value == null) { value = new HashSet(); logger.debug("Creating new Container Set for switch {}", node); } value.add(container.getContainerName()); nodeToContainers.put(node, value); } container.addPortToSwitch(port); putContainerDataByName(containerName, container); // added nc->containers mapping CopyOnWriteArrayList slist = nodeConnectorToContainers.get(port); if (slist == null) { slist = new CopyOnWriteArrayList(); } slist.add(container.getContainerName()); nodeConnectorToContainers.put(port, slist); } } return new Status(StatusCode.SUCCESS); } private Status validateContainerFlowAddRemoval(String containerName, ContainerFlow cFlow, boolean delete) { /* * It used to be the comment below: ~~~~~~~~~~~~~~~~~~~~ If Link Sharing * at Host facing interfaces, then disallow last ContainerFlow removal * ~~~~~~~~~~~~~~~~~~~~ But the interface being host facing is a * condition that can change at runtime and so the final effect will be * unreliable. So now we will always allow the container flow removal, * if this is a link host facing and is shared by many that will cause * issues but that validation should be done not at the configuration * but in the UI/northbound side. */ ContainerData container = this.getContainerByName(containerName); if (container == null) { String error = String.format("Cannot validate flow specs for container %s: (Container does not exist)", containerName); logger.warn(error); return new Status(StatusCode.BADREQUEST, error); } if (delete) { Set thisContainerPorts = container.getNodeConnectors(); // Go through all the installed containers for (Map.Entry entry : containerData.entrySet()) { if (containerName.equalsIgnoreCase(entry.getKey())) { continue; } // Derive the common ports Set commonPorts = entry.getValue().getNodeConnectors(); commonPorts.retainAll(thisContainerPorts); if (commonPorts.isEmpty()) { continue; } // Check if this operation would remove the only flow spec // assigned to this container if (container.getFlowSpecCount() == 1) { if (!container.hasStaticVlanAssigned()) { // Ports are shared and static vlan is not present: this // is a failure // regardless the shared ports are host facing or // interswitch ports return new Status(StatusCode.BADREQUEST, "Container shares port with another container: " + "The only one flow spec assigned to this container cannot be removed," + "because this container is not assigned any static vlan"); } // Check on host facing port ITopologyManager topologyManager = (ITopologyManager) ServiceHelper.getInstance( ITopologyManager.class, GlobalConstants.DEFAULT.toString(), this); if (topologyManager == null) { return new Status(StatusCode.NOSERVICE, "Cannot validate the request: Required service is not available"); } for (NodeConnector nc : commonPorts) { /* * Shared link case : For internal port check if it has * a vlan configured. If vlan is configured, allow the * flowspec to be deleted If the port is host-facing, do * not allow the flowspec to be deleted */ if (!topologyManager.isInternal(nc)) { return new Status(StatusCode.BADREQUEST, String.format( "Port %s is shared and is host facing port: " + "The only one flow spec assigned to this container cannot be removed", nc)); } } } } } else { // Adding a new flow spec: need to check if other containers with common // ports do not have same flow spec Set thisContainerPorts = container.getNodeConnectors(); List proposed = new ArrayList(container.getContainerFlowSpecs()); proposed.add(cFlow); for (Map.Entry entry : containerData.entrySet()) { if (containerName.equalsIgnoreCase(entry.getKey())) { continue; } ContainerData otherContainer = entry.getValue(); Set commonPorts = otherContainer.getNodeConnectors(); commonPorts.retainAll(thisContainerPorts); if (!commonPorts.isEmpty()) { Status status = checkCommonContainerFlow(otherContainer.getContainerFlowSpecs(), proposed); if (!status.isSuccess()) { return new Status(StatusCode.BADREQUEST, String.format( "Container %s which shares ports with this container has overlapping flow spec: %s", entry.getKey(), status.getDescription())); } } } } return new Status(StatusCode.SUCCESS); } /** * Checks if the passed list of node connectors can be safely applied to the * specified existing container in terms of port sharing with other containers. * * @param containerName * the name of the existing container * @param portList * the list of node connectors to be added to the container * @return the status object representing the result of the check */ private Status validatePortSharing(String containerName, List portList) { ContainerData container = this.getContainerByName(containerName); if (container == null) { String error = String .format("Cannot validate port sharing for container %s: (container does not exist)", containerName); logger.error(error); return new Status(StatusCode.BADREQUEST, error); } return validatePortSharingInternal(portList, container.getContainerFlowSpecs()); } /** * Checks if the proposed container configuration is valid to be applied in * terms of port sharing with other containers. * * @param containerConf * the container configuration object containing the list of node * connectors * @return the status object representing the result of the check */ private Status validatePortSharing(ContainerConfig containerConf) { return validatePortSharingInternal(containerConf.getPortList(), containerConf.getContainerFlowSpecs()); } /* * If any port is shared with an existing container, need flowSpec to be * configured. If no flowSpec for this or other container, or if containers have any * overlapping flowspec in common, then let the caller know this * configuration has to be rejected. */ private Status validatePortSharingInternal(List portList, List flowSpecList) { for (NodeConnector port : portList) { List slist = nodeConnectorToContainers.get(port); if (slist != null && !slist.isEmpty()) { for (String otherContainerName : slist) { String msg = null; ContainerData other = containerData.get(otherContainerName); if (flowSpecList.isEmpty()) { msg = String.format("Port %s is shared and flow spec is emtpy for this container", port); } else if (other.isFlowSpecEmpty()) { msg = String.format("Port %s is shared and flow spec is emtpy for the other container", port); } else if (!checkCommonContainerFlow(flowSpecList, other.getContainerFlowSpecs()).isSuccess()) { msg = String.format("Port %s is shared and other container has common flow spec", port); } if (msg != null) { logger.debug(msg); return new Status(StatusCode.BADREQUEST, msg); } } } } return new Status(StatusCode.SUCCESS); } /** * Utility function to check if two lists of container flows share any same * or overlapping container flows. * * @param oneFlowList * One of the two lists of container flows to test * @param twoFlowList * One of the two lists of container flows to test * @return The status of the check. Either SUCCESS or CONFLICT. In case of * conflict, the Status will contain the description for the failed * check. */ private Status checkCommonContainerFlow(List oneFlowList, List twoFlowList) { for (ContainerFlow oneFlow : oneFlowList) { for (ContainerFlow twoFlow : twoFlowList) { if (oneFlow.getMatch().intersetcs(twoFlow.getMatch())) { return new Status(StatusCode.CONFLICT, String.format("Flow Specs overlap: %s %s", oneFlow.getMatch(), twoFlow.getMatch())); } } } return new Status(StatusCode.SUCCESS); } /** * Return the ContainerData object for the passed container name. Given this is a * backend database, the lower case version of the passed name is used while * searching for the corresponding ContainerData object. * * @param name * The container name in any case * @return The corresponding ContainerData object */ private ContainerData getContainerByName(String name) { return containerData.get(name.toLowerCase(Locale.ENGLISH)); } /** * Add a ContainerData object for the given container name. * * @param name * The container name in any case * @param sData * The container data object */ private void putContainerDataByName(String name, ContainerData sData) { containerData.put(name.toLowerCase(Locale.ENGLISH), sData); } /** * Removes the ContainerData object for the given container name. * * @param name * The container name in any case */ private void removeContainerDataByName(String name) { containerData.remove(name.toLowerCase(Locale.ENGLISH)); } @Override public List getContainerConfigList() { return new ArrayList(containerConfigs.values()); } @Override public ContainerConfig getContainerConfig(String containerName) { ContainerConfig target = containerConfigs.get(containerName); return (target == null) ? null : new ContainerConfig(target); } @Override public List getContainerNameList() { /* * Return container names as they were configured by user (case sensitive) * along with the default container */ List containerNameList = new ArrayList(); containerNameList.add(GlobalConstants.DEFAULT.toString()); containerNameList.addAll(containerConfigs.keySet()); return containerNameList; } @Override public Map> getContainerFlows() { Map> flowSpecConfig = new HashMap>(); for (Map.Entry entry : containerConfigs.entrySet()) { List set = entry.getValue().getContainerFlowConfigs(); flowSpecConfig.put(entry.getKey(), set); } return flowSpecConfig; } private void loadConfigurations() { /* * Read containers, container flows and finally containers' entries from file * and program the database accordingly */ if (containerConfigs.isEmpty()) { loadContainerConfig(); } } private Status saveContainerConfig() { return saveContainerConfigLocal(); } public Status saveContainerConfigLocal() { ObjectWriter objWriter = new ObjectWriter(); Status status = objWriter.write(new ConcurrentHashMap(containerConfigs), containersFileName); if (!status.isSuccess()) { return new Status(StatusCode.INTERNALERROR, "Failed to save container configurations: " + status.getDescription()); } return new Status(StatusCode.SUCCESS); } private void removeComponentsStartUpfiles(String containerName) { String startupLocation = String.format("./%s", GlobalConstants.STARTUPHOME.toString()); String containerPrint = String.format("_%s.", containerName.toLowerCase(Locale.ENGLISH)); File directory = new File(startupLocation); String[] fileList = directory.list(); logger.trace("Deleteing startup configuration files for container {}", containerName); if (fileList != null) { for (String fileName : fileList) { if (fileName.contains(containerPrint)) { String fullPath = String.format("%s/%s", startupLocation, fileName); File file = new File(fullPath); boolean done = file.delete(); logger.trace("{} {}", (done ? "Deleted: " : "Failed to delete: "), fileName); } } } } /** * Create and initialize default all resource group and create association * with default well known users and profiles, if not already learnt from * another cluster node */ private void createDefaultAuthorizationGroups() { allResourcesGroupName = ContainerManager.allContainersGroup; // Add the default container to the all containers group if needed String defaultContainer = GlobalConstants.DEFAULT.toString(); Set allContainers = (resourceGroups.containsKey(allResourcesGroupName)) ? resourceGroups .get(allResourcesGroupName) : new HashSet(); if (!allContainers.contains(defaultContainer)) { // Add Default container allContainers.add(defaultContainer); // Update cluster resourceGroups.put(allResourcesGroupName, allContainers); } // Add the controller well known roles, if not known already if (!roles.containsKey(UserLevel.SYSTEMADMIN.toString())) { roles.put(UserLevel.SYSTEMADMIN.toString(), AppRoleLevel.APPADMIN); } if (!roles.containsKey(UserLevel.NETWORKADMIN.toString())) { roles.put(UserLevel.NETWORKADMIN.toString(), AppRoleLevel.APPADMIN); } if (!roles.containsKey(UserLevel.NETWORKOPERATOR.toString())) { roles.put(UserLevel.NETWORKOPERATOR.toString(), AppRoleLevel.APPOPERATOR); } /* * Create and add the all containers user groups and associate them to the * default well known user roles, if not present already */ if (!groupsAuthorizations.containsKey(UserLevel.NETWORKADMIN.toString())) { Set writeProfile = new HashSet(1); Set readProfile = new HashSet(1); writeProfile.add(new ResourceGroup(allResourcesGroupName, Privilege.WRITE)); readProfile.add(new ResourceGroup(allResourcesGroupName, Privilege.READ)); groupsAuthorizations.put(UserLevel.SYSTEMADMIN.toString(), writeProfile); groupsAuthorizations.put(UserLevel.NETWORKADMIN.toString(), writeProfile); groupsAuthorizations.put(UserLevel.NETWORKOPERATOR.toString(), readProfile); } } /** * Until manual configuration is not available, automatically maintain the * well known resource groups * * @param containerName * @param delete */ private void updateResourceGroups(String containerName, boolean delete) { String containerProfile = System.getProperty("container.profile"); if (containerProfile == null) containerProfile = "Container"; // Container Roles and Container Resource Group String groupName = containerProfile+"-" + containerName; String containerAdminRole = containerProfile+"-" + containerName + "-Admin"; String containerOperatorRole = containerProfile+"-" + containerName + "-Operator"; Set allContainerSet = resourceGroups.get(allResourcesGroupName); if (delete) { resourceGroups.remove(groupName); groupsAuthorizations.remove(containerAdminRole); groupsAuthorizations.remove(containerOperatorRole); roles.remove(containerAdminRole); roles.remove(containerOperatorRole); // Update the all container group allContainerSet.remove(containerName); } else { Set resources = new HashSet(1); resources.add(containerName); resourceGroups.put(groupName, resources); Set adminGroups = new HashSet(1); Set operatorGroups = new HashSet(1); adminGroups.add(new ResourceGroup(groupName, Privilege.WRITE)); operatorGroups.add(new ResourceGroup(groupName, Privilege.READ)); groupsAuthorizations.put(containerAdminRole, adminGroups); groupsAuthorizations.put(containerOperatorRole, operatorGroups); roles.put(containerAdminRole, AppRoleLevel.APPADMIN); roles.put(containerOperatorRole, AppRoleLevel.APPOPERATOR); // Update the all containers resource group allContainerSet.add(containerName); } // Update resource groups in cluster resourceGroups.put(allResourcesGroupName, allContainerSet); } /** * Notify ContainerAware listeners of the creation/deletion of the container * * @param containerName * @param delete * true is container was removed, false otherwise */ private void notifyContainerAwareListeners(String containerName, boolean delete) { // Back-end World: container name forced to lower case String name = containerName.toLowerCase(Locale.ENGLISH); synchronized (this.iContainerAware) { for (IContainerAware i : this.iContainerAware) { if (delete) { i.containerDestroy(name); } else { i.containerCreate(name); } } } } /** * Notify the ContainerListener listeners in case the container mode has changed * following a container configuration operation Note: this call must happen * after the configuration db has been updated * * @param lastActionDelete * true if the last container configuration operation was a container * delete operation */ private void notifyContainerModeChange(boolean lastActionDelete) { if (lastActionDelete == false && containerConfigs.size() == 1) { logger.info("First container Creation. Inform listeners"); synchronized (this.iContainerListener) { for (IContainerListener i : this.iContainerListener) { i.containerModeUpdated(UpdateType.ADDED); } } } else if (lastActionDelete == true && containerConfigs.isEmpty()) { logger.info("Last container Deletion. Inform listeners"); synchronized (this.iContainerListener) { for (IContainerListener i : this.iContainerListener) { i.containerModeUpdated(UpdateType.REMOVED); } } } } private Status addRemoveContainerEntries(String containerName, List nodeConnectorsString, boolean delete) { // Construct action message String action = String.format("Node conenctor(s) %s container %s: %s", delete ? "removal from" : "addition to", containerName, nodeConnectorsString); // Validity Check if (nodeConnectorsString == null || nodeConnectorsString.isEmpty()) { return new Status(StatusCode.BADREQUEST, "Node connector list is null or empty"); } // Presence check ContainerConfig entryConf = containerConfigs.get(containerName); if (entryConf == null) { String msg = String.format("Container not found: %s", containerName); String error = String.format("Failed to apply %s: (%s)", action, msg); logger.warn(error); return new Status(StatusCode.NOTFOUND, msg); } // Validation check Status status = ContainerConfig.validateNodeConnectors(nodeConnectorsString); if (!status.isSuccess()) { String error = String.format("Failed to apply %s: (%s)", action, status.getDescription()); logger.warn(error); return status; } List nodeConnectors = ContainerConfig.nodeConnectorsFromString(nodeConnectorsString); // Port sharing check if (!delete) { /* * Check if the ports being added to this container already belong to * other containers. If so check whether the the appropriate flow specs * are configured on this container */ status = validatePortSharing(containerName, nodeConnectors); if (!status.isSuccess()) { String error = String.format("Failed to apply %s: (%s)", action, status.getDescription()); logger.warn(error); return status; } } // Update Database status = updateContainerEntryDatabase(containerName, nodeConnectors, delete); if (!status.isSuccess()) { String error = String.format("Failed to apply %s: (%s)", action, status.getDescription()); logger.warn(error); return status; } // Update Configuration status = (delete) ? entryConf.removeNodeConnectors(nodeConnectorsString) : entryConf .addNodeConnectors(nodeConnectorsString); if (!status.isSuccess()) { String error = String.format("Failed to modify config for %s: (%s)", action, status.getDescription()); logger.warn(error); // Revert backend changes Status statusRevert = updateContainerEntryDatabase(containerName, nodeConnectors, !delete); if (!statusRevert.isSuccess()) { // Unlikely logger.error("Failed to revert changes in database (CRITICAL)"); } return status; } // Update cluster Configuration cache containerConfigs.put(containerName, entryConf); // Notify UpdateType update = (delete) ? UpdateType.REMOVED : UpdateType.ADDED; notifyContainerEntryChangeInternal(containerName, nodeConnectors, update); // Trigger cluster notification containerChangeEvents.put(containerName, new NodeConnectorsChangeEvent(nodeConnectors, update)); return status; } private void notifyContainerChangeInternal(ContainerConfig conf, UpdateType update) { String containerName = conf.getContainerName(); logger.trace("Notifying listeners on {} for container {}", update, containerName); // Back-end World: container name forced to lower case String container = containerName.toLowerCase(Locale.ENGLISH); boolean delete = (update == UpdateType.REMOVED); // Check if a container mode change notification is needed notifyContainerModeChange(delete); // Notify listeners notifyContainerAwareListeners(container, delete); } private void notifyContainerEntryChangeInternal(String containerName, List ncList, UpdateType update) { logger.trace("Notifying listeners on {} for ports {} in container {}", update, ncList, containerName); // Back-end World: container name forced to lower case String container = containerName.toLowerCase(Locale.ENGLISH); for (NodeConnector nodeConnector : ncList) { // Now signal that the port has been added/removed synchronized (this.iContainerListener) { for (IContainerListener i : this.iContainerListener) { i.nodeConnectorUpdated(container, nodeConnector, update); } } } } private void notifyCFlowChangeInternal(String containerName, List confList, UpdateType update) { logger.trace("Notifying listeners on {} for flow specs {} in container {}", update, confList, containerName); // Back-end World: container name forced to lower case String container = containerName.toLowerCase(Locale.ENGLISH); synchronized (this.iContainerListener) { for (ContainerFlowConfig conf : confList) { for (Match match : conf.getMatches()) { ContainerFlow cFlow = new ContainerFlow(match); for (IContainerListener i : this.iContainerListener) { i.containerFlowUpdated(container, cFlow, cFlow, update); } } } } } private Status addRemoveContainerFlow(String containerName, List cFlowConfList, boolean delete) { // Construct action message String action = String.format("Flow spec(s) %s container %s: %s", delete ? "removal from" : "addition to", containerName, cFlowConfList); // Presence check ContainerConfig containerConfig = this.containerConfigs.get(containerName); if (containerConfig == null) { String msg = String.format("Container not found: %s", containerName); String error = String.format("Failed to apply %s: (%s)", action, msg); logger.warn(error); return new Status(StatusCode.NOTFOUND, "Container not present"); } // Validity check, check for overlaps on current container configuration Status status = containerConfig.validateContainerFlowModify(cFlowConfList, delete); if (!status.isSuccess()) { String msg = status.getDescription(); String error = String.format("Failed to apply %s: (%s)", action, msg); logger.warn(error); return new Status(StatusCode.BADREQUEST, msg); } // Validate the operation in terms to the port sharing with other containers for (ContainerFlowConfig conf : cFlowConfList) { for (Match match : conf.getMatches()) { ContainerFlow cFlow = new ContainerFlow(match); status = validateContainerFlowAddRemoval(containerName, cFlow, delete); if (!status.isSuccess()) { String msg = "Validation failed: " + status.getDescription(); String error = String.format("Failed to apply %s: (%s)", action, msg); logger.warn(error); return new Status(StatusCode.BADREQUEST, msg); } } } // Update Database status = updateContainerFlow(containerName, cFlowConfList, delete); if (!status.isSuccess()) { String error = String.format("Failed to apply %s: (%s)", action, status.getDescription()); logger.error(error); return status; } // Update Configuration status = (delete) ? containerConfig.removeContainerFlows(cFlowConfList) : containerConfig .addContainerFlows(cFlowConfList); if (!status.isSuccess()) { String error = String.format("Failed to modify config for %s: (%s)", action, status.getDescription()); logger.error(error); // Revert backend changes Status statusRevert = updateContainerFlow(containerName, cFlowConfList, !delete); if (!statusRevert.isSuccess()) { // Unlikely logger.error("Failed to revert changes in database (CRITICAL)"); } return status; } // Update cluster cache this.containerConfigs.put(containerName, containerConfig); // Notify listeners UpdateType update = (delete) ? UpdateType.REMOVED : UpdateType.ADDED; notifyCFlowChangeInternal(containerName, cFlowConfList, update); // Trigger cluster notification containerChangeEvents.put(containerName, new ContainerFlowChangeEvent(cFlowConfList, update)); return status; } private Status addRemoveContainer(ContainerConfig containerConf, boolean delete) { // Construct action message String action = String.format("Container %s", delete ? "removal" : "creation"); // Valid configuration check Status status = null; String error = (containerConfigs == null) ? String.format("Invalid %s configuration: (null config object)", action) : (!(status = containerConf.validate()).isSuccess()) ? String.format("Invalid %s configuration: (%s)", action, status.getDescription()) : null; if (error != null) { logger.warn(error); return new Status(StatusCode.BADREQUEST, error); } // Configuration presence check String containerName = containerConf.getContainerName(); if (delete) { if (!containerConfigs.containsKey(containerName)) { String msg = String.format("%s Failed: (Container does not exist: %s)", action, containerName); logger.warn(msg); return new Status(StatusCode.NOTFOUND, msg); } } else { if (containerConfigs.containsKey(containerName)) { String msg = String.format("%s Failed: (Container already exist: %s)", action, containerName); logger.warn(msg); return new Status(StatusCode.CONFLICT, msg); } } /* * The proposed container configuration could be a complex one containing * both ports and flow spec. If so, check if it has shared ports with * other existing containers. If that is the case verify flow spec isolation * is in place. No need to check on flow spec validation first. This * would take care of both */ if (!delete) { status = validatePortSharing(containerConf); if (!status.isSuccess()) { error = String.format("%s Failed: (%s)", action, status.getDescription()); logger.error(error); return status; } } // Update Database status = updateContainerDatabase(containerConf, delete); // Abort and exit here if back-end database update failed if (!status.isSuccess()) { return status; } /* * This is a quick fix until configuration service becomes the * centralized configuration management place. Here container manager will * remove the startup files for all the bundles that are present in the * container being deleted. Do the cleanup here in Container manger as do not * want to put this temporary code in Configuration manager yet which is * ODL. */ if (delete) { // TODO: remove when Config Mgr takes over removeComponentsStartUpfiles(containerName); } /* * Update Configuration: This will trigger the notifications on cache * update callback locally and on the other cluster nodes */ if (delete) { this.containerConfigs.remove(containerName); } else { this.containerConfigs.put(containerName, containerConf); } // Automatically create and populate user and resource groups updateResourceGroups(containerName, delete); // Notify listeners UpdateType update = (delete) ? UpdateType.REMOVED : UpdateType.ADDED; notifyContainerChangeInternal(containerConf, update); // Trigger cluster notification containerChangeEvents.put(containerName, new ContainerChangeEvent(containerConf, update)); if (update == UpdateType.ADDED) { if (containerConf.hasFlowSpecs()) { List specList = containerConf.getContainerFlowConfigs(); // Notify flow spec addition notifyCFlowChangeInternal(containerName, specList, update); // Trigger cluster notification containerChangeEvents.put(containerName, new ContainerFlowChangeEvent(specList, update)); } if (containerConf.hasNodeConnectors()) { List ncList = containerConf.getPortList(); // Notify port(s) addition notifyContainerEntryChangeInternal(containerName, ncList, update); // Trigger cluster notification containerChangeEvents.put(containerName, new NodeConnectorsChangeEvent(ncList, update)); } } return status; } @Override public Status addContainer(ContainerConfig containerConf) { return addRemoveContainer(containerConf, false); } @Override public Status removeContainer(ContainerConfig containerConf) { return addRemoveContainer(containerConf, true); } @Override public Status removeContainer(String containerName) { // Construct action message String action = String.format("Container removal: %s", containerName); ContainerConfig containerConf = containerConfigs.get(containerName); if (containerConf == null) { String msg = String.format("Container not found"); String error = String.format("Failed to apply %s: (%s)", action, msg); logger.warn(error); return new Status(StatusCode.NOTFOUND, msg); } return addRemoveContainer(containerConf, true); } @Override public Status addContainerEntry(String containerName, List nodeConnectors) { return addRemoveContainerEntries(containerName, nodeConnectors, false); } @Override public Status removeContainerEntry(String containerName, List nodeConnectors) { return addRemoveContainerEntries(containerName, nodeConnectors, true); } @Override public Status addContainerFlows(String containerName, List fSpecConf) { return addRemoveContainerFlow(containerName, fSpecConf, false); } @Override public Status removeContainerFlows(String containerName, List fSpecConf) { return addRemoveContainerFlow(containerName, fSpecConf, true); } @Override public Status removeContainerFlows(String containerName, Set names) { // Construct action message String action = String.format("Flow spec(s) removal from container %s: %s", containerName, names); // Presence check ContainerConfig sc = containerConfigs.get(containerName); if (sc == null) { String msg = String.format("Container not found: %s", containerName); String error = String.format("Failed to apply %s: (%s)", action, msg); logger.warn(error); return new Status(StatusCode.NOTFOUND, msg); } List list = sc.getContainerFlowConfigs(names); if (list.isEmpty() || list.size() != names.size()) { String msg = String.format("Cannot find all the specified flow specs"); String error = String.format("Failed to apply %s: (%s)", action, msg); logger.warn(error); return new Status(StatusCode.BADREQUEST, msg); } return addRemoveContainerFlow(containerName, list, true); } @Override public List getContainerFlows(String containerName) { ContainerConfig sc = containerConfigs.get(containerName); return (sc == null) ? new ArrayList(0) : sc.getContainerFlowConfigs(); } @Override public List getContainerFlowNameList(String containerName) { ContainerConfig sc = containerConfigs.get(containerName); return (sc == null) ? new ArrayList(0) : sc.getContainerFlowConfigsNames(); } @Override public Object readObject(ObjectInputStream ois) throws FileNotFoundException, IOException, ClassNotFoundException { // Perform the class deserialization locally, from inside the package // where the class is defined return ois.readObject(); } @SuppressWarnings("unchecked") private void loadContainerConfig() { ObjectReader objReader = new ObjectReader(); ConcurrentMap configMap = (ConcurrentMap) objReader.read(this, containersFileName); if (configMap == null) { return; } for (Map.Entry configEntry : configMap.entrySet()) { addContainer(configEntry.getValue()); } } public void _psc(CommandInterpreter ci) { for (Map.Entry entry : containerConfigs.entrySet()) { ContainerConfig sc = entry.getValue(); ci.println(String.format("%s: %s", sc.getContainerName(), sc.toString())); } ci.println("Total number of containers: " + containerConfigs.entrySet().size()); } public void _pfc(CommandInterpreter ci) { for (Map.Entry entry : containerConfigs.entrySet()) { ContainerConfig sc = entry.getValue(); ci.println(String.format("%s: %s", sc.getContainerName(), sc.getContainerFlowConfigs())); } } public void _psd(CommandInterpreter ci) { for (String containerName : containerData.keySet()) { ContainerData sd = containerData.get(containerName); for (Node sid : sd.getSwPorts().keySet()) { Set s = sd.getSwPorts().get(sid); ci.println("\t" + sid + " : " + s); } for (ContainerFlow s : sd.getContainerFlowSpecs()) { ci.println("\t" + s.toString()); } } } public void _psp(CommandInterpreter ci) { for (NodeConnector sp : nodeConnectorToContainers.keySet()) { ci.println(nodeConnectorToContainers.get(sp)); } } public void _psm(CommandInterpreter ci) { for (Node sp : nodeToContainers.keySet()) { ci.println(nodeToContainers.get(sp)); } } public void _addContainer(CommandInterpreter ci) { String containerName = ci.nextArgument(); if (containerName == null) { ci.print("Container Name not specified"); return; } String staticVlan = ci.nextArgument(); if (staticVlan == null) { ci.print("Static Vlan not specified"); return; } ContainerConfig containerConfig = new ContainerConfig(containerName, staticVlan, null, null); ci.println(this.addRemoveContainer(containerConfig, false)); } public void _createContainer(CommandInterpreter ci) { String containerName = ci.nextArgument(); if (containerName == null) { ci.print("Container Name not specified"); return; } String staticVlan = ci.nextArgument(); if (staticVlan == null) { ci.print("Static Vlan not specified"); return; } List ports = new ArrayList(); for (long l = 1L; l < 10L; l++) { ports.add(NodeConnectorCreator.createOFNodeConnector((short) 1, NodeCreator.createOFNode(l)).toString()); } List cFlowList = new ArrayList(); cFlowList.add(this.createSampleContainerFlowConfig("tcp", true)); ContainerConfig containerConfig = new ContainerConfig(containerName, staticVlan, ports, cFlowList); ci.println(this.addRemoveContainer(containerConfig, false)); } public void _removeContainer(CommandInterpreter ci) { String containerName = ci.nextArgument(); if (containerName == null) { ci.print("Container Name not specified"); return; } ContainerConfig containerConfig = new ContainerConfig(containerName, "", null, null); ci.println(this.addRemoveContainer(containerConfig, true)); } public void _addContainerEntry(CommandInterpreter ci) { String containerName = ci.nextArgument(); if (containerName == null) { ci.print("Container Name not specified"); return; } String nodeId = ci.nextArgument(); if (nodeId == null) { ci.print("Node Id not specified"); return; } String portId = ci.nextArgument(); if (portId == null) { ci.print("Port not specified"); return; } Node node = NodeCreator.createOFNode(Long.valueOf(nodeId)); Short port = Short.valueOf(portId); NodeConnector nc = NodeConnectorCreator.createOFNodeConnector(port, node); List portList = new ArrayList(1); portList.add(nc.toString()); ci.println(this.addRemoveContainerEntries(containerName, portList, false)); } public void _removeContainerEntry(CommandInterpreter ci) { String containerName = ci.nextArgument(); if (containerName == null) { ci.print("Container Name not specified"); return; } String nodeId = ci.nextArgument(); if (nodeId == null) { ci.print("Node Id not specified"); return; } String portId = ci.nextArgument(); if (portId == null) { ci.print("Port not specified"); return; } Node node = NodeCreator.createOFNode(Long.valueOf(nodeId)); Short port = Short.valueOf(portId); NodeConnector nc = NodeConnectorCreator.createOFNodeConnector(port, node); List portList = new ArrayList(1); portList.add(nc.toString()); ci.println(this.addRemoveContainerEntries(containerName, portList, true)); } private ContainerFlowConfig createSampleContainerFlowConfig(String cflowName, boolean boolUnidirectional) { ContainerFlowConfig cfg = new ContainerFlowConfig(cflowName, "9.9.1.0/24", "19.9.1.2", "TCP", "1234", "25"); return cfg; } public void _addContainerFlow(CommandInterpreter ci) { String containerName = ci.nextArgument(); if (containerName == null) { ci.print("Container Name not specified"); return; } String cflowName = ci.nextArgument(); if (cflowName == null) { ci.print("cflowName not specified"); return; } String unidirectional = ci.nextArgument(); boolean boolUnidirectional = Boolean.parseBoolean(unidirectional); List list = new ArrayList(); list.add(createSampleContainerFlowConfig(cflowName, boolUnidirectional)); ci.println(this.addRemoveContainerFlow(containerName, list, false)); } public void _removeContainerFlow(CommandInterpreter ci) { String containerName = ci.nextArgument(); if (containerName == null) { ci.print("Container Name not specified"); return; } String cflowName = ci.nextArgument(); if (cflowName == null) { ci.print("cflowName not specified"); return; } Set set = new HashSet(1); set.add(cflowName); ci.println(this.removeContainerFlows(containerName, set)); } @Override public String getHelp() { StringBuffer help = new StringBuffer(); help.append("---ContainerManager Testing---\n"); help.append("\tpsc - Print ContainerConfigs\n"); help.append("\tpfc - Print FlowSpecConfigs\n"); help.append("\tpsd - Print ContainerData\n"); help.append("\tpsp - Print nodeConnectorToContainers\n"); help.append("\tpsm - Print nodeToContainers\n"); help.append("\t addContainer \n"); help.append("\t removeContainer \n"); help.append("\t addContainerEntry \n"); help.append("\t removeContainerEntry \n"); help.append("\t addContainerFlow \n"); help.append("\t removeContainerFlow \n"); return help.toString(); } @Override public boolean doesContainerExist(String containerName) { // Test for default container if (GlobalConstants.DEFAULT.toString().equalsIgnoreCase(containerName)) { return true; } // Test for non-default one return (getContainerByName(containerName) != null); } @Override public ContainerData getContainerData(String containerName) { return (getContainerByName(containerName)); } @Override public Status saveConfiguration() { return saveContainerConfig(); } public void _containermgrGetRoles(CommandInterpreter ci) { ci.println("Configured roles for Container Mgr:"); List list = this.getRoles(); for (String role : list) { ci.println(role + "\t" + roles.get(role)); } } public void _containermgrGetAuthorizedGroups(CommandInterpreter ci) { String roleName = ci.nextArgument(); if (roleName == null || roleName.trim().isEmpty()) { ci.println("Invalid argument"); ci.println("mmGetAuthorizedGroups "); return; } ci.println("Resource Groups associated to role " + roleName + ":"); List list = this.getAuthorizedGroups(roleName); for (ResourceGroup group : list) { ci.println(group.toString()); } } public void _containermgrGetAuthorizedResources(CommandInterpreter ci) { String roleName = ci.nextArgument(); if (roleName == null || roleName.trim().isEmpty()) { ci.println("Invalid argument"); ci.println("mmGetAuthorizedResources "); return; } ci.println("Resource associated to role " + roleName + ":"); List list = this.getAuthorizedResources(roleName); for (Resource resource : list) { ci.println(resource.toString()); } } public void _containermgrGetResourcesForGroup(CommandInterpreter ci) { String groupName = ci.nextArgument(); if (groupName == null || groupName.trim().isEmpty()) { ci.println("Invalid argument"); ci.println("containermgrResourcesForGroup "); return; } ci.println("Group " + groupName + " contains the following resources:"); List resources = this.getResources(groupName); for (Object resource : resources) { ci.println(resource.toString()); } } public void _containermgrGetUserLevel(CommandInterpreter ci) { String userName = ci.nextArgument(); if (userName == null || userName.trim().isEmpty()) { ci.println("Invalid argument"); ci.println("containermgrGetUserLevel "); return; } ci.println("User " + userName + " has level: " + this.getUserLevel(userName)); } public void _containermgrGetUserResources(CommandInterpreter ci) { String userName = ci.nextArgument(); if (userName == null || userName.trim().isEmpty()) { ci.println("Invalid argument"); ci.println("containermgrGetUserResources "); return; } ci.println("User " + userName + " owns the following resources: "); Set resources = this.getAllResourcesforUser(userName); for (Resource resource : resources) { ci.println(resource.toString()); } } /* * For scalability testing where as of now controller gui is unresponsive * providing here an osgi hook to trigger the save config so that DT do not * have to reaply the scalable configuration each time they restart the * controller */ // TODO: remove when no longer needed public void _saveConfig(CommandInterpreter ci) { Status status = new Status(StatusCode.NOSERVICE, "Configuration service not reachable"); IConfigurationService configService = (IConfigurationService) ServiceHelper.getGlobalInstance( IConfigurationService.class, this); if (configService != null) { status = configService.saveConfigurations(); } ci.println(status.toString()); } @Override public List getContainerNames() { return getContainerNameList(); } @Override public boolean hasNonDefaultContainer() { return !containerConfigs.keySet().isEmpty(); } }