1.为何要分析SNMP MIB文件。 分析MIB文件主要为了提取MIB文件中的oid定义及其对应的OID值,包含(OBJECT-IDENTIFIER 和OBJECT-TYPE等具有OID值的对象).
2. 提取OID对象和其值后,有何用户呢?
搞SNMP网管开发时,业务对象一般是居于C++,Java,C#等类,而且这些类一般只包含基础成员,没有指针,没有对象成员,没有集合成员等。这些业务对象再也SNMP Agent进行交互时,需要转换为SNMP 消息。 这个转换虽然比较简单。但是很繁琐,特别是网管的规模大的时候,oid不可能只有几个,往往都是上千个oid。这个工作量大,而且容易出错。---如果避免工作量和错误的重复出现,只有自动化的工具才可以。市面上有一些工具,大部分是针对Agent的代码自动生成,都是商业的。
3. 目标
分析提取oid后,根据OBJECT-GROUP,和NOTIFITION-TYPE 等构造对应的C++类。并根据每个OID的属性(SYNTAX,ACCESS,INDEX)等构造SNMP Get,SNMPSet,和SNMP RSP或者Trap包的解析方法。
4.此工程主要使用Scala语言,包含文法分析、文法分析中间结果和文法分析的结果
SnmpToolkit.scala 包含一些辅助数据结构
package cn.newzai.parser.snmp.result
import scala.collection.mutable
import cn.newzai.parser.snmp.SnmpParser
object SnmpGroupType extends Enumeration{
val OBJECT_GROUP =Value
val NOTIFICATION_TYPE = Value
//val NOTIFICATION_GROUP = Value
}
object SnmpSyntax extends Enumeration{
val NA = Value
val OCTET = Value
val INTEGER = Value
val Gauge = Value
val Counter32 = Value
val Counter = Value
val Counter64 = Value
val OBJECT = Value
val IpAddress = Value
val TimeTicks = Value
val Integer32 = Value
val Unsigned32 = Value
val cplusplus_type= Map(
OCTET->"std::string",
INTEGER->"int",
Gauge->"unsigned int",
Counter32->"unsigned int",
Counter->"unsigned int",
Counter64->"unsigned int",
OBJECT->"std::string",
IpAddress->"std::string",
TimeTicks->"unsigned int",
Integer32->"int",
Unsigned32->"unsigned int")
//ASNMP 库对应的数据类型
val snmp_type = Map(
OCTET->"OctetStr",
INTEGER->"SnmpInt32",
Gauge->"Gauge32",
Counter32->"Counter32",
Counter->"Counter32",
Counter64->"Counter64",
OBJECT->"Oid",
IpAddress->"IpAddress",
TimeTicks->"TimeTicks",
Integer32->"SnmpInt32",
Unsigned32->"SnmpUInt32")
val convert_to_snmp = Map(
OCTET->".c_str()",
INTEGER->"long",
Gauge->"unsigned long",
Counter32->"unsigned long",
Counter->"unsigned long",
Counter64->"unsigned long",
OBJECT->".c_str()",
IpAddress->".c_str()",
TimeTicks->"unsigned long",
Integer32->"long",
Unsigned32->"unsigned long"
)
val convert_to_cplusplus = Map(
OCTET->".to_string()",
INTEGER->"int",
Gauge->"unsigned int",
Counter32->"unsigned int",
Counter->"unsigned int",
Counter64->"unsigned int",
OBJECT->".to_string()",
IpAddress->".to_string()",
TimeTicks->"unsigned int",
Integer32->"int",
Unsigned32->"unsigned int"
)
val is_ref_type = Map(
OCTET->true,
INTEGER->false,
Gauge->false,
Counter32->false,
Counter->false,
Counter64->false,
OBJECT->true,
IpAddress->true,
TimeTicks->false,
Integer32->false,
Unsigned32->false
)
val default_value = Map(
OCTET->"\"\"",
INTEGER->"0",
Gauge->"0",
Counter32->"0",
Counter->"0",
Counter64->"0",
OBJECT->"\"\"",
IpAddress->"\"\"",
TimeTicks->"0",
Integer32->"0",
Unsigned32->"0"
)
val format = Map(
OCTET->"%s",
INTEGER->"%d",
Gauge->"%d",
Counter32->"%d",
Counter->"%d",
Counter64->"%d",
OBJECT->"%s",
IpAddress->"%s",
TimeTicks->"%d",
Integer32->"%d",
Unsigned32->"%d"
)
def getSyntax( name :String) ={
try{
withName(name)
} catch{
case e : NoSuchElementException =>
NA
}
}
def getCPlusPlusType( syntax : Value) = cplusplus_type(syntax)
def getSnmpType( syntax :Value) = snmp_type(syntax)
def getConvertToSnmp( syntax : Value) = convert_to_snmp(syntax)
def getConvertToCPlusPlus(syntax : Value) = convert_to_cplusplus(syntax)
def getFormat(syntax : Value) = format(syntax)
}
object SnmpAccess extends Enumeration {
val accesses = new mutable.HashSet[String]()
val read_only , accessible_for_notify, read_write, read_create, not_accessible = Value
def getAccess( access : String) ={
try{
withName(access.trim.replaceAll("-","_"))
}catch {
case e : NoSuchElementException => not_accessible
}
}
}
object SnmpToolkit {
def toMethodName( name :String) ={
var first = true
val r =for( c <-name) yield {
if( first){
first = false
c.toUpper
}else{
c
}
}
r.mkString("")
}
def toClassName( name :String) ={
toMethodName(name)
}
def toFieldName( name :String) ={
var lastIsUpper = false
val r = for ( c<- name) yield {
if( lastIsUpper){
if( c.isUpper){
s"${c.toLower}"
}else{
lastIsUpper = false;
s"${c}"
}
}else{
if( c.isUpper){
lastIsUpper = true
s"_${c.toLower}"
} else{
s"${c}"
}
}
}
r.mkString("").concat("_")
}
}
SnmpLexical.scala 文件包含SNMP MIB文件的文法和词法分析过程
package cn.newzai.parser.snmp
import scala.util.parsing.combinator.lexical.StdLexical
import scala.util.parsing.input.CharArrayReader._
import scala.util.parsing.combinator.syntactical.StandardTokenParsers
import scala.io.Source
import scala.collection.mutable
import cn.newzai.parser.snmp.result._
/**
*
* @param name oid节点的名称
* @param parent 除了oid为1(iso) 和o(zeroDotZero)两个节点以为,其它oid节点都有一个父节点
* @param instance 本oid节点在父节点中的位置
*/
class MibNode(val name :String,val parent : String, val instance : Int){
override def toString: String = s" ${name} ::= { ${parent} ${instance}}"
}
/**
* nodes 保存解析过程中得到的所有 oid节点信息
* zeroDotZero oid值为0的节点
* iso oid值为1的节点 绝大部分OID为iso的下级节点
*/
object MibNode {
private val nodes =new mutable.HashMap[String, MibNode]()
private val zeroDotZero = MibNode("0",0)
private val iso = MibNode("iso",1)
def clear() {
nodes.clear()
}
def apply( name :String, parent :String, instance : Int) = {
new MibNode(name,parent,instance)
}
def apply( name :String,instance : Int) = {
new MibNode(name,null,instance)
}
def addMibNode( name :String,parent :String, instance : Int){
val newNode = MibNode(name,parent,instance)
nodes(name) = newNode
}
def hasMibNode( name :String) = {
name match {
case "0" => true
case "iso" => true
case _ => nodes.contains(name)
}
}
def getMibNode(name :String) ={
name match {
case "0" => zeroDotZero
case "iso" => iso
case _ => nodes(name)
}
}
private def getOid_(name :String) : List[Int]={
name match {
case "0" => List(0)
case "iso" => List(1)
case _ =>
val node = nodes(name)
node.instance :: getOid_(node.parent)
}
}
private def getOid( name :String ) = getOid_(name).reverse
/**
*
* @param name oid名称
* @return 获取name对于的oid只,得到的是点分号的格式, eg “1.3.4.1334”
*/
def getOidString( name :String) = {
val oids = getOid(name)
val oidsStr = for( o <- oids) yield { o.toString }
oidsStr.mkString(".")
}
}
/**
* SNMP 词法分析,Scala提供的标准的词法分析无法满足SNMP的词法分析。
* 主要为 标识符,snmp可以包含 破折号(-)
* 注释 SNMP注释以 两个破折号(--)考开始,直到行尾.
*/
class SnmpLexical extends StdLexical{
// SNMP MIB文件的标识符(OID名称等包含 - 符号
override def identChar = letter | elem('_') | elem('-')
// SNMP MIB文件的注释,包含 -- ,均为含注释
override def whitespace:Parser[Any] = rep(
whitespaceChar
| '/' ~ '*' ~ comment
| '/' ~ '/' ~ rep( chrExcept(EofCh, '\n') )
| '-' ~ '-' ~ rep( chrExcept(EofCh, '\n') )
| '/' ~ '*' ~ failure("unclosed comment")
)
override def token: Parser[Token] =
( identChar ~ rep( identChar | digit ) ^^ { case first ~ rest => processIdent(first :: rest mkString "") }
| digit ~ rep( digit ) ^^ { case first ~ rest => NumericLit(first :: rest mkString "") }
| '\'' ~ rep( chrExcept('\'', '\n', EofCh) ) ~ '\'' ^^ { case '\'' ~ chars ~ '\'' => StringLit(chars mkString "") }
| '\"' ~ rep( chrExcept('\"', EofCh) ) ~ '\"' ^^ { case '\"' ~ chars ~ '\"' => StringLit(chars mkString "") }
| EofCh ^^^ EOF
| '\'' ~> failure("unclosed string literal")
| '\"' ~> failure("unclosed string literal")
| delim
| failure("illegal character")
)
}
/**
* SNMP MIB文件词法分析,每个SnmpParser对于一个SNMP MIB 文件 , 从StandardTokenParsers继承
* @param dir SNMP MIB文件的所在文件夹,被import进来的文件都在相同的文件夹
* @param file 当前解析的MIB文件
* @param importFile 标识是因和原因解析该文件,true时,表示该文件为import文件,不提取OBJECT-GROUP信息,只提供OID值。
* 顶层文件时为false,分析所有内容.
*/
class SnmpParser(val dir :String, val file :String, val importFile : Boolean) extends StandardTokenParsers {
/**
* 覆盖基类的lexical字段,scala 的字段和方法统一处理,也可以被覆盖.
*/
override val lexical = new SnmpLexical
/*
定义SNMP MIB 文件的分隔符,分隔符定义时,采用最大化匹配原则
*/
lexical.delimiters +=("::=","(",")","{","}",",",";","..","-","+")
/*
SNMP MIB 的保留字(包含关键字和一部分SNMP MIB的类型定义)
*/
lexical.reserved +=("DEFINITIONS","BEGIN","END","IMPORTS","FROM","MODULE-IDENTITY","MODULE-IDENTITY",
"OBJECT-TYPE", "NOTIFICATION-TYPE", "MODULE-COMPLIANCE", "OBJECT-GROUP", "NOTIFICATION-GROUP","OBJECT",
"IDENTIFIER","OBJECT-TYPE","SIZE","MAX-ACCESS","ACCESS","MIN-ACCESS","SYNTAX","STATUS","INDEX","SEQUENCE",
"MANDATORY-GROUPS","GROUP","MODULE","OBJECTS","NOTIFICATIONS","TEXTUAL-CONVENTION","OCTET","STRING","OF","AUGMENTS",
"DEFVAL","OBJECT-IDENTITY")
def keyword: Parser[String] =
"MODULE-IDENTITY"|"MODULE-IDENTITY"|"OBJECT-TYPE"|"NOTIFICATION-TYPE"|
"MODULE-COMPLIANCE"|"OBJECT-GROUP"|"NOTIFICATION-GROUP"|"OBJECT"|"IDENTIFIER"|"SEQUENCE"|"OF"|
"TEXTUAL-CONVENTION"|"OBJECT-IDENTITY"|"OCTET"|"STRING" ^^ { case x => x}
/**
*
* @param oid "::={ parent instance } " 格式的定义,
* @return Option[(String,String)] 提取 parent,instance 值
*/
def getOidParent( oid :String) ={
val matchParent = """\s*::=\s+\{\s*([\w-]+)\s+([\d]+)\s*\}""".r
oid match {
case matchParent(parent,instance) => Some((parent,instance))
case _ => None
}
}
/**
* 解析SNMP 内容
* @return
*/
def parserContent : Parser[Any] = {
parserObjectIdentifier|
parserObjectIdentify|
parserTextualConvention |
parserSimpleTextualConvention|
parserObjectType|
parserSequence|
parserNotificationType|
parserModuleCompliance|
parserObjectGroup
}
/**
* 解析SNMP文件的定义,SNMP 文件解析的入口方法.
* @return
*/
def parserDefinitions : Parser[Any] = {
ident ~ "DEFINITIONS" ~ "::=" ~ "BEGIN" ~opt(parserImports)~opt(parserMODULE_IDENTITY)~rep(parserContent)~ "END" ^^ {
case n~ "DEFINITIONS" ~ "::=" ~ "BEGIN"~ im~mi~contents ~ "END" =>
s"""${n} DEFINITIONS ::= BEGIN
|${im.getOrElse("")}
|${mi.getOrElse("")}
|${contents mkString("\n")}
|END
""".stripMargin
}
}
/*
解析导入的文件名,并且进入被导入的文件进行递归解析
*/
def parserFrom : Parser[String] ={
repsep(ident|keyword,",")~"FROM"~ident ^^ {
case f ~ "FROM"~n =>
val macroMibFile = Set("SNMPv2-CONF","RFC-1212")
if( !macroMibFile.contains(n) && !SnmpParser.parseredFile.contains(n))
{
SnmpParser.parseredFile.add(n)
val fromParser = new SnmpParser(this.dir, n, true)
val r = fromParser.parserFile(fromParser.parserDefinitions)
if (!r.successful) {
println(s"parser file ${n} error.")
println(r)
} else {
println(s"parser file ${n} OK")
}
}
s"""${f.mkString(",")} FROM ${n}"""
}
}
/**
* 解析IMPORTS 段
* @return
*/
def parserImports : Parser[String] = {
"IMPORTS"~rep(parserFrom)~";" ^^ {
case "IMPORTS"~f~";" =>
s"""
|IMPORTS
|${f.mkString("\n")}
|;""".stripMargin
}
}
/**
* 解析 DESCRIPTION和那些以字符串作为值的属性,例如LAST-UPDATED等。
* @return
*/
def parserDescription : Parser[String] = {
ident~stringLit ^^ {case f ~ d =>
s"""${f} "${d}" """}
}
def parserOIDPath : Parser[Any] = {
ident~"("~numericLit~")" ^^ {
case p~"("~i~")" => s"${p}(${i})"
}
}
def parserOIDPaths : Parser[Any] ={
rep(parserOIDPath) ^^ {
case paths => paths.mkString(" ")
}
}
/**
* 解析OID 值
*
* @return
*/
def parserOID : Parser[String] ={
"::="~"{"~ident~parserOIDPaths~numericLit~"}" ^^ {
case "::="~"{"~i~p~n~"}" => s"::= { ${i} ${n}}"
}
}
def parserOIDGetValue : Parser[(String,Int)] = {
"::="~"{"~ident~numericLit~"}" ^^ {
case "::="~"{"~i~n~"}" =>(i, n.toInt)
}
}
def getOIDValue( parentOID : String) = {
val r = phrase(parserOIDGetValue)( new lexical.Scanner(parentOID))
r.get
}
/**
* 捕获解析结果,保存在中间变量中.
* @param name oid名称
* @param parent parent 路径
*/
def insertOid( name :String, parent : String){
val p = getOIDValue(parent)
MibNode.addMibNode(name,p._1,p._2)
}
/**
* 解析模块定义
* @return
*/
def parserMODULE_IDENTITY : Parser[String] ={
ident ~ "MODULE-IDENTITY" ~rep(parserDescription)~parserOID ^^ {
case i ~ "MODULE-IDENTITY" ~ d ~ oid =>
insertOid(i, oid)
s"""${i} MODULE-IDENTITY
|${d mkString("\n")}
|${oid}
""".stripMargin
}
}
/**
* 解析 OBJECT IDENTITIFER
* @return
*/
def parserObjectIdentifier : Parser[Any] = {
ident ~ "OBJECT"~"IDENTIFIER"~parserOID ^^ {
case i ~ "OBJECT"~"IDENTIFIER" ~ oid =>
insertOid(i,oid)
s"${i} OBJECT IDENTIFIER ${oid}"
}
}
def parserObjectIdentify : Parser[Any] = {
ident ~"OBJECT-IDENTITY"~parserStatus~parserDescription~parserOID ^^ {
case i ~"OBJECT-IDENTITY"~s~d~oid =>
insertOid(i,oid)
s"""${i} OBJECT-IDENTITY
|${s}
|${d}
|${oid}
""".stripMargin
}
}
/**
* 解析OCTET STRING 的大小限制
* @return
*/
def parserSize :Parser[Any] = {
"("~"SIZE"~"("~numericLit~".."~numericLit~")"~")" ^^ {
case "("~"SIZE"~"("~start~".."~end~")"~")" => s"(SIZE(${start}..${end}))"
}
}
/**
* 解析整数的 范围限制
* @return
*/
def parserRange:Parser[Any] = {
"("~numericLit~".."~numericLit~")" ^^ {
case "("~start~".."~end~")" => s"(${start}..${end})"
}
}
def parserEnumSimple : Parser[Any] ={
ident ~ "("~numericLit~")" ^^ {
case n ~ "("~v~")" &