GET VRNI GRAPHS DIRECTLY FROM A KUBECTL VRNI COMMAND!

Share on:

kubectl vrni plugin

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: Steps

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(&amp;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 := &amp;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, &amp;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 := &amp;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, &amp;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, &amp;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&amp;start=%d&amp;end=%d&amp;interval=1800&amp;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, &amp;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: kubectl vrni plugin

DISCLAIMER: The views and opinions expressed on this blog are our own and may not reflect the views and opinions of our employer