go.ligato.io/vpp-agent/v3@v3.5.0/client/dynamic_config.go (about) 1 package client 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/goccy/go-yaml" 8 "go.ligato.io/cn-infra/v2/logging/logrus" 9 "google.golang.org/protobuf/encoding/protojson" 10 "google.golang.org/protobuf/proto" 11 "google.golang.org/protobuf/reflect/protodesc" 12 "google.golang.org/protobuf/reflect/protoreflect" 13 "google.golang.org/protobuf/reflect/protoregistry" 14 "google.golang.org/protobuf/types/descriptorpb" 15 "google.golang.org/protobuf/types/dynamicpb" 16 17 "go.ligato.io/vpp-agent/v3/pkg/models" 18 "go.ligato.io/vpp-agent/v3/pkg/util" 19 "go.ligato.io/vpp-agent/v3/proto/ligato/generic" 20 ) 21 22 // field proto name/json name constants (can't be changes to not break json/yaml compatibility with configurator.Config) 23 const ( 24 // configGroupSuffix is field proto name suffix that all fields referencing config groups has 25 configGroupSuffix = "Config" 26 // repeatedFieldsSuffix is suffix added to repeated fields inside config group message 27 repeatedFieldsSuffix = "_list" 28 ) 29 30 // names is supporting structure for remembering proto field name and json name 31 type names struct { 32 protoName, jsonName string 33 } 34 35 // TODO: generate backwardCompatibleNames dynamically by searching given known model in configurator.Config 36 // and extracting proto field name and json name? 37 38 // backwardCompatibleNames is mappging from dynamic Config fields (derived from currently known models) to 39 // hardcoded names (proto field name/json name) in hardcoded configurator.Config. This mapping should allow 40 // dynamically-created Config to read/write configuration from/to json/yaml files in the same way as it is 41 // for hardcoded configurator.Config. 42 var backwardCompatibleNames = map[string]names{ 43 "netallocConfig.IPAllocation": names{protoName: "ip_addresses", jsonName: "ipAddresses"}, 44 "linuxConfig.Interface": names{protoName: "interfaces", jsonName: "interfaces"}, 45 "linuxConfig.ARPEntry": names{protoName: "arp_entries", jsonName: "arpEntries"}, 46 "linuxConfig.Route": names{protoName: "routes", jsonName: "routes"}, 47 "linuxConfig.RuleChain": names{protoName: "RuleChain", jsonName: "RuleChain"}, 48 "vppConfig.ABF": names{protoName: "abfs", jsonName: "abfs"}, 49 "vppConfig.ACL": names{protoName: "acls", jsonName: "acls"}, 50 "vppConfig.SecurityPolicyDatabase": names{protoName: "ipsec_spds", jsonName: "ipsecSpds"}, 51 "vppConfig.SecurityPolicy": names{protoName: "ipsec_sps", jsonName: "ipsecSps"}, 52 "vppConfig.SecurityAssociation": names{protoName: "ipsec_sas", jsonName: "ipsecSas"}, 53 "vppConfig.TunnelProtection": names{protoName: "ipsec_tunnel_protections", jsonName: "ipsecTunnelProtections"}, 54 "vppConfig.Interface": names{protoName: "interfaces", jsonName: "interfaces"}, 55 "vppConfig.Span": names{protoName: "spans", jsonName: "spans"}, 56 "vppConfig.IPFIX": names{protoName: "ipfix_global", jsonName: "ipfixGlobal"}, 57 "vppConfig.FlowProbeParams": names{protoName: "ipfix_flowprobe_params", jsonName: "ipfixFlowprobeParams"}, 58 "vppConfig.FlowProbeFeature": names{protoName: "ipfix_flowprobes", jsonName: "ipfixFlowprobes"}, 59 "vppConfig.BridgeDomain": names{protoName: "bridge_domains", jsonName: "bridgeDomains"}, 60 "vppConfig.FIBEntry": names{protoName: "fibs", jsonName: "fibs"}, 61 "vppConfig.XConnectPair": names{protoName: "xconnect_pairs", jsonName: "xconnectPairs"}, 62 "vppConfig.ARPEntry": names{protoName: "arps", jsonName: "arps"}, 63 "vppConfig.Route": names{protoName: "routes", jsonName: "routes"}, 64 "vppConfig.ProxyARP": names{protoName: "proxy_arp", jsonName: "proxyArp"}, 65 "vppConfig.IPScanNeighbor": names{protoName: "ipscan_neighbor", jsonName: "ipscanNeighbor"}, 66 "vppConfig.VrfTable": names{protoName: "vrfs", jsonName: "vrfs"}, 67 "vppConfig.DHCPProxy": names{protoName: "dhcp_proxies", jsonName: "dhcpProxies"}, 68 "vppConfig.L3XConnect": names{protoName: "l3xconnects", jsonName: "l3xconnects"}, 69 "vppConfig.TeibEntry": names{protoName: "teib_entries", jsonName: "teibEntries"}, 70 "vppConfig.Nat44Global": names{protoName: "nat44_global", jsonName: "nat44Global"}, 71 "vppConfig.DNat44": names{protoName: "dnat44s", jsonName: "dnat44s"}, 72 "vppConfig.Nat44Interface": names{protoName: "nat44_interfaces", jsonName: "nat44Interfaces"}, 73 "vppConfig.Nat44AddressPool": names{protoName: "nat44_pools", jsonName: "nat44Pools"}, 74 "vppConfig.IPRedirect": names{protoName: "punt_ipredirects", jsonName: "puntIpredirects"}, 75 "vppConfig.ToHost": names{protoName: "punt_tohosts", jsonName: "puntTohosts"}, 76 "vppConfig.Exception": names{protoName: "punt_exceptions", jsonName: "puntExceptions"}, 77 "vppConfig.LocalSID": names{protoName: "srv6_localsids", jsonName: "srv6Localsids"}, 78 "vppConfig.Policy": names{protoName: "srv6_policies", jsonName: "srv6Policies"}, 79 "vppConfig.Steering": names{protoName: "srv6_steerings", jsonName: "srv6Steerings"}, 80 "vppConfig.SRv6Global": names{protoName: "srv6_global", jsonName: "srv6Global"}, 81 } 82 83 // NewDynamicConfig creates dynamically proto Message that contains all given configuration models(knowModels). 84 // This proto message(when all VPP-Agent models are given as input) is json/yaml compatible with 85 // configurator.Config. The configurator.Config config have all models hardcoded (generated from config 86 // proto model, but that model is hardcoded). Dynamic config can contain also custom 3rd party models 87 // and therefore can be used to import/export config data also for 3rd party models that are registered, but not 88 // part of VPP-Agent repository and therefore not know to hardcoded configurator.Config. 89 func NewDynamicConfig(knownModels []*models.ModelInfo) (*dynamicpb.Message, error) { 90 // create dependency registry 91 dependencyRegistry, err := createFileDescRegistry(knownModels) 92 if err != nil { 93 return nil, fmt.Errorf("cannot create dependency file descriptor registry due to: %w", err) 94 } 95 96 // get file descriptor proto for give known models 97 fileDP, rootMsgName, err := createDynamicConfigDescriptorProto(knownModels, dependencyRegistry) 98 if err != nil { 99 return nil, fmt.Errorf("cannot create descriptor proto for dynamic config due to: %v", err) 100 } 101 102 // convert file descriptor proto to file descriptor 103 fd, err := protodesc.NewFile(fileDP, dependencyRegistry) 104 if err != nil { 105 return nil, fmt.Errorf("cannot convert file descriptor proto to file descriptor due to: %v", err) 106 } 107 108 // get descriptor for config root message 109 rootMsg := fd.Messages().ByName(rootMsgName) 110 111 // create dynamic config message 112 return dynamicpb.NewMessage(rootMsg), nil 113 } 114 115 // createFileDescRegistry extracts file descriptors from given known models and returns them in convenient 116 // registry (in form of protodesc.Resolver). 117 func createFileDescRegistry(knownModels []*models.ModelInfo) (protodesc.Resolver, error) { 118 reg := &protoregistry.Files{} 119 for _, knownModel := range knownModels { 120 fileDesc := knownModel.MessageDescriptor.ParentFile() 121 if _, err := reg.FindDescriptorByName(fileDesc.FullName()); err == protoregistry.NotFound { 122 if e := reg.RegisterFile(fileDesc); e != nil { 123 logrus.DefaultLogger().Warnf("Failed to add Proto file %v "+ 124 "to dependency registry: %v.", fileDesc.Path(), e) 125 } else { 126 logrus.DefaultLogger().Debugf("Proto file %v was successfully "+ 127 "added to dependency registry.", fileDesc.Path()) 128 } 129 } 130 } 131 return reg, nil 132 } 133 134 // createDynamicConfigDescriptorProto creates descriptor proto for configuration. The construction of the descriptor 135 // proto is the way how the configuration from known models are added to the configuration proto message. 136 // The constructed file descriptor proto is used to get file descriptor that in turn can be used to instantiate 137 // proto message with all the configs from knownModels. This method conveniently provides also the configuration 138 // root message (proto file has many messages, but we need to know which one is the root for our configuration). 139 func createDynamicConfigDescriptorProto(knownModels []*ModelInfo, dependencyRegistry protodesc.Resolver) ( 140 fileDP *descriptorpb.FileDescriptorProto, rootMsgName protoreflect.Name, error error) { 141 142 // file descriptor proto for dynamic config proto model 143 fileDP = &descriptorpb.FileDescriptorProto{ 144 Syntax: proto.String("proto3"), 145 Name: proto.String("ligato/configurator/dynamicconfigurator.proto"), 146 Package: proto.String("ligato.configurator"), 147 } 148 149 // create config message 150 configDP := &descriptorpb.DescriptorProto{ 151 Name: proto.String("Dynamic_config"), 152 } 153 154 // add config message to proto file 155 fileDP.MessageType = []*descriptorpb.DescriptorProto{configDP} 156 157 // define configuration root (for users of this function) 158 rootMsgName = protoreflect.Name(*(configDP.Name)) 159 160 // fill dynamic message with given known models 161 configGroups := make(map[string]*descriptorpb.DescriptorProto) 162 importedDependency := make(map[string]struct{}) // just for deduplication checking 163 for _, modelDetail := range knownModels { 164 // get/create group config for this know model (all configs are grouped into groups based on their module) 165 configGroupName := DynamicConfigGroupFieldNaming(modelDetail) 166 configGroup, found := configGroups[configGroupName] 167 if !found { // create it! 168 // create new message that represents new config group 169 configGroup = &descriptorpb.DescriptorProto{ 170 Name: proto.String(configGroupName), 171 } 172 173 // add config group message to message definitions 174 fileDP.MessageType = append(fileDP.MessageType, configGroup) 175 176 // create reference to the new config group message from main config message 177 configDP.Field = append(configDP.Field, &descriptorpb.FieldDescriptorProto{ 178 Name: proto.String(configGroupName), 179 Number: proto.Int32(int32(len(configDP.Field) + 1)), 180 JsonName: proto.String(configGroupName), 181 Type: protoType(descriptorpb.FieldDescriptorProto_TYPE_MESSAGE), 182 TypeName: proto.String(fmt.Sprintf(".%v.%v", *fileDP.Package, *configGroup.Name)), 183 Label: protoLabel(descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL), 184 }) 185 186 // cache config group for reuse by other known models 187 configGroups[configGroupName] = configGroup 188 } 189 190 // fill config group message with currently handled known model 191 label := protoLabel(descriptorpb.FieldDescriptorProto_LABEL_REPEATED) 192 if !existsModelOptionFor("nameTemplate", modelDetail.Options) { 193 label = protoLabel(descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL) 194 } 195 protoName, jsonName := DynamicConfigKnownModelFieldNaming(modelDetail) 196 configGroup.Field = append(configGroup.Field, &descriptorpb.FieldDescriptorProto{ 197 Name: proto.String(protoName), 198 Number: proto.Int32(int32(len(configGroup.Field) + 1)), 199 JsonName: proto.String(jsonName), 200 Label: label, 201 Type: protoType(descriptorpb.FieldDescriptorProto_TYPE_MESSAGE), 202 TypeName: proto.String(fmt.Sprintf(".%v", modelDetail.ProtoName)), 203 }) 204 205 // add proto file dependency for this known model (+ check that it is in dependency file descriptor registry) 206 protoFile, err := ModelOptionFor("protoFile", modelDetail.Options) 207 if err != nil { 208 error = fmt.Errorf("cannot retrieve protoFile from model options "+ 209 "from model %v due to: %v", modelDetail.ProtoName, err) 210 return 211 } 212 if _, found := importedDependency[protoFile]; !found { 213 importedDependency[protoFile] = struct{}{} 214 215 // add proto file dependency for this known model 216 fileDP.Dependency = append(fileDP.Dependency, protoFile) 217 218 // checking dependency registry that should already contain the linked dependency 219 if _, err := dependencyRegistry.FindFileByPath(protoFile); err != nil { 220 if err == protoregistry.NotFound { 221 error = fmt.Errorf("proto file %v need to be referenced in dynamic config, but it "+ 222 "is not in dependency registry that was created from file descriptor proto input "+ 223 "(missing in input? check debug output from creating dependency registry) ", protoFile) 224 return 225 } 226 error = fmt.Errorf("cannot verify that proto file %v is in "+ 227 "dependency registry, it is due to: %v", protoFile, err) 228 return 229 } 230 } 231 } 232 return 233 } 234 235 // DynamicConfigGroupFieldNaming computes for given known model the naming of configuration group proto field 236 // containing the instances of given model inside the dynamic config describing the whole VPP-Agent configuration. 237 // The json name of the field is the same as proto name of field. 238 func DynamicConfigGroupFieldNaming(modelDetail *models.ModelInfo) string { 239 return fmt.Sprintf("%v%v", modulePrefix(models.ToSpec(modelDetail.Spec).ModelName()), configGroupSuffix) 240 } 241 242 // DynamicConfigKnownModelFieldNaming compute for given known model the (proto and json) naming of proto field 243 // containing the instances of given model inside the dynamic config describing the whole VPP-Agent configuration. 244 func DynamicConfigKnownModelFieldNaming(modelDetail *models.ModelInfo) (protoName, jsonName string) { 245 simpleProtoName := simpleProtoName(modelDetail.ProtoName) 246 configGroupName := DynamicConfigGroupFieldNaming(modelDetail) 247 compatibilityKey := fmt.Sprintf("%v.%v", configGroupName, simpleProtoName) 248 249 if newNames, found := backwardCompatibleNames[compatibilityKey]; found { 250 // using field names from hardcoded configurator.Config to achieve json/yaml backward compatibility 251 protoName = newNames.protoName 252 jsonName = newNames.jsonName 253 } else if !existsModelOptionFor("nameTemplate", modelDetail.Options) { 254 protoName = simpleProtoName 255 jsonName = simpleProtoName 256 } else { 257 protoName = simpleProtoName + repeatedFieldsSuffix 258 jsonName = simpleProtoName + repeatedFieldsSuffix 259 } 260 261 return protoName, jsonName 262 } 263 264 // DynamicConfigExport exports from dynamic config the proto.Messages corresponding to known models that 265 // were given as input when dynamic config was created using NewDynamicConfig. This is a convenient 266 // method how to extract data for generic client usage (proto.Message instances) from value-filled 267 // dynamic config (i.e. after json/yaml loading into dynamic config). 268 func DynamicConfigExport(dynamicConfig *dynamicpb.Message) ([]proto.Message, error) { 269 if dynamicConfig == nil { 270 return nil, fmt.Errorf("dynamic config cannot be nil") 271 } 272 273 // iterate over config group messages and extract proto message from them 274 result := make([]proto.Message, 0) 275 fields := dynamicConfig.Descriptor().Fields() 276 for i := 0; i < fields.Len(); i++ { 277 fieldName := fields.Get(i).Name() 278 if strings.HasSuffix(string(fieldName), configGroupSuffix) { 279 configGroupMessage := dynamicConfig.Get(fields.Get(i)).Message() 280 281 // handling export from inner config layers by using helper method 282 result = append(result, exportFromConfigGroupMessage(configGroupMessage)...) 283 } 284 } 285 return result, nil 286 } 287 288 // ExportDynamicConfigStructure is a debugging helper function revealing current structure of dynamic config. 289 // Debugging tools can't reveal that because dynamic config is dynamic proto message with no fields named by 290 // proto fields as it is in generated proto messages. 291 func ExportDynamicConfigStructure(dynamicConfig proto.Message) (string, error) { 292 // fill dynamic message with nothing (one proto message that will not map to anything), but relaying 293 // on side effect that will fill the structure with empty messages 294 anyProtoMessage := []proto.Message{&generic.Item{}} 295 util.PlaceProtosIntoProtos(anyProtoMessage, 1000, dynamicConfig) 296 297 // export dynamic config to json and then into yaml format 298 m := protojson.MarshalOptions{ 299 Indent: "", 300 // this will also fill non-Message fields (Message fields are filled by util.PlaceProtosIntoProtos side effect) 301 EmitUnpopulated: true, 302 } 303 b, err := m.Marshal(dynamicConfig) 304 if err != nil { 305 return "", fmt.Errorf("cannot marshal dynamic config to json due to: %v", err) 306 } 307 var jsonObj interface{} 308 err = yaml.UnmarshalWithOptions(b, &jsonObj, yaml.UseOrderedMap()) 309 if err != nil { 310 return "", fmt.Errorf("cannot convert dynamic config's json bytes to "+ 311 "json struct for yaml marshalling due to: %v", err) 312 } 313 bb, err := yaml.Marshal(jsonObj) 314 if err != nil { 315 return "", fmt.Errorf("cannot marshal dynamic config from json to yaml due to: %v", err) 316 } 317 return string(bb), nil 318 } 319 320 // exportFromConfigGroupMessage exports proto messages from config group message layer of dynamic config 321 func exportFromConfigGroupMessage(configGroupMessage protoreflect.Message) []proto.Message { 322 result := make([]proto.Message, 0) 323 fields := configGroupMessage.Descriptor().Fields() 324 for i := 0; i < fields.Len(); i++ { 325 groupField := fields.Get(i) 326 if groupField.IsList() { // repeated field 327 repeatedValue := configGroupMessage.Get(groupField).List() 328 for j := 0; j < repeatedValue.Len(); j++ { 329 result = append(result, repeatedValue.Get(j).Message().Interface()) 330 } 331 } else { // optional field (there are only optional and repeated fields) 332 fieldValue := configGroupMessage.Get(groupField) 333 if fieldValue.Message().IsValid() { 334 // use only non-nil real value (validity check used for this is implementation 335 // dependent, but there seems to be no other way) 336 result = append(result, fieldValue.Message().Interface()) 337 } 338 } 339 } 340 return result 341 } 342 343 func simpleProtoName(fullProtoName string) string { 344 nameSplit := strings.Split(fullProtoName, ".") 345 return nameSplit[len(nameSplit)-1] 346 } 347 348 // ModelOptionFor extracts value for given model detail option key 349 func ModelOptionFor(key string, options []*generic.ModelDetail_Option) (string, error) { 350 for _, option := range options { 351 if option.Key == key { 352 if len(option.Values) == 0 { 353 return "", fmt.Errorf("there is no value for key %v in model options", key) 354 } 355 if strings.TrimSpace(option.Values[0]) == "" { 356 return "", fmt.Errorf("there is no value(only empty string "+ 357 "after trimming) for key %v in model options", key) 358 } 359 return option.Values[0], nil 360 } 361 } 362 return "", fmt.Errorf("there is no model option with key %v (model options=%+v))", key, options) 363 } 364 365 func existsModelOptionFor(key string, options []*generic.ModelDetail_Option) bool { 366 _, err := ModelOptionFor(key, options) 367 return err == nil 368 } 369 370 func modulePrefix(modelName string) string { 371 return strings.Split(modelName, ".")[0] // modelname = modulname(it has modulname prefix) + simple name of model 372 } 373 374 func protoType(typ descriptorpb.FieldDescriptorProto_Type) *descriptorpb.FieldDescriptorProto_Type { 375 return &typ 376 } 377 378 func protoLabel(label descriptorpb.FieldDescriptorProto_Label) *descriptorpb.FieldDescriptorProto_Label { 379 return &label 380 }