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  }