blob: 148cc8f70a0304a9446f42ce53f2b8dea12f6440 [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.d.config.sync.impl.netconf;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.slf4j.LoggerFactory.getLogger;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.CompletableFuture;
import org.onlab.util.XmlString;
import org.onosproject.d.config.ResourceIds;
import org.onosproject.d.config.sync.DeviceConfigSynchronizationProvider;
import org.onosproject.d.config.sync.impl.netconf.NetconfDeviceConfigSynchronizerComponent.NetconfContext;
import org.onosproject.d.config.sync.operation.SetRequest;
import org.onosproject.d.config.sync.operation.SetRequest.Change;
import org.onosproject.d.config.sync.operation.SetRequest.Change.Operation;
import org.onosproject.d.config.sync.operation.SetResponse;
import org.onosproject.d.config.sync.operation.SetResponse.Code;
import org.onosproject.net.DeviceId;
import org.onosproject.net.provider.AbstractProvider;
import org.onosproject.net.provider.ProviderId;
import org.onosproject.netconf.NetconfDevice;
import org.onosproject.netconf.NetconfException;
import org.onosproject.netconf.NetconfSession;
import org.onosproject.yang.model.DataNode;
import org.onosproject.yang.model.DefaultResourceData;
import org.onosproject.yang.model.InnerNode;
import org.onosproject.yang.model.ResourceData;
import org.onosproject.yang.model.ResourceId;
import org.onosproject.yang.runtime.AnnotatedNodeInfo;
import org.onosproject.yang.runtime.Annotation;
import org.onosproject.yang.runtime.CompositeData;
import org.onosproject.yang.runtime.CompositeStream;
import org.onosproject.yang.runtime.DefaultAnnotatedNodeInfo;
import org.onosproject.yang.runtime.DefaultAnnotation;
import org.onosproject.yang.runtime.DefaultCompositeData;
import org.onosproject.yang.runtime.DefaultRuntimeContext;
import org.onosproject.yang.runtime.RuntimeContext;
import org.slf4j.Logger;
import com.google.common.io.CharStreams;
/**
* Dynamic config synchronizer provider for NETCONF.
* <p>
* <ul>
* <li> Converts POJO YANG into XML.
* <li> Adds NETCONF envelope around it.
* <li> Send request down to the device over NETCONF
* </ul>
*/
public class NetconfDeviceConfigSynchronizerProvider
extends AbstractProvider
implements DeviceConfigSynchronizationProvider {
private static final Logger log = getLogger(NetconfDeviceConfigSynchronizerProvider.class);
// TODO this should probably be defined on YRT Serializer side
/**
* {@link RuntimeContext} parameter Dataformat specifying XML.
*/
private static final String DATAFORMAT_XML = "xml";
private static final String XMLNS_XC = "xmlns:xc";
private static final String NETCONF_1_0_BASE_NAMESPACE =
"urn:ietf:params:xml:ns:netconf:base:1.0";
/**
* Annotation to add xc namespace declaration.
* {@value #XMLNS_XC}={@value #NETCONF_1_0_BASE_NAMESPACE}
*/
private static final DefaultAnnotation XMLNS_XC_ANNOTATION =
new DefaultAnnotation(XMLNS_XC, NETCONF_1_0_BASE_NAMESPACE);
private static final String XC_OPERATION = "xc:operation";
private NetconfContext context;
protected NetconfDeviceConfigSynchronizerProvider(ProviderId id,
NetconfContext context) {
super(id);
this.context = checkNotNull(context);
}
@Override
public CompletableFuture<SetResponse> setConfiguration(DeviceId deviceId,
SetRequest request) {
// sanity check and handle empty change?
// TODOs:
// - Construct convert request object into XML
// -- [FutureWork] may need to introduce behaviour for Device specific
// workaround insertion
StringBuilder rpc = new StringBuilder();
// - Add NETCONF envelope
rpc.append("<rpc xmlns=\"").append(NETCONF_1_0_BASE_NAMESPACE).append('"')
.append(">");
rpc.append("<edit-config>");
rpc.append("<target>");
// TODO directly writing to running for now
rpc.append("<running/>");
rpc.append("</target>\n");
rpc.append("<config ")
.append(XMLNS_XC).append("=\"").append(NETCONF_1_0_BASE_NAMESPACE).append("\">");
// TODO netconf SBI should probably be adding these envelopes once
// netconf SBI is in better shape
// TODO In such case netconf sbi need to define namespace externally visible.
// ("xc" in above instance)
// to be used to add operations on config tree nodes
// Convert change(s) into a DataNode tree
for (Change change : request.changes()) {
log.trace("change={}", change);
// TODO switch statement can probably be removed
switch (change.op()) {
case REPLACE:
case UPDATE:
case DELETE:
// convert DataNode -> ResourceData
ResourceData data = toResourceData(change);
// build CompositeData
DefaultCompositeData.Builder compositeData =
DefaultCompositeData.builder();
// add ResourceData
compositeData.resourceData(data);
// add AnnotatedNodeInfo operation
compositeData.addAnnotatedNodeInfo(toAnnotatedNodeInfo(change.op(), change.path()));
RuntimeContext yrtContext = new DefaultRuntimeContext.Builder()
.setDataFormat(DATAFORMAT_XML)
.addAnnotation(XMLNS_XC_ANNOTATION)
.build();
CompositeData cdata = compositeData.build();
log.trace("CompositeData:{}", cdata);
CompositeStream xml = context.yangRuntime().encode(cdata,
yrtContext);
try {
CharStreams.copy(new InputStreamReader(xml.resourceData(), UTF_8), rpc);
} catch (IOException e) {
log.error("IOException thrown", e);
// FIXME handle error
}
break;
default:
log.error("Should never reach here. {}", change);
break;
}
}
// - close NETCONF envelope
// TODO eventually these should be handled by NETCONF SBI side
rpc.append('\n');
rpc.append("</config>");
rpc.append("</edit-config>");
rpc.append("</rpc>");
// - send requests down to the device
NetconfSession session = getNetconfSession(deviceId);
if (session == null) {
log.error("No session available for {}", deviceId);
return completedFuture(SetResponse.response(request,
Code.FAILED_PRECONDITION,
"No session for " + deviceId));
}
try {
// FIXME Netconf async API is currently screwed up, need to fix
// NetconfSession, etc.
CompletableFuture<String> response = session.rpc(rpc.toString());
log.trace("raw request:\n{}", rpc);
log.trace("prettified request:\n{}", XmlString.prettifyXml(rpc));
return response.handle((resp, err) -> {
if (err == null) {
log.trace("reply:\n{}", XmlString.prettifyXml(resp));
// FIXME check response properly
return SetResponse.ok(request);
} else {
return SetResponse.response(request, Code.UNKNOWN, err.getMessage());
}
});
} catch (NetconfException e) {
// TODO Handle error
log.error("NetconfException thrown", e);
return completedFuture(SetResponse.response(request, Code.UNKNOWN, e.getMessage()));
}
}
// overridable for ease of testing
/**
* Returns a session for the specified deviceId.
*
* @param deviceId for which we wish to retrieve a session
* @return a NetconfSession with the specified node
* or null if this node does not have the session to the specified Device.
*/
protected NetconfSession getNetconfSession(DeviceId deviceId) {
NetconfDevice device = context.netconfController().getNetconfDevice(deviceId);
checkNotNull(device, "The specified deviceId could not be found by the NETCONF controller.");
NetconfSession session = device.getSession();
checkNotNull(session, "A session could not be retrieved for the specified deviceId.");
return session;
}
/**
* Creates AnnotatedNodeInfo for {@code node}.
*
* @param op operation
* @param parent resourceId
* @param node the node
* @return AnnotatedNodeInfo
*/
static AnnotatedNodeInfo annotatedNodeInfo(Operation op,
ResourceId parent,
DataNode node) {
return DefaultAnnotatedNodeInfo.builder()
.resourceId(ResourceIds.resourceId(parent, node))
.addAnnotation(toAnnotation(op))
.build();
}
/**
* Creates AnnotatedNodeInfo for specified resource path.
*
* @param op operation
* @param path resourceId
* @return AnnotatedNodeInfo
*/
static AnnotatedNodeInfo toAnnotatedNodeInfo(Operation op,
ResourceId path) {
return DefaultAnnotatedNodeInfo.builder()
.resourceId(path)
.addAnnotation(toAnnotation(op))
.build();
}
/**
* Transform DataNode into a ResourceData.
*
* @param change object
* @return ResourceData
*/
static ResourceData toResourceData(Change change) {
DefaultResourceData.Builder builder = DefaultResourceData.builder();
builder.resourceId(change.path());
if (change.op() != Change.Operation.DELETE) {
DataNode dataNode = change.val();
if (dataNode instanceof InnerNode) {
((InnerNode) dataNode).childNodes().values().forEach(builder::addDataNode);
} else {
log.error("Unexpected DataNode encountered", change);
}
}
return builder.build();
}
static Annotation toAnnotation(Operation op) {
switch (op) {
case DELETE:
return new DefaultAnnotation(XC_OPERATION, "remove");
case REPLACE:
return new DefaultAnnotation(XC_OPERATION, "replace");
case UPDATE:
return new DefaultAnnotation(XC_OPERATION, "merge");
default:
throw new IllegalArgumentException("Unknown operation " + op);
}
}
}