diff --git a/README.md b/README.md index 0e4e6f2..8924865 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,28 @@ Nsxtea is a small CLI to interact with the VMware NSX-T Search API. Sip a cup of The CLI will only work with NSX-T versions `3.0` and above. Unfortunately, the API endpoints `nsxtea` relies on are not available in previous versions. ## Installation +The CLI can be installed via two methods. + +### Installation via binary +Binaries for Linux, macOS and Windows are available on the [repos's release page](https://github.com/uempfel/nsxtea/releases). +To the install `nsxtea` this way, follow these steps: + +```bash +# Set a variable to the release version you want to download +export NSXTEA_VERSION=0.1.0 +# Download the release for your platform (macOS in this example) +curl -L https://github.com/uempfel/nsxtea/releases/download/v${NSXTEA_VERSION}/nsxtea_${NSXTEA_VERSION}_Darwin_x86_64.tar.gz -o nsxtea.tar.gz + +# Unpack the compressed folder +tar -xvzf nsxtea.tar.gz +x LICENSE +x README.md +x nsxtea +# Move the binary to your PATH +mv nsxtea /usr/local/bin +``` + +### Installation via go Assuming you have already [installed go](https://golang.org/doc/install): ```sh @@ -37,6 +59,10 @@ export NSXTEA_INSECURE='true' ## Usage +Currently, nsxtea supports two commands, which are documented below: +* [search](#search-command) +* [apply](#apply-command) + Simply type `nsxtea --help` to get help about `nsxtea`'s usage ```bash @@ -51,6 +77,7 @@ Usage: nsxtea [command] Available Commands: + apply Interact with the Hierarchical Policy API help Help about any command search Interact with the Policy or Manager Search API @@ -141,6 +168,94 @@ The reserved characters are: `+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /` Failing to escape these reserved characters correctly would lead to syntax errors and prevent the query from running. +### Apply command +This command lets you interact with Hierarchical Policy API. It's enables declarative creation, updates and deletion of Policy Objects. To get an overview on how to create objects with the Hierarchical API, this [VMware Blog by Madhukar Krishnarao](https://blogs.vmware.com/networkvirtualization/2020/06/navigating-nsxt-policy-apis.html/) is highly recommended. +Please refer to [the official API docs](https://vdc-download.vmware.com/vmwb-repository/dcr-public/d6de7a5e-636f-4677-8dbd-6f4ba91fa5e0/36b4881c-41cd-4c46-81d1-b2ca3a6c693b/api_includes/method_PatchInfra.html) for all objects that can be created. + +```bash +Decalaratively apply configurations via yaml or json +files. + +Examples: +nsxtea apply -f infra.yaml +nsxtea apply -f infra.json + +Usage: + nsxtea apply [flags] + +Flags: + -f, --filepath string Path to the file that contains the configuration to apply + -h, --help help for apply +``` + +#### Example +The following example is taken from the Blog referenced above. It creates a Tier0 and a connected Tier1 Router in one call. + +1) Create a file containing the Objects you want to create. The `apply` command accepts json and yaml input. +* Example as yaml: `infra.yaml` +```yaml +resource_type: Infra +display_name: infra +children: + - resource_type: ChildTier1 + marked_for_delete: "false" + Tier1: + resource_type: Tier1 + display_name: my-Tier-1-GW-Prod + id: my-Tier-1-GW-Prod + tier0_path: /infra/tier-0s/Tier-0-GW-West-01 + - resource_type: ChildTier0 + marked_for_delete: "false" + Tier0: + resource_type: Tier0 + display_name: Tier-0-GW-West-01-Disconnected + id: Tier-0-GW-West-01 +``` + +* The same Objects a json (taken from [Madhukar Krishnarao's Blog](https://blogs.vmware.com/networkvirtualization/2020/06/navigating-nsxt-policy-apis.html/)): `infra.json` +```json +{ + "resource_type": "Infra", + "display_name": "infra", + "children": [ + { + "resource_type": "ChildTier1", + "marked_for_delete": "false", + "Tier1": { + "resource_type": "Tier1", + "display_name": "my-Tier-1-GW-Prod", + "id": "my-Tier-1-GW-Prod", + "tier0_path": "/infra/tier-0s/Tier-0-GW-West-01" + } + }, + { + "resource_type": "ChildTier0", + "marked_for_delete": "false", + "Tier0": { + "resource_type": "Tier0", + "display_name": "Tier-0-GW-West-01-Disconnected", + "id": "Tier-0-GW-West-01" + } + } + ] +} +```` + +2) Run the apply command providing the path to the file you created +```bash +# Apply as yaml +nsxtea apply -f path/to/infra.yaml +# Apply as json +nsxtea apply -f path/to/infra.json +``` + +That's it! The objects should be created and be available after a short time. + +#### Updating and deleting objects +To update objects, simply adapt the file with the necessary configuration and re-run `nsxtea apply`. + +Deleting the objects created in the example above is as simple as changing the `marked_for_delete` properties from `true` to `false`. Once you've done that, simply re-run `nsxtea apply` and the objects should be deleted after a short time. + ### Image Credits * Gopher: [Maria Letta - Free Gophers Pack](https://github.com/MariaLetta/free-gophers-pack) * Teacup: [RROOK, NL](https://thenounproject.com/term/cup-of-tea/2870740/) diff --git a/cmd/apply.go b/cmd/apply.go new file mode 100644 index 0000000..3f79c33 --- /dev/null +++ b/cmd/apply.go @@ -0,0 +1,87 @@ +/* +Copyright © 2021 NAME HERE + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "bytes" + "fmt" + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + "io/ioutil" + "net/http" + "os" +) + +var filePath string + +var applyCmd = &cobra.Command{ + Use: "apply", + Short: "Interact with the Hierarchical Policy API", + Long: `Decalaratively apply configurations via yaml or json +files. + +Examples: +nsxtea apply -f infra.yaml +nsxtea apply -f infra.json`, + Run: handleApply, +} + +func handleApply(cmd *cobra.Command, args []string) { + + fileContent, err := ioutil.ReadFile(filePath) + if err != nil { + fmt.Println("Error parsing file\n", err) + os.Exit(1) + } + + fileContent, err = yaml.YAMLToJSON(fileContent) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + endpoint := "/policy/api/v1/infra" + body := bytes.NewBuffer(fileContent) + req, err := http.NewRequest("PATCH", "https://"+url+endpoint, body) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + req.Header.Set("Content-Type", "application/json") + req.SetBasicAuth(userName, password) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if resp.StatusCode != 200 { + bodyText, err := ioutil.ReadAll(resp.Body) + printErrIfNotNil(err) + s := string(bodyText) + fmt.Println(s) + os.Exit(1) + } + fmt.Println(resp.Status) +} + +func init() { + rootCmd.AddCommand(applyCmd) + applyCmd.PersistentFlags().StringVarP(&filePath, "filepath", "f", "", "Path to the file that contains the configuration to apply") + cobra.MarkFlagRequired(applyCmd.PersistentFlags(), "filepath") +} diff --git a/cmd/root.go b/cmd/root.go index 774c992..0126c78 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,13 +18,16 @@ package cmd import ( "crypto/tls" "fmt" + "github.com/spf13/cobra" "net/http" "os" "strconv" - "github.com/spf13/cobra" ) -var cfgFile string +var userName string +var password string +var url string +var nsxtInsecure bool // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ @@ -48,6 +51,10 @@ func Execute() { func init() { + userName = os.Getenv("NSXTEA_USERNAME") + password = os.Getenv("NSXTEA_PASSWORD") + url = os.Getenv("NSXTEA_URL") + nsxtInsecure, err := strconv.ParseBool(os.Getenv("NSXTEA_INSECURE")) if err != nil { nsxtInsecure = false diff --git a/cmd/search.go b/cmd/search.go index b6db402..9ea4259 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -19,11 +19,11 @@ import ( "fmt" "github.com/spf13/cobra" - "os" - "net/http" "io/ioutil" + "net/http" + "os" "strconv" - "strings" + "strings" ) var includedFields string @@ -33,8 +33,6 @@ var cursor string var pageSize string var useManagerApi bool - - // searchCmd represents the search command var searchCmd = &cobra.Command{ Use: "search ", @@ -96,43 +94,43 @@ To search for (a+b)=c The reserved characters are: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ / Failing to escape these reserved characters correctly would lead to syntax errors and prevent the query from running. `, - Run: handleSearch, - Args: cobra.MinimumNArgs(1), + Run: handleSearch, + Args: cobra.MinimumNArgs(1), } func handleSearch(cmd *cobra.Command, args []string) { - endpoint := "/api/v1/search/query?" - if !useManagerApi { - endpoint = "/policy" + endpoint - } - req, err := http.NewRequest("GET", "https://"+ os.Getenv("NSXTEA_URL")+ endpoint, nil) - - query := concatArgs(args) - q := req.URL.Query() - q.Add("query", query) - q.Add("included_fields", includedFields) - q.Add("sort_ascending", strconv.FormatBool(sortAscending)) - q.Add("sort_by", sortBy) - q.Add("cursor", cursor) - q.Add("page_size", pageSize) - req.URL.RawQuery = q.Encode() - - req.SetBasicAuth( - os.Getenv("NSXTEA_USERNAME"), - os.Getenv("NSXTEA_PASSWORD"), - ) - client := &http.Client{} - resp, err := client.Do(req) - if err != nil{ - fmt.Println(err) - os.Exit(1) - } - - bodyText, err := ioutil.ReadAll(resp.Body) - printErrIfNotNil(err) - s := string(bodyText) - fmt.Println(s) + endpoint := "/api/v1/search/query?" + if !useManagerApi { + endpoint = "/policy" + endpoint + } + req, err := http.NewRequest("GET", "https://"+url+endpoint, nil) + + query := concatArgs(args) + q := req.URL.Query() + q.Add("query", query) + q.Add("included_fields", includedFields) + q.Add("sort_ascending", strconv.FormatBool(sortAscending)) + q.Add("sort_by", sortBy) + q.Add("cursor", cursor) + q.Add("page_size", pageSize) + req.URL.RawQuery = q.Encode() + + req.SetBasicAuth( + userName, + password, + ) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + bodyText, err := ioutil.ReadAll(resp.Body) + printErrIfNotNil(err) + s := string(bodyText) + fmt.Println(s) } func init() { @@ -140,13 +138,13 @@ func init() { rootCmd.AddCommand(searchCmd) searchCmd.PersistentFlags().StringVarP(&includedFields, "included_fields", "f", "", "Comma separated list of fields that should be included in query result") searchCmd.PersistentFlags().StringVarP(&sortBy, "sort_by", "s", "", "Field by which records are sorted") - searchCmd.PersistentFlags().BoolVarP(&sortAscending, "sort_ascending","a", true, "Sorting order of the query results") - searchCmd.PersistentFlags().StringVarP(&cursor, "cursor", "c", "", "Opaque cursor to be used for getting next page of records (supplied by current result page)") - searchCmd.PersistentFlags().StringVarP(&pageSize, "page_size", "p", "1000", "Maximum number of results to return in this page \nMin: 0, Max: 1000") - searchCmd.PersistentFlags().BoolVarP(&useManagerApi, "manager", "m", false, "Use the Manager API for the search request") + searchCmd.PersistentFlags().BoolVarP(&sortAscending, "sort_ascending", "a", true, "Sorting order of the query results") + searchCmd.PersistentFlags().StringVarP(&cursor, "cursor", "c", "", "Opaque cursor to be used for getting next page of records (supplied by current result page)") + searchCmd.PersistentFlags().StringVarP(&pageSize, "page_size", "p", "1000", "Maximum number of results to return in this page \nMin: 0, Max: 1000") + searchCmd.PersistentFlags().BoolVarP(&useManagerApi, "manager", "m", false, "Use the Manager API for the search request") } -func concatArgs(args[] string) string { +func concatArgs(args []string) string { var concatArgs string for i := 0; i < len(args); i++ { concatArgs += args[i] + " " @@ -159,4 +157,3 @@ func printErrIfNotNil(err error) { fmt.Println(err) } } - diff --git a/go.mod b/go.mod index dcce219..2c59274 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/uempfel/nsxtea go 1.15 -require github.com/spf13/cobra v1.1.3 +require ( + github.com/ghodss/yaml v1.0.0 + github.com/spf13/cobra v1.1.3 +) diff --git a/go.sum b/go.sum index a759029..23863b2 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -99,8 +100,10 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -271,6 +274,7 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiq google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -278,6 +282,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=