八、OpenDaylight应用基础开发(ODL控制器初级开发流程总结)

1 开发环境介绍

1.系统环境:Ubuntu

2.软件环境:
jdk 1.8
maven 3.3.9
OpenDaylight
Mininet
Eclipse/IntelliJ IDEA

2 构建工程

2.1 生成项目骨架

根据OpenDaylight官方文档提供的Maven命令生成项目骨架

mvn archetype:generate -DarchetypeGroupId=org.opendaylight.controller -DarchetypeArtifactId=opendaylight-startup-archetype -DarchetypeRepository=http://nexus.opendaylight.org/content/repositories/opendaylight.release/ -DarchetypeCatalog=remote -DarchetypeVersion=1.5.1

构建成功后会在当前目录生成项目文件夹,进入到文件夹下即可看到工程目录结构
在这里插入图片描述

2.2 编译工程

对构建好的工程进行编译

mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true

2.3 启动控制器

启动控制器,具体启动如下图
在这里插入图片描述

2.4 导入项目到开发IDE中

3 实例开发

3.1 RPC和YANG模型

3.1.1 什么是RPC

RPC 全称 Remote Procedure Call——远程过程调用。在学校学编程,我们写一个函数都是在本地调用就行了。但是在互联网公司,服务都是部署在不同服务器上的分布式系统,如何调用呢?
RPC技术简单说就是为了解决远程调用服务的一种技术,使得调用者像调用本地服务一样方便透明。

OpenDaylight采用RPC这种通信方式主要是基于以下几点:

  1. 可以实现跨语言跨平台调用,不同业务系统都可以进行调用OpenDaylight对外提供的服务,使得OpenDaylight服务更具通用性;
  2. 每个调用方只需要提出需求,不需要了解OpenDaylight具体的代码逻辑实现;
  3. RPC易于拓展,易于复用。

3.1.2 什么是YANG模型

早在2003年,IETF成立了一个NETCONF工作组,提出一种基于XML的网络配置管理协议,也就是NETCONF(Network Configuration Protocol),因为该协议的配置功能非常强大,所以广泛采用来配置网络。

NETCONF协议分为传输层、RPC层、操作层和内容层。其中,内容层是唯一没有标准化的层,于是一种新的建模语言YANG产生了,它的目标是对NETCONF数据模型、操作进行建模,覆盖NETCONF协议的操作层和内容层

在这里插入图片描述
YANG模型的产生主要是对以下三类数据进行建模:

  1. 远程过程调用(rpc):对可在网元上触发的远程程序调用进行建模;
  2. 资源(data):对要操控的资源进行建模,也就是对网元设备的的configure或者operational数据进行建模;
  3. 通知(notification):对网元发出的事件通知消息进行建模。

OpenDaylight控制器目前主要采用MD-SAL架构,这里的M指的就是YANG 模型。在开发过程中,一般先根据需求先设计YANG模型来确定准备提供哪些数据和服务,然后使用Maven进行编译时,通过YANG Tools插件将YANG文件的转译为相应的java类、接口。开发者通过实现自动生成的Java代码来实现具体的API和服务内容。

由于YANG文件通俗易懂,我们完全可以仅仅通过YANG文件就可以大概了解到系统提供的数据和服务内容,十分的方便。同时,OpenDaylight访问Data Store的API完全基于Yang模型,这部分代码Yang Tools插件自动生成,这样开发人员只需关注如何进行提供服务,大大提高了开发效率和降低了OpenDaylight控制器的开发难度。

3.1 YANG模型中定义RPC

  1. 编辑api/src/main/yang目录下的topology.yang,进行yang文件编写,完整的topology.yang文件如下:

    module topology {
     
    yang-version 1;
     
    namespace "urn:opendaylight:params:xml:ns:yang:topology";
     
    prefix "topology";
     
    import ietf-inet-types { prefix inet; revision-date 2013-07-15; }
     
    description "get links and ports information";
     
    revision "2018-03-07" {
     
        description "Initial revision of topology model";
     
    }
     
    grouping output-link{
     
        leaf link-id{
     
            type inet:uri;
     
            description "link id";
     
        }
     
        leaf src-device{
     
            type inet:uri;
     
            description "source device";
     
        }
     
        leaf src-port{
     
            type int32;
     
            description "source port";
     
        }
     
        leaf dst-device{
     
            type inet:uri;
     
             description "destination device";
     
        }
     
        leaf dst-port{
     
            type int32;
     
            description "destination port";
     
        }
     
    }
     
    grouping output-port{
     
        leaf device-id{
     
            type inet:uri;
     
            description "link id";
     
        }
     
        leaf port-number{
     
            type string;
     
            description "port number";
     
        }
     
        leaf port-name{
     
            type string;
     
            description "port name";
     
        }
     
        leaf hardware-address{
     
            type string;
     
            description "hardware address";
     
        }
     
        leaf current-speed{
     
            type int64;
     
            description "current speed";
     
        }
     
        leaf maximum-speed{
     
            type int64;
     
            description "maximum speed";
     
        }
     
        leaf link-down{
     
            type boolean;
     
            description "link down";
     
        }
     
    }
     
    rpc list-links-info{
     
        output {
     
            list links-info{
     
                uses output-link;
     
                description "link info";
     
            }
     
        }
     
    }
     
    rpc list-ports-info{
     
        output{
     
            list ports-info{
     
                uses output-port;
     
                description "port info";
     
            }
     
        }
     
    }
     
    }
    

    解析:
    ①module:在实际项目中,一个工程中YANG模型很多,所以可以通过module来划分不同模块的YANG。各个module描述各自的数据模型,不同module之间可以互相引用,十分灵活。

    ②namespace和prefix :每个module有一个独立的namespace以避免命名冲突,同时还有一个简称prefix。

    ③import:通过import导入外部模块,可以在本模块中使用导入模块中定义的数据结构或数据类型。导入时会给外部模块起一个prefix,引用导入模块中的内容时以prefix开头即可。

    ④description:描述YANG文件的功能或者各个节点的功能。

    ⑤revision:版本信息。

    ⑥grouping:一种可重用的schema nodes的集合。该集合可能在定义处的module中被使用,也可能在包含它的modules中使用,还可能在导入它的modules中被使用。grouping不会生成任何节点,可以看成一种临时树干。

    ⑦leaf:一种数据节点,在data tree中最多只能有一个实例存在。一个leaf节点只能有一个值,并且不能有子节点。leaf主要用来定义属性值。

    ⑧type:指定leaf的数据类型,该类型可以是YANG RFC规定的基本类型,也可以是自定义的派生类型。在YANG文件中,int32为基本类型,inet:uri为派生类型,派生类型的定义由typedef定义。基本的原生数据类型有uint8、uint16、string、bits、binary、boolean等等。

    ⑨rpc:远程过程调用。它可能包含input和output子节点,分别是该rpc所需要的输入和输出数据结构。若没有则表明该操作不需要输入数据或者没有输出数据。

    ⑩uses:实例化在grouping声明中定义的schema nodes。

    更为详细的YANG语法知识可以参考rfc 6020(https://tools.ietf.org/html/rfc6020)

    需要注意的是,由于在YANG文件中引入了ietf-inet-types,需要在api目录下的pom.xml中添加相关依赖:

    <dependencies>
     
        <dependency>
     
            <groupId>org.opendaylight.mdsal.model</groupId>
     
            <artifactId>ietf-inet-types-2013-07-15</artifactId>
     
            <version>1.2.2-Carbon</version>
     
        </dependency>
     
    </dependencies>
    

    Tip:dependencies标签如果存在则直接添加dependency这一层标签即可。

  2. 定义好YANG文件,需要重新编译工程,编译时无需整体编译,只需进入到工程主目录下的api目录下进行编译即可,具体命令如下:

    cd api/
    mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true
    
  3. 编译时,maven会通过Yang Tools插件生成基本的Java文件,生成的Java文件如下图所示:
    在这里插入图片描述

3.2 RPC实现

在OpenDaylight中默认采用是DataStore这种数据库。DataStore采用树型结构进行数据存储,它是一种高效的In-Memory(内存)数据库。

DataStore中的数据存储分两种形式:config和operational。config持有由应用所写的数据,一般是设备的配置信息或者是需要持久化的数据,而operational反映了设备运行时的数据,表明设备的运行状态,从设备读取数据,如果没有错误即可以看到设备的当前实际信息

config和operational相同之处在于两者的数据都以树型节点的形式存储在内存中;不同点是config数据生命周期比较长,控制器断电重启后数据会被持久化到硬盘中,持久化主要采用快照(snapshot)和增量日志(journal)这两种方式,而operational数据生命周期比较短,控制器断电重启后数据失效。

本案例中的全网拓扑信息以及获取网卡信息都是从OpenDaylight的operational数据库中读取出来的状态数据。

由于DataStore的内容比较多,我们小组将会在后面的系列文章中,对DataStore的知识进行深入探讨。

3.2.1 编写服务提供端service代码

impl/src/main/java/org/opendaylight/topology/impl/TopologyProvider.java文件进行代码实现,由于代码比较少,这里贴出TopologyProvider.java的所有代码(不包括package和import):

public class TopologyProvider implements TopologyService {
 
private static final Logger LOG = LoggerFactory.getLogger(TopologyProvider.class);
 
private final DataBroker dataBroker;
 
private static final String FLOWID = "flow:1";
 
public TopologyProvider(final DataBroker dataBroker) {
 
    this.dataBroker = dataBroker;
 
}
 
public void init() {
 
    LOG.info("TopologyProvider Session Initiated");
 
}
 
public void close() {
 
    LOG.info("TopologyProvider Closed");
 
}
 
@Override
 
public Future<RpcResult<ListLinksInfoOutput>> listLinksInfo() {
 
    final SettableFuture<RpcResult<ListLinksInfoOutput>> futureResult = SettableFuture.create();
 
    ListLinksInfoOutputBuilder outputBuilder = new ListLinksInfoOutputBuilder();
 
    final InstanceIdentifier.InstanceIdentifierBuilder<Topology> topologyId = InstanceIdentifier.builder(NetworkTopology.class).
 
            child(Topology.class, new TopologyKey(new TopologyId(new Uri(FLOWID))));
 
    InstanceIdentifier<Topology> topologyIId = topologyId.build();
 
    Topology topology = read(LogicalDatastoreType.OPERATIONAL, topologyIId);
 
 
 
    if (topology == null || topology.getLink() == null || topology.getLink().size() < 1) {
 
        futureResult.set(RpcResultBuilder.success(outputBuilder.build()).build());
 
        return futureResult;
 
    }
 
    List<LinksInfo> linkInfos = new ArrayList<>();
 
    topology.getLink().forEach(temp -> {
 
        LinksInfoBuilder lib = new LinksInfoBuilder();
 
 
 
        lib.setLinkId(temp.getLinkId())
 
                .setSrcDevice(temp.getSource().getSourceNode())
 
                .setDstPort(getPort(temp.getSource().getSourceTp().getValue()))
 
                .setDstDevice(temp.getDestination().getDestNode())
 
                .setDstPort(getPort(temp.getDestination().getDestTp().getValue()));
 
        linkInfos.add(lib.build());
 
    });
 
    outputBuilder.setLinksInfo(linkInfos);
 
    futureResult.set(RpcResultBuilder.success(outputBuilder.build()).build());
 
    return futureResult;
 
}
 
@Override
 
public Future<RpcResult<ListPortsInfoOutput>> listPortsInfo() {
 
    final SettableFuture<RpcResult<ListPortsInfoOutput>> futureResult = SettableFuture.create();
 
    ListPortsInfoOutputBuilder listPortsInfoOutputBuilder = new ListPortsInfoOutputBuilder();
 
    Nodes nodes = queryAllNode(LogicalDatastoreType.OPERATIONAL);
 
    if (nodes == null || nodes.getNode() == null || nodes.getNode().size() < 1) {
 
        futureResult.set(RpcResultBuilder.success(listPortsInfoOutputBuilder.build()).build());
 
        return futureResult;
 
    }
 
    List<PortsInfo> portsInfos = new ArrayList<>();
 
    nodes.getNode().forEach(tempNode -> {
 
        List<NodeConnector> nodeConnectors = filterNodeConnectors(tempNode);
 
        if (nodeConnectors == null || nodeConnectors.size() < 1) {
 
            return;
 
        }
 
        nodeConnectors.forEach(tempPort -> {
 
            PortsInfoBuilder pi = new PortsInfoBuilder();
 
            FlowCapableNodeConnector augmentation = tempPort.getAugmentation(FlowCapableNodeConnector.class);
 
            pi.setDeviceId(tempNode.getId())
 
                    .setPortNumber(new String(augmentation.getPortNumber().getValue()))
 
                    .setPortName(augmentation.getName())
 
                    .setHardwareAddress(augmentation.getHardwareAddress().getValue())
 
                    .setLinkDown(augmentation.getState().isLinkDown())
 
                    .setMaximumSpeed(augmentation.getMaximumSpeed())
 
                    .setCurrentSpeed(augmentation.getCurrentSpeed());
 
 
 
            portsInfos.add(pi.build());
 
        });
 
    });
 
    listPortsInfoOutputBuilder.setPortsInfo(portsInfos);
 
    futureResult.set(RpcResultBuilder.success(listPortsInfoOutputBuilder.build()).build());
 
    return futureResult;
 
}
 
private <D extends DataObject> D read(final LogicalDatastoreType store, final InstanceIdentifier<D> path) {
 
    D result = null;
 
    final ReadOnlyTransaction transaction = dataBroker.newReadOnlyTransaction();
 
    Optional<D> optionalDataObject;
 
    final CheckedFuture<Optional<D>, ReadFailedException> future = transaction.read(store, path);
 
    try {
 
        optionalDataObject = future.checkedGet();
 
        if (optionalDataObject.isPresent()) {
 
            result = optionalDataObject.get();
 
        } else {
 
            LOG.debug("{}: Failed to read {}", Thread.currentThread().getStackTrace()[1], path);
 
        }
 
    } catch (final ReadFailedException e) {
 
        LOG.warn("Failed to read {} ", path, e);
 
    }
 
    transaction.close();
 
    return result;
 
}
 
private int getPort(String tp) {
 
    return Integer.parseInt(tp.split(":")[2]);
 
}
 
private Nodes queryAllNode(LogicalDatastoreType configuration) {
 
    final InstanceIdentifier<Nodes> identifierNodes = InstanceIdentifier.create(Nodes.class);
 
    return read(configuration, identifierNodes);
 
}
 
private List<NodeConnector> filterNodeConnectors(Node node) {
 
    final List<NodeConnector> connectors = Lists.newArrayList();
 
    final List<NodeConnector> list = node.getNodeConnector();
 
    if (list != null && list.size() > 0) {
 
        for (final NodeConnector nodeConnector : list) {
 
 
 
            if (!nodeConnector.getId().getValue().endsWith("LOCAL")) {
 
                connectors.add(nodeConnector);
 
            }
 
        }
 
    }
 
    return connectors;
 
}
 
}
 

获取网络拓扑信息(listLinks方法)的大致流程如下:
①从network-topology的operational数据库中读取“flow:1”节点的所有内容。
②过滤“flow:1”下的节点内容,从而获取所需的link信息。
③将获取到的link信息封装成Future<RpcResult>进行返回给消费端。

获取网卡信息(listPortsInfo方法)的大致流程如下:
①从opendaylight-inventory中获取所有的Node节点,Node表示连接在控制器上的SDN交换机设备。
②获取每个Node节点的所有NodeConnector,然后获取需要的NodeConnector上的参数信息,NodeConnector相当于交换机的端口。
③将获取到信息封装成Future<RpcResult>进行返回给消费端。

3.2.2 完善相关依赖

我们在代码中使用了ietf-topology、model-inventory、model-flow-service,需要在impl目录的pom.xml文件中添加如下依赖。

<dependency>
 
<groupId>org.opendaylight.mdsal.model</groupId>
 
<artifactId>ietf-topology</artifactId>
 
<version>2013.10.21.10.2-Carbon</version>
 
</dependency>
 
<dependency>
 
<groupId>org.opendaylight.controller.model</groupId>
 
<artifactId>model-inventory</artifactId>
 
<version>1.5.2-Carbon</version>
 
</dependency>
 
<dependency>
 
<groupId>org.opendaylight.openflowplugin.model</groupId>
 
<artifactId>model-flow-service</artifactId>
 
<version>0.4.2-Carbon</version>
 
</dependency>
 

与之相对应的是需要在features目录下的src/main/features/features.xml文件添加引用仓库:

<repository>
 
mvn:org.opendaylight.openflowplugin/features-openflowplugin/${openflowplugin.version}/xml/features
 
</repository>

同时也需要在features.xml文件中的name为odl-topology-api的feature标签中添加如下feature:odl-openflowplugin-flow-services,该句添加位置如下:

<feature name='odl-topology-api' version='${project.version}' description='OpenDaylight :: topology:: api'>
 
<feature version='${mdsal.model.version}'>odl-mdsal-models</feature>
 
<feature version="${openflowplugin.version}">odl-openflowplugin-flow-services</feature>
 
<bundle>mvn:org.opendaylight.topology/topology-api/{{VERSION}}</bundle>
 
</feature>

其中openflowplugin.version在features目录下pom.xml文件中定义,需要在其中的properties标签内添加:

<openflowplugin.version>0.4.2-Carbon</openflowplugin.version>

4 RPC注册

编写完RPC实现的代码后,需要对RPC进行注册到OpenDaylight控制器以供外部调用,注册RPC方式有多种,后面的系列文章中会对RPC注册问题作详细叙述,本案例中采用的是Blueprint方式来注册RPC

Blueprint是针对OSGi的依赖注入解决方案,用法非常类似Spring。当使用服务的时候,Blueprint会马上创建并注入一个代理(Proxy)。对这些服务进行调用时,如果服务在当前不可用的话,将会产生阻塞,直至能够获取到服务或超时。

其实就是我们把一些原本需要写代码去实现的内容用xml文件的方式简化了,这个xml文件固定放在bundle的src\main\resources\org\opendaylight\blueprint目录下,否则获取不到文件内容,至于文件名可以随意。可以想象到有专门去解析这个xml文件的代码,这个代码的位置就在controller\opendaylight\blueprint目录下,有兴趣的读者可以去看下源码。

具体操作过程:在impl/src/main/resources/org/opendaylight/blueprint/impl-blueprint.xml中添加:<odl:rpc-implementation ref=“provider”/>,其中provider是实例化类的id,id名称可以改变但必须两者保持一致。

impl-blueprint.xml文件内容如下:

<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
 
xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
 
odl:use-default-for-reference-types="true">
 
<reference id="dataBroker"
 
interface="org.opendaylight.controller.md.sal.binding.api.DataBroker"
 
odl:type="default" />
 
<bean id="provider"
 
class="org.opendaylight.topology.impl.TopologyProvider"
 
init-method="init" destroy-method="close">
 
<argument ref="dataBroker" />
 
</bean>
 
<odl:rpc-implementation ref="provider"/>
 
</blueprint>

5 RPC测试

RPC测试需要用到OpenDaylight WEB控制台和l2switch相关组件。因此在编译项目之前,需要在features目录下的src/main/features/features.xml文件添加引用仓库:

<repository>
 
mvn:org.opendaylight.l2switch/features-l2switch/${l2switch.version}/xml/features
 
</repository>

在这里插入图片描述

5.1 启动服务

编译项目

mvn clean install -DskipTests -Dmaven.javadoc.skip=true -Dcheckstyle.skip=true
 
./karaf/target/assembly/bin/karaf

启动完成后,可以在karaf控制台命令查看开发的bundle是否安装成功,如果显示Active表示安装成功,具体命令为:
在这里插入图片描述

5.2 调用RPC

使用OpenDaylight WEB控制台的Yangman进行调用RPC,登录到WEB控制台后,依次鼠标点击Yangman—>topology rev.2018.03.07—>operations
在这里插入图片描述
在这里插入图片描述

补充:ODL中RPC接口的restconf化原理
参考:https://blog.csdn.net/sunquan291/article/details/82494264

  • 2
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值