From b7e46247732afda7a93cba67761195b0d3d1827e Mon Sep 17 00:00:00 2001 From: hellt Date: Sat, 23 Jan 2021 18:48:03 +0200 Subject: [PATCH] make graph possible in offline mode --- cmd/graph.go | 89 +++++++++++++++++++++++++++++++++-------------- docs/cmd/graph.md | 87 ++++++++++++++++++++++++--------------------- 2 files changed, 111 insertions(+), 65 deletions(-) diff --git a/cmd/graph.go b/cmd/graph.go index 119092f8d..1858957fd 100644 --- a/cmd/graph.go +++ b/cmd/graph.go @@ -9,6 +9,7 @@ import ( "sort" "strings" + "github.com/docker/docker/api/types" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/srl-wim/container-lab/clab" @@ -20,6 +21,8 @@ const ( var srv string var tmpl string +var offline bool +var dot bool type graphTopo struct { Nodes []containerDetails `json:"nodes,omitempty"` @@ -41,6 +44,7 @@ type topoData struct { var graphCmd = &cobra.Command{ Use: "graph", Short: "generate a topology graph", + Long: "generate topology graph based on the topology definition file and running containers\nreference: https://containerlab.srlinux.dev/cmd/graph/", RunE: func(cmd *cobra.Command, args []string) error { opts := []clab.ClabOption{ @@ -56,7 +60,7 @@ var graphCmd = &cobra.Command{ return err } - if srv == "" { + if dot { if err := c.GenerateGraph(topo); err != nil { return err } @@ -66,34 +70,28 @@ var graphCmd = &cobra.Command{ Nodes: make([]containerDetails, 0, len(c.Nodes)), Links: make([]link, 0, len(c.Links)), } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() - containers, err := c.ListContainers(ctx, []string{fmt.Sprintf("containerlab=lab-%s", c.Config.Name)}) - if err != nil { - log.Errorf("could not list containers: %v", err) - } - log.Debugf("found %d containers", len(containers)) - if len(containers) == 0 { - return fmt.Errorf("no containers found, is the lab deployed?") - } - for _, cont := range containers { - var name string - if len(cont.Names) > 0 { - name = strings.TrimPrefix(cont.Names[0], fmt.Sprintf("/clab-%s-", c.Config.Name)) - } - log.Debugf("looking for node name %s", name) - if node, ok := c.Nodes[name]; ok { - gtopo.Nodes = append(gtopo.Nodes, containerDetails{ - Name: name, - Kind: node.Kind, - Image: cont.Image, - Group: node.Group, - State: fmt.Sprintf("%s/%s", cont.State, cont.Status), - IPv4Address: getContainerIPv4(cont, c.Config.Mgmt.Network), - IPv6Address: getContainerIPv6(cont, c.Config.Mgmt.Network), - }) + + var containers []types.Container + // if offline mode is not enforced, list containers matching lab name + if !offline { + var err error + containers, err = c.ListContainers(ctx, []string{fmt.Sprintf("containerlab=lab-%s", c.Config.Name)}) + if err != nil { + log.Errorf("could not list containers: %v", err) } + log.Debugf("found %d containers", len(containers)) + } + + switch { + case len(containers) == 0: + buildGraphFromTopo(>opo, c) + case len(containers) > 0: + buildGraphFromDeployedLab(>opo, c, containers) } + sort.Slice(gtopo.Nodes, func(i, j int) bool { return gtopo.Nodes[i].Name < gtopo.Nodes[j].Name }) @@ -128,8 +126,47 @@ var graphCmd = &cobra.Command{ }, } +func buildGraphFromTopo(g *graphTopo, c *clab.CLab) { + log.Info("building graph from topology file") + for _, node := range c.Nodes { + g.Nodes = append(g.Nodes, containerDetails{ + Name: node.ShortName, + Kind: node.Kind, + Image: node.Image, + Group: node.Group, + State: "N/A", + IPv4Address: node.MgmtIPv4Address, + IPv6Address: node.MgmtIPv6Address, + }) + } + +} + +func buildGraphFromDeployedLab(g *graphTopo, c *clab.CLab, containers []types.Container) { + for _, cont := range containers { + var name string + if len(cont.Names) > 0 { + name = strings.TrimPrefix(cont.Names[0], fmt.Sprintf("/clab-%s-", c.Config.Name)) + } + log.Debugf("looking for node name %s", name) + if node, ok := c.Nodes[name]; ok { + g.Nodes = append(g.Nodes, containerDetails{ + Name: name, + Kind: node.Kind, + Image: cont.Image, + Group: node.Group, + State: fmt.Sprintf("%s/%s", cont.State, cont.Status), + IPv4Address: getContainerIPv4(cont, c.Config.Mgmt.Network), + IPv6Address: getContainerIPv6(cont, c.Config.Mgmt.Network), + }) + } + } +} + func init() { rootCmd.AddCommand(graphCmd) - graphCmd.Flags().StringVarP(&srv, "srv", "s", "", "HTTP server address to view, customize and export your topology") + graphCmd.Flags().StringVarP(&srv, "srv", "s", ":50080", "HTTP server address to view, customize and export your topology") + graphCmd.Flags().BoolVarP(&offline, "offline", "o", false, "use only information from topo file when building graph") + graphCmd.Flags().BoolVarP(&dot, "dot", "", false, "generate dot file instead of launching the web server") graphCmd.Flags().StringVarP(&tmpl, "template", "", defaultTemplatePath, "Go html template used to generate the graph") } diff --git a/docs/cmd/graph.md b/docs/cmd/graph.md index a503f17ab..899456ba1 100644 --- a/docs/cmd/graph.md +++ b/docs/cmd/graph.md @@ -2,7 +2,7 @@ ### Description -The `graph` command generates graphical representaitons of the deployed topology. +The `graph` command generates graphical representations of the topology. Two graphing options are available: @@ -11,43 +11,46 @@ Two graphing options are available: #### HTML -The HTML based graph is created by rendering a [Go HTML](https://golang.org/pkg/html/template/) template against a `struct` containing the topology name as well as a `json` string where 2 lists are present: `nodes` and `links` . +The HTML based graph is the default graphing option. The topology will be graphed and served online using the embedded web server. + +The graph is created by rendering a [Go HTML](https://golang.org/pkg/html/template/) template against a data structure containing the topology name as well as a `json` string where 2 lists are present: `nodes` and `links` . `nodes` contains data about the lab nodes, such as name, kind, type, image, state, IP addresses,... `links` contains the list of links defined by source node and target node, as well as the endpoint names -```json -{ - "nodes": [ - { - "name": "node1-1", - "image": "srlinux:20.6.1-286", - "kind": "srl", - "group": "tier-1", - "state": "running/Up 21 seconds", - "ipv4_address": "172.23.23.3/24", - "ipv6_address": "2001:172:23:23::3/80" - }, - // omitted rest of nodes - ], - "links": [ +???info "example of the json string" + ```json { - "source": "node1-2", - "source_endpoint": "e1-1", - "target": "node2-1", - "target_endpoint": "e1-2" - }, - { - "source": "node2-1", - "source_endpoint": "e1-4", - "target": "node3-1", - "target_endpoint": "e1-1" - }, - // omitted rest of links - ] -} -``` + "nodes": [ + { + "name": "node1-1", + "image": "srlinux:20.6.1-286", + "kind": "srl", + "group": "tier-1", + "state": "running/Up 21 seconds", + "ipv4_address": "172.23.23.3/24", + "ipv6_address": "2001:172:23:23::3/80" + }, + // omitted rest of nodes + ], + "links": [ + { + "source": "node1-2", + "source_endpoint": "e1-1", + "target": "node2-1", + "target_endpoint": "e1-2" + }, + { + "source": "node2-1", + "source_endpoint": "e1-4", + "target": "node3-1", + "target_endpoint": "e1-1" + }, + // the rest is omitted + ] + } + ``` Within the template, Javascript libraries such as [**d3js directed force graph**](https://observablehq.com/collection/@d3/d3-force) or [**vis.js network**](https://visjs.github.io/vis-network/docs/network/) can be used to generate custom topology graphs. @@ -63,19 +66,25 @@ When `graph` command is called without the `--srv` flag, containerlab will gener The dot file can be used to view the graphical representation of the topology either by rendering the dot file into a PNG file or using [online dot viewer](https://dreampuf.github.io/GraphvizOnline/). +### Online vs offline graphing +When HTML graph option is used, containerlab will try to build the topology graph by inspecting the running containers which are part of the lab. This essentially means, that the lab must be running. Although this method provides some additional details (like IP addresses), it is not always convenient to run a lab to see its graph. + +The other option is to use the topology file solely to build the graph. This is done by adding `--offline` flag. + +If `--offline` flag was not provided and no containers were found matching the lab name, containerlab will use the topo file only (as if offline mode was set). ### Usage `containerlab [global-flags] graph [local-flags]` ### Flags -#### name +#### topology -With the global `--name | -n` flag a user sets the name of the lab that will be graphed. +With the global `--topo | -t` flag a user sets the path to the topology file that will be used to get the . #### srv -The `--srv` flag enables the HTML graph representation if present, it specifies an address a HTTP server will listen to. +The `--srv` flag allows a user to customize the HTTP address and port for the web server. Default value is `:50080`. A single path `/` is served, where the graph is generated based on either a default template or on the template supplied using `--template`. @@ -83,17 +92,17 @@ A single path `/` is served, where the graph is generated based on either a defa The `--template` flag allows to customize the HTML based graph by supplying a user defined template that will be rendered and exposed on the address specified by `--srv`. +#### dot +With `--dot` flag provided containerlab will generate the `dot` file instead of serving the topology with embedded HTTP server. ### Examples ```bash -# generate a graph description file in dot format for topo1 topology +# render a graph from running lab or topo file if lab is not running# +# using HTML graph option with default server address :50080 containerlab graph --topo /path/to/topo1.yaml -# start an http server on :3002 where topo1 graph will be rendered using the default template -containerlab graph --topo /path/to/topo1.yaml --srv ":3002" - # start an http server on :3002 where topo1 graph will be rendered using a custom template my_template.html containerlab graph --topo /path/to/topo1.yaml --srv ":3002" --template my_template.html ```