|
| 1 | +// Copyright 2025 Google LLC |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +// Package solver provides a library for solving a testbed reservation request |
| 16 | +// against a given inventory of available devices and their connections. |
| 17 | +package solver |
| 18 | + |
| 19 | +import ( |
| 20 | + "fmt" |
| 21 | + |
| 22 | + "golang.org/x/net/context" |
| 23 | + |
| 24 | + "github.com/openconfig/ondatra/binding" |
| 25 | + "github.com/openconfig/ondatra/binding/portgraph" |
| 26 | + opb "github.com/openconfig/ondatra/proto" |
| 27 | +) |
| 28 | + |
| 29 | +// Inventory of available binding devices and links. |
| 30 | +// This struct is provided by a specific binding implementation. |
| 31 | +type Inventory struct { |
| 32 | + DUTs []binding.DUT |
| 33 | + ATEs []binding.ATE |
| 34 | + Links map[*binding.Port]*binding.Port // Maps a port to its peer. |
| 35 | +} |
| 36 | + |
| 37 | +// SolveResult contains the result of a successful testbed solve. |
| 38 | +type SolveResult struct { |
| 39 | + Assignment *portgraph.Assignment |
| 40 | + AbsNode2Dev map[*portgraph.AbstractNode]*opb.Device |
| 41 | + AbsPort2Port map[*portgraph.AbstractPort]*opb.Port |
| 42 | + ConNode2BindDev map[*portgraph.ConcreteNode]*binding.Device |
| 43 | + ConPort2BindPort map[*portgraph.ConcretePort]*binding.Port |
| 44 | +} |
| 45 | + |
| 46 | +// Solve finds an assignment to the provided testbed from the given inventory. |
| 47 | +// It returns the low-level portgraph.Assignment and maps to correlate |
| 48 | +// graph elements back to the original binding and testbed elements. |
| 49 | +func Solve(ctx context.Context, tb *opb.Testbed, inv *Inventory, partial map[string]string) (*SolveResult, error) { |
| 50 | + abstractGraph, absNode2Dev, absPort2Port, err := portgraph.TestbedToAbstractGraph(tb, partial) |
| 51 | + if err != nil { |
| 52 | + return nil, fmt.Errorf("could not parse specified testbed: %w", err) |
| 53 | + } |
| 54 | + |
| 55 | + superGraph, conNode2BindDev, conPort2BindPort, err := inventoryToConcreteGraph(inv) |
| 56 | + if err != nil { |
| 57 | + return nil, fmt.Errorf("could not convert inventory to concrete graph: %w", err) |
| 58 | + } |
| 59 | + |
| 60 | + assignment, err := portgraph.Solve(ctx, abstractGraph, superGraph) |
| 61 | + if err != nil { |
| 62 | + return nil, fmt.Errorf("could not solve for specified testbed: %w", err) |
| 63 | + } |
| 64 | + |
| 65 | + return &SolveResult{ |
| 66 | + Assignment: assignment, |
| 67 | + AbsNode2Dev: absNode2Dev, |
| 68 | + AbsPort2Port: absPort2Port, |
| 69 | + ConNode2BindDev: conNode2BindDev, |
| 70 | + ConPort2BindPort: conPort2BindPort, |
| 71 | + }, nil |
| 72 | +} |
| 73 | + |
| 74 | +// inventoryToConcreteGraph converts the generic Inventory struct to a portgraph.ConcreteGraph. |
| 75 | +func inventoryToConcreteGraph(inv *Inventory) (*portgraph.ConcreteGraph, map[*portgraph.ConcreteNode]*binding.Device, map[*portgraph.ConcretePort]*binding.Port, error) { |
| 76 | + cg := &portgraph.ConcreteGraph{Desc: "Generic Inventory"} |
| 77 | + conNode2BindDev := make(map[*portgraph.ConcreteNode]*binding.Device) |
| 78 | + conPort2BindPort := make(map[*portgraph.ConcretePort]*binding.Port) |
| 79 | + bindPortToConPort := make(map[*binding.Port]*portgraph.ConcretePort) |
| 80 | + |
| 81 | + addDevice := func(dev binding.Device, role string) error { |
| 82 | + var dims *binding.Dims |
| 83 | + // Type assert to interface with Dims() to access device dimensions |
| 84 | + switch d := dev.(type) { |
| 85 | + case interface{ Dims() *binding.Dims }: |
| 86 | + dims = d.Dims() |
| 87 | + default: |
| 88 | + return fmt.Errorf("device %s (%T) does not provide a Dims() method", dev.Name(), dev) |
| 89 | + } |
| 90 | + |
| 91 | + if dims == nil { |
| 92 | + return fmt.Errorf("device %s has nil Dims", dev.Name()) |
| 93 | + } |
| 94 | + attrs := map[string]string{ |
| 95 | + portgraph.RoleAttr: role, |
| 96 | + portgraph.VendorAttr: dev.Vendor().String(), |
| 97 | + portgraph.HWAttr: dev.HardwareModel(), |
| 98 | + portgraph.SWAttr: dev.SoftwareVersion(), |
| 99 | + portgraph.NameAttr: dev.Name(), |
| 100 | + } |
| 101 | + |
| 102 | + var ports []*portgraph.ConcretePort |
| 103 | + // Accessing ports using the map from the Dims struct. |
| 104 | + for portID, bp := range dims.Ports { |
| 105 | + if bp == nil { |
| 106 | + return fmt.Errorf("device %s port ID %s has a nil *binding.Port", dev.Name(), portID) |
| 107 | + } |
| 108 | + pAttrs := map[string]string{ |
| 109 | + portgraph.NameAttr: bp.Name, |
| 110 | + portgraph.SpeedAttr: bp.Speed.String(), |
| 111 | + portgraph.PMDAttr: bp.PMD.String(), |
| 112 | + } |
| 113 | + cp := &portgraph.ConcretePort{ |
| 114 | + // Desc should be unique for each concrete port. |
| 115 | + Desc: fmt.Sprintf("%s:%s", dev.Name(), bp.Name), |
| 116 | + Attrs: pAttrs, |
| 117 | + } |
| 118 | + ports = append(ports, cp) |
| 119 | + conPort2BindPort[cp] = bp |
| 120 | + bindPortToConPort[bp] = cp |
| 121 | + } |
| 122 | + |
| 123 | + node := &portgraph.ConcreteNode{ |
| 124 | + Desc: dev.Name(), |
| 125 | + Ports: ports, |
| 126 | + Attrs: attrs, |
| 127 | + } |
| 128 | + cg.Nodes = append(cg.Nodes, node) |
| 129 | + conNode2BindDev[node] = &dev |
| 130 | + return nil |
| 131 | + } |
| 132 | + |
| 133 | + for _, dut := range inv.DUTs { |
| 134 | + if err := addDevice(dut, portgraph.RoleDUT); err != nil { |
| 135 | + return nil, nil, nil, err |
| 136 | + } |
| 137 | + } |
| 138 | + for _, ate := range inv.ATEs { |
| 139 | + if err := addDevice(ate, portgraph.RoleATE); err != nil { |
| 140 | + return nil, nil, nil, err |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + edgeSet := make(map[struct { |
| 145 | + Src *portgraph.ConcretePort |
| 146 | + Dst *portgraph.ConcretePort |
| 147 | + }]bool) |
| 148 | + for srcBP, dstBP := range inv.Links { |
| 149 | + srcCP, srcOk := bindPortToConPort[srcBP] |
| 150 | + if !srcOk { |
| 151 | + return nil, nil, nil, fmt.Errorf("link source port %s not found in inventory device ports", srcBP.Name) |
| 152 | + } |
| 153 | + dstCP, dstOk := bindPortToConPort[dstBP] |
| 154 | + if !dstOk { |
| 155 | + return nil, nil, nil, fmt.Errorf("link destination port %s not found in inventory device ports", dstBP.Name) |
| 156 | + } |
| 157 | + |
| 158 | + // Ensure we only add each edge once by creating a canonical key. |
| 159 | + key := struct { |
| 160 | + Src *portgraph.ConcretePort |
| 161 | + Dst *portgraph.ConcretePort |
| 162 | + }{Src: srcCP, Dst: dstCP} |
| 163 | + if srcCP.Desc > dstCP.Desc { |
| 164 | + key = struct { |
| 165 | + Src *portgraph.ConcretePort |
| 166 | + Dst *portgraph.ConcretePort |
| 167 | + }{Src: dstCP, Dst: srcCP} |
| 168 | + } |
| 169 | + |
| 170 | + if !edgeSet[key] { |
| 171 | + cg.Edges = append(cg.Edges, &portgraph.ConcreteEdge{Src: key.Src, Dst: key.Dst}) |
| 172 | + edgeSet[key] = true |
| 173 | + } |
| 174 | + } |
| 175 | + return cg, conNode2BindDev, conPort2BindPort, nil |
| 176 | +} |
0 commit comments