开发第一个OpenDaylight中的OSGI bundle

http://www.frank-durr.de/?p=84

学弟推荐的一个非常详细的opendaylight开发入门教程,这里尽力翻译一下,为了自己更好的理解。


    在这个教程里,我会详细解释如何为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控制器来增加或修改控制逻辑

opendaylight-osgi


   

我们可以看到,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文件。内容如下:这里建议参考源教程,拷贝过来之后缩进有些问题。

  <pre name="code" class="html"><?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工程

执行一下命令:

    user@host:$ cd ~/myctrlapp
    user@host:$ mvn eclipse: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文件。(代码参见原教程)

  <pre name="code" class="java">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.

代码如下:

<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来编译打包:

  1. user@host:$ cd ~/myctrlapp
  2. user@host:$ mvn package
如果我们的POM文件和代码都是正确的话,以上指令就会创建正确的JAR文件:~/myctrlapp/target/myctrlapp-0.1.jar

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

    user@host:$ cd ~/controller/opendaylight/distribution/opendaylight/target/distribution.opendaylight-osgipackage/opendaylight/
    user@host:$ ./runs.sh
然后install我们的bundle

    osgi> install file:/home/user/myctrlapp/target/myctrlapp-0.1.jar
    Bundle id is 256
可以看到我们的编号是256

现在我们启动budle

osgi> start 256

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

osgi> ss

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

    osgi> stop 256
    osgi> uninstall 256

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


    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。

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

这个ip 填控制器的ip

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

    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就完成了!!

发布了68 篇原创文章 · 获赞 6 · 访问量 2万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章