开发OpenDaylight组件的完整流程

风格不统一 提交于 2020-03-02 18:23:21

在前面介绍学习了OpenDaylight的几个重要模块后,这里再来介绍下完整开发一个模块的过程。

 

OSGI的bundles提供被其他OSGI组件调用的服务。这个教程中展示的是Data Packet Service去解析数据包(其接口为 IDataPacketService)。

这个小组件不能提供函数供其他bundles调用,但是可以让我们很好的理解为了接收到一个
Packet-in消息事件,必须要实现 IListenDataPacket 接口。当Packet-in消息到达控制器,SAL模块就会通知实现这些接口的组件。

详细过程请见   http://www.frank-durr.de/?p=84   实验室的婷婷学姐翻译了此篇文章,就直接贴过来了,供大家学习

译文

在这个教程里,我会详细解释如何为opendaylight开发一个OSGI组件来实现常规的网络控制逻辑。与REST 接口不同,当一个packet到达并在交换设备流表中失配的时候,将会触发一个packet-in事件,OSGI组件接收packet-in事件。因此,为了实现流响应式编程,OSGI组件是研究OpenDaylight一个很好的切入口。

 

即使是对于有经验的java程序员,开发OSGI组件的学习曲线也是相当陡峭的,OpenDaylight使用十分强大的开发工具,例如Maven和OSGI、这些程序架构都十分的复杂,众多的java类也是一个巨大的挑战。然而,正如你将在本教程里看到的这样,由于maven提供的帮助,开发过程是很简单明了的。

    我将会一步一步展示一个简单的OSGI组件的开发过程。这个组件并不做什么贡献,只是简单的呈现一个收到的ipv4数据包的目的地址。数据路径id(data path id),以及ingress port。然而,你可以学习到许多能够帮助你今后开发自己的控制组件的知识,例如

  • 如何搭建一个opendaylight maven 工程?
  • 如何 在opendaylight运行时刻安装(install),卸载(uninstall),开启(start),结束(stop)一个OSGI bundle?
  • 如何管理OSGI组件依赖和生命周期?
  • 如何通过data packet listenners接收packet-in事件?
  • 如何使用Opendaylight Data Packet Service解析数据包?

这里应该提一下,我使用opendaylight的API驱动的服务抽象层(API-Driven Service Abstraction Layer SAL),OpenDaylight 还实现了一个模型驱动的SAL。在后面的教程中将会涉及。

 

the big picture

 

    下图展示了我们的系统架构。它由一系列将java类,资源,配置文件捆绑在一起的bundles组成。其中,MyControlApp是我们在这个教程中将要开发的。其他的bundles来自OpenDaylight工程,例如SAL bundles。

    bundles在OSGI框架中运行(Opendaylight中的Equinox)。OSGI最有趣的地方在于bundles可以在运行时安装和卸载,因而我们无需停下SDN控制器来增加或修改控制逻辑

 

我们可以看到,OSGI bundles可以提供可供其他OSGI组件调用的服务。其中我们要使用的是Data Packet Service(interface IDataPacketService)来解析数据包。

    尽管我们的简单控制组件并没有给其他bundle提供任何功能,但是我们必须知道,假如想要收到packet-in事件,我们必须实现IListenDataPacket接口。到一个OpenFlow packet-in消息到达控制器的时候,SAL将会激活实现了IListenDataPacket接口的组件。当然,其中就有我们的bundle。

   

Prerequisites

 

    在我们开始开发我们的组件之前,我们应该拿到opendaylight可运行版本。http://www.opendaylight.org/software/downloads可以从以下地址中获得,或者你也可以从opendaylight GIT仓库中获得,并自己编译

 

  1. user@host:$ git clone https://git.opendaylight.org/gerrit/p/controller.git  
  2. user@host:$ cd ./controller/opendaylight/distribution/opendaylight/  
  3. user@host:$ mvn clean install 

其实要开发一个opendaylight OSGI组件,你并不需要拿到opendaylight源码。我们下面将会介绍,我们只需要将要用到的组件从opendaylight仓库中以Jar包的形式导入就可以了。

 

 

    在编译的过程中,你将会看到Maven下载很多java packages。如果你从来没有用过Maven,可能会对此感到困惑,我们刚才难道没有用Git下载整个工程吗?实际上,Maven可以从远程仓库自动的下载工程依赖项(库,plugins)并且将它们放进你的本地仓库,通常会位于~/.m2。如果你在编译完opendaylight之后查看这个仓库,你将会看到所有Maven下载下来的库文件。

例如,你可能会看到Maven下载了Apache Xerces XML解析器,我们将会在讨论工程依赖的时候解释这些依赖项。

 

 

Creating the Maven Project

 

现在,我们开始开发我们的OSGI 组件。因为Opendaylight 是基于Maven的。因此我们最好也使用Maven 来开发自己的工程。所以我们首先为我们的OSGI组件创建一个Maven工程。首先我们创建如下目录结构,我们用~/myctrlapp来代表该组件的根目录。

  1. myctrlapp  
  2.     |--src  
  3.           |--main  
  4.                    |--java  
  5.                              |--de  
  6.                                   |--frank_durr  
  7.                                              |--myctrlapp  


    显然,Java实现在src/main/java中,我们使用de.frank_durr.myctrlapp来实现我们的控制组件。

 

    Maven中有个重要文件叫POM(project object model)我们在~/myctrlapp文件夹中创建pom.xml文件。内容如下:这里建议参考源教程,拷贝过来之后缩进有些问题。

[html] view plain copy

  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">  
  3.    
  4.     <modelVersion>4.0.0</modelVersion>  
  5.    
  6.     <groupId>de.frank_durr</groupId>  
  7.     <artifactId>myctrlapp</artifactId>  
  8.     <version>0.1</version>  
  9.     <packaging>bundle</packaging>  
  10.    
  11.     <build>  
  12.         <plugins>  
  13.             <plugin>  
  14.                 <groupId>org.apache.felix</groupId>  
  15.                 <artifactId>maven-bundle-plugin</artifactId>  
  16.                 <version>2.3.7</version>  
  17.                 <extensions>true</extensions>  
  18.                 <configuration>  
  19.                     <instructions>  
  20.                         <Import-Package>  
  21.                             *  
  22.                         </Import-Package>  
  23.                         <Export-Package>  
  24.                             de.frank_durr.myctrlapp  
  25.                         </Export-Package>  
  26.                         <Bundle-Activator>  
  27.                             de.frank_durr.myctrlapp.Activator  
  28.                         </Bundle-Activator>  
  29.                     </instructions>  
  30.                     <manifestLocation>${project.basedir}/META-INF</manifestLocation>  
  31.                 </configuration>  
  32.             </plugin>  
  33.         </plugins>  
  34.     </build>  
  35.    
  36.     <dependencies>  
  37.         <dependency>  
  38.             <groupId>org.opendaylight.controller</groupId>  
  39.             <artifactId>sal</artifactId>  
  40.             <version>0.7.0</version>  
  41.         </dependency>  
  42.     </dependencies>  
  43.    
  44.     <repositories>  
  45.         <!-- OpenDaylight releases -->  
  46.         <repository>  
  47.             <id>opendaylight-mirror</id>  
  48.             <name>opendaylight-mirror</name>  
  49.             <url>http://nexus.opendaylight.org/content/groups/public/</url>  
  50.             <snapshots>  
  51.                 <enabled>false</enabled>  
  52.             </snapshots>  
  53.             <releases>  
  54.                 <enabled>true</enabled>  
  55.                 <updatePolicy>never</updatePolicy>  
  56.             </releases>  
  57.         </repository>  
  58.         <!-- OpenDaylight snapshots -->  
  59.         <repository>  
  60.             <id>opendaylight-snapshot</id>  
  61.             <name>opendaylight-snapshot</name>  
  62.             <url>http://nexus.opendaylight.org/content/repositories/opendaylight.snapshot/</url>  
  63.             <snapshots>  
  64.                 <enabled>true</enabled>  
  65.             </snapshots>  
  66.             <releases>  
  67.                 <enabled>false</enabled>  
  68.             </releases>  
  69.         </repository>  
  70.     </repositories>  
  71. </project>  

 

首先,我们定义自己的group id(我们组织的唯一id),以及artifact id(我们工程的名字)以及一个 版本号version number。

    在Maven创建的过程中,plugins被激活。其中一个Apache Felix 非常重要,它创建了我们的OSGI工程,它指明了每一个需要导入的包,通配符*导入了每一个在bundle中不存在,但是在bundle中有引用过的bundle。这比指定每一个imports更加合理,简便。另外,我们导出我们package中的所有实现。

    bundle activator在bundle生命周期中的每一次 start与stop时调用。下面,我会介绍如何使用activator来注册我们的服务,如何向外提供我们组件的接口。

    dependency元素指明了我们的组件的依赖项。还记得我说过Maven会自动下载所有需要的库(jars)到你的本地仓库吗~/m2?当然,只有你告诉Maven你需要什么,它才会这么做。我们只需要opendaylight的SAL。opendaylight 工程给我们的仓库提供了一个已经编译好的组件,因此,Maven会从远程仓库下载JARs。所以,我们并不需要将所有的opendaylight工程源码导入到Eclipse!~在我的例子中,我使用了0.7.0版本,你也可以将它改成0.7.0-SNAPSHOT来使用快照版本。

    从POM文件中,我们可以创建一个Eclipse工程

执行一下命令:

  1. user@host:$ cd ~/myctrlapp  
  2. user@host:$ mvn eclipse:eclipse 

当你改动了pom文件的时候,一定要重新执行以上指令。接下来,你可以将工程导入到Eclipse当中了。

 

  1. Menu Import / General / Existing projects into workspace  
  2. Select root folder ~/myctrlapp  

Implementation of OSGi Component: The Activator

要实现我们的OSGI组件,我们还需要两个类文件:一个OSGI activator,来向OSGI框架注册我们的组件,一个packet handler来实现控制逻辑以及当packet-in事件来到的时候执行相应的动作。

 

 

 

首先我们在~/myctrlapp/src/main/java/frank_durr/myctrlapp文件夹下创建Activator.java文件。(代码参见原教程)

[java] view plain copy

  1. package de.frank_durr.myctrlapp;  
  2.   
  3. import java.util.Dictionary;  
  4. import java.util.Hashtable;  
  5.   
  6. import org.apache.felix.dm.*;  
  7. import org.opendaylight.controller.sal.core.ComponentActivatorAbstractBase;  
  8. import org.opendaylight.controller.sal.packet.*;  
  9. import org.slf4j.Logger;  
  10. import org.slf4j.LoggerFactory;  
  11.   
  12.   
  13.   
  14. public class Activator extends ComponentActivatorAbstractBase{  
  15.       
  16.     private static final Logger log = LoggerFactory.getLogger(PacketHandler.class);  
  17.     public Object[] getImplementations(){  
  18.         log.trace("getting implementations");  
  19.         Object[] res ={PacketHandler.class};  
  20.         return res;  
  21.     }  
  22.       
  23.     public void configureInstance(Component c,Object imp,String containerName){  
  24.         log.trace("Configuring instance");  
  25.         if(imp.equals(PacketHandler.class)){  
  26.             Dictionary<String,Object> props=new Hashtable<String,Object>();  
  27.             props.put("salListenerName","mypackethandler");  
  28.             c.setInterface(new String[] {IListenDataPacket.class.getName()},props);  
  29.             c.add(createContainerServiceDependency(containerName).setService(IDataPacketService.class).setCallbacks("setDataPacketService","unsetDataPacketService").setRequired(true));  
  30.               
  31.         }  
  32.     }  
  33. }  

 

 

我们扩展opendaylight controller中的基本类 ComponentActivatorAbstractBase。已经熟悉OSGI的开发者知道当budle启动或停止的时候,OSGI框架会相应调用两个方法start()和stop()。这两个方法在ComponentActivatorAbstractBase里面被重写以管理opendaylight组件的生命周期。getImplementations()和configureInstance()将在这过程中被调用。

 

    getImplementations()方法返回实现这个bundle组件的class。一个bundle可以实现超过1个组件,例如,一个bundle中可以包括一个ARP请求的packethandler组件以及一个IP数据包的packethandler组件。我们的bundle中只实现了一个组件,该组件仅仅响应packet-in事件,由我们的PacketHandler类来实现,所以我们只返回了一个实现。

    configureInstance()方法配置了组件,并且,声明了导出的服务接口以及使用到的服务。因为一个OSGIbundle可以实现多于一个组件,所以在26行这里最好检查一下应该配置哪个组件。

    接下来我们声明我们的组件导出的服务。回忆一下,为了接收packet-in事件,组件必须要实现IListenDataPacket接口。因而,在34行,
我们明确的将我们的组件注册为packet-in事件的监听者。另外,在31行,我们使用salListenerName属性为我们的listenner命名,如果你想要知道注册时的细节,建议你可以查看org.opendaylight.controller.sal.implementation.internal.DataPacketService类里面的setListenDataPacket()方法,你将会看到,packet handler将会相继的被调用,可能会有很多组件注册了packet-in事件。你不能期望opendaylight 在其他组件的listenner收到事件通知前先调用你的listenner。listenners被调用的顺序并没有被指定。但是你可以利用salListennerDependency属性来创造dependency list。另外,你可以使用属性salListennerFilter。你可以给listener设置一个org.opendaylight.controller.sal.match.Match对象来按照包首部过滤packets,否则的话你将会收到所有的packet(如果在我们的handler被调用前没有被其他的listenner消耗掉的话(consume))。

   在configureInstance()方法中 除了导出我们的packet listenner实现,我们也可以指定我们使用到的其他的服务,这些依赖在37行声明,在我们的例子当中,我们只用到了实现了IDataPacketService接口的服务。你可能会问:”我如何得到提供该服务的对象来调用服务呢?“  你可以定义两个回调函数作为你的组件(PacketHandler)类的一部分。这里称为setDataPacketService()和unsetDataPacketService()这两个函数在引用服务将被调用。(PacketHandler的实现在下面)

 

Implementation of OSGi Component: The Packet Handler

 

 

    我们实现的第二个部分就是packetHandler。它接收packet-in事件,(这个类你已经在activator里面配置过了)。我们在这个目录下创建PacketHandler.java文件:~/myctrlapp/src/main/java/de/frank_durr/myctrlapp.

代码如下:

 

[java] view plain copy

  1. <pre name="code" class="java">package de.frank_durr.myctrlapp;  
  2.    
  3. import java.net.InetAddress;  
  4. import java.net.UnknownHostException;  
  5.    
  6. import org.opendaylight.controller.sal.core.Node;  
  7. import org.opendaylight.controller.sal.core.NodeConnector;  
  8. import org.opendaylight.controller.sal.packet.Ethernet;  
  9. import org.opendaylight.controller.sal.packet.IDataPacketService;  
  10. import org.opendaylight.controller.sal.packet.IListenDataPacket;  
  11. import org.opendaylight.controller.sal.packet.IPv4;  
  12. import org.opendaylight.controller.sal.packet.Packet;  
  13. import org.opendaylight.controller.sal.packet.PacketResult;  
  14. import org.opendaylight.controller.sal.packet.RawPacket;  
  15. import org.slf4j.Logger;  
  16. import org.slf4j.LoggerFactory;  
  17.    
  18. public class PacketHandler implements IListenDataPacket {  
  19.    
  20.     private static final Logger log = LoggerFactory.getLogger(PacketHandler.class);  
  21.     private IDataPacketService dataPacketService;  
  22.    
  23.     static private InetAddress intToInetAddress(int i) {  
  24.         byte b[] = new byte[] { (byte) ((i>>24)&0xff), (byte) ((i>>16)&0xff), (byte) ((i>>8)&0xff), (byte) (i&0xff) };  
  25.         InetAddress addr;  
  26.         try {  
  27.             addr = InetAddress.getByAddress(b);  
  28.         } catch (UnknownHostException e) {  
  29.             return null;  
  30.         }  
  31.    
  32.         return addr;  
  33.     }  
  34.    
  35.     /* 
  36.      * Sets a reference to the requested DataPacketService 
  37.      * See Activator.configureInstance(...): 
  38.      * c.add(createContainerServiceDependency(containerName).setService( 
  39.      * IDataPacketService.class).setCallbacks( 
  40.      * "setDataPacketService", "unsetDataPacketService") 
  41.      * .setRequired(true)); 
  42.      */  
  43.     void setDataPacketService(IDataPacketService s) {  
  44.         log.trace("Set DataPacketService.");  
  45.    
  46.         dataPacketService = s;  
  47.     }  
  48.    
  49.     /* 
  50.      * Unsets DataPacketService 
  51.      * See Activator.configureInstance(...): 
  52.      * c.add(createContainerServiceDependency(containerName).setService( 
  53.      * IDataPacketService.class).setCallbacks( 
  54.      * "setDataPacketService", "unsetDataPacketService") 
  55.      * .setRequired(true)); 
  56.      */  
  57.     void unsetDataPacketService(IDataPacketService s) {  
  58.         log.trace("Removed DataPacketService.");  
  59.    
  60.         if (dataPacketService == s) {  
  61.             dataPacketService = null;  
  62.         }  
  63.     }  
  64.    
  65.     @Override  
  66.     public PacketResult receiveDataPacket(RawPacket inPkt) {  
  67.         //System.out.println("Received data packet.");  
  68.    
  69.         // The connector, the packet came from ("port")  
  70.         NodeConnector ingressConnector = inPkt.getIncomingNodeConnector();  
  71.         // The node that received the packet ("switch")  
  72.         Node node = ingressConnector.getNode();  
  73.    
  74.         // Use DataPacketService to decode the packet.  
  75.         Packet l2pkt = dataPacketService.decodeDataPacket(inPkt);  
  76.    
  77.         if (l2pkt instanceof Ethernet) {  
  78.             Object l3Pkt = l2pkt.getPayload();  
  79.             if (l3Pkt instanceof IPv4) {  
  80.                 IPv4 ipv4Pkt = (IPv4) l3Pkt;  
  81.                 int dstAddr = ipv4Pkt.getDestinationAddress();  
  82.                 InetAddress addr = intToInetAddress(dstAddr);  
  83.                 System.out.println("Pkt. to " + addr.toString() + " received by node " + node.getNodeIDString() + " on connector " + ingressConnector.getNodeConnectorIDString());  
  84.                 return PacketResult.KEEP_PROCESSING;  
  85.             }  
  86.         }  
  87.         // We did not process the packet -> let someone else do the job.  
  88.         return PacketResult.IGNORED;  
  89.     }  
  90. }  


我们可以看到,我们的handler实现了IListenDataPacket接口, 这个接口声明了recieveDataPacket()函数,该函数在packet-in事件到达的时候被自动调用,参数为 raw packet。

 

 

    要解析raw packet,我们需要使用OpenDaylight Data Packet Service.正如在Activator中描述的那样,在组件的配置过程中,我们在handler实现中设置了两个回调函数,setDataPacketService()以及unsetDataPacketService()。当我们需要data packet service的时候,setDataPacketservice将会被调用,用来解析raw packet。当我们收到raw packet “inPkt”的时候。我们会调用dataPacketService.decodeDataPacket(inPkt)来获得一个L2数据帧。使用 instanceof 。我们可以检查这个数据包的类型。如果是以太网数据帧,我们获得这个数据帧中的payload(我的理解就是去掉二层的包头和尾,得到L3数据包)。再一次检查类型,如果是IPV4数据包,我们输出目的IP。

    另外,这个例子展示了如何获得收到数据包的node(交换机节点)以及connector(端口号)

    最后,我们决定这个数据包应该继续被后续handler处理或者消耗掉这个packet并返回一个相应的值。PacketResult.KEEP_PROCESSING说明我们的handler已经处理好了这个packet。接下来其他的handler也可以继续处理。PacketResult.CONSUME表示,在我们处理完之后其他的handler不能再处理了。PacketResult.IGNORED说明。packet 处理流水线应当继续进行,而且我们并没有处理这个数据包。

 

Deploying the OSGI Bundle

我们已经实现了我们的组件,可以使用maven来编译打包:

 

 

<ol class="linenums" style="color: rgb(85, 85, 85); font-family: monospace; font-size: 15px; line-height: 35px; white-space: pre-wrap;"><li class="L0"><span class="pln">user@host</span><span class="pun">:</span><span class="pln">$ cd </span><span class="pun">~/</span><span class="pln">myctrlapp</span></li><li class="L1"><span class="pln">user@host</span><span class="pun">:</span><span class="pln">$ mvn </span><span class="kwd">package</span></li></ol>

如果我们的POM文件和代码都是正确的话,以上指令就会创建正确的JAR文件:~/myctrlapp/target/myctrlapp-0.1.jar

 

这个bundle现在可以被安装到OSGI框架当中(opendaylight中Equinox)首先我们启动控制器。

 

[html] view plaincopy

  1. user@host:$ cd ~/controller/opendaylight/distribution/opendaylight/target/distribution.opendaylight-osgipackage/opendaylight/  
  2. user@host:$ ./runs.sh  

然后install我们的bundle

 

 

[html] view plaincopy

  1. osgi> install file:/home/user/myctrlapp/target/myctrlapp-0.1.jar  
  2. Bundle id is 256  

可以看到我们的编号是256

 

现在我们启动budle

 

[html] view plaincopy

  1. osgi> start 256  


你可以检查一下现在正在运行的OSGIbundle。使用命令:

 

 

[html] view plaincopy

  1. osgi> ss  


类似的你可以stop和uninstall这个bundle使用如下命令:

 

 

[html] view plaincopy

  1. osgi> stop 256  
  2. osgi> uninstall 256  


在我们开始测试这个bundle之前,我们stopOpendaylight的两个服务。Simple Forwarding Service和LoadBalancing Service:

 

 

 

[html] view plaincopy

  1. osgi> <span style="color:#CC0000;">ss | grep simple</span>  
  2. 171 ACTIVE org.opendaylight.controller.samples.simpleforwarding_0.4.1.SNAPSHOT  
  3. true  
  4. osgi> stop 171  
  5. osgi> <span style="color:#FF0000;">osgi> ss | grep loadbalance</span>r  
  6. 150 ACTIVE org.opendaylight.controller.samples.loadbalancer.northbound_0.4.1.SNAPSHOT  
  7. 187 ACTIVE org.opendaylight.controller.samples.loadbalancer_0.5.1.SNAPSHOT  
  8. true  
  9. osgi> stop 187  


    为什么要这么做呢,因为这两项服务也实现了packet listenner,为了测试,我们必须要确保这两个服务没有把packet consume掉,否则的话我们就不能取得数据包。

 

 

 

Testing

  我们使用简单的linear Mininet 拓扑只有两个交换机,两个host。

 

 

[html] view plaincopy

  1. user@host:$ sudo mn --controller=remote,ip=129.69.210.89 --topo linear,2  


这个ip 填控制器的ip

 

现在我们用host1 ping host2 然后看osgi控制台的输出。

 

[html] view plaincopy

  1. mininet> h1 ping h2  
  2.    
  3. osgi>  
  4. Pkt. to /10.0.0.2 received by node 00:00:00:00:00:00:00:01 on connector 1  
  5. Pkt. to /10.0.0.1 received by node 00:00:00:00:00:00:00:02 on connector 1  



我们可以看到我们的handler从两个交换机都接收到了数据包,data path id 为00:00:00:00:00:00:00:01和00:00:00:00:00:00:00:02以及,目的ip为10.0.0.2与10.0.0.1都是从port1接收到的。

 

 

 


至此!一个简单的OSGI bundle就完成了!!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!