ODL应用开发之MD-SAL中级教程

时间:2022-05-07
本文章向大家介绍ODL应用开发之MD-SAL中级教程,主要内容包括1. 简介、2. branch0分支、2.2 ODL-skeleton-api、2.3 ODL-skeleton-impl、2.4 ODL-skeleton-consumer、2.5 ODL-skeleton-it、2.6 features、3. branch1分支、4. branch2分支、5. branch3分支、6. branch4分支、2. branch0分支、2.2 ODL-skeleton-api、2.3 ODL-skeleton-impl、2.4 ODL-skeleton-consumer、2.5 ODL-skeleton-it、2.6 features、3. branch1分支、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

1. 简介

本次我们从开始设计到最终完成一个应用的开发,主要设计datastore和RPC定义和实现。Opendaylight 开发使用了OSGi框架,OSGi框架的好处在于程序设计模块化,实现紧聚合和松耦合。

Apache Karaf 是一个OSGi的容器,它可以支持部署新的应用。在OSGi里面一个bundle可能会依赖于其他的bundle。这里可能会出现一种情况,假如bundle A依赖与bundle B,bundle B 依赖于 bundle C,那么如果我们想解析bundle A,则必须解析bundle C和bundle B。

基本的ODL框架图大家都了解(此图来自官方wiki):

这里我们需要一个ODL应用的开发,首先提供一个骨架代码,放在我的github上,下面是地址,请自行下载,地址如下:https://github.com/wyqbupt/ODL-skeleton/

下载命令:

git clone git@github.com:wyqbupt/ODL-skeleton.git

2. branch0分支

这是我照着官网的教程边做边改后自己总结出的一个框架,大体跟官网提供的turorial框架一致,非大型应用都可以参照着这么写,这个骨架的目录结构如下: ├── artifacts ├── distribution-karaf ├── features │ └── src │ └── main │ └── resources │ └── features.xml ├── parent ├── ODL-skeleton-api ├── ODL-skeleton-consumer ├── ODL-skeleton-impl └── ODL-skeleton-it 以下这部分内容的完整代码在wyqbupt/ODL-skeleton 的branch0分支中,里面每一个文件夹都是一个module,这边有个印象即可,我会细细讲的。

2.1 parent

首先打开应用根路径下的pom.xml:

<parent>
<groupId>org.opendaylight.ODL-skeleton</groupId>
<artifactId>ODL-skeleton-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>parent</relativePath>
</parent>

这里定义了整个应用的根模块位置,也就是所谓的父模块。groupID和 artifactId大家都知道分别表示组织编号和个体编号。 version确定版本号,自己定义一个,OSGi解析的版本号默认是三位,这个具体请参考规范,一般初始版本写成0.0.1吧。 relativePath这个很重要,这是我们能够找到根模块的地方,我们打开parent目录可以看见,maven 解析这些子模块的时候会先去读取根模块的内容。

理解这一点很重要,因为我们在后面的pom.xml中又很多变量名,这些变量名就在根模块的pom.xml定义!如果在根模块找不到,则会报一些变量无法解析的错误。

下面是本模块的描述:

<modelVersion>4.0.0</modelVersion>
<artifactId>ODL-skeleton-aggregator</artifactId>
<description>ODL-skeleton Project Archetype Top Level POM</description>
<packaging>pom</packaging>
<name>${project.artifactId}</name>

下面是模块的定义,我们可发现这些模块名都是那些目录名,所有需要最终组合到一起的模块都要写进去,最终组合安装的过程会把下面这些模块都扔进OSGi容器内。

<modules>
<!-- the ODL convention is to have parent and artifacts subdirectories -->
<module>parent</module>
<module>artifacts</module>
 
<!-- TODO: add new features here -->
<module>ODL-skeleton-impl</module>
<module>ODL-skeleton-api</module>
<module>ODL-skeleton-consumer</module>
<module>features</module>
<module>distribution-karaf</module>
</modules>

2.2 ODL-skeleton-api

这里定义了基本的api,还有yang文件的定义,后面我们写完yang文件后用yangtools去生成相应的java API和REST API。别问我为啥现在里面没有yang文件,还没写呢。

先看pom.xml文件

<parent>
<groupId>org.opendaylight.ODL-skeleton</groupId>
<artifactId>ODL-skeleton-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>

这里重点是 relativePath,我们发现它也指向的是我们的应用根模块,这样就可以使用根模块的一些定义,具体什么定义,后面会讲。

2.3 ODL-skeleton-impl

它主要包括相应的实现代码和应用的配置型代码,这部分唯一需要注意的是它是对api模块的依赖,打开所对应的pom.xml:

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>ODL-skeleton-api</artifactId>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
</dependency>
</dependencies>

所以我们在依赖性介绍上看到了ODL-skeleton-api,由于需要在里面利用 ODL-skeleton-api,我们加入了依赖关系,这也是加依赖关系的方法。

2.4 ODL-skeleton-consumer

因为在一个应用中,服务提供方是provider,接收方是comsumer。一个应用可以有provider或者consumer,或者两个都有。这个在应用中是可选的,暂时不做。

2.5 ODL-skeleton-it

这里面是最后应用整合安装的时候跑的测试性代码。

2.6 features

这是一个比较重要的module。对于Karaf容器来说,每一个应用都是一系列的feature,他们可以被安装进容器内。这个文件夹内定义了我们写的应用的feature和依赖关系。比如我们的应用都是链接在MD—SAL上,所以必须依赖MD-SAL。

先看pom.xml,这里我是参考了一下SDNhub上放的那个例子的feature的依赖关系,将那些org.apache.jasper之类的都放进去了。重点是前几个自己加的。

这是yangtools,这是利用yang来生成java代码的工具.

<dependency>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>features-yangtools</artifactId>
<classifier>features</classifier>
<type>xml</type>
</dependency>

这是md-sal,基本所有的模块都接在上面,它充当一个连接各个应用的桥梁。

<dependency>
<groupId>org.opendaylight.controller</groupId>
<artifactId>features-mdsal</artifactId>
<classifier>features</classifier>
<type>xml</type>
</dependency>

这是restapi配置时会用的:

<dependency>
<groupId>org.opendaylight.controller</groupId>
<artifactId>features-restconf</artifactId>
<classifier>features</classifier>
<type>xml</type>
</dependency>

这是openflowplugin,实现openflow功能的一个plugin,是opendaylight里单独的一个项目,交换机发来的包先到openflowjava,再到openflowplugin,最后进入上层监听各类openflow消息的应用,如learning switch。

<dependency>
<groupId>org.opendaylight.openflowplugin</groupId>
<artifactId>features-openflowplugin</artifactId>
<version>${openflowplugin.version}</version>
<classifier>features</classifier>
<type>xml</type>
</dependency>

我们再来看feature内的src/main/resources/features.xml。

下面这些内容是我们依赖的feature,以标示,这里面大家发现了${yangtools.version}之类的变量是吧?这些就是我前面说的parent根模块的作用,在parent根模块的pom.xml文件中定义这些变量,maven会自动去解析替换,至于这些版本应该定义成多少,或者说ODL提供了那些版本,我们可以在http://nexus.opendaylight.org/content/repositories/opendaylight.snapshot/自行查找相应的项目存在的版本号。

这里需要注意的是,所有定义在这里的依赖feature,必须在feature的pom.xml文件中加入依赖!不然编译的时候过不去。

<repository>mvn:org.opendaylight.yangtools/features-yangtools/${yangtools.version}/xml/features</repository>
<repository>mvn:org.opendaylight.controller/features-mdsal/${controller.mdsal.version}/xml/features</repository>
<repository>mvn:org.opendaylight.controller/features-nsf/${nsf.version}/xml/features</repository>
<repository>mvn:org.opendaylight.controller/features-restconf/${controller.restconf.version}/xml/features</repository>
<repository>mvn:org.opendaylight.openflowplugin/features-openflowplugin/${openflowplugin.version}/xml/features</repository>

剩下是我们之前讲的3个模块api,impl,consumer。这里就讲讲一个模块的feature的结构,每个feature都会依赖其他的feature,比如api依赖yangtools,那么我们就将odl-yangtools-common 和odl-yangtools-binding写入api内部包含的feature。

<feature name='odl-ODL-skeleton-api' version='${project.version}' description='OpenDaylight :: ODL-skeleton :: API'>
<feature version='${yangtools.version}'>odl-yangtools-common</feature>
<feature version='${yangtools.version}'>odl-yangtools-binding</feature>
<!--<feature version='${restconf.version}'>odl-restconf</feature>-->
<bundle>mvn:org.opendaylight.ODL-skeleton/ODL-skeleton-api/${project.version}</bundle>
</feature>

以上是整个框架的讲解。请先在根目录下mvn clean install试一下能否成功编译生成,成功之后karaf的运行路径在distribution-karaf/target/assembly/bin,以后启动我们自己的karaf就在这里。

branch0分支内容就此结束,以下这部分内容的完整代码在wyqbupt/ODL-skeleton 的branch1分支。

3. branch1分支

第一步,我们需要讲我们的provider接入到MD-SAL,接触过opendaylight都知道有config subsystem,它提供了服务的周期管理,同时能够提供对JMX 和 NETCONF的接入性配置。config subsystem在MD-SAL之外而且被用来提供把应用接入MD-SAL的功能。

我们首先完成配置性关系的yang文件,后面会用yangtools来生成java代码,通过这部分可以在ODL启动的时候加载相应的服务。

module ODL-skeleton-impl-config {
   yang-version 1;
   namespace "urn:opendaylight:params:xml:ns:yang:ODL-skeleton:impl:config";
   prefix "ODL-skeleton-impl-config";
   import config { prefix config; revision-date 2013-04-05; }
   import opendaylight-md-sal-binding { prefix md-sal-binding; revision-date 2013-10-28;}
   description
       "Service definition for ODL-skeleton project";
   revision "2014-12-10" {
       description
           "Initial revision";
   }
   identity ODL-skeleton-impl-config {
       base config:module-type;
       config:java-name-prefix ODL-skeletonImpl;
   }
   augment "/config:modules/config:module/config:configuration" {
       case ODL-skeleton-impl-config {
           when "/config:modules/config:module/config:type = 'ODL-skeleton-impl-config'";
           container binding-aware-broker {
               uses config:service-ref {
                   refine type {
                       mandatory true;
                       config:required-identity md-sal-binding:binding-broker-osgi-registry;
                   }
               }
           }
       }
   }
}

yang是一种模型化语言,是IETF的一项标准,rfc号是rfc6020,相应yang的定义请google IETF rfc6020。这里面的identity在yang的模型定义语言中作为一个标示,用于作为我们应用的provider提供的服务的全局标识,唯一的标记我们连接config subsystem。

augment用来扩展一个现有的ODL配置,增加了一个节点来标识 ODL-skeleton-impl。我们其中依赖了一个MD-SAL的服务 binding-aware-broker,这个跟MD-SAL的数据存取的datastore有关,我后面会讲到如何去用。

通过上面的yang文件,我们可以引导控制器的config subsystem解释ODL-skeleton 的配置文件。

下一步写响应的配置文件ODL-skeleton-impl-config.xml,由于MD-SAL提供多种服务(每个服务其实就是一个接口),每个服务可能有多种实现,我们这个配置文件里就是需要来指定要用哪种服务的哪种实现。

<?xml version="1.0" encoding="UTF-8"?>
<!-- vi: set et smarttab sw=4 tabstop=4: -->
<snapshot>
<required-capabilities>
<capability>urn:opendaylight:params:xml:ns:yang:ODL-skeleton:impl:config?module=ODL-skeleton-impl-config&revision=2014-12-10</capability>
<capability>urn:opendaylight:params:xml:ns:yang:controller:md:sal:binding?module=opendaylight-md-sal-binding&revision=2013-10-28</capability>
</required-capabilities>
<configuration>
 
<data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config">
<module>
<type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:ODL-skeleton:impl:config">prefix:ODL-skeleton-impl-config</type>
<name>ODL-skeleton-impl</name>
<binding-aware-broker>
<type xmlns:binding="urn:opendaylight:params:xml:ns:yang:controller:md:sal:binding">binding:binding-broker-osgi-registry</type>
<name>binding-osgi-broker</name>
</binding-aware-broker>
</module>
</modules>
</data>
</configuration>
</snapshot>

这里我们指定使用了inding-aware-broker service的binding-osgi-broker实现来满足需求。这里我只加入了这一个配置型服务,当然你可以加入其他的,方法就是在标签内加,然后我们在impl模块的pom.xml写入代码生成器。

<plugin>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-maven-plugin</artifactId>
<!-- TODO: I grabbed this from Ed's sample proj -->
<dependencies>
<dependency>
<groupId>org.opendaylight.controller</groupId>
<artifactId>yang-jmx-generator-plugin</artifactId>
<version>${controller.config.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>config</id>
<goals>
<goal>generate-sources</goal>
</goals>
<configuration>
 
<generator>
org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator
<outputBaseDir>${jmxGeneratorPath}</outputBaseDir>
<additionalConfiguration>
<namespaceToPackage1>urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang</namespaceToPackage1>
</additionalConfiguration>
</generator>
<generator>
org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl
<outputBaseDir>${salGeneratorPath}</outputBaseDir>
</generator>
 
<inspectDependencies>true</inspectDependencies>
</configuration>
</execution>
</executions>
</plugin>

Yantools提供了2个code生成器,一般的生成器CodeGeneratorImpl生成产生正常API和数据传输对象的(DTO),它主要的作用是将我们定义的非配置性yang,产生相应的java文件;另一个生成器JmxGenerator产生配置性的代码,也就是对我们上面写的那个配置性yang文件作用。

改完ODl-skeleton-impl的pom.xml(具体参考的代码请看分支branch1内的代码),现在我们在ODL-skeleton-impl运行:

mvn clean generate-sources

这时我们会发现两个文件被建立了,ODLSkeletonImplModule.java这个java文件中的createInstance()创建一个ODl-skeleton-impl的实例,由这个实例来操作本应用的各个服务;ODLSkeletonImplModuleFactory.java它用来创建ODLSkeletonImplModule的实例,我们目前不用修改此文件。 下面我们修改一下ODLSkeletonImplModule的createInstance()方法

ODLSkeletonImpl provider = new  ODLSkeletonImpl();
getBindingAwareBrokerDependency().registerProvider(provider, null);
return provider;

这里创建了一个ODLSkeletonImpl 实例,BindingAwareBrokerDependency是MD-SAL已经创建的一个服务,用来向MD-SAL注册我们的provider。

上面的代码里我们利用了一个类 ODLSkeletonImpl,但是我们并没有实现它这个类,需要自己定义 在ODL-skeleton/ODL-skeleton-impl/src/main/java/org/opendaylight下创建目录:

mkdir -p odl/skeleton
cd odl/skeleton

然后创建文件ODLSkeletonImpl.java,加入如下内容:

package org.opendaylight.odl.skeleton;
import org.opendaylight.controller.sal.binding.api.BindingAwareBroker.ProviderContext;
import org.opendaylight.controller.sal.binding.api.BindingAwareProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class ODLSkeletonImpl implements BindingAwareProvider, AutoCloseable {
    private static final Logger LOG = LoggerFactory.getLogger(ODLSkeletonImpl.class);
 
    @Override
    public void close() throws Exception {
        // TODO Auto-generated method stub
    }
    @Override
    public void onSessionInitiated(ProviderContext arg0) {
        LOG.info("ODLSkeletonImpl initialized!");
    }
 
}

加入完之后我们需要在刚才的ODLSkeletonImplModule中加入这个文件:

import org.opendaylight.odl.skeleton.ODLSkeletonImpl;

然后在应用根目录下:

mvn clean install

这时可以生成karaf的文件:

cd distribution-karaf/target/assembly/bin

打开karaf后执行:

feature:list

可以看到我们写的应用的feature:

branch1分支内容就此结束,以下这部分内容的完整代码在wyqbupt/ODL-skeleton 的branch2分支。

4. branch2分支

这部分我们设计API,能够实现对MD-SAL的数据的定义的存取。MD-SAL使用datastore来存取数据,每个模块有自己的一个标识,来确定数据是属于哪个模块的。

首先定义yang file,我们在应用路径 ODL-skeleton-api 下创建目录。

mkdir -p src/main/yang

进入src/main/yang,创建yang文件skeleton.yang:

module skeleton {
   namespace "http://netconfcentral.org/ns/skeleton";
   prefix multi;
 
   contact
     "Yuqi Wang <wyqbupt@163.com>";
 
   description
     "YANG version of the MIB-file.";
 
   revision "2015-05-24" {
     description
       "skeleton module in progress.";
   }
 
   container Skeleton {
     presence
       "Indicates the  service is available";
     description
       "The container for all node name and the description .";
     leaf name {
       type string;
       config false;
       mandatory true;
       description
         "The name of the node.";
     }
 
     leaf description {
       type string;
       config false;
       mandatory true;
       description
         "The description of the node.";
     }
 
     leaf num {
       type uint32;
       config true;
       default 0;
       description
         "The number can be sent into container";
     }
   }  // container 
 
   rpc send-message {
       description "Sending message to datastroe.";
       input {
           leaf name {
                 type string;
           }
           leaf description {
                 type string;
           }
       }
   }
 
 }  // module 
 
     leaf protocol-description {
       type string;
       config false;
       mandatory true;
       description
         "The description of the protocol.";
     }
 
     leaf protocol-num {
       type uint32;
       config true;
       default 0;
       description
         "The number of the protocols in container.";
     }
   }  // container 
 
 }  // module 

下面我们解释一下这个yang文件,module后面是我们的yang模型的名称,前面都是些描述性的东西。revision定义了文件的时间。container定义了容器,这是这个yang文件的主要内容之一,它有3个叶子节点,name,description,num。

这里注意观察这3个leaf,他们有个参数config,config定义为false是operational data,config定义为true是config data,他们的写入方式也不太一样。operational data是运行时模块的各种状态信息,这些数据可能在运行期间改变。config数据是程序配置性数据,比如操作的参数什么的,这些数据由用户提供并且由用户来设定程序的运行方式。

然后我们在 ODL-skeleton-api修改pom.xml,首先加入3项依赖关系,我们要用到yangtools,所有有两项是yangtools的:

<dependency>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-binding</artifactId>
<version>${yangtools.version}</version>
</dependency>
<dependency>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-common</artifactId>
<version>${yangtools.version}</version>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.core</artifactId>
</dependency>

build的内容中加入yang文件的路径,已经定义好生成数据的code CodeGeneratorImpl。

<build>
<plugins>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yang-maven-plugin</artifactId>
<version>${yangtools.version}</version>
<executions>
<execution>
<goals>
<goal>generate-sources</goal>
</goals>
<configuration>
<yangFilesRootDir>src/main/yang</yangFilesRootDir>
 
<generator>
org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl
<outputBaseDir>${salGeneratorPath}</outputBaseDir>
</generator>
 
<inspectDependencies>true</inspectDependencies>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

最后需要修改features文件夹下的features.xml,这个文件的作用之前也讲过,主要是用来定义我们自己的应用的依赖feature,其中 odl-ODL-skeleton-impl需要用到datastore,所以我们加入了依赖 odl-mdsal-broker,它提供datastore相关的服务。

<?xml version="1.0" encoding="UTF-8"?>
<features name="odl-ODL-skeleton-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.2.0"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://karaf.apache.org/xmlns/features/v1.2.0 http://karaf.apache.org/xmlns/features/v1.2.0">
 
<repository>mvn:org.opendaylight.yangtools/features-yangtools/${yangtools.version}/xml/features</repository>
<repository>mvn:org.opendaylight.controller/features-mdsal/${controller.mdsal.version}/xml/features</repository>
<repository>mvn:org.opendaylight.controller/features-nsf/${nsf.version}/xml/features</repository>
<repository>mvn:org.opendaylight.controller/features-restconf/${controller.restconf.version}/xml/features</repository>
<repository>mvn:org.opendaylight.openflowplugin/features-openflowplugin/${openflowplugin.version}/xml/features</repository>
<feature name='odl-ODL-skeleton-api' version='${project.version}' description='OpenDaylight :: ODL-skeleton :: API'>
<feature version='${yangtools.version}'>odl-yangtools-common</feature>
<feature version='${yangtools.version}'>odl-yangtools-binding</feature>
<feature version='${restconf.version}'>odl-restconf</feature>
<bundle>mvn:org.opendaylight.ODL-skeleton/ODL-skeleton-api/${project.version}</bundle>
</feature>
 
<feature name='odl-ODL-skeleton-impl' version='${project.version}' description='OpenDaylight :: ODL-skeleton :: Impl'>
<!--<feature version='${yangtools.version}'>odl-yangtools-common</feature>-->
<!--<feature version='${yangtools.version}'>odl-yangtools-binding</feature>-->
<feature version='${controller.mdsal.version}'>odl-mdsal-broker</feature>
<feature version='${project.version}'>odl-ODL-skeleton-api</feature>
<feature version='${restconf.version}'>odl-restconf</feature>
<bundle>mvn:org.opendaylight.ODL-skeleton/ODL-skeleton-impl/${project.version}</bundle>
<configfile finalname="ODL-skeleton-impl-config.xml">mvn:org.opendaylight.ODL-skeleton/ODL-skeleton-impl/${project.version}/xml/config</configfile>
</feature>
 
<feature name='odl-ODL-skeleton-consumer' version='${project.version}' description='OpenDaylight :: ODL-skeleton :: Consumer'>
<!--<feature version='${yangtools.version}'>odl-yangtools-common</feature>-->
<!--<feature version='${yangtools.version}'>odl-yangtools-binding</feature>-->
<feature version='${controller.mdsal.version}'>odl-mdsal-broker</feature>
<feature version='${project.version}'>odl-ODL-skeleton-api</feature>
<feature version='${project.version}'>odl-ODL-skeleton-impl</feature>
<feature version='${restconf.version}'>odl-restconf</feature>
<bundle>mvn:org.opendaylight.ODL-skeleton/ODL-skeleton-consumer/${project.version}</bundle>
</feature>
</features>

最后我们在应用根路径下执行:

mvn clean install

(我们这次修改都在ODL-skeleton-api下,建议下在ODL-skeleton-api下执行mvn clean install 这样排错更加容易一点)

这里大家要注意,我们在 ODL-skeleton-api中定义的yang文件最后生成的java文件的路径在ODL-skeleton-api/target/generated-sources/generated-sources/sal/org/opendaylight/yang/gen/v1/http/netconfcentral/org/ns/skeleton/rev150524/内,总共有6个文件,每个文件内都有响应的解释,我下面简要介绍几个文件: SendMessageInputBuilder.java SkeletonBuilder.java Skeleton.java SendMessageInput.java SkeletonData.java SkeletonService.java

SendMessageInput.java是定义的rpc的输入接口; SendMessageInputBuilder.java是对SendMessageInput.java的方法实现; SkeletonBuilder.java是我们定义的yang module的实例操作; SkeletonService.java是rpc里的方法,我们会在以后实现这个rpc;

如果生成没报错,继续往下进行。

cd distribution-karaf/target/assembly/bin
./karaf

因为我们目前在yang文件中定义的3个leaft,有一个是config true的,也就是config data,它不需要再写代码就可以配置,而operational data则需要额外写代码。 现在我们用curl来测试一下config数据的写入和读取,写入命令如下:(注意Skeleton的大小写!)

curl -H 'Content-Type: application/json' -X PUT -d '{"Skeleton": {"num": 10}}' --verbose -u admin:admin http://localhost:8181/restconf/config/skeleton:Skeleton

截图如下:

然后读取:

curl --verbose -u admin:admin http://localhost:8181/restconf/config/skeleton:Skeleton

截图如下:

分支branch2内容就此结束,以下这部分内容的完整代码在wyqbupt/ODL-skeleton 的branch3分支。

5. branch3分支

这部分我们实现yang文件内定义的两个配置型数据的操作。这里我们操作再次转到ODL-skeleton-impl内。

我们开始修改ODLSkeletonImpl.java,先加入3个类变量。

    private ProviderContext providerContext; //保存上下文关系
    private DataBroker dataService; //数据存取服务
    public static final InstanceIdentifier<Skeleton> Skeleton_ID = InstanceIdentifier.builder(Skeleton.class).build();//数据存取标志

这里我们定义下面两个变量,分别用来作为向datastore存储的内容,至于为什么在代码里进行配置,是operational不能像config data一样在外部写入,外部提供的API只能获取,当然如果你定义了RPC从外部写入Operational data当然也可以,但是RPC和默认API访问是两码事。我们用两个string 类型来存储TCP协议的名称和描述,描述来自http://en.wikipedia.org/wiki/Transmission_Control_Protocol。

private static final java.lang.String NAME = new java.lang.String("TCP"); private static final java.lang.String PROTOCOL = new java.lang.String("The Transmission Control Protocol (TCP) is a core protocol of the Internet Protocol Suite. It originated in the initial network implementation in which it complemented the Internet Protocol (IP). Therefore, the entire suite is commonly referred to as TCP/IP. TCP provides reliable, ordered, and error-checked delivery of a stream of octets between applications running on hosts communicating over an IP network. TCP is the protocol that major Internet applications such as the World Wide Web, email, remote administration and file transfer rely on. Applications that do not require reliable data stream service may use the User Datagram Protocol (UDP), which provides a connectionless datagram service that emphasizes reduced latency over reliability.");

下面我们需要定义两个函数initOperational和initConfiguration,分别初始化config data和operational data。 这里有一点需要注意,Karaf容器读取和安装我们的ODL-skeleton-impl时会执行onSessionInitiated函数,所以我们在onSessionInitiated函数内加入initOperational和initConfiguration。

代码如下:

public void onSessionInitiated(ProviderContext session) {
        this.providerContext = session;
        this.dataService = session.getSALService(DataBroker.class);
 
        // Initialize operational and default config data in MD-SAL data store
        initOperational();
        initConfiguration();
 
        LOG.info("onSessionInitiated: initialization done");
        LOG.info("ODLSkeletonImpl initialized!");
    }
private void initOperational() {
        // Build the initial test protocol operational data
        Skeleton protocol = new SkeletonBuilder()
                .setName( NAME )
                .setDescription( PROTOCOL )
                .build();
 
        // Put the protocol operational data into the MD-SAL data store
        WriteTransaction tx = dataService.newWriteOnlyTransaction();
        tx.put(LogicalDatastoreType.OPERATIONAL, Skeleton_ID, protocol);
 
        // Perform the tx.submit asynchronously
        Futures.addCallback(tx.submit(), new FutureCallback<Void>() {
            @Override
            public void onSuccess(final Void result) {
                LOG.info("initOperational: transaction succeeded");
            }
 
            @Override
            public void onFailure(final Throwable t) {
                LOG.error("initOperational: transaction failed");
            }
        });
 
        LOG.info("initOperational: operational status populated: {}", protocol);
    }
    private void initConfiguration() {
        // Build the default Multiprotocol config data
        Skeleton protocol = new SkeletonBuilder().setNum((long)1 ).build();
 
        // Place default config data in data store tree
        WriteTransaction tx = dataService.newWriteOnlyTransaction();
        tx.put(LogicalDatastoreType.CONFIGURATION, Skeleton_ID, protocol);
        // Perform the tx.submit synchronously
        tx.submit();
 
        LOG.info("initOperational: default config populated: {}", protocol);
    }

最后我们在ODL-skeleton-impl先执行mvn clean install查一下错。如果没有错我们就在ODL-skeleton根目录下执行。

mvn clean install

启动karaf;

用curl开始测试;

curl --verbose -u admin:admin http://localhost:8181/restconf/operational/skeleton:Skeleton

返回的json结果如下:

{"Skeleton":{"description":"The Transmission Control Protocol (TCP) is a core protocol of the Internet Protocol Suite. It originated in the initial network implementation in which it complemented the Internet Protocol (IP). Therefore, the entire suite is commonly referred to as TCP/IP. TCP provides reliable, ordered, and error-checked delivery of a stream of octets between applications running on hosts communicating over an IP network. TCP is the protocol that major Internet applications such as the World Wide Web, email, remote administration and file transfer rely on. Applications that do not require reliable data stream service may use the User Datagram Protocol (UDP), which provides a connectionless datagram service that emphasizes reduced latency over reliability.","name":"TCP"}}

截图如下:

curl --verbose -u admin:admin http://localhost:8181/restconf/config/skeleton:Skeleton

返回的json结果如下:

{"Skeleton":{"num":1}}

截图如下:

分支branch3内容就此结束,以下这部分内容的完整代码在wyqbupt/ODL-skeleton 的branch4分支。

6. branch4分支

这部分我们实现yang文件的rpc功能,我们当时定义的rpc为

rpc send-message {
       description "Sending message to datastroe.";
       input {
           leaf name {
                 type string;
           }
           leaf description {
                 type string;
           }
       }
   }

输入是两个字符串,把这两个字符串写入到datastore,这次我们的修改还是在ODLSkeletonImpl.java。加入这个类成员,定义一个SkeletonService的rpc。

private BindingAwareBroker.RpcRegistration<SkeletonService> rpcReg;

这里我们引入了很多头文件,注意,这些头文件的顺序不能随便换!check-style插件会对这些包的引入进行检查,必须符合一定的规则,规则如下: (1)同组的包放在一起 (2)同组的包按照一定的顺序,一般是按照字典顺序 (3)不同组之间的包流一个空行

所以说我们摆放的时候必须按照字典序列,如果错了编译过不了的。我们在ODLSkeletonImpl实现SkeletonService接口:

 public Future<RpcResult<java.lang.Void>> sendMessage(SendMessageInput input) {
        LOG.info("send protocol: {}", input);
        Skeleton protocol = new SkeletonBuilder()
                .setName( input.getName() )
                .setDescription( input.getDescription() )
                .build(); //读取 SendMessageInput的信息构造成 Skeleton
 
// 写入 MD-SAL data store
        WriteTransaction tx = dataService.newWriteOnlyTransaction();
        tx.put(LogicalDatastoreType.OPERATIONAL, Skeleton_ID, protocol);
 
        // Perform the tx.submit asynchronously
        Futures.addCallback(tx.submit(), new FutureCallback<Void>() {
            @Override
            public void onSuccess(final Void result) {
                LOG.info("initOperational: transaction succeeded");
            }
 
            @Override
            public void onFailure(final Throwable t) {
                LOG.error("initOperational: transaction failed");
            }
        });
 
        return Futures.immediateFuture( RpcResultBuilder.<Void> success().build() );
}

写完以后进行编译

mvn clean install

启动karaf;

因为我们已经实现了一个rpc,可以从外部配置name和description两个数据,我们先从外部发post写入消息,curl命令如下:

curl -H 'Content-Type: application/json' -X POST -d '{"input" : {"Skeleton:name" : "LLDP","Skeleton:description":"The Link Layer Discovery Protocol (LLDP) is a vendor-neutral link layer protocol in the Internet Protocol Suite used by network devices for advertising their identity, capabilities, and neighbors on an IEEE 802 local area network, principally wired Ethernet.[1] The protocol is formally referred to by the IEEE as Station and Media Access Control Connectivity Discovery specified in standards document IEEE 802.1AB"}}' --verbose -u admin:admin http://localhost:8181/restconf/operations/skeleton:send-message

截图如下:

200代码数据传输成功。然后我们再获取一次数据,看数据是否已经存入datastore,curl命令如下:

curl --verbose -u admin:admin http://localhost:8181/restconf/operational/skeleton:Skeleton

获取到的json数据如下:

{"Skeleton":{"description":"The Link Layer Discovery Protocol (LLDP) is a vendor-neutral link layer protocol in the Internet Protocol Suite used by network devices for advertising their identity, capabilities, and neighbors on an IEEE 802 local area network, principally wired Ethernet.[1] The protocol is formally referred to by the IEEE as Station and Media Access Control Connectivity Discovery specified in standards document IEEE 802.1AB","name":"LLDP"}}

截图如下:

分支branch4内容就此结束。以上完成了本次教程的全部内容,后面我还在做加入openflowplugin模块来制作操作openflow消息的应用,等写完了再跟大家分享。1. 简介

本次我们从开始设计到最终完成一个应用的开发,主要设计datastore和RPC定义和实现。Opendaylight 开发使用了OSGi框架,OSGi框架的好处在于程序设计模块化,实现紧聚合和松耦合。

Apache Karaf 是一个OSGi的容器,它可以支持部署新的应用。在OSGi里面一个bundle可能会依赖于其他的bundle。这里可能会出现一种情况,假如bundle A依赖与bundle B,bundle B 依赖于 bundle C,那么如果我们想解析bundle A,则必须解析bundle C和bundle B。

基本的ODL框架图大家都了解(此图来自官方wiki):

这里我们需要一个ODL应用的开发,首先提供一个骨架代码,放在我的github上,下面是地址,请自行下载,地址如下:https://github.com/wyqbupt/ODL-skeleton/

下载命令:

1

git clone git@github.com:wyqbupt/ODL-skeleton.git

2. branch0分支

这是我照着官网的教程边做边改后自己总结出的一个框架,大体跟官网提供的turorial框架一致,非大型应用都可以参照着这么写,这个骨架的目录结构如下: ├── artifacts ├── distribution-karaf ├── features │ └── src │ └── main │ └── resources │ └── features.xml ├── parent ├── ODL-skeleton-api ├── ODL-skeleton-consumer ├── ODL-skeleton-impl └── ODL-skeleton-it 以下这部分内容的完整代码在wyqbupt/ODL-skeleton 的branch0分支中,里面每一个文件夹都是一个module,这边有个印象即可,我会细细讲的。

2.1 parent

首先打开应用根路径下的pom.xml:

123456

<parent><groupId>org.opendaylight.ODL-skeleton</groupId><artifactId>ODL-skeleton-parent</artifactId><version>0.0.1-SNAPSHOT</version><relativePath>parent</relativePath></parent>

这里定义了整个应用的根模块位置,也就是所谓的父模块。groupID和 artifactId大家都知道分别表示组织编号和个体编号。 version确定版本号,自己定义一个,OSGi解析的版本号默认是三位,这个具体请参考规范,一般初始版本写成0.0.1吧。 relativePath这个很重要,这是我们能够找到根模块的地方,我们打开parent目录可以看见,maven 解析这些子模块的时候会先去读取根模块的内容。

理解这一点很重要,因为我们在后面的pom.xml中又很多变量名,这些变量名就在根模块的pom.xml定义!如果在根模块找不到,则会报一些变量无法解析的错误。

下面是本模块的描述:

12345

<modelVersion>4.0.0</modelVersion><artifactId>ODL-skeleton-aggregator</artifactId><description>ODL-skeleton Project Archetype Top Level POM</description><packaging>pom</packaging><name>${project.artifactId}</name>

下面是模块的定义,我们可发现这些模块名都是那些目录名,所有需要最终组合到一起的模块都要写进去,最终组合安装的过程会把下面这些模块都扔进OSGi容器内。

123456789101112

<modules><!-- the ODL convention is to have parent and artifacts subdirectories --><module>parent</module><module>artifacts</module><!-- TODO: add new features here --><module>ODL-skeleton-impl</module><module>ODL-skeleton-api</module><module>ODL-skeleton-consumer</module><module>features</module><module>distribution-karaf</module></modules>

2.2 ODL-skeleton-api

这里定义了基本的api,还有yang文件的定义,后面我们写完yang文件后用yangtools去生成相应的java API和REST API。别问我为啥现在里面没有yang文件,还没写呢。

先看pom.xml文件

123456

<parent><groupId>org.opendaylight.ODL-skeleton</groupId><artifactId>ODL-skeleton-parent</artifactId><version>0.0.1-SNAPSHOT</version><relativePath>../parent</relativePath></parent>

这里重点是 relativePath,我们发现它也指向的是我们的应用根模块,这样就可以使用根模块的一些定义,具体什么定义,后面会讲。

2.3 ODL-skeleton-impl

它主要包括相应的实现代码和应用的配置型代码,这部分唯一需要注意的是它是对api模块的依赖,打开所对应的pom.xml:

12345678910

<dependencies><dependency><groupId>${project.groupId}</groupId><artifactId>ODL-skeleton-api</artifactId></dependency><dependency><groupId>org.osgi</groupId><artifactId>org.osgi.core</artifactId></dependency></dependencies>

所以我们在依赖性介绍上看到了ODL-skeleton-api,由于需要在里面利用 ODL-skeleton-api,我们加入了依赖关系,这也是加依赖关系的方法。

2.4 ODL-skeleton-consumer

因为在一个应用中,服务提供方是provider,接收方是comsumer。一个应用可以有provider或者consumer,或者两个都有。这个在应用中是可选的,暂时不做。

2.5 ODL-skeleton-it

这里面是最后应用整合安装的时候跑的测试性代码。

2.6 features

这是一个比较重要的module。对于Karaf容器来说,每一个应用都是一系列的feature,他们可以被安装进容器内。这个文件夹内定义了我们写的应用的feature和依赖关系。比如我们的应用都是链接在MD—SAL上,所以必须依赖MD-SAL。

先看pom.xml,这里我是参考了一下SDNhub上放的那个例子的feature的依赖关系,将那些org.apache.jasper之类的都放进去了。重点是前几个自己加的。

这是yangtools,这是利用yang来生成java代码的工具.

123456

<dependency><groupId>org.opendaylight.yangtools</groupId><artifactId>features-yangtools</artifactId><classifier>features</classifier><type>xml</type></dependency>

这是md-sal,基本所有的模块都接在上面,它充当一个连接各个应用的桥梁。

123456

<dependency><groupId>org.opendaylight.controller</groupId><artifactId>features-mdsal</artifactId><classifier>features</classifier><type>xml</type></dependency>

这是restapi配置时会用的:

123456

<dependency><groupId>org.opendaylight.controller</groupId><artifactId>features-restconf</artifactId><classifier>features</classifier><type>xml</type></dependency>

这是openflowplugin,实现openflow功能的一个plugin,是opendaylight里单独的一个项目,交换机发来的包先到openflowjava,再到openflowplugin,最后进入上层监听各类openflow消息的应用,如learning switch。

1234567

<dependency><groupId>org.opendaylight.openflowplugin</groupId><artifactId>features-openflowplugin</artifactId><version>${openflowplugin.version}</version><classifier>features</classifier><type>xml</type></dependency>

我们再来看feature内的src/main/resources/features.xml。

下面这些内容是我们依赖的feature,以标示,这里面大家发现了${yangtools.version}之类的变量是吧?这些就是我前面说的parent根模块的作用,在parent根模块的pom.xml文件中定义这些变量,maven会自动去解析替换,至于这些版本应该定义成多少,或者说ODL提供了那些版本,我们可以在http://nexus.opendaylight.org/content/repositories/opendaylight.snapshot/自行查找相应的项目存在的版本号。

这里需要注意的是,所有定义在这里的依赖feature,必须在feature的pom.xml文件中加入依赖!不然编译的时候过不去。

12345

<repository>mvn:org.opendaylight.yangtools/features-yangtools/${yangtools.version}/xml/features</repository><repository>mvn:org.opendaylight.controller/features-mdsal/${controller.mdsal.version}/xml/features</repository><repository>mvn:org.opendaylight.controller/features-nsf/${nsf.version}/xml/features</repository><repository>mvn:org.opendaylight.controller/features-restconf/${controller.restconf.version}/xml/features</repository><repository>mvn:org.opendaylight.openflowplugin/features-openflowplugin/${openflowplugin.version}/xml/features</repository>

剩下是我们之前讲的3个模块api,impl,consumer。这里就讲讲一个模块的feature的结构,每个feature都会依赖其他的feature,比如api依赖yangtools,那么我们就将odl-yangtools-common 和odl-yangtools-binding写入api内部包含的feature。

123456

<feature name='odl-ODL-skeleton-api' version='${project.version}' description='OpenDaylight :: ODL-skeleton :: API'><feature version='${yangtools.version}'>odl-yangtools-common</feature><feature version='${yangtools.version}'>odl-yangtools-binding</feature><!--<feature version='${restconf.version}'>odl-restconf</feature>--><bundle>mvn:org.opendaylight.ODL-skeleton/ODL-skeleton-api/${project.version}</bundle></feature>

以上是整个框架的讲解。请先在根目录下mvn clean install试一下能否成功编译生成,成功之后karaf的运行路径在distribution-karaf/target/assembly/bin,以后启动我们自己的karaf就在这里。

branch0分支内容就此结束,以下这部分内容的完整代码在wyqbupt/ODL-skeleton 的branch1分支。

3. branch1分支

第一步,我们需要讲我们的provider接入到MD-SAL,接触过opendaylight都知道有config subsystem,它提供了服务的周期管理,同时能够提供对JMX 和 NETCONF的接入性配置。config subsystem在MD-SAL之外而且被用来提供把应用接入MD-SAL的功能。

我们首先完成配置性关系的yang文件,后面会用yangtools来生成java代码,通过这部分可以在ODL启动的时候加载相应的服务。

123456789101112131415161718192021222324252627282930

module ODL-skeleton-impl-config { yang-version 1; namespace "urn:opendaylight:params:xml:ns:yang:ODL-skeleton:impl:config"; prefix "ODL-skeleton-impl-config"; import config { prefix config; revision-date 2013-04-05; } import opendaylight-md-sal-binding { prefix md-sal-binding; revision-date 2013-10-28;} description "Service definition for ODL-skeleton project"; revision "2014-12-10" { description "Initial revision"; } identity ODL-skeleton-impl-config { base config:module-type; config:java-name-prefix ODL-skeletonImpl; } augment "/config:modules/config:module/config:configuration" { case ODL-skeleton-impl-config { when "/config:modules/config:module/config:type = 'ODL-skeleton-impl-config'"; container binding-aware-broker { uses config:service-ref { refine type { mandatory true; config:required-identity md-sal-binding:binding-broker-osgi-registry; } } } } }}

yang是一种模型化语言,是IETF的一项标准,rfc号是rfc6020,相应yang的定义请google IETF rfc6020。这里面的identity在yang的模型定义语言中作为一个标示,用于作为我们应用的provider提供的服务的全局标识,唯一的标记我们连接config subsystem。

augment用来扩展一个现有的ODL配置,增加了一个节点来标识 ODL-skeleton-impl。我们其中依赖了一个MD-SAL的服务 binding-aware-broker,这个跟MD-SAL的数据存取的datastore有关,我后面会讲到如何去用。

通过上面的yang文件,我们可以引导控制器的config subsystem解释ODL-skeleton 的配置文件。

下一步写响应的配置文件ODL-skeleton-impl-config.xml,由于MD-SAL提供多种服务(每个服务其实就是一个接口),每个服务可能有多种实现,我们这个配置文件里就是需要来指定要用哪种服务的哪种实现。

1234567891011121314151617181920212223

<?xml version="1.0" encoding="UTF-8"?><!-- vi: set et smarttab sw=4 tabstop=4: --><snapshot><required-capabilities><capability>urn:opendaylight:params:xml:ns:yang:ODL-skeleton:impl:config?module=ODL-skeleton-impl-config&revision=2014-12-10</capability><capability>urn:opendaylight:params:xml:ns:yang:controller:md:sal:binding?module=opendaylight-md-sal-binding&revision=2013-10-28</capability></required-capabilities><configuration><data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config"><module><type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:ODL-skeleton:impl:config">prefix:ODL-skeleton-impl-config</type><name>ODL-skeleton-impl</name><binding-aware-broker><type xmlns:binding="urn:opendaylight:params:xml:ns:yang:controller:md:sal:binding">binding:binding-broker-osgi-registry</type><name>binding-osgi-broker</name></binding-aware-broker></module></modules></data></configuration></snapshot>

这里我们指定使用了inding-aware-broker service的binding-osgi-broker实现来满足需求。这里我只加入了这一个配置型服务,当然你可以加入其他的,方法就是在标签内加,然后我们在impl模块的pom.xml写入代码生成器。

123456789101112131415161718192021222324252627282930313233343536

<plugin><groupId>org.opendaylight.yangtools</groupId><artifactId>yang-maven-plugin</artifactId><!-- TODO: I grabbed this from Ed's sample proj --><dependencies><dependency><groupId>org.opendaylight.controller</groupId><artifactId>yang-jmx-generator-plugin</artifactId><version>${controller.config.version}</version></dependency></dependencies><executions><execution><id>config</id><goals><goal>generate-sources</goal></goals><configuration><generator>org.opendaylight.controller.config.yangjmxgenerator.plugin.JMXGenerator<outputBaseDir>${jmxGeneratorPath}</outputBaseDir><additionalConfiguration><namespaceToPackage1>urn:opendaylight:params:xml:ns:yang:controller==org.opendaylight.controller.config.yang</namespaceToPackage1></additionalConfiguration></generator><generator>org.opendaylight.yangtools.maven.sal.api.gen.plugin.CodeGeneratorImpl<outputBaseDir>${salGeneratorPath}</outputBaseDir></generator><inspectDependencies>true</inspectDependencies></configuration></execution></executions></plugin>

Yantools提供了2个code生成器,一般的生成器CodeGeneratorImpl生成产生正常API和数据传输对象的(DTO),它主要的作用是将我们定义的非配置性yang,产生相应的java文件;另一个生成器JmxGenerator产生配置性的代码,也就是对我们上面写的那个配置性yang文件作用。

改完ODl-skeleton-impl的pom.xml(具体参考的代码请看分支branch1内的代码),现在我们在ODL-skeleton-impl运行:

1

mvn clean generate-sources

这时我们会发现两个文件被建立了,ODLSkeletonImplModule.java这个java文件中的createInstance()创建一个ODl-skeleton-impl的实例,由这个实例来操作本应用的各个服务;ODLSkeletonImplModuleFactory.java它用来创建ODLSkeletonImplModule的实例,我们目前不用修改此文件。 下面我们修改一下ODLSkeletonImplModule的createInstance()方法

123

ODLSkeletonImpl provider = new ODLSkeletonImpl();getBindingAwareBrokerDependency().registerProvider(provider, null);return provider;

这里创建了一个ODLSkeletonImpl 实例,BindingAwareBrokerDependency是MD-SAL已经创建的一个服务,用来向MD-SAL注册我们的provider。

上面的代码里我们利用了一个类 ODLSkeletonImpl,但是我们并没有实现它这个类,需要自己定义 在ODL-skeleton/ODL-skeleton-impl/src/main/java/org/opendaylight下创建目录:

12

mkdir -p odl/skeletoncd odl/skeleton

然后创建文件ODLSkeletonImpl.java,加入如下内容:

12345678910111213141516171819

package org.opendaylight.odl.skeleton;import org.opendaylight.controller.sal.binding.api.BindingAwareBroker.ProviderContext;import org.opendaylight.controller.sal.binding.api.BindingAwareProvider;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class ODLSkeletonImpl implements BindingAwareProvider, AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(ODLSkeletonImpl.class); @Override public void close() throws Exception { // TODO Auto-generated method stub } @Override public void onSessionInitiated(ProviderContext arg0) { LOG.info("ODLSkeletonImpl initialized!"); }}

加入完之后我们需要在刚才的ODLSkeletonImplModule中加入这个文件:

1

import org.opendaylight.odl.skeleton.ODLSkeletonImpl;

然后在应用根目录下:

1

mvn clean install

这时可以生成karaf的文件:

1

cd distribution-karaf/target/assembly/bin

打开karaf后执行:

1

feature:list

可以看到我们写的应用的feature:

branch1分支内容就此结束,以下这部分内容的完整代码在wyqbupt/ODL-skeleton 的branch2分支。

文章未完,详情请阅读原文。


想与其他SDN爱好者交流?点击图片并长按,再选择“识别二维码”,加入SDNLAB的SDN讨论群。