OpenDaylight系列文章(五):我的第一个ODL工程(上篇)


一、概述

从本篇文章开始,我们将会带领大家从OpenDaylight工程构建开始,一步一步对OpenDaylight控制器的开发知识进行讲解和实战,打造一个属于自己的OpenDaylight控制器。

本篇文章主要通过获取全网拓扑信息以及SDN交换设备的网卡信息来了解RPC的YANG模型定义、RPC注册以及RPC功能实现。

二、环境准备

1.系统环境:Ubuntu 14.04

2.软件环境:
  • jdk 1.8
  • maven 3.3.9
  • OpenDaylight Carbon SR2
  • Mininet
  • Eclipse/IntelliJ IDEA


三、工程构建

1. 在工程构建开始之前,需要配置maven的settings.xml(Linux系统settings.xml的默认位置在/root/.m2),方便起见,可以将settings.xml内容直接替换下面链接的内容。
https://raw.githubusercontent.com/opendaylight/odlparent/stable/carbon/settings.xml

2. 根据OpenDaylight官方文档提供的Maven命令生成项目骨架:
mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -DarchetypeGroupId=org.opendaylight.controller -DarchetypeArtifactId=opendaylight-startup-archetype -DarchetypeRepository=https://nexus.opendaylight.org/content/repositories/public -DarchetypeVersion=1.3.2-Carbon

参数说明:
1.png

运行此命令,输入生成工程的基本信息:
2.png

红色箭头所指的部分为需要填写的信息,其余部分回车即可。

此处也可以以参数的方式进行填写相关工程信息,具体命令如下:
mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -DarchetypeGroupId=org.opendaylight.controller -DarchetypeArtifactId=opendaylight-startup-archetype -DarchetypeRepository=https://nexus.opendaylight.org/content/repositories/public -DarchetypeVersion=1.3.2-Carbon -DgroupId=org.opendaylight.topology -DartifactId=topology -DclassPrefix=Topology -Dcopyright=ZebraDecoder

3.构建成功后会在当前目录生成名称为topology的文件夹,进入到topology文件夹下即可看到工程目录结构:
3.png

各部分组成的含义为:

4.png

4.进入到topology文件夹下对构建好的工程进行编译(第一次编译时速度有点慢,等待即可),具体的编译命令为:
mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true

Tips:
-DskipTests表示不执行测试用例
-Dmaven.javadoc.skip=true表示跳过javadoc
-Dcheckstyle.skip=true表示跳过checkstyle检查

编译成功后如下图所示:
5.png

5.启动控制器,具体启动如下图:
6.png

接下来,将构建好的项目导入到Java IDE中。常用来开发OpenDaylight的Java IDE工具有Eclipse和IntelliJ IDEA,如果使用的是IntelliJ IDEA的话,需要在topology目录下执行mvn idea:idea,生成topology-aggregator.ipr文件,双击该文件或者在idea中导入该文件即可打开工程。
7.png

四、实例开发

1.RPC和YANG模型介绍


  • 什么是RPC?
    RPC的含义是远程过程调用,全称是Remote Procedure Call Protocol,最早由在上世纪 80 年代由 Bruce Jay Nelson 提出。RPC是一种进程间的通信方式。其目标是让远程过程(服务)调用透明化。远程过程调用简单说就是发送一个请求给远程机器,远程机器返回一个结果回来的过程。OpenDaylight采用RPC这种通信方式主要是基于以下几点:
    (1)可以实现跨语言跨平台调用,不同业务系统都可以进行调用OpenDaylight对外提供的服务,使得OpenDaylight服务更具通用性;
    (2)每个调用方只需要提出需求,不需要了解OpenDaylight具体的代码逻辑实现;
    (3) RPC易于拓展,易于复用。

  • 什么是YANG?
    YANG数据建模语言由互联网工程任务组(IETF)中的NETMOD 工作组开发,并以RFC 6020在2010年10月发表。YANG模型作为一种数据建模语言,其产生是为了对NETCONF协议所操作的数据进行建模。YANG模型描述数据模型的层级嵌套结构以及各属性的数据类型时主要采用树形结构的节点定义,总的来说,YANG模型的产生主要是对以下三类数据进行建模:
    (1)远程过程调用(rpc):对可在网元上触发的远程程序调用进行建模;
    (2)资源(data):对要操控的资源进行建模,也就是对网元设备的的configure或者operational数据进行建模;
    (3)通知(notification):对网元发出的事件通知消息进行建模。


OpenDaylight控制器目前主要采用MD-SAL架构,这里的M指的就是YANG 模型。在开发过程中,一般先根据需求先设计YANG模型来确定准备提供哪些数据和服务,然后使用Maven进行编译时,通过YANG Tools插件将YANG文件的转译为相应的java类、接口。开发者通过实现自动生成的Java代码来实现具体的API和服务内容。

由于YANG文件通俗易懂,我们完全可以仅仅通过YANG文件就可以大概了解到系统提供的数据和服务内容,十分的方便。同时,OpenDaylight访问Data Store的API完全基于Yang模型,这部分代码Yang Tools插件自动生成,这样开发人员只需关注如何进行提供服务,大大提高了开发效率和降低了OpenDaylight控制器的开发难度。

2.YANG模型中定义RPC

(1)编辑api/src/main/yang目录下的topology.yang,进行yang文件编写,完整的topology.yang文件如下:

module topology {
yang-version 1;
namespace "urn:opendaylight:params:xml:ns:yang:topology";
prefix "topology";
import ietf-inet-types { prefix inet; revision-date 2013-07-15; }
description "get links and ports information";
revision "2018-03-07" {
    description "Initial revision of topology model";
}
grouping output-link{
    leaf link-id{
        type inet:uri;
        description "link id";
    }
    leaf src-device{
        type inet:uri;
        description "source device";
    }
    leaf src-port{
        type int32;
        description "source port";
    }
    leaf dst-device{
        type inet:uri;
         description "destination device";
    }
    leaf dst-port{
        type int32;
        description "destination port";
    }
}
grouping output-port{
    leaf device-id{
        type inet:uri;
        description "link id";
    }
    leaf port-number{
        type string;
        description "port number";
    }
    leaf port-name{
        type string;
        description "port name";
    }
    leaf hardware-address{
        type string;
        description "hardware address";
    }
    leaf current-speed{
        type int64;
        description "current speed";
    }
    leaf maximum-speed{
        type int64;
        description "maximum speed";
    }
    leaf link-down{
        type boolean;
        description "link down";
    }
}
rpc list-links-info{
    output {
        list links-info{
            uses output-link;
            description "link info";
        }
    }
}
rpc list-ports-info{
    output{
        list ports-info{
            uses output-port;
            description "port info";
        }
    }
}
}

(2)下面来解读一下这个yang文件相关语法:

①module:在实际项目中,一个工程中YANG模型很多,所以可以通过module来划分不同模块的YANG。各个module描述各自的数据模型,不同module之间可以互相引用,十分灵活。

②namespace和prefix :每个module有一个独立的namespace以避免命名冲突,同时还有一个简称prefix。

③import:通过import导入外部模块,可以在本模块中使用导入模块中定义的数据结构或数据类型。导入时会给外部模块起一个prefix,引用导入模块中的内容时以prefix开头即可。

④description:描述YANG文件的功能或者各个节点的功能。

⑤revision:版本信息。

⑥grouping:一种可重用的schema nodes的集合。该集合可能在定义处的module中被使用,也可能在包含它的modules中使用,还可能在导入它的modules中被使用。grouping不会生成任何节点,可以看成一种临时树干。

⑦leaf:一种数据节点,在data tree中最多只能有一个实例存在。一个leaf节点只能有一个值,并且不能有子节点。leaf主要用来定义属性值。

⑧type:指定leaf的数据类型,该类型可以是YANG RFC规定的基本类型,也可以是自定义的派生类型。在YANG文件中,int32为基本类型,inet:uri为派生类型,派生类型的定义由typedef定义。基本的原生数据类型有uint8、uint16、string、bits、binary、boolean等等。

⑨rpc:远程过程调用。它可能包含input和output子节点,分别是该rpc所需要的输入和输出数据结构。若没有则表明该操作不需要输入数据或者没有输出数据。

⑩uses:实例化在grouping声明中定义的schema nodes。

更为详细的YANG语法知识可以参考rfc 6020(https://tools.ietf.org/html/rfc6020

需要注意的是,由于在YANG文件中引入了ietf-inet-types,需要在api目录下的pom.xml中添加相关依赖:
<dependencies>
    <dependency>
        <groupId>org.opendaylight.mdsal.model</groupId>
        <artifactId>ietf-inet-types-2013-07-15</artifactId>
        <version>1.2.2-Carbon</version>
    </dependency>
</dependencies> 

Tip:dependencies标签如果存在则直接添加dependency这一层标签即可。

(3)定义好YANG文件,需要重新编译工程,编译时无需整体编译,只需进入到工程主目录下的api目录下进行编译即可,具体命令如下:
cd api/
mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true


(4)编译时,maven会通过Yang Tools插件生成基本的Java文件,生成的Java文件如下图所示:

8.png


如果大家对如何通过Yang Tools插件将yang文件映射成java文件的细节感兴趣,请参考OpenDaylight的官方wiki的相关介绍:“YANG Tools:YANG to Java Mapping”(https://wiki.opendaylight.org/view/Yang_Tools:Code_Generation_Demo:YANG2JAVA_Mapping),这里不再展开叙述。

3.RPC实现

在进行RPC代码实现之前,需要了解一下OpenDaylight默认的数据存储方式。在OpenDaylight中默认采用是DataStore这种数据库。DataStore采用树型结构进行数据存储,它是一种高效的In-Memory(内存)数据库。

DataStore中的数据存储分两种形式:config和operational。config持有由应用所写的数据,一般是设备的配置信息或者是需要持久化的数据,而operational反映了设备运行时的数据,表明设备的运行状态,从设备读取数据,如果没有错误即可以看到设备的当前实际信息。

config和operational相同之处在于两者的数据都以树型节点的形式存储在内存中;不同点是config数据生命周期比较长,控制器断电重启后数据会被持久化到硬盘中,持久化主要采用快照(snapshot)和增量日志(journal)这两种方式,而operational数据生命周期比较短,控制器断电重启后数据失效。

本案例中的全网拓扑信息以及获取网卡信息都是从OpenDaylight的operational数据库中读取出来的状态数据。

由于DataStore的内容比较多,我们小组将会在后面的系列文章中,对DataStore的知识进行深入探讨。

具体RPC代码逻辑实现过程:
(1)编辑topology文件夹下的impl/src/main/java/org/opendaylight/topology/impl/TopologyProvider.java文件进行代码实现,由于代码比较少,这里贴出TopologyProvider.java的所有代码(不包括package和import):
public class TopologyProvider implements TopologyService {
private static final Logger LOG = LoggerFactory.getLogger(TopologyProvider.class);
private final DataBroker dataBroker;
private static final String FLOWID = "flow:1";
public TopologyProvider(final DataBroker dataBroker) {
    this.dataBroker = dataBroker;
}
public void init() {
    LOG.info("TopologyProvider Session Initiated");
}
public void close() {
    LOG.info("TopologyProvider Closed");
}
@Override
public Future<RpcResult<ListLinksInfoOutput>> listLinksInfo() {
    final SettableFuture<RpcResult<ListLinksInfoOutput>> futureResult = SettableFuture.create();
    ListLinksInfoOutputBuilder outputBuilder = new ListLinksInfoOutputBuilder();
    final InstanceIdentifier.InstanceIdentifierBuilder<Topology> topologyId = InstanceIdentifier.builder(NetworkTopology.class).
            child(Topology.class, new TopologyKey(new TopologyId(new Uri(FLOWID))));
    InstanceIdentifier<Topology> topologyIId = topologyId.build();
    Topology topology = read(LogicalDatastoreType.OPERATIONAL, topologyIId);

    if (topology == null || topology.getLink() == null || topology.getLink().size() < 1) {
        futureResult.set(RpcResultBuilder.success(outputBuilder.build()).build());
        return futureResult;
    }
    List<LinksInfo> linkInfos = new ArrayList<>();
    topology.getLink().forEach(temp -> {
        LinksInfoBuilder lib = new LinksInfoBuilder();

        lib.setLinkId(temp.getLinkId())
                .setSrcDevice(temp.getSource().getSourceNode())
                .setDstPort(getPort(temp.getSource().getSourceTp().getValue()))
                .setDstDevice(temp.getDestination().getDestNode())
                .setDstPort(getPort(temp.getDestination().getDestTp().getValue()));
        linkInfos.add(lib.build());
    });
    outputBuilder.setLinksInfo(linkInfos);
    futureResult.set(RpcResultBuilder.success(outputBuilder.build()).build());
    return futureResult;
}
@Override
public Future<RpcResult<ListPortsInfoOutput>> listPortsInfo() {
    final SettableFuture<RpcResult<ListPortsInfoOutput>> futureResult = SettableFuture.create();
    ListPortsInfoOutputBuilder listPortsInfoOutputBuilder = new ListPortsInfoOutputBuilder();
    Nodes nodes = queryAllNode(LogicalDatastoreType.OPERATIONAL);
    if (nodes == null || nodes.getNode() == null || nodes.getNode().size() < 1) {
        futureResult.set(RpcResultBuilder.success(listPortsInfoOutputBuilder.build()).build());
        return futureResult;
    }
    List<PortsInfo> portsInfos = new ArrayList<>();
    nodes.getNode().forEach(tempNode -> {
        List<NodeConnector> nodeConnectors = filterNodeConnectors(tempNode);
        if (nodeConnectors == null || nodeConnectors.size() < 1) {
            return;
        }
        nodeConnectors.forEach(tempPort -> {
            PortsInfoBuilder pi = new PortsInfoBuilder();
            FlowCapableNodeConnector augmentation = tempPort.getAugmentation(FlowCapableNodeConnector.class);
            pi.setDeviceId(tempNode.getId())
                    .setPortNumber(new String(augmentation.getPortNumber().getValue()))
                    .setPortName(augmentation.getName())
                    .setHardwareAddress(augmentation.getHardwareAddress().getValue())
                    .setLinkDown(augmentation.getState().isLinkDown())
                    .setMaximumSpeed(augmentation.getMaximumSpeed())
                    .setCurrentSpeed(augmentation.getCurrentSpeed());

            portsInfos.add(pi.build());
        });
    });
    listPortsInfoOutputBuilder.setPortsInfo(portsInfos);
    futureResult.set(RpcResultBuilder.success(listPortsInfoOutputBuilder.build()).build());
    return futureResult;
}
private <D extends DataObject> D read(final LogicalDatastoreType store, final InstanceIdentifier<D> path) {
    D result = null;
    final ReadOnlyTransaction transaction = dataBroker.newReadOnlyTransaction();
    Optional<D> optionalDataObject;
    final CheckedFuture<Optional<D>, ReadFailedException> future = transaction.read(store, path);
    try {
        optionalDataObject = future.checkedGet();
        if (optionalDataObject.isPresent()) {
            result = optionalDataObject.get();
        } else {
            LOG.debug("{}: Failed to read {}", Thread.currentThread().getStackTrace()[1], path);
        }
    } catch (final ReadFailedException e) {
        LOG.warn("Failed to read {} ", path, e);
    }
    transaction.close();
    return result;
}
private int getPort(String tp) {
    return Integer.parseInt(tp.split(":")[2]);
}
private Nodes queryAllNode(LogicalDatastoreType configuration) {
    final InstanceIdentifier<Nodes> identifierNodes = InstanceIdentifier.create(Nodes.class);
    return read(configuration, identifierNodes);
}
private List<NodeConnector> filterNodeConnectors(Node node) {
    final List<NodeConnector> connectors = Lists.newArrayList();
    final List<NodeConnector> list = node.getNodeConnector();
    if (list != null && list.size() > 0) {
        for (final NodeConnector nodeConnector : list) {

            if (!nodeConnector.getId().getValue().endsWith("LOCAL")) {
                connectors.add(nodeConnector);
            }
        }
    }
    return connectors;
}
}

获取网络拓扑信息(listLinks方法)的大致流程如下:
从network-topology的operational数据库中读取“flow:1”节点的所有内容。
过滤“flow:1”下的节点内容,从而获取所需的link信息。
将获取到的link信息封装成Future<RpcResult<ListLinksOutput>>进行返回给消费端。

获取网卡信息(listPortsInfo方法)的大致流程如下:
从opendaylight-inventory中获取所有的Node节点,Node表示连接在控制器上的SDN交换机设备。
获取每个Node节点的所有NodeConnector,然后获取需要的NodeConnector上的参数信息,NodeConnector相当于交换机的端口。
将获取到信息封装成Future<RpcResult<ListPortsInfoOutput>>进行返回给消费端。

(2)我们在代码中使用了ietf-topology、model-inventory、model-flow-service,需要在impl目录的pom.xml文件中添加如下依赖:
<dependency>
<groupId>org.opendaylight.mdsal.model</groupId>
<artifactId>ietf-topology</artifactId>
<version>2013.10.21.10.2-Carbon</version>
</dependency>
<dependency>
<groupId>org.opendaylight.controller.model</groupId>
<artifactId>model-inventory</artifactId>
<version>1.5.2-Carbon</version>
</dependency>
<dependency>
<groupId>org.opendaylight.openflowplugin.model</groupId>
<artifactId>model-flow-service</artifactId>
<version>0.4.2-Carbon</version>
</dependency>

(3)与之相对应的是需要在features目录下的src/main/features/features.xml文件添加引用仓库:
<repository>
mvn:org.opendaylight.openflowplugin/features-openflowplugin/${openflowplugin.version}/xml/features
</repository>


(4)同时也需要在features.xml文件中的name为odl-topology-api的feature标签中添加如下feature:<feature version="${openflowplugin.version}">odl-openflowplugin-flow-services</feature>,该句添加位置如下:
<feature name='odl-topology-api' version='${project.version}' description='OpenDaylight :: topology:: api'>
<feature version='${mdsal.model.version}'>odl-mdsal-models</feature>
<feature version="${openflowplugin.version}">odl-openflowplugin-flow-services</feature>
<bundle>mvn:org.opendaylight.topology/topology-api/{{VERSION}}</bundle>
</feature>


(5)其中openflowplugin.version在features目录下pom.xml文件中定义,需要在其中的properties标签内添加:
<openflowplugin.version>0.4.2-Carbon</openflowplugin.version>

4.RPC注册

编写完RPC实现的代码后,需要对RPC进行注册到OpenDaylight控制器以供外部调用,注册RPC方式有多种,后面的系列文章中会对RPC注册问题作详细叙述,本案例中采用的是Blueprint方式来注册RPC。

Blueprint是针对OSGi的依赖注入解决方案,用法非常类似Spring。当使用服务的时候,Blueprint会马上创建并注入一个代理(Proxy)。对这些服务进行调用时,如果服务在当前不可用的话,将会产生阻塞,直至能够获取到服务或超时。

其实就是我们把一些原本需要写代码去实现的内容用xml文件的方式简化了,这个xml文件固定放在bundle的src\main\resources\org\opendaylight\blueprint目录下,否则获取不到文件内容,至于文件名可以随意。可以想象到有专门去解析这个xml文件的代码,这个代码的位置就在controller\opendaylight\blueprint目录下,有兴趣的读者可以去看下源码。

具体操作过程:在impl/src/main/resources/org/opendaylight/blueprint/impl-blueprint.xml中添加:<odl:rpc-implementation ref="provider"/>,其中provider是实例化类的id,id名称可以改变但必须两者保持一致。

impl-blueprint.xml文件内容如下:
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
odl:use-default-for-reference-types="true">
<reference id="dataBroker"
interface="org.opendaylight.controller.md.sal.binding.api.DataBroker"
odl:type="default" />
<bean id="provider"
class="org.opendaylight.topology.impl.TopologyProvider"
init-method="init" destroy-method="close">
<argument ref="dataBroker" />
</bean>
<odl:rpc-implementation ref="provider"/>
</blueprint>

5.RPC测试

(1)RPC测试需要用到OpenDaylight WEB控制台和l2switch相关组件。因此在编译项目之前,需要在features目录下的src/main/features/features.xml文件添加引用仓库:
<repository>
mvn:org.opendaylight.l2switch/features-l2switch/${l2switch.version}/xml/features
</repository>

其中l2switch.version变量在features目录下pom.xml文件中定义,需要在其中的properties标签内添加:
<l2switch.version>0.5.2-Carbon</l2switch.version>

(2)进行编译项目,编译完成后启动控制器,具体命令如下:
mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true
./karaf/target/assembly/bin/karaf

9.png

启动完成后,可以在karaf控制台命令查看开发的bundle是否安装成功,如果显示Active表示安装成功,具体命令为:
10.png

(3)使用mininet生成网络拓扑:
sudo mn --mac --switch=ovsk,protocols=OpenFlow13 --controller=remote,ip=127.0.0.1 --topo linear,3

(4)在karaf控制台下安装相关组件:
opendaylight-user@root>feature:install odl-dluxapps-nodes odl-dluxapps-yangui odl-dluxapps-topology odl-openflowjava-all odl-openflowplugin-flow-services-ui odl-l2switch-all

其中odl-dluxapps-nodes odl-dluxapps-yangui odl-dluxapps-topology odl-openflowjava-all odl-openflowplugin-flow-services-ui为OpenDaylight WEB控制台所需组件,odl-l2switch-all组件安装目的是交换机之间进行拓扑发现,从而生成链路。

组件安装完成后,可以通过浏览器访问OpenDaylight WEB控制台,访问的url为:http://{controller_ip}:8181/index.html
其中{controller_ip}为OpenDaylight控制器的IP地址,如果是本机,则ip地址可以为127.0.0.1。
11.png

(5)上图已经可以看到由mininet生成的拓扑,也可以调用开发的RPC来获得拓扑信息。可以使用OpenDaylight WEB控制台的Yangman进行调用RPC,登录到WEB控制台后,依次鼠标点击Yangman--->topology rev.2018.03.07--->operations

此时可以看到在operations下有两个RPC:
12.png

(6)对RPC进行调用。
获取网络拓扑:选中list-links-info,然后点击SEND,调用结果如下图所示:
13.png

获取网卡信息:选中list-ports-info,然后点击SEND,调用结果如下图所示:
14.png

至此,关于OpenDaylight的RPC实例的基本开发流程已经介绍完毕(本篇文章的案例项目全部代码已经放在附件中供大家下载查看),下一篇文章将带领大家了解notification和onDataTreeChanged,敬请期待!

19 个评论

求源码,可以分享下代码吗
在附件中,可以直接下载
赞,希望我能跑起来,网上的好多坑
赞,我现在在学习odl与sdn安全相关的知识,请问楼主有什么好的资料吗?网上的安全方面的资料太少了,而且odl的实例也少,我都不知道从哪里入手
没研究关安全相关的知识,所以无法给你提供建议。odl实例的话,官网的Toaster比较适合入门,Toaster代码在:https://github.com/opendaylight/controller/tree/master/opendaylight/md-sal/samples,然后Toaster相关文档介绍:https://wiki.opendaylight.org/view/OpenDaylight_Controller:MD-SAL:Toaster_Step-By-Step
官方wiki还有一个ping的小demo也可以借鉴:https://wiki.opendaylight.org/view/Ping
好的,谢谢你
请问DarchetypeVersion=1.3.2-Carbon这个版本从哪里获取的?比如我要拿最新的版本
可以访问:https://nexus.opendaylight.org/#nexus-search;quick~opendaylight-startup-archetype,version那一栏就是-DarchetypeVersion
你好,我按照上面步骤做到“登录到WEB控制台后,依次鼠标点击Yangman--->topology rev.2018.03.07--->operations” 时怎么也找不到operations,请问是什么原因?
先看看看看log信息有没有报错,还有就是blueprint里面rpc注册了吗(<odl:rpc-implementation ref="provider"/>),再不行的话,看看我附件中的源码和你的本地代码比对一下差异。
先看看看看log信息有没有报错,还有就是blueprint里面rpc注册了吗(<odl:rpc-implementation ref="provider"/>),再不行的话,看看我附件中的源码和你的本地代码比对一下差异。
知道原因了,是因为浏览器的缘故,换成google就好了
nitrogen版本的跟你现在这个版本差别好像还挺大。有试过nitrogen的吗
没试过,感觉Nitrogen版本作为开发版本的话还不是很稳定
跟着官网一步步下来,各种卡壳。。。
博主你好,我在安装feature组件的时候卡住了,odl-openflowplugin,odl-l2switch等几个有没有怎么办啊,启动之后mininet连接不上odl。
博主你好,还有个问题,我把odl源码mvn为eclipse项目后,导入搭配eclipse中的是6个独立的项目,请问在完成修改之后该怎么编译呢?是要把他分别编译吗,整体无法进行编译
文章中的3.RPC实现的(3)(4)步检查一下
按照我上面的命令可以整体编译的,整体编译不成功和IDE没有关系,应该是代码相关问题,注意一下整体编译失败的错误信息

要回复文章请先登录注册