blob: 8d7abedf2e74a6e23b7cf109179f346a9906fa3a [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hdds.scm.net;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hadoop.hdds.scm.net.NodeSchema.LayerType;
import org.yaml.snakeyaml.Yaml;
/**
* A Network topology layer schema loading tool that loads user defined network
* layer schema data from a XML configuration file.
*/
public final class NodeSchemaLoader {
private static final Logger LOG
= LoggerFactory.getLogger(NodeSchemaLoader.class);
private static final String CONFIGURATION_TAG = "configuration";
private static final String LAYOUT_VERSION_TAG = "layoutversion";
private static final String TOPOLOGY_TAG = "topology";
private static final String TOPOLOGY_PATH = "path";
private static final String TOPOLOGY_ENFORCE_PREFIX = "enforceprefix";
private static final String LAYERS_TAG = "layers";
private static final String LAYER_TAG = "layer";
private static final String LAYER_ID = "id";
private static final String LAYER_TYPE = "type";
private static final String LAYER_COST = "cost";
private static final String LAYER_PREFIX = "prefix";
private static final String LAYER_DEFAULT_NAME = "default";
private static final int LAYOUT_VERSION = 1;
private volatile static NodeSchemaLoader instance = null;
private NodeSchemaLoader() {}
public static NodeSchemaLoader getInstance() {
if (instance == null) {
instance = new NodeSchemaLoader();
}
return instance;
}
/**
* Class to house keep the result of parsing a network topology schema file.
*/
public static class NodeSchemaLoadResult {
private List<NodeSchema> schemaList;
private boolean enforcePrefix;
NodeSchemaLoadResult(List<NodeSchema> schemaList, boolean enforcePrefix) {
this.schemaList = schemaList;
this.enforcePrefix = enforcePrefix;
}
public boolean isEnforePrefix() {
return enforcePrefix;
}
public List<NodeSchema> getSchemaList() {
return schemaList;
}
}
/**
* Load user defined network layer schemas from a XML/YAML configuration file.
* @param schemaFilePath path of schema file
* @return all valid node schemas defined in schema file
*/
public NodeSchemaLoadResult loadSchemaFromFile(String schemaFilePath)
throws IllegalArgumentException, FileNotFoundException {
try {
File schemaFile = new File(schemaFilePath);
if (schemaFile.exists()) {
LOG.info("Load network topology schema file " +
schemaFile.getAbsolutePath());
try (FileInputStream inputStream = new FileInputStream(schemaFile)) {
return loadSchemaFromStream(schemaFilePath, inputStream);
}
} else {
// try to load with classloader
ClassLoader classloader =
Thread.currentThread().getContextClassLoader();
if (classloader == null) {
classloader = NodeSchemaLoader.class.getClassLoader();
}
if (classloader != null) {
try (InputStream stream = classloader
.getResourceAsStream(schemaFilePath)) {
if (stream != null) {
LOG.info("Loading file from " + classloader
.getResources(schemaFilePath));
return loadSchemaFromStream(schemaFilePath, stream);
}
}
}
}
String msg = "Network topology layer schema file " +
schemaFilePath + "[" + schemaFile.getAbsolutePath() +
"] is not found.";
LOG.warn(msg);
throw new FileNotFoundException(msg);
} catch (FileNotFoundException e) {
throw e;
} catch (ParserConfigurationException | IOException | SAXException e) {
throw new IllegalArgumentException("Failed to load network topology node"
+ " schema file: " + schemaFilePath + " , error:" + e.getMessage(),
e);
}
}
private NodeSchemaLoadResult loadSchemaFromStream(String schemaFilePath,
InputStream stream)
throws ParserConfigurationException, SAXException, IOException {
if (FilenameUtils.getExtension(schemaFilePath).toLowerCase()
.compareTo("yaml") == 0) {
return loadSchemaFromYaml(stream);
} else {
return loadSchema(stream);
}
}
/**
* Load network topology layer schemas from a XML configuration file.
* @param inputStream schema file as an inputStream
* @return all valid node schemas defined in schema file
* @throws ParserConfigurationException ParserConfigurationException happen
* @throws IOException no such schema file
* @throws SAXException xml file has some invalid elements
* @throws IllegalArgumentException xml file content is logically invalid
*/
private NodeSchemaLoadResult loadSchema(InputStream inputStream) throws
ParserConfigurationException, SAXException, IOException {
LOG.info("Loading network topology layer schema file");
// Read and parse the schema file.
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setIgnoringComments(true);
DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(inputStream);
Element root = doc.getDocumentElement();
if (!CONFIGURATION_TAG.equals(root.getTagName())) {
throw new IllegalArgumentException("Bad network topology layer schema " +
"configuration file: top-level element not <" + CONFIGURATION_TAG +
">");
}
NodeSchemaLoadResult schemaList;
if (root.getElementsByTagName(LAYOUT_VERSION_TAG).getLength() == 1) {
if (loadLayoutVersion(root) == LAYOUT_VERSION) {
if (root.getElementsByTagName(LAYERS_TAG).getLength() == 1) {
Map<String, NodeSchema> schemas = loadLayersSection(root);
if (root.getElementsByTagName(TOPOLOGY_TAG).getLength() == 1) {
schemaList = loadTopologySection(root, schemas);
} else {
throw new IllegalArgumentException("Bad network topology layer " +
"schema configuration file: no or multiple <" + TOPOLOGY_TAG +
"> element");
}
} else {
throw new IllegalArgumentException("Bad network topology layer schema"
+ " configuration file: no or multiple <" + LAYERS_TAG +
">element");
}
} else {
throw new IllegalArgumentException("The parse failed because of bad "
+ LAYOUT_VERSION_TAG + " value, expected:" + LAYOUT_VERSION);
}
} else {
throw new IllegalArgumentException("Bad network topology layer schema " +
"configuration file: no or multiple <" + LAYOUT_VERSION_TAG +
"> elements");
}
return schemaList;
}
/**
* Load network topology layer schemas from a YAML configuration file.
* @param schemaFile as inputStream
* @return all valid node schemas defined in schema file
* @throws ParserConfigurationException ParserConfigurationException happen
* @throws IOException no such schema file
* @throws SAXException xml file has some invalid elements
* @throws IllegalArgumentException xml file content is logically invalid
*/
private NodeSchemaLoadResult loadSchemaFromYaml(InputStream schemaFile) {
LOG.info("Loading network topology layer schema file {}", schemaFile);
NodeSchemaLoadResult finalSchema;
try {
Yaml yaml = new Yaml();
NodeSchema nodeTree;
nodeTree = yaml.loadAs(schemaFile, NodeSchema.class);
List<NodeSchema> schemaList = new ArrayList<>();
if (nodeTree.getType() != LayerType.ROOT) {
throw new IllegalArgumentException("First layer is not a ROOT node."
+ " schema file.");
}
schemaList.add(nodeTree);
if (nodeTree.getSublayer() != null) {
nodeTree = nodeTree.getSublayer().get(0);
}
while (nodeTree != null) {
if (nodeTree.getType() == LayerType.LEAF_NODE
&& nodeTree.getSublayer() != null) {
throw new IllegalArgumentException("Leaf node in the middle of path."
+ " schema file.");
}
if (nodeTree.getType() == LayerType.ROOT) {
throw new IllegalArgumentException("Multiple root nodes are defined."
+ " schema file.");
}
schemaList.add(nodeTree);
if (nodeTree.getSublayer() != null) {
nodeTree = nodeTree.getSublayer().get(0);
} else {
break;
}
}
finalSchema = new NodeSchemaLoadResult(schemaList, true);
} catch (Exception e) {
throw new IllegalArgumentException("Fail to load network topology node"
+ " schema file: " + schemaFile + " , error:"
+ e.getMessage(), e);
}
return finalSchema;
}
/**
* Load layoutVersion from root element in the XML configuration file.
* @param root root element
* @return layout version
*/
private int loadLayoutVersion(Element root) {
int layoutVersion;
Text text = (Text) root.getElementsByTagName(LAYOUT_VERSION_TAG)
.item(0).getFirstChild();
if (text != null) {
String value = text.getData().trim();
try {
layoutVersion = Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Bad " + LAYOUT_VERSION_TAG +
" value " + value + " is found. It should be an integer.");
}
} else {
throw new IllegalArgumentException("Value of <" + LAYOUT_VERSION_TAG +
"> is null");
}
return layoutVersion;
}
/**
* Load layers from root element in the XML configuration file.
* @param root root element
* @return A map of node schemas with layer ID and layer schema
*/
private Map<String, NodeSchema> loadLayersSection(Element root) {
NodeList elements = root.getElementsByTagName(LAYER_TAG);
Map<String, NodeSchema> schemas = new HashMap<String, NodeSchema>();
for (int i = 0; i < elements.getLength(); i++) {
Node node = elements.item(i);
if (node instanceof Element) {
Element element = (Element) node;
if (LAYER_TAG.equals(element.getTagName())) {
String layerId = element.getAttribute(LAYER_ID);
NodeSchema schema = parseLayerElement(element);
if (!schemas.containsValue(schema)) {
schemas.put(layerId, schema);
} else {
throw new IllegalArgumentException("Repetitive layer in network " +
"topology node schema configuration file: " + layerId);
}
} else {
throw new IllegalArgumentException("Bad element in network topology "
+ "node schema configuration file: " + element.getTagName());
}
}
}
// Integrity check, only one ROOT and one LEAF is allowed
boolean foundRoot = false;
boolean foundLeaf = false;
for(NodeSchema schema: schemas.values()) {
if (schema.getType() == LayerType.ROOT) {
if (foundRoot) {
throw new IllegalArgumentException("Multiple ROOT layers are found" +
" in network topology schema configuration file");
} else {
foundRoot = true;
}
}
if (schema.getType() == LayerType.LEAF_NODE) {
if (foundLeaf) {
throw new IllegalArgumentException("Multiple LEAF layers are found" +
" in network topology schema configuration file");
} else {
foundLeaf = true;
}
}
}
if (!foundRoot) {
throw new IllegalArgumentException("No ROOT layer is found" +
" in network topology schema configuration file");
}
if (!foundLeaf) {
throw new IllegalArgumentException("No LEAF layer is found" +
" in network topology schema configuration file");
}
return schemas;
}
/**
* Load network topology from root element in the XML configuration file and
* sort node schemas according to the topology path.
* @param root root element
* @param schemas schema map
* @return all valid node schemas defined in schema file
*/
private NodeSchemaLoadResult loadTopologySection(Element root,
Map<String, NodeSchema> schemas) {
NodeList elements = root.getElementsByTagName(TOPOLOGY_TAG)
.item(0).getChildNodes();
List<NodeSchema> schemaList = new ArrayList<NodeSchema>();
boolean enforecePrefix = false;
for (int i = 0; i < elements.getLength(); i++) {
Node node = elements.item(i);
if (node instanceof Element) {
Element element = (Element) node;
String tagName = element.getTagName();
// Get the nonnull text value.
Text text = (Text) element.getFirstChild();
String value;
if (text != null) {
value = text.getData().trim();
if (value.isEmpty()) {
// Element with empty value is ignored
continue;
}
} else {
throw new IllegalArgumentException("Value of <" + tagName
+ "> is null");
}
if (TOPOLOGY_PATH.equals(tagName)) {
if(value.startsWith(NetConstants.PATH_SEPARATOR_STR)) {
value = value.substring(1, value.length());
}
String[] layerIDs = value.split(NetConstants.PATH_SEPARATOR_STR);
if (layerIDs == null || layerIDs.length != schemas.size()) {
throw new IllegalArgumentException("Topology path depth doesn't "
+ "match layer element numbers");
}
for (int j = 0; j < layerIDs.length; j++) {
if (schemas.get(layerIDs[j]) == null) {
throw new IllegalArgumentException("No layer found for id " +
layerIDs[j]);
}
}
if (schemas.get(layerIDs[0]).getType() != LayerType.ROOT) {
throw new IllegalArgumentException("Topology path doesn't start "
+ "with ROOT layer");
}
if (schemas.get(layerIDs[layerIDs.length -1]).getType() !=
LayerType.LEAF_NODE) {
throw new IllegalArgumentException("Topology path doesn't end "
+ "with LEAF layer");
}
for (int j = 0; j < layerIDs.length; j++) {
schemaList.add(schemas.get(layerIDs[j]));
}
} else if (TOPOLOGY_ENFORCE_PREFIX.equalsIgnoreCase(tagName)) {
enforecePrefix = Boolean.parseBoolean(value);
} else {
throw new IllegalArgumentException("Unsupported Element <" +
tagName + ">");
}
}
}
// Integrity check
if (enforecePrefix) {
// Every InnerNode should have prefix defined
for (NodeSchema schema: schemas.values()) {
if (schema.getType() == LayerType.INNER_NODE &&
schema.getPrefix() == null) {
throw new IllegalArgumentException("There is layer without prefix " +
"defined while prefix is enforced.");
}
}
}
return new NodeSchemaLoadResult(schemaList, enforecePrefix);
}
/**
* Load a layer from a layer element in the XML configuration file.
* @param element network topology node layer element
* @return ECSchema
*/
private NodeSchema parseLayerElement(Element element) {
NodeList fields = element.getChildNodes();
LayerType type = null;
int cost = 0;
String prefix = null;
String defaultName = null;
for (int i = 0; i < fields.getLength(); i++) {
Node fieldNode = fields.item(i);
if (fieldNode instanceof Element) {
Element field = (Element) fieldNode;
String tagName = field.getTagName();
// Get the nonnull text value.
Text text = (Text) field.getFirstChild();
String value;
if (text != null) {
value = text.getData().trim();
if (value.isEmpty()) {
// Element with empty value is ignored
continue;
}
} else {
continue;
}
if (LAYER_COST.equalsIgnoreCase(tagName)) {
cost = Integer.parseInt(value);
if (cost < 0) {
throw new IllegalArgumentException(
"Cost should be positive number or 0");
}
} else if (LAYER_TYPE.equalsIgnoreCase(tagName)) {
type = NodeSchema.LayerType.getType(value);
if (type == null) {
throw new IllegalArgumentException(
"Unsupported layer type:" + value);
}
} else if (LAYER_PREFIX.equalsIgnoreCase(tagName)) {
prefix = value;
} else if (LAYER_DEFAULT_NAME.equalsIgnoreCase(tagName)) {
defaultName = value;
} else {
throw new IllegalArgumentException("Unsupported Element <" + tagName
+ ">");
}
}
}
// type is a mandatory property
if (type == null) {
throw new IllegalArgumentException("Missing type Element");
}
return new NodeSchema(type, cost, prefix, defaultName);
}
}