在前面介绍学习了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仓库中获得,并自己编译
- user@host:$ git clone https://git.opendaylight.org/gerrit/p/controller.git
- user@host:$ cd ./controller/opendaylight/distribution/opendaylight/
- 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来代表该组件的根目录。
- myctrlapp
- |--src
- |--main
- |--java
- |--de
- |--frank_durr
- |--myctrlapp
显然,Java实现在src/main/java中,我们使用de.frank_durr.myctrlapp来实现我们的控制组件。
Maven中有个重要文件叫POM(project object model)我们在~/myctrlapp文件夹中创建pom.xml文件。内容如下:这里建议参考源教程,拷贝过来之后缩进有些问题。
[html] view plain copy
- <?xml version="1.0" encoding="UTF-8"?>
- <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">
- <modelVersion>4.0.0</modelVersion>
- <groupId>de.frank_durr</groupId>
- <artifactId>myctrlapp</artifactId>
- <version>0.1</version>
- <packaging>bundle</packaging>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.felix</groupId>
- <artifactId>maven-bundle-plugin</artifactId>
- <version>2.3.7</version>
- <extensions>true</extensions>
- <configuration>
- <instructions>
- <Import-Package>
- *
- </Import-Package>
- <Export-Package>
- de.frank_durr.myctrlapp
- </Export-Package>
- <Bundle-Activator>
- de.frank_durr.myctrlapp.Activator
- </Bundle-Activator>
- </instructions>
- <manifestLocation>${project.basedir}/META-INF</manifestLocation>
- </configuration>
- </plugin>
- </plugins>
- </build>
- <dependencies>
- <dependency>
- <groupId>org.opendaylight.controller</groupId>
- <artifactId>sal</artifactId>
- <version>0.7.0</version>
- </dependency>
- </dependencies>
- <repositories>
- <!-- OpenDaylight releases -->
- <repository>
- <id>opendaylight-mirror</id>
- <name>opendaylight-mirror</name>
- <url>http://nexus.opendaylight.org/content/groups/public/</url>
- <snapshots>
- <enabled>false</enabled>
- </snapshots>
- <releases>
- <enabled>true</enabled>
- <updatePolicy>never</updatePolicy>
- </releases>
- </repository>
- <!-- OpenDaylight snapshots -->
- <repository>
- <id>opendaylight-snapshot</id>
- <name>opendaylight-snapshot</name>
- <url>http://nexus.opendaylight.org/content/repositories/opendaylight.snapshot/</url>
- <snapshots>
- <enabled>true</enabled>
- </snapshots>
- <releases>
- <enabled>false</enabled>
- </releases>
- </repository>
- </repositories>
- </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工程
执行一下命令:
当你改动了pom文件的时候,一定要重新执行以上指令。接下来,你可以将工程导入到Eclipse当中了。
- Menu Import / General / Existing projects into workspace
- 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
- package de.frank_durr.myctrlapp;
- import java.util.Dictionary;
- import java.util.Hashtable;
- import org.apache.felix.dm.*;
- import org.opendaylight.controller.sal.core.ComponentActivatorAbstractBase;
- import org.opendaylight.controller.sal.packet.*;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- public class Activator extends ComponentActivatorAbstractBase{
- private static final Logger log = LoggerFactory.getLogger(PacketHandler.class);
- public Object[] getImplementations(){
- log.trace("getting implementations");
- Object[] res ={PacketHandler.class};
- return res;
- }
- public void configureInstance(Component c,Object imp,String containerName){
- log.trace("Configuring instance");
- if(imp.equals(PacketHandler.class)){
- Dictionary<String,Object> props=new Hashtable<String,Object>();
- props.put("salListenerName","mypackethandler");
- c.setInterface(new String[] {IListenDataPacket.class.getName()},props);
- c.add(createContainerServiceDependency(containerName).setService(IDataPacketService.class).setCallbacks("setDataPacketService","unsetDataPacketService").setRequired(true));
- }
- }
- }
我们扩展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
- <pre name="code" class="java">package de.frank_durr.myctrlapp;
- import java.net.InetAddress;
- import java.net.UnknownHostException;
- import org.opendaylight.controller.sal.core.Node;
- import org.opendaylight.controller.sal.core.NodeConnector;
- import org.opendaylight.controller.sal.packet.Ethernet;
- import org.opendaylight.controller.sal.packet.IDataPacketService;
- import org.opendaylight.controller.sal.packet.IListenDataPacket;
- import org.opendaylight.controller.sal.packet.IPv4;
- import org.opendaylight.controller.sal.packet.Packet;
- import org.opendaylight.controller.sal.packet.PacketResult;
- import org.opendaylight.controller.sal.packet.RawPacket;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- public class PacketHandler implements IListenDataPacket {
- private static final Logger log = LoggerFactory.getLogger(PacketHandler.class);
- private IDataPacketService dataPacketService;
- static private InetAddress intToInetAddress(int i) {
- byte b[] = new byte[] { (byte) ((i>>24)&0xff), (byte) ((i>>16)&0xff), (byte) ((i>>8)&0xff), (byte) (i&0xff) };
- InetAddress addr;
- try {
- addr = InetAddress.getByAddress(b);
- } catch (UnknownHostException e) {
- return null;
- }
- return addr;
- }
- /*
- * Sets a reference to the requested DataPacketService
- * See Activator.configureInstance(...):
- * c.add(createContainerServiceDependency(containerName).setService(
- * IDataPacketService.class).setCallbacks(
- * "setDataPacketService", "unsetDataPacketService")
- * .setRequired(true));
- */
- void setDataPacketService(IDataPacketService s) {
- log.trace("Set DataPacketService.");
- dataPacketService = s;
- }
- /*
- * Unsets DataPacketService
- * See Activator.configureInstance(...):
- * c.add(createContainerServiceDependency(containerName).setService(
- * IDataPacketService.class).setCallbacks(
- * "setDataPacketService", "unsetDataPacketService")
- * .setRequired(true));
- */
- void unsetDataPacketService(IDataPacketService s) {
- log.trace("Removed DataPacketService.");
- if (dataPacketService == s) {
- dataPacketService = null;
- }
- }
- @Override
- public PacketResult receiveDataPacket(RawPacket inPkt) {
- //System.out.println("Received data packet.");
- // The connector, the packet came from ("port")
- NodeConnector ingressConnector = inPkt.getIncomingNodeConnector();
- // The node that received the packet ("switch")
- Node node = ingressConnector.getNode();
- // Use DataPacketService to decode the packet.
- Packet l2pkt = dataPacketService.decodeDataPacket(inPkt);
- if (l2pkt instanceof Ethernet) {
- Object l3Pkt = l2pkt.getPayload();
- if (l3Pkt instanceof IPv4) {
- IPv4 ipv4Pkt = (IPv4) l3Pkt;
- int dstAddr = ipv4Pkt.getDestinationAddress();
- InetAddress addr = intToInetAddress(dstAddr);
- System.out.println("Pkt. to " + addr.toString() + " received by node " + node.getNodeIDString() + " on connector " + ingressConnector.getNodeConnectorIDString());
- return PacketResult.KEEP_PROCESSING;
- }
- }
- // We did not process the packet -> let someone else do the job.
- return PacketResult.IGNORED;
- }
- }
我们可以看到,我们的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
- user@host:$ cd ~/controller/opendaylight/distribution/opendaylight/target/distribution.opendaylight-osgipackage/opendaylight/
- user@host:$ ./runs.sh
然后install我们的bundle
[html] view plaincopy
- osgi> install file:/home/user/myctrlapp/target/myctrlapp-0.1.jar
- Bundle id is 256
可以看到我们的编号是256
现在我们启动budle
[html] view plaincopy
- osgi> start 256
你可以检查一下现在正在运行的OSGIbundle。使用命令:
[html] view plaincopy
- osgi> ss
类似的你可以stop和uninstall这个bundle使用如下命令:
[html] view plaincopy
- osgi> stop 256
- osgi> uninstall 256
在我们开始测试这个bundle之前,我们stopOpendaylight的两个服务。Simple Forwarding Service和LoadBalancing Service:
[html] view plaincopy
- osgi> <span style="color:#CC0000;">ss | grep simple</span>
- 171 ACTIVE org.opendaylight.controller.samples.simpleforwarding_0.4.1.SNAPSHOT
- true
- osgi> stop 171
- osgi> <span style="color:#FF0000;">osgi> ss | grep loadbalance</span>r
- 150 ACTIVE org.opendaylight.controller.samples.loadbalancer.northbound_0.4.1.SNAPSHOT
- 187 ACTIVE org.opendaylight.controller.samples.loadbalancer_0.5.1.SNAPSHOT
- true
- osgi> stop 187
为什么要这么做呢,因为这两项服务也实现了packet listenner,为了测试,我们必须要确保这两个服务没有把packet consume掉,否则的话我们就不能取得数据包。
Testing
我们使用简单的linear Mininet 拓扑只有两个交换机,两个host。
[html] view plaincopy
- user@host:$ sudo mn --controller=remote,ip=129.69.210.89 --topo linear,2
这个ip 填控制器的ip
现在我们用host1 ping host2 然后看osgi控制台的输出。
[html] view plaincopy
- mininet> h1 ping h2
- osgi>
- Pkt. to /10.0.0.2 received by node 00:00:00:00:00:00:00:01 on connector 1
- 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就完成了!!
来源:oschina
链接:https://my.oschina.net/u/2245781/blog/1834753