diff --git a/dataplane/standalone/apigen/BUILD b/dataplane/standalone/apigen/BUILD index 412a6d7a..2366a84c 100644 --- a/dataplane/standalone/apigen/BUILD +++ b/dataplane/standalone/apigen/BUILD @@ -2,10 +2,16 @@ load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") go_library( name = "apigen_lib", - srcs = ["apigen.go"], + srcs = [ + "apigen.go", + "xml.go", + ], importpath = "github.com/openconfig/lemming/dataplane/standalone/apigen", visibility = ["//visibility:private"], - deps = ["@org_modernc_cc_v4//:cc"], + deps = [ + "@com_github_stoewer_go_strcase//:go-strcase", + "@org_modernc_cc_v4//:cc", + ], ) go_binary( diff --git a/dataplane/standalone/apigen/apigen.go b/dataplane/standalone/apigen/apigen.go index 716a5c3d..5df15c57 100644 --- a/dataplane/standalone/apigen/apigen.go +++ b/dataplane/standalone/apigen/apigen.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// The apigen command generates C++ and protobuf code for the SAI API. package main import ( @@ -24,6 +25,7 @@ import ( "strings" "text/template" + strcase "github.com/stoewer/go-strcase" cc "modernc.org/cc/v4" ) @@ -217,7 +219,7 @@ type templateFunc struct { Entry string } -type templateData struct { +type ccTemplateData struct { IncludeGuard string Header string APIType string @@ -260,6 +262,254 @@ var ( funcExpr = regexp.MustCompile(`^([a-z]*_)(\w*)_(attribute|stats_ext|stats)|([a-z]*)_(\w*)$`) ) +// TODO: Enable generation. +var _ = template.Must(template.New("cc").Parse(` +syntax = "proto3"; + +package lemming.dataplane.sai; + +option go_package = "github.com/openconfig/lemming/proto/dataplane/sai"; + +{{ range .Messages }} +message {{ .RequestName }} { + {{ .RequestFieldsWrapperStart -}} + {{- range .RequestFields }} + {{ .ProtoType }} {{ .Name }} = {{ .Index }}; + {{- end }} + {{ .RequestFieldsWrapperEnd }} +} + +message {{ .ResponseName }} { + {{ .ResponseFieldsWrapperStart -}} + {{- range .ResponseFields }} + {{ .ProtoType }} {{ .Name }} = {{ .Index }}; + {{- end }} + {{ .ResponseFieldsWrapperEnd }} +} + +{{ end }} + + +service {{ .ServiceName }} { + {{- range .RPCs }} + rpc {{ .Name }} ({{ .RequestName }}) returns ({{ .ResponseName }}) {} + {{- end }} +} +`)) + +type protoTmplData struct { + Messages []protoTmplMessage + RPCs []protoRPC + ServiceName string +} + +type protoTmplMessage struct { + RequestName string + ResponseName string + RequestFieldsWrapperStart string + RequestFieldsWrapperEnd string + RequestFields []protoTmplField + ResponseFieldsWrapperStart string + ResponseFieldsWrapperEnd string + ResponseFields []protoTmplField +} + +type protoTmplField struct { + ProtoType string + Name string + Index int +} + +type protoRPC struct { + RequestName string + ResponseName string + Name string +} + +type saiTypeInfo struct { + Repeated bool + ProtoType string +} + +var saiTypeToProto = map[string]saiTypeInfo{ + "bool": { + ProtoType: "bool", + }, + "char": { + ProtoType: "bytes", + }, + "sai_uint8_t": { + ProtoType: "uint32", + }, + "sai_int8_t": { + ProtoType: "int32", + }, + "sai_uint16_t": { + ProtoType: "uint32", + }, + "sai_int16_t": { + ProtoType: "int32", + }, + "sai_uint32_t": { + ProtoType: "uint32", + }, + "sai_int32_t": { + ProtoType: "uint32", + }, + "sai_uint64_t": { + ProtoType: "uint64", + }, + "sai_int64_t": { + ProtoType: "int64", + }, + "sai_mac_t": { + ProtoType: "bytes", + }, + "sai_ip4_t": { + ProtoType: "bytes", + }, + "sai_ip6_t": { + ProtoType: "bytes", + }, + "sai_s32_list_t": { + Repeated: true, + ProtoType: "int32", + }, + "sai_object_id_t": { + ProtoType: "uint64", + }, + "sai_object_list_t": { + Repeated: true, + ProtoType: "uint64", + }, + "sai_encrypt_key_t": { + ProtoType: "bytes", + }, + "sai_auth_key_t": { + ProtoType: "bytes", + }, + "sai_macsec_sak_t": { + ProtoType: "bytes", + }, + "sai_macsec_auth_key_t": { + ProtoType: "bytes", + }, + "sai_macsec_salt_t": { + ProtoType: "bytes", + }, + "sai_u32_list_t": { + Repeated: true, + ProtoType: "uint32", + }, + "sai_segment_list_t": { + Repeated: true, + ProtoType: "bytes", + }, + "sai_s8_list_t": { + Repeated: true, + ProtoType: "int32", + }, + "sai_u8_list_t": { + Repeated: true, + ProtoType: "uint32", + }, + "sai_port_err_status_list_t": { + Repeated: true, + ProtoType: "sai_port_err_status_t", + }, + "sai_vlan_list_t": { + Repeated: true, + ProtoType: "uint32", + }, + "sai_u32_range_t": { + ProtoType: "Uint32Range", + }, + "sai_ip_address_t": { + ProtoType: "IpAddress", + }, + "sai_map_list_t": { + Repeated: true, + ProtoType: "Uint32Range", + }, + "sai_tlv_list_t": { + Repeated: true, + ProtoType: "TLV", + }, + "sai_qos_map_list_t": { + Repeated: true, + ProtoType: "QOSMap", + }, + "sai_system_port_config_t": { + Repeated: true, + ProtoType: "SystemPortConfig", + }, + "sai_system_port_config_list_t": { + Repeated: true, + ProtoType: "SystemPortConfig", + }, + "sai_ip_address_list_t": { + Repeated: true, + ProtoType: "IpAddress", + }, + "sai_port_eye_values_list_t": { + Repeated: true, + ProtoType: "PortEyeValues", + }, + "sai_prbs_rx_state_t": { + ProtoType: "PRBS_RXState", + }, + "sai_fabric_port_reachability_t": { + ProtoType: "FabricPortReachability", + }, + "sai_acl_resource_list_t": { + Repeated: true, + ProtoType: "ACLResource", + }, + "sai_acl_capability_t": { + Repeated: true, + ProtoType: "ACLCapability", + }, +} + +// saiTypeToProtoTypeCompound handles compound sai types (eg list of enums). +// The map key contains the base type (eg list) and func accepts the subtype (eg an enum type) +// and returns the full type string (eg repeated sample_enum). +var saiTypeToProtoTypeCompound = map[string]func(subType string, xmlInfo *xmlInfo) string{ + "sai_s32_list_t": func(subType string, xmlInfo *xmlInfo) string { + if _, ok := xmlInfo.enums[subType]; !ok { + return "" + } + return "repeated " + subType + }, + // TODO: Support these types + "sai_acl_field_data_t": func(next string, xmlInfo *xmlInfo) string { return "-" }, + "sai_acl_action_data_t": func(next string, xmlInfo *xmlInfo) string { return "-" }, + "sai_pointer_t": func(next string, xmlInfo *xmlInfo) string { return "-" }, +} + +// saiTypeToProtoType returns the protobuf type string for a SAI type. +// example: sai_u8_list_t -> repeated uint32 +func saiTypeToProtoType(saiType string, xmlInfo *xmlInfo) (string, error) { + var typ string + if protoType, ok := saiTypeToProto[saiType]; ok { + typ = protoType.ProtoType + if protoType.Repeated { + typ = "repeated " + typ + } + } else if _, ok := xmlInfo.enums[saiType]; ok { + typ = saiType + } else if splits := strings.Split(saiType, " "); len(splits) == 2 { + fn, ok := saiTypeToProtoTypeCompound[splits[0]] + if !ok { + return "", fmt.Errorf("unknown sai type: %v", saiType) + } + typ = fn(splits[1], xmlInfo) + } else { + return "", fmt.Errorf("unknown sai type: %v", saiType) + } + return typ, nil +} + func generate() error { headerFile, err := filepath.Abs(filepath.Join(saiPath, "inc/sai.h")) if err != nil { @@ -278,15 +528,22 @@ func generate() error { return err } sai := getFuncAndTypes(ast) + xmlInfo, err := parseSAIXMLDir() + if err != nil { + return err + } for _, iface := range sai.ifaces { nameTrimmed := strings.TrimSuffix(strings.TrimPrefix(iface.name, "sai_"), "_api_t") - data := templateData{ + ccData := ccTemplateData{ IncludeGuard: fmt.Sprintf("DATAPLANE_STANDALONE_SAI_%s_H_", strings.ToUpper(nameTrimmed)), Header: fmt.Sprintf("%s.h", nameTrimmed), APIType: iface.name, APIName: nameTrimmed, } + protoData := protoTmplData{ + ServiceName: nameTrimmed, // TODO: prettier name + } for _, fn := range iface.funcs { name := strings.TrimSuffix(strings.TrimPrefix(fn.name, "sai_"), "_fn") tf := templateFunc{ @@ -330,9 +587,81 @@ func generate() error { if strings.Contains(tf.TypeName, "PORT_ALL") || strings.Contains(tf.TypeName, "ALL_NEIGHBOR") { tf.UseCommonAPI = false } - data.Funcs = append(data.Funcs, tf) + ccData.Funcs = append(ccData.Funcs, tf) + + // TODO: handle proto in its own func. + msg := protoTmplMessage{ + RequestName: strcase.UpperCamelCase(tf.Name + "_request"), + ResponseName: strcase.UpperCamelCase(tf.Name + "_response"), + } + + // Handle proto generation + // TODO: Enable proto generation and handle other funcs. + switch tf.Operation { + case "create": + for i, attr := range xmlInfo.attrs[tf.TypeName].createFields { + field := protoTmplField{ + Index: i + 1, + Name: attr.MemberName, + } + typ, err := saiTypeToProtoType(attr.SaiType, xmlInfo) + if err != nil { + return err + } + field.ProtoType = typ + msg.RequestFields = append(msg.RequestFields, field) + } + protoData.Messages = append(protoData.Messages, msg) + protoData.RPCs = append(protoData.RPCs, protoRPC{ + RequestName: msg.RequestName, + ResponseName: msg.ResponseName, + Name: strcase.UpperCamelCase(tf.Name), + }) + case "set_attribute": + for i, attr := range xmlInfo.attrs[tf.TypeName].setFields { + field := protoTmplField{ + Index: i + 1, + Name: attr.MemberName, + } + msg.RequestFieldsWrapperStart = "oneof attr {" + msg.RequestFieldsWrapperEnd = "}" + typ, err := saiTypeToProtoType(attr.SaiType, xmlInfo) + if err != nil { + return fmt.Errorf("failed to get proto type for attr %s: %v", attr.MemberName, err) + } + field.ProtoType = typ + msg.RequestFields = append(msg.RequestFields, field) + } + protoData.Messages = append(protoData.Messages, msg) + protoData.RPCs = append(protoData.RPCs, protoRPC{ + RequestName: msg.RequestName, + ResponseName: msg.ResponseName, + Name: strcase.UpperCamelCase(tf.Name), + }) + case "get_attribute": + for i, attr := range xmlInfo.attrs[tf.TypeName].readFields { + field := protoTmplField{ + Index: i + 1, + Name: attr.MemberName, + } + msg.ResponseFieldsWrapperStart = "oneof attr {" + msg.ResponseFieldsWrapperEnd = "}" + typ, err := saiTypeToProtoType(attr.SaiType, xmlInfo) + if err != nil { + return fmt.Errorf("failed to get proto type for attr %s: %v", attr.MemberName, err) + } + field.ProtoType = typ + msg.ResponseFields = append(msg.ResponseFields, field) + } + protoData.Messages = append(protoData.Messages, msg) + protoData.RPCs = append(protoData.RPCs, protoRPC{ + RequestName: msg.RequestName, + ResponseName: msg.ResponseName, + Name: strcase.UpperCamelCase(tf.Name), + }) + } } - header, err := os.Create(filepath.Join(outDir, data.Header)) + header, err := os.Create(filepath.Join(outDir, ccData.Header)) if err != nil { return err } @@ -340,10 +669,10 @@ func generate() error { if err != nil { return err } - if err := headerTmpl.Execute(header, data); err != nil { + if err := headerTmpl.Execute(header, ccData); err != nil { return err } - if err := ccTmpl.Execute(impl, data); err != nil { + if err := ccTmpl.Execute(impl, ccData); err != nil { return err } } diff --git a/dataplane/standalone/apigen/xml.go b/dataplane/standalone/apigen/xml.go new file mode 100644 index 00000000..abff7697 --- /dev/null +++ b/dataplane/standalone/apigen/xml.go @@ -0,0 +1,200 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/xml" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" +) + +// Doxygen is the root of the generated xml struct. +type Doxygen struct { + CompoundDef CompoundDef `xml:"compounddef"` +} + +// CompoundDef contains a list sections in the xml. +type CompoundDef struct { + Title string `xml:"title"` + SectionDef []SectionDef `xml:"sectiondef"` +} + +// SectionDef contains a list of members. +type SectionDef struct { + MemberDef []MemberDef `xml:"memberdef"` +} + +// MemberDef is the definition of a single type. +type MemberDef struct { + Name string `xml:"name"` + EnumValues []EnumValue `xml:"enumvalue"` + Kind string `xml:"kind,attr"` +} + +// EnumValue is a single values in a enum. +type EnumValue struct { + Name string `xml:"name"` + Initializer string `xml:"initializer"` + DetailedDescription DetailedDescription `xml:"detaileddescription"` +} + +// DetailedDescription contains extra information about an enum value. +type DetailedDescription struct { + Paragraph Paragraph `xml:"para"` +} + +// Paragraph is a generic paragraph. +type Paragraph struct { + SimpleSect []SimpleSect `xml:"simplesect"` +} + +// SimpleSect contains a description of an element. +type SimpleSect struct { + Para string `xml:"para"` +} + +const xmlPath = "dataplane/standalone/apigen/xml" + +// parseSAIXMLDir parses all the SAI Doxygen XML files in a directory. +func parseSAIXMLDir() (*xmlInfo, error) { + i := &xmlInfo{ + attrs: make(map[string]attrInfo), + enums: make(map[string][]string), + } + files, err := os.ReadDir(xmlPath) + if err != nil { + return nil, err + } + for _, file := range files { + if err := parseXMLFile(filepath.Join(xmlPath, file.Name()), i); err != nil { + return nil, err + } + } + return i, nil +} + +var typeNameExpr = regexp.MustCompile("sai_(.*)_attr.*") + +// handleEnumAttr converts the MemberDef into attrInfo extracting the enum values, +// their types, and if they createable, readable, and/or writable. +func handleEnumAttr(enum MemberDef) attrInfo { + info := attrInfo{} + trimStr := strings.TrimSuffix(strings.TrimPrefix(enum.Name, "_"), "_t") + "_" + for _, value := range enum.EnumValues { + var canCreate, canRead, canSet bool + var saiType string + for _, details := range value.DetailedDescription.Paragraph.SimpleSect { + annotation := strings.TrimSpace(details.Para) + switch { + case strings.HasPrefix(annotation, "@@type"): + saiType = strings.TrimSpace(strings.TrimPrefix(annotation, "@@type")) + case strings.HasPrefix(annotation, "@@flags"): + switch { + case strings.Contains(annotation, "CREATE_ONLY"): + canCreate = true + canRead = true + case strings.Contains(annotation, "CREATE_AND_SET"): + canCreate = true + canRead = true + canSet = true + case strings.Contains(annotation, "READ_ONLY"): + canRead = true + } + } + } + if !canCreate && !canRead && !canSet { + continue + } + atn := attrTypeName{ + EnumName: value.Name, + MemberName: strings.TrimPrefix(strings.ToLower(value.Name), trimStr), + SaiType: saiType, + } + if canCreate { + info.createFields = append(info.createFields, atn) + } + if canRead { + info.readFields = append(info.readFields, atn) + } + if canSet { + info.setFields = append(info.setFields, atn) + } + } + return info +} + +func memberToEnumValueStrings(enum MemberDef) []string { + res := []string{} + for _, value := range enum.EnumValues { + res = append(res, value.Name) + } + return res +} + +// parseXMLFile parses a single XML and appends the values into xmlInfo. +func parseXMLFile(file string, xmlInfo *xmlInfo) error { + b, err := os.ReadFile(file) + if err != nil { + return err + } + dox := &Doxygen{} + if err := xml.Unmarshal(b, dox); err != nil { + return err + } + + for _, enum := range dox.CompoundDef.SectionDef[0].MemberDef { + if enum.Kind != "enum" { + fmt.Printf("skipping kind %s\n", enum.Kind) + continue + } + + if strings.Contains(enum.Name, "attr_t") { + matches := typeNameExpr.FindStringSubmatch(enum.Name) + if len(matches) != 2 { + return fmt.Errorf("unexpected number of matches: got %v", matches) + } + info := handleEnumAttr(enum) + xmlInfo.attrs[strings.ToUpper(matches[1])] = info + } else { + xmlInfo.enums[strings.TrimPrefix(enum.Name, "_")] = memberToEnumValueStrings(enum) + } + } + return nil +} + +// attrInfo holds values and types for an attribute enum. +type attrInfo struct { + createFields []attrTypeName + setFields []attrTypeName + readFields []attrTypeName +} + +// attrTypeName contains the type and name of the attribute. +type attrTypeName struct { + MemberName string + SaiType string + EnumName string +} + +// xmlInfo contains all the info parsed from the doxygen. +type xmlInfo struct { + // attrs is a map from sai type (sai_port_t) to its attributes. + attrs map[string]attrInfo + // attrs is a map from enum name (sai_port_media_type_t) to the values of the enum. + enums map[string][]string +} diff --git a/go.mod b/go.mod index 1b7636a6..995bac7a 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 + github.com/stoewer/go-strcase v1.3.0 github.com/vishvananda/netlink v1.2.1-beta.2 github.com/wenovus/gobgp/v3 v3.0.0-20230512225508-639b46f0d89c go.uber.org/mock v0.2.0 diff --git a/go.sum b/go.sum index ebaebe6e..71f36378 100644 --- a/go.sum +++ b/go.sum @@ -959,6 +959,8 @@ github.com/srl-labs/srl-controller v0.6.0/go.mod h1:PedxdPZPtDcC+wDOKhG6uXR4xgkH github.com/srl-labs/srlinux-scrapli v0.6.0 h1:YQjckD+a7f6u2M+k4SmJUrDa7BFvoOTb2mMbPe6hLZM= github.com/srl-labs/srlinux-scrapli v0.6.0/go.mod h1:8hCoel3XaSyZD8hxSs8Pij/uZqaccd57mfeHgc0oJhM= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/repositories.bzl b/repositories.bzl index ef1a0365..131bd472 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -1621,8 +1621,8 @@ def go_repositories(): go_repository( name = "com_github_stoewer_go_strcase", importpath = "github.com/stoewer/go-strcase", - sum = "h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=", - version = "v1.2.0", + sum = "h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=", + version = "v1.3.0", ) go_repository( name = "com_github_stretchr_objx",