開發第一個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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章