GET VRNI GRAPHS DIRECTLY FROM A KUBECTL VRNI COMMAND!
Hi, today in this blog, I’m going to explain how to develop a small kubectl plugin. As you may know, VMware has a wonderful solution to collect and analyze network flows from many different sources, including Kubernetes: vRealize Network Insight (aka vRNI). My objective is to make this rich information easily and quickly available to a kube admin, thanks to a kubectl command.
What I want to achieve in this part 1 is a kubectl vrni flow IP command that will display some charts representing the flows for this specific IP (source or destination). This information can help to troubleshoot any networking issue. I will handle Kubernetes POD to IP translation in part 2.
Here are the different steps to code:
I’m going to develop this code in Go. Because we’ll have to deal with Kubernetes API in Part 2, Go is the best language to start with.
Let’s start by creating a client that we’ll use to speak to vRNI:
1package main
2
3import (
4 "bytes"
5 "crypto/tls"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "github.com/guptarohit/asciigraph"
10 "io/ioutil"
11 "net/http"
12 "os"
13 "net/http"
14 "time"
15)
16
17const (
18 // BaseURL ...
19 BaseURL = "https://field-demo.vrni.cmbu.local/api/ni"
20)
21
22type Client struct {
23 BaseURL string
24 token string
25 HTTPClient *http.Client
26}
27
28type errorResponse struct {
29 Code int `json:"code"`
30 Message string `json:"message"`
31}
32
33type successResponse struct {
34 Code int `json:"code"`
35 Data interface{} `json:"data"`
36}
37
38type authResponse struct {
39 Token string `json:"token"`
40 Expiry int64 `json:"expiry"`
41}
42
43// Domain ...
44type Domain struct {
45 DomainType string `json:"domain_type"`
46 Value string `json:"value,omitempty"`
47}
48
49// AuthBody ...
50type AuthBody struct {
51 Username string `json:"username"`
52 Password string `json:"password"`
53 Domain Domain `json:"domain"`
54}
55
56// NewClient ...
57func NewClient() *Client {
58 return &Client{
59 BaseURL: BaseURL,
60 token: "",
61 HTTPClient: &http.Client{
62 Timeout: time.Minute,
63 },
64 }
65}
66
67func (c *Client) sendRequest(req *http.Request) ([]byte, error) {
68 req.Header.Set("Content-Type", "application/json; charset=utf-8")
69 req.Header.Set("Accept", "application/json; charset=utf-8")
70 req.Header.Set("Authorization", fmt.Sprintf("NetworkInsight %s", c.token))
71
72 http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
73 res, err := c.HTTPClient.Do(req)
74 if err != nil {
75 return nil, err
76 }
77
78 defer res.Body.Close()
79
80 if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
81 var errRes errorResponse
82 if err = json.NewDecoder(res.Body).Decode(&errRes); err == nil {
83 return nil, errors.New(errRes.Message)
84 }
85
86 return nil, fmt.Errorf("unknown error, status code: %d", res.StatusCode)
87 }
88
89 body, err := ioutil.ReadAll(res.Body)
90 if err != nil {
91 return nil, err
92 }
93
94 return body, nil
95}
96
97// Auth authenticates vRNI requests
98func Auth(c *Client) string {
99 var err error
100
101 // Build request body
102 authBody := &AuthBody{
103 Username: "**********",
104 Password: "**********",
105 Domain: Domain{
106 DomainType: "LDAP", // Otherwise = LOCAL
107 Value: "****", // Otherwise = none
108 },
109 }
110 buf := new(bytes.Buffer)
111 json.NewEncoder(buf).Encode(authBody)
112
113 // Build the request
114 req, err := http.NewRequest("POST", fmt.Sprintf("%s/auth/token", c.BaseURL), buf)
115 if err != nil {
116 fmt.Println(err.Error())
117 return ""
118 }
119
120 // Send the request
121 res, err := c.sendRequest(req)
122 if err != nil {
123 fmt.Println(err.Error())
124 return ""
125 }
126
127 // Decode the response
128 var fullResponse authResponse
129 if err = json.Unmarshal(res, &fullResponse); err != nil {
130 fmt.Println("error decoding")
131 fmt.Println(err)
132 return ""
133 }
134
135 c.token = fullResponse.Token
136 return fullResponse.Token
137
138}
What we’ve done in this piece of code is:
- define a sendRequest function to be able to send the REST API call to vRNI
- call /auth/token method to invoke the creation of a token
- get the token and set it in the client, so all subsequent requests will add a NetworkInsight token header as an authentication mechanism
Then, we need to search the flows for a specific IP. We’re going to use /search method for that part.
1// SearchBody ...
2type SearchBody struct {
3 EntityType string `json:"entity_type"`
4 Filter string `json:"filter"`
5}
6
7// SearchResult ...
8type SearchResult struct {
9 EntityID string `json:"entity_id"`
10 EntityType string `json:"entity_type"`
11 Time int64 `json:"time"`
12}
13
14// SearchResults ...
15type SearchResults struct {
16 Results []SearchResult `json:"results"`
17 TotalCount int64 `json:"total_count"`
18}
19
20// Search is searching Flows...
21func Search(c *Client, SearchRequest string) SearchResults {
22 var err error
23
24 // Build request body
25 searchBody := &SearchBody{
26 EntityType: "Flow",
27 Filter: SearchRequest,
28 }
29 buf := new(bytes.Buffer)
30 // json.NewEncoder(buf).Encode(authBody)
31 json.NewEncoder(buf).Encode(searchBody)
32
33 // Build the request
34 req, err := http.NewRequest("POST", fmt.Sprintf("%s/search", c.BaseURL), buf)
35 if err != nil {
36 fmt.Println(err.Error())
37 return SearchResults{}
38 }
39
40 // Send the request
41 res, err := c.sendRequest(req)
42 if err != nil {
43 fmt.Println(err.Error())
44 return SearchResults{}
45 }
46
47 // Decode the response
48 var fullResponse SearchResults
49 if err = json.Unmarshal(res, &fullResponse); err != nil {
50 fmt.Println("error decoding")
51 fmt.Println(err)
52 return SearchResults{}
53 }
54
55 return fullResponse
56
57}
Here, we define:
- the search body request
- the expected search result
Then, the function Search takes a SearchRequest string as an argument. This enables me to use this function as a generic one. For example, if I call Search(c, destination_ip.ip_address = ‘10.0.0.1’ or source_ip.ip_address = ‘10.0.0.1’
), I expect to to fill a SearchResults structure with flows matching source or destination IP 10.0.0.1.
Now, I need a function to get some information for each flow (at least the name):
1// EntityResult ...
2type EntityResult struct {
3 Name string `json:"name"`
4}
5
6// GetEntity is getting details about the flow...
7func GetEntity(c *Client, ID string) EntityResult {
8 var err error
9
10 // Build the request
11 req, err := http.NewRequest("GET", fmt.Sprintf("%s/entities/flows/%s", c.BaseURL, ID), nil)
12 if err != nil {
13 fmt.Println(err.Error())
14 return EntityResult{}
15 }
16
17 // Send the request
18 res, err := c.sendRequest(req)
19 if err != nil {
20 fmt.Println(err.Error())
21 return EntityResult{}
22 }
23
24 // Decode the response
25 var fullResponse EntityResult
26 if err = json.Unmarshal(res, &fullResponse); err != nil {
27 fmt.Println("error decoding")
28 fmt.Println(err)
29 return EntityResult{}
30 }
31
32 return fullResponse
33
34}
For that, I’m using a /entities/flow method to get some details. For now, I just want to retrieve the name.
Now that I have my flows and their name, I need to get their metrics:
1// MetricsResults ...
2type MetricsResults struct {
3 Metric string `json:"metric"`
4 DisplayName string `json:"display_name"`
5 Interval int64 `json:"interval"`
6 Unit string `json:"unit"`
7 PointList [][]float64 `json:"pointlist"`
8 Start int64 `json:"start"`
9 End int64 `json:"end"`
10}
11
12// GetMetrics is listing VMs...
13func GetMetrics(c *Client, ID string) MetricsResults {
14 var err error
15 now := time.Now().Unix()
16
17 // Build the request
18 req, err := http.NewRequest("GET", fmt.Sprintf("%s/metrics?entity_id=%s&start=%d&end=%d&interval=1800&metric=flow.totalBytesRate.rate.average.bitsPerSecond", c.BaseURL, ID, now-86400, now), nil)
19 if err != nil {
20 fmt.Println(err.Error())
21 return MetricsResults{}
22 }
23
24 // Send the request
25 res, err := c.sendRequest(req)
26 if err != nil {
27 fmt.Println(err.Error())
28 return MetricsResults{}
29 }
30
31 // Decode the response
32 var fullResponse MetricsResults
33 if err = json.Unmarshal(res, &fullResponse); err != nil {
34 fmt.Println("error decoding")
35 fmt.Println(err)
36 return MetricsResults{}
37 }
38
39 return fullResponse
40}
I’m using the /metrics method for that. The interesting part of this call in the parameters. I need to get actual time translated in epochs. I use this function: now := time.Now().Unix(). I use now at the end metric period and now-86400 for the start metric period. 86400s = 24h, so these params will get the last 24-hours of data. The interval is 1800 (30minutes), so we should collect 48 items. The last part is the metric that we want to use: I chose flow.totalBytesRate.rate.average.bitsPerSecond in this post. (If you need to list all accepted metrics, you can use this method: /api/ni/schema/Flow/metrics)
Almost done! We just have to use all these functions in the right order and plot the charts! (For that, I’ll use this package: https://github.com/guptarohit/asciigraph)
Here is the main function:
1// InfoColor ...
2const (
3 InfoColor = "\033[1;34m%s\033[0m\n"
4 NoticeColor = "\033[1;36m%s\033[0m\n"
5 WarningColor = "\033[1;33m%s\033[0m\n"
6 ErrorColor = "\033[1;31m%s\033[0m\n"
7 DebugColor = "\033[0;36m%s\033[0m\n"
8)
9
10func main() {
11
12 arg := os.Args[1:]
13
14 if len(arg) <= 1 {
15 fmt.Printf(ErrorColor, "Please specify at least one arg")
16 return
17 }
18
19 client := NewClient()
20
21 // Authentication
22 token := Auth(client)
23 if token == "" {
24 fmt.Printf(ErrorColor, "Failed to authenticate")
25 return
26 }
27
28
29 res := Search(client, fmt.Sprintf(`destination_ip.ip_address = '%s' or source_ip.ip_address = '%s'`, arg[1], arg[1]))
30
31 if len(res.Results) == 0 {
32 fmt.Printf(WarningColor, "no found flows")
33 return
34 }
35
36 for i := range res.Results {
37
38 metrics := GetMetrics(client, res.Results[i].EntityID)
39 flowName := GetEntity(client, res.Results[i].EntityID)
40
41 data := []float64{}
42 for _, s := range metrics.PointList {
43 data = append(data, s[1]/1000000)
44 }
45
46 // Display Charts
47 fmt.Println("")
48 fmt.Printf(NoticeColor, flowName.Name)
49 fmt.Println("")
50 fmt.Printf(InfoColor, "Average MBps (last 24Hours)")
51 fmt.Println("")
52 graph := asciigraph.Plot(data)
53 fmt.Println(graph)
54 fmt.Println("")
55
56 }
57
58}
The code is straightforward to understand, the only trick is s[1]/1000000. I decided to display charts in Mbps instead of bps, so I divide each point by 1M.
Making this code a kubectl pluging is more than easy. You just have to build the code with an output binary name starting by kubectl-, and move this executable in your system path.
1adeleporte@adeleporte-a01 adeleporte % go build -o kubectl-vrni
2adeleporte@adeleporte-a01 adeleporte % mv kubectl-vrni /usr/local/bin
3adeleporte@adeleporte-a01 adeleporte %
You should now be able to plot nice graphs by calling kubectl vrni flows and adding any IP: