go.ligato.io/vpp-agent/v3@v3.5.0/plugins/kvscheduler/graph.go (about) 1 package kvscheduler 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 "text/template" 11 "time" 12 13 "google.golang.org/protobuf/encoding/prototext" 14 15 "go.ligato.io/vpp-agent/v3/pkg/graphviz" 16 kvs "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/api" 17 "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/internal/graph" 18 "go.ligato.io/vpp-agent/v3/plugins/kvscheduler/internal/utils" 19 "go.ligato.io/vpp-agent/v3/proto/ligato/kvscheduler" 20 ) 21 22 const ( 23 minlen = 1 24 ) 25 26 func (s *Scheduler) renderDotOutput(graphNodes []*graph.RecordedNode, txn *kvs.RecordedTxn) ([]byte, error) { 27 title := fmt.Sprintf("%d keys", len(graphNodes)) 28 updatedKeys := utils.NewMapBasedKeySet() 29 graphTimestamp := time.Now() 30 if txn != nil { 31 graphTimestamp = txn.Stop 32 title += fmt.Sprintf(" - SeqNum: %d (%s)", txn.SeqNum, graphTimestamp.Format(time.RFC822)) 33 for _, op := range txn.Executed { 34 updatedKeys.Add(op.Key) 35 } 36 } else { 37 title += " - current" 38 } 39 40 cluster := NewDotCluster("nodes") 41 cluster.Attrs = dotAttrs{ 42 "bgcolor": "white", 43 "label": title, 44 "labelloc": "t", 45 "labeljust": "c", 46 "fontsize": "14", 47 "fontname": "Arial", 48 "tooltip": "", 49 } 50 51 // TODO: how to link transaction recording inside of the main cluster title (SeqNum: %d)? 52 //if txn != nil { 53 // cluster.Attrs["href"] = fmt.Sprintf(txnHistoryURL + "?seq-num=%d", txn.SeqNum) 54 //} 55 56 var ( 57 nodes []*dotNode 58 edges []*dotEdge 59 ) 60 61 nodeMap := make(map[string]*dotNode) 62 edgeMap := make(map[string]*dotEdge) 63 64 var getGraphNode = func(key string) *graph.RecordedNode { 65 for _, graphNode := range graphNodes { 66 if graphNode.Key == key { 67 return graphNode 68 } 69 } 70 return nil 71 } 72 73 var processGraphNode = func(graphNode *graph.RecordedNode) *dotNode { 74 key := graphNode.Key 75 if n, ok := nodeMap[key]; ok { 76 return n 77 } 78 79 attrs := make(dotAttrs) 80 attrs["penwidth"] = "1" 81 attrs["fontsize"] = "9" 82 attrs["width"] = "0" 83 attrs["height"] = "0" 84 attrs["color"] = "Black" 85 attrs["style"] = "filled" 86 attrs["href"] = fmt.Sprintf(keyTimelineURL+"?key=%s&time=%d", key, graphTimestamp.UnixNano()) 87 88 if updatedKeys.Has(key) { 89 attrs["penwidth"] = "2" 90 attrs["color"] = "Gold" 91 } 92 93 c := cluster 94 95 label := graphNode.Label 96 var descriptorName string 97 if descriptorFlag := graphNode.GetFlag(DescriptorFlagIndex); descriptorFlag != nil { 98 descriptorName = descriptorFlag.GetValue() 99 } else { 100 // for missing dependencies 101 if descriptor := s.registry.GetDescriptorForKey(key); descriptor != nil { 102 descriptorName = descriptor.Name 103 if descriptor.KeyLabel != nil { 104 label = descriptor.KeyLabel(key) 105 } 106 } 107 } 108 109 if label != "" { 110 attrs["label"] = label 111 } 112 113 if descriptorName != "" { 114 attrs["fillcolor"] = "PaleGreen" 115 116 if _, ok := c.Clusters[descriptorName]; !ok { 117 c.Clusters[descriptorName] = &dotCluster{ 118 ID: descriptorName, 119 Clusters: make(map[string]*dotCluster), 120 Attrs: dotAttrs{ 121 "fontsize": "10", 122 "label": fmt.Sprintf("< %s >", descriptorName), 123 "style": "filled", 124 "fillcolor": "#e6ecfa", 125 "pad": "0.015", 126 "margin": "4", 127 }, 128 } 129 } 130 c = c.Clusters[descriptorName] 131 } 132 133 var ( 134 dashedStyle bool 135 valueState kvscheduler.ValueState 136 ) 137 isDerived := graphNode.GetFlag(DerivedFlagIndex) != nil 138 stateFlag := graphNode.GetFlag(ValueStateFlagIndex) 139 if stateFlag != nil { 140 valueState = stateFlag.(*ValueStateFlag).valueState 141 } 142 143 // set colors 144 switch valueState { 145 case kvscheduler.ValueState_NONEXISTENT: 146 attrs["fontcolor"] = "White" 147 attrs["fillcolor"] = "Black" 148 case kvscheduler.ValueState_MISSING: 149 attrs["fillcolor"] = "Dimgray" 150 dashedStyle = true 151 case kvscheduler.ValueState_UNIMPLEMENTED: 152 attrs["fillcolor"] = "Darkkhaki" 153 dashedStyle = true 154 case kvscheduler.ValueState_REMOVED: 155 attrs["fontcolor"] = "White" 156 attrs["fillcolor"] = "Black" 157 dashedStyle = true 158 // case kvs.ValueState_CONFIGURED // leave default 159 case kvscheduler.ValueState_OBTAINED: 160 attrs["fillcolor"] = "LightCyan" 161 case kvscheduler.ValueState_DISCOVERED: 162 attrs["fillcolor"] = "Lime" 163 case kvscheduler.ValueState_PENDING: 164 dashedStyle = true 165 attrs["fillcolor"] = "Pink" 166 case kvscheduler.ValueState_INVALID: 167 attrs["fontcolor"] = "White" 168 attrs["fillcolor"] = "Maroon" 169 case kvscheduler.ValueState_FAILED: 170 attrs["fillcolor"] = "Orangered" 171 case kvscheduler.ValueState_RETRYING: 172 attrs["fillcolor"] = "Deeppink" 173 } 174 if isDerived && ((valueState == kvscheduler.ValueState_CONFIGURED) || 175 (valueState == kvscheduler.ValueState_OBTAINED) || 176 (valueState == kvscheduler.ValueState_DISCOVERED)) { 177 attrs["fillcolor"] = "LightYellow" 178 attrs["color"] = "bisque4" 179 } 180 181 // set style 182 attrs["style"] = "filled" 183 if isDerived { 184 attrs["style"] += ",rounded" 185 } 186 if dashedStyle { 187 attrs["style"] += ",dashed" 188 } 189 190 value := graphNode.Value 191 if rec, ok := value.(*utils.RecordedProtoMessage); ok { 192 value = rec.Message 193 } 194 attrs["tooltip"] = fmt.Sprintf("[%s] %s\n-----\n%s", valueState, key, prototext.Format(value)) 195 196 n := &dotNode{ 197 ID: key, 198 Attrs: attrs, 199 } 200 c.Nodes = append(c.Nodes, n) 201 nodeMap[key] = n 202 return n 203 } 204 205 var addEdge = func(e *dotEdge) { 206 edgeKey := fmt.Sprintf("%s->%s", e.From.ID, e.To.ID) 207 if _, ok := edgeMap[edgeKey]; !ok { 208 edges = append(edges, e) 209 edgeMap[edgeKey] = e 210 } 211 } 212 213 for _, graphNode := range graphNodes { 214 n := processGraphNode(graphNode) 215 targets := graphNode.Targets 216 217 for i := targets.RelationBegin(DerivesRelation); i < len(targets); i++ { 218 if targets[i].Relation != DerivesRelation { 219 break 220 } 221 for _, dKey := range targets[i].MatchingKeys.Iterate() { 222 dn := processGraphNode(getGraphNode(dKey)) 223 attrs := make(dotAttrs) 224 attrs["color"] = "bisque4" 225 attrs["arrowhead"] = "invempty" 226 e := &dotEdge{ 227 From: n, 228 To: dn, 229 Attrs: attrs, 230 } 231 addEdge(e) 232 } 233 } 234 235 for i := targets.RelationBegin(DependencyRelation); i < len(targets); i++ { 236 target := targets[i] 237 if target.Relation != DependencyRelation { 238 break 239 } 240 type depNode struct { 241 node *dotNode 242 label string 243 satisfied bool 244 } 245 var deps []depNode 246 if target.MatchingKeys.Length() == 0 { 247 var dn *dotNode 248 if target.ExpectedKey != "" { 249 dn = processGraphNode(&graph.RecordedNode{ 250 Key: target.ExpectedKey, 251 }) 252 } else { 253 dn = processGraphNode(&graph.RecordedNode{ 254 Key: "? " + target.Label + " ?", 255 }) 256 } 257 deps = append(deps, depNode{node: dn, label: target.Label}) 258 } 259 for _, dKey := range target.MatchingKeys.Iterate() { 260 dn := processGraphNode(getGraphNode(dKey)) 261 deps = append(deps, depNode{node: dn, label: target.Label, satisfied: true}) 262 } 263 for _, d := range deps { 264 attrs := make(dotAttrs) 265 attrs["tooltip"] = d.label 266 if !d.satisfied { 267 attrs["color"] = "Red" 268 } 269 e := &dotEdge{ 270 From: n, 271 To: d.node, 272 Attrs: attrs, 273 } 274 addEdge(e) 275 } 276 } 277 } 278 279 hostname, _ := os.Hostname() 280 footer := fmt.Sprintf("KVScheduler Graph - generated at %s on %s (PID: %d)", 281 time.Now().Format(time.RFC1123), hostname, os.Getpid(), 282 ) 283 284 dot := &dotGraph{ 285 Title: footer, 286 Minlen: minlen, 287 Cluster: cluster, 288 Nodes: nodes, 289 Edges: edges, 290 Options: map[string]string{ 291 "minlen": fmt.Sprint(minlen), 292 }, 293 } 294 295 var buf bytes.Buffer 296 if err := WriteDot(&buf, dot); err != nil { 297 return nil, err 298 } 299 300 return buf.Bytes(), nil 301 } 302 303 func validateDot(output []byte) ([]byte, error) { 304 dot, err := graphviz.RenderDot(output) 305 if err != nil { 306 return nil, fmt.Errorf("rendering dot failed: %v\nRaw output:%s", err, output) 307 } 308 return dot, nil 309 } 310 311 func dotToImage(outfname string, format string, dot []byte) (string, error) { 312 var img string 313 if outfname == "" { 314 img = filepath.Join(os.TempDir(), fmt.Sprintf("kvscheduler-graph.%s", format)) 315 } else { 316 img = fmt.Sprintf("%s.%s", outfname, format) 317 } 318 319 err := graphviz.RenderFilename(img, format, dot) 320 if err != nil { 321 return "", err 322 } 323 324 return img, nil 325 } 326 327 const tmplGraph = `digraph kvscheduler { 328 label="{{.Title}}"; 329 labelloc="b"; 330 labeljust="c"; 331 fontsize="10"; 332 rankdir="LR"; 333 bgcolor="lightgray"; 334 style="solid"; 335 pad="0.035"; 336 ranksep="0.35"; 337 nodesep="0.03"; 338 //nodesep="{{.Options.nodesep}}"; 339 ordering="out"; 340 newrank="true"; 341 compound="true"; 342 343 node [shape="box" style="filled" color="black" fontname="Courier" fillcolor="honeydew"]; 344 edge [minlen="{{.Options.minlen}}"] 345 346 {{template "cluster" .Cluster}} 347 348 {{- range .Edges}} 349 {{template "edge" .}} 350 {{- end}} 351 352 {{range .Nodes}} 353 {{template "node" .}} 354 {{- end}} 355 } 356 ` 357 const tmplNode = `{{define "edge" -}} 358 {{printf "%q -> %q [ %s ]" .From .To .Attrs}} 359 {{- end}}` 360 361 const tmplEdge = `{{define "node" -}} 362 {{printf "%q [ %s ]" .ID .Attrs}} 363 {{- end}}` 364 365 const tmplCluster = `{{define "cluster" -}} 366 {{printf "subgraph %q {" .}} 367 {{printf "%s" .Attrs.Lines}} 368 {{range .Nodes}} 369 {{template "node" .}} 370 {{- end}} 371 {{range .Clusters}} 372 {{template "cluster" .}} 373 {{- end}} 374 {{println "}" }} 375 {{- end}}` 376 377 type dotGraph struct { 378 Title string 379 Minlen uint 380 Attrs dotAttrs 381 Cluster *dotCluster 382 Nodes []*dotNode 383 Edges []*dotEdge 384 Options map[string]string 385 } 386 387 type dotCluster struct { 388 ID string 389 Clusters map[string]*dotCluster 390 Nodes []*dotNode 391 Attrs dotAttrs 392 } 393 394 type dotNode struct { 395 ID string 396 Attrs dotAttrs 397 } 398 399 type dotEdge struct { 400 From *dotNode 401 To *dotNode 402 Attrs dotAttrs 403 } 404 405 type dotAttrs map[string]string 406 407 func NewDotCluster(id string) *dotCluster { 408 return &dotCluster{ 409 ID: id, 410 Clusters: make(map[string]*dotCluster), 411 Attrs: make(dotAttrs), 412 } 413 } 414 415 func (c *dotCluster) String() string { 416 return fmt.Sprintf("cluster_%s", c.ID) 417 } 418 func (n *dotNode) String() string { 419 return n.ID 420 } 421 422 func (p dotAttrs) List() []string { 423 l := []string{} 424 for k, v := range p { 425 l = append(l, fmt.Sprintf("%s=%q", k, v)) 426 } 427 return l 428 } 429 430 func (p dotAttrs) String() string { 431 return strings.Join(p.List(), " ") 432 } 433 434 func (p dotAttrs) Lines() string { 435 return fmt.Sprintf("%s;", strings.Join(p.List(), ";\n")) 436 } 437 438 func WriteDot(w io.Writer, g *dotGraph) error { 439 t := template.New("dot") 440 for _, s := range []string{tmplCluster, tmplNode, tmplEdge, tmplGraph} { 441 if _, err := t.Parse(s); err != nil { 442 return err 443 } 444 } 445 var buf bytes.Buffer 446 if err := t.Execute(&buf, g); err != nil { 447 return err 448 } 449 _, err := buf.WriteTo(w) 450 return err 451 }