blob: 33b52a4529cf72093574d82146528d9a825618b4 [file] [log] [blame]
/*
* Copyright 2017-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.netconf.client.impl;
import com.google.common.annotations.Beta;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.onosproject.config.DynamicConfigEvent;
import org.onosproject.config.DynamicConfigListener;
import org.onosproject.config.DynamicConfigService;
import org.onosproject.config.ResourceIdParser;
import org.onosproject.config.Filter;
import org.onosproject.mastership.MastershipService;
import org.onosproject.net.DeviceId;
import org.onosproject.netconf.NetconfController;
import org.onosproject.netconf.NetconfException;
import org.onosproject.netconf.client.NetconfTranslator;
import org.onosproject.netconf.client.NetconfTranslator.OperationType;
import org.onosproject.yang.model.DataNode;
import org.onosproject.yang.model.InnerNode;
import org.onosproject.yang.model.LeafNode;
import org.onosproject.yang.model.ListKey;
import org.onosproject.yang.model.NodeKey;
import org.onosproject.yang.model.ResourceId;
import org.onosproject.yang.model.DefaultResourceData;
import org.onlab.util.AbstractAccumulator;
import org.onlab.util.Accumulator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import static com.google.common.base.Preconditions.checkNotNull;
@Beta
@Component(immediate = true)
public class NetconfActiveComponent implements DynamicConfigListener {
private static final Logger log = LoggerFactory.getLogger(NetconfActiveComponent.class);
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected DynamicConfigService cfgService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected NetconfTranslator netconfTranslator;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected MastershipService mastershipService;
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected NetconfController controller;
private final Accumulator<DynamicConfigEvent> accumulator = new InternalEventAccummulator();
private static final String DEVNMSPACE = "ne-l3vpn-api";
private static final String DEVICES = "devices";
private static final String DEVICE = "device";
private static final String DEVICE_ID = "deviceid";
private static final String DEF_IP = "0:0:0"; //hack, remove later
//Symbolic constants for use with the accumulator
private static final int MAX_EVENTS = 1000;
private static final int MAX_BATCH_MS = 5000;
private static final int MAX_IDLE_MS = 1000;
private ResourceId defParent = new ResourceId.Builder()
.addBranchPointSchema(DEVICES, DEVNMSPACE)
.addBranchPointSchema(DEVICE, DEVNMSPACE)
//.addBranchPointSchema(DEVICE_ID, DEVNMSPACE)
.addKeyLeaf(DEVICE_ID, DEVNMSPACE, DEF_IP)
.build();
//TODO remove this hack after store ordering is fixed
private static final String EXCPATH = "root|devices#ne-l3vpn-api|" +
"device#ne-l3vpn-api$deviceid#ne-l3vpn-api#netconf:172.16.5.11:22|" +
"l3vpn#ne-l3vpn-api|l3vpncomm#ne-l3vpn-api|l3vpnInstances#ne-l3vpn-api|" +
"l3vpnInstance#ne-l3vpn-api$vrfName#ne-l3vpn-api#vrf2|l3vpnIfs#ne-l3vpn-api";
//end of hack
@Activate
protected void activate() {
cfgService.addListener(this);
log.info("Started");
}
@Deactivate
protected void deactivate() {
cfgService.removeListener(this);
log.info("Stopped");
}
@Override
public boolean isRelevant(DynamicConfigEvent event) {
String resId = ResourceIdParser.parseResId(event.subject());
String refId = ResourceIdParser.parseResId(defParent);
refId = refId.substring(0, refId.length() - (DEF_IP.length() + 1));
if (!resId.contains(refId)) {
return false;
}
if (resId.length() < refId.length()) {
return false;
}
return (resId.substring(0, (refId.length())).compareTo(refId) == 0);
}
public boolean isMaster(DeviceId deviceId) {
return mastershipService.isLocalMaster(deviceId);
}
@Override
public void event(DynamicConfigEvent event) {
accumulator.add(event);
}
/**
* Performs the delete operation corresponding to the passed event.
*
* @param node a relevant dataNode
* @param deviceId the deviceId of the device to be updated
* @param resourceId the resourceId of the root of the subtree to be edited
* @return true if the update succeeds false otherwise
*/
private boolean configDelete(DataNode node, DeviceId deviceId, ResourceId resourceId) {
return parseAndEdit(node, deviceId, resourceId, OperationType.DELETE);
}
/**
* Performs the update operation corresponding to the passed event.
*
* @param node a relevant dataNode
* @param deviceId the deviceId of the device to be updated
* @param resourceId the resourceId of the root of the subtree to be edited
* @return true if the update succeeds false otherwise
*/
private boolean configUpdate(DataNode node, DeviceId deviceId, ResourceId resourceId) {
return parseAndEdit(node, deviceId, resourceId, OperationType.REPLACE);
}
/**
* Parses the incoming event and pushes configuration to the effected
* device.
*
* @param node the dataNode effecting a particular device of which this node
* is master
* @param deviceId the deviceId of the device to be modified
* @param resourceId the resourceId of the root of the subtree to be edited
* @param operationType the type of editing to be performed
* @return true if the operation succeeds, false otherwise
*/
private boolean parseAndEdit(DataNode node, DeviceId deviceId,
ResourceId resourceId,
NetconfTranslator.OperationType operationType) {
//FIXME Separate edit and delete, delete can proceed with a null node
DefaultResourceData.Builder builder = DefaultResourceData.builder();
if (node != null) {
//add all low level nodes of devices
Iterator<Map.Entry<NodeKey, DataNode>> it = ((InnerNode) node)
.childNodes().entrySet().iterator();
while (it.hasNext()) {
DataNode n = it.next().getValue();
if (!n.key().schemaId().name().equals("deviceid")) {
builder.addDataNode(n);
}
}
}
//add resouce id //TODO: check if it is correct
try {
return netconfTranslator.editDeviceConfig(
deviceId, builder.build(), operationType);
} catch (IOException e) {
log.debug("parseAndEdit()", e);
return false;
}
}
/**
* Retrieves device id from Data node.
*
* @param node the node associated with the event
* @return the deviceId of the effected device
*/
@Beta
public DeviceId getDeviceId(DataNode node) {
String[] temp;
String ip, port;
if (node.type() == DataNode.Type.SINGLE_INSTANCE_LEAF_VALUE_NODE) {
temp = ((LeafNode) node).asString().split("\\:");
if (temp.length != 3) {
throw new IllegalStateException(new NetconfException("Invalid device id form, cannot apply"));
}
ip = temp[1];
port = temp[2];
} else if (node.type() == DataNode.Type.MULTI_INSTANCE_NODE) {
ListKey key = (ListKey) node.key();
temp = key.keyLeafs().get(0).leafValAsString().split("\\:");
if (temp.length != 3) {
throw new IllegalStateException(new NetconfException("Invalid device id form, cannot apply"));
}
ip = temp[1];
port = temp[2];
} else {
throw new IllegalStateException(new NetconfException("Invalid device id type, cannot apply"));
}
try {
return DeviceId.deviceId(new URI("netconf", ip + ":" + port, (String) null));
} catch (URISyntaxException var4) {
throw new IllegalArgumentException("Unable to build deviceID for device " + ip + ":" + port, var4);
}
}
/**
* Inititates a Netconf connection to the device.
*
* @param deviceId of the added device
*/
private void initiateConnection(DeviceId deviceId) {
if (controller.getNetconfDevice(deviceId) == null) {
try {
//if (this.isReachable(deviceId)) {
this.controller.connectDevice(deviceId);
//}
} catch (Exception ex) {
throw new IllegalStateException(new NetconfException("Unable to connect to NETCONF device on " +
deviceId, ex));
}
}
}
/**
* Retrieves device key from Resource id.
*
* @param path associated with the event
* @return the deviceId of the effected device
*/
@Beta
public ResourceId getDeviceKey(ResourceId path) {
String resId = ResourceIdParser.parseResId(path);
String[] el = resId.split(ResourceIdParser.EL_CHK);
if (el.length < 3) {
throw new IllegalStateException(new NetconfException("Invalid resource id, cannot apply"));
}
if (!el[2].contains((ResourceIdParser.KEY_SEP))) {
throw new IllegalStateException(new NetconfException("Invalid device id key, cannot apply"));
}
String[] keys = el[2].split(ResourceIdParser.KEY_CHK);
if (keys.length < 2) {
throw new IllegalStateException(new NetconfException("Invalid device id key, cannot apply"));
}
String[] parts = keys[1].split(ResourceIdParser.NM_CHK);
if (parts.length < 3) {
throw new IllegalStateException(new NetconfException("Invalid device id key, cannot apply"));
}
if (parts[2].split("\\:").length != 3) {
throw new IllegalStateException(new NetconfException("Invalid device id form, cannot apply"));
}
return (new ResourceId.Builder()
.addBranchPointSchema(el[1].split(ResourceIdParser.NM_CHK)[0],
el[1].split(ResourceIdParser.NM_CHK)[1])
.addBranchPointSchema(keys[0].split(ResourceIdParser.NM_CHK)[0],
keys[0].split(ResourceIdParser.NM_CHK)[1])
.addKeyLeaf(parts[0], parts[1], parts[2])
.build());
}
/**
* Retrieves device id from Resource id.
*
* @param path associated with the event
* @return the deviceId of the effected device
*/
@Beta
public DeviceId getDeviceId(ResourceId path) {
String resId = ResourceIdParser.parseResId(path);
String[] el = resId.split(ResourceIdParser.EL_CHK);
if (el.length < 3) {
throw new IllegalStateException(new NetconfException("Invalid resource id, cannot apply"));
}
if (!el[2].contains((ResourceIdParser.KEY_SEP))) {
throw new IllegalStateException(new NetconfException("Invalid device id key, cannot apply"));
}
String[] keys = el[2].split(ResourceIdParser.KEY_CHK);
if (keys.length < 2) {
throw new IllegalStateException(new NetconfException("Invalid device id key, cannot apply"));
}
String[] parts = keys[1].split(ResourceIdParser.NM_CHK);
if (parts.length < 3) {
throw new IllegalStateException(new NetconfException("Invalid device id key, cannot apply"));
}
String[] temp = parts[2].split("\\:");
String ip, port;
if (temp.length != 3) {
throw new IllegalStateException(new NetconfException("Invalid device id form, cannot apply"));
}
ip = temp[1];
port = temp[2];
try {
return DeviceId.deviceId(new URI("netconf", ip + ":" + port, (String) null));
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Unable to build deviceID for device " + ip + ":" + port, ex);
}
}
/* Accumulates events to allow processing after a desired number of events were accumulated.
*/
private class InternalEventAccummulator extends AbstractAccumulator<DynamicConfigEvent> {
protected InternalEventAccummulator() {
super(new Timer("dyncfgevt-timer"), MAX_EVENTS, MAX_BATCH_MS, MAX_IDLE_MS);
}
@Override
public void processItems(List<DynamicConfigEvent> events) {
Map<ResourceId, List<DynamicConfigEvent>> evtmap = new LinkedHashMap<>();
Boolean handleEx = false; //hack
ResourceId exId = null; //hack
for (DynamicConfigEvent e : events) {
checkNotNull(e, "process:Event cannot be null");
ResourceId cur = e.subject();
//TODO remove this hack after store ordering is fixed
String expat = ResourceIdParser.parseResId(cur);
if ((expat.compareTo(EXCPATH) == 0) && (e.type() == DynamicConfigEvent.Type.NODE_ADDED)) {
handleEx = true;
exId = cur;
} else { //actual code
ResourceId key = getDeviceKey(e.subject());
List<DynamicConfigEvent> el = evtmap.get(key);
if (el == null) {
el = new ArrayList<>();
}
el.add(e);
evtmap.put(key, el);
}
}
evtmap.forEach((k, v) -> {
DeviceId curDevice = getDeviceId(k);
if (!isMaster(curDevice)) {
log.info("NetConfListener: not master, ignoring config for device {}", k);
return;
}
initiateConnection(curDevice);
for (DynamicConfigEvent curEvt : v) {
switch (curEvt.type()) {
case NODE_ADDED:
case NODE_UPDATED:
case NODE_REPLACED:
Filter filt = Filter.builder().build();
DataNode node = cfgService.readNode(k, filt);
configUpdate(node, curDevice, k);
break;
case NODE_DELETED:
configDelete(null, curDevice, k); //TODO curEvt.subject())??
break;
case UNKNOWN_OPRN:
default:
log.warn("NetConfListener: unknown event: {}", curEvt.type());
break;
}
}
});
//TODO remove this hack after store ordering is fixed
if (handleEx) {
DeviceId exDevice = getDeviceId(exId);
if (!isMaster(exDevice)) {
log.info("NetConfListener: not master, ignoring config for expath {}", exId);
return;
}
initiateConnection(exDevice);
Filter filt = Filter.builder().build();
DataNode exnode = cfgService.readNode(exId, filt);
configUpdate(exnode, exDevice, exId);
} //end of hack
}
}
}