| /* |
| * 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 java.util.concurrent.atomic.AtomicInteger; |
| |
| 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.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; |
| |
| // FIXME remove and let netconf southbound deal with message-id generation |
| private final AtomicInteger messageId = new AtomicInteger(1); |
| |
| 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("message-id=\"").append(messageId.getAndIncrement()).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()) { |
| |
| // 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(); |
| CompositeStream xml = context.yangRuntime().encode(compositeData.build(), |
| 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.request(rpc.toString()); |
| log.info("TRACE: request:\n{}", XmlString.prettifyXml(rpc)); |
| return response.handle((resp, err) -> { |
| if (err == null) { |
| log.info("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); |
| } |
| } |
| |
| } |