config: several refactorings and interactive fixes
This also enables log in attempt and reuse of http client once a user
successfully logs in. This speeds up the CLI experience when username
and password credentials are used instead of api/secret keys.
Signed-off-by: Rohit Yadav <rohit@apache.org>
diff --git a/cli/completer.go b/cli/completer.go
index bd2822a..9c02eff 100644
--- a/cli/completer.go
+++ b/cli/completer.go
@@ -97,7 +97,6 @@
}
func (t *autoCompleter) Do(line []rune, pos int) (options [][]rune, offset int) {
-
apiMap := buildAPICacheMap(t.Config.GetAPIVerbMap())
var verbs []string
@@ -206,14 +205,16 @@
return nil, 0
}
- r := cmd.NewRequest(nil, completer.Config, nil, nil)
+ r := cmd.NewRequest(nil, completer.Config, nil)
autocompleteAPIArgs := []string{"listall=true"}
if autocompleteAPI.Noun == "templates" {
- autocompleteAPIArgs = append(autocompleteAPIArgs, "templatefilter=all")
+ autocompleteAPIArgs = append(autocompleteAPIArgs, "templatefilter=executable")
}
- fmt.Printf("\nFetching options, please wait...")
+
+ fmt.Println("")
+ spinner := t.Config.StartSpinner("fetching options, please wait...")
response, _ := cmd.NewAPIRequest(r, autocompleteAPI.Name, autocompleteAPIArgs, false)
- fmt.Printf("\r")
+ t.Config.StopSpinner(spinner)
var autocompleteOptions []selectOption
for _, v := range response {
diff --git a/cli/exec.go b/cli/exec.go
index d64daa1..17d5829 100644
--- a/cli/exec.go
+++ b/cli/exec.go
@@ -20,20 +20,19 @@
import (
"cloudmonkey/cmd"
"cloudmonkey/config"
- "github.com/chzyer/readline"
)
// ExecCmd executes a single provided command
-func ExecCmd(cfg *config.Config, args []string, shell *readline.Instance) error {
+func ExecCmd(cfg *config.Config, args []string) error {
if len(args) < 1 {
return nil
}
command := cmd.FindCommand(args[0])
if command != nil {
- return command.Handle(cmd.NewRequest(command, cfg, shell, args[1:]))
+ return command.Handle(cmd.NewRequest(command, cfg, args[1:]))
}
catchAllHandler := cmd.GetAPIHandler()
- return catchAllHandler.Handle(cmd.NewRequest(catchAllHandler, cfg, shell, args))
+ return catchAllHandler.Handle(cmd.NewRequest(catchAllHandler, cfg, args))
}
diff --git a/cli/shell.go b/cli/shell.go
index 884150c..d9ffea1 100644
--- a/cli/shell.go
+++ b/cli/shell.go
@@ -31,6 +31,7 @@
)
var completer *autoCompleter
+var shell *readline.Instance
// ExecShell starts a shell
func ExecShell(sysArgs []string) {
@@ -39,6 +40,15 @@
Config: cfg,
}
+ if len(sysArgs) > 0 {
+ err := ExecCmd(cfg, sysArgs)
+ if err != nil {
+ fmt.Println("🙈 Error:", err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+ }
+
shell, err := readline.NewEx(&readline.Config{
Prompt: cfg.GetPrompt(),
HistoryFile: cfg.HistoryFile,
@@ -61,15 +71,7 @@
}
defer shell.Close()
- if len(sysArgs) > 0 {
- err := ExecCmd(cfg, sysArgs, nil)
- if err != nil {
- fmt.Println("🙈 Error:", err)
- os.Exit(1)
- }
- os.Exit(0)
- }
-
+ cfg.HasShell = true
cfg.PrintHeader()
for {
@@ -99,7 +101,7 @@
args = strings.Split(line, " ")
}
- err = ExecCmd(cfg, args, shell)
+ err = ExecCmd(cfg, args)
if err != nil {
fmt.Println("🙈 Error:", err)
}
diff --git a/cmd/login.go b/cmd/login.go
index 3a67103..bb1655d 100644
--- a/cmd/login.go
+++ b/cmd/login.go
@@ -80,8 +80,7 @@
r.Config.ActiveProfile.Password = password
r.Config.ActiveProfile.Domain = domain
- client, _, err := Login(r)
- if client == nil || err != nil {
+ if sessionKey, err := Login(r); err != nil || sessionKey == "" {
fmt.Println("Failed to login, check credentials and try again.")
} else {
fmt.Println("Successfully logged in and saved credentials to the server profile.")
diff --git a/cmd/network.go b/cmd/network.go
index c3e5b9f..67d08e6 100644
--- a/cmd/network.go
+++ b/cmd/network.go
@@ -21,28 +21,60 @@
"bytes"
"crypto/hmac"
"crypto/sha1"
- "crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
- "github.com/briandowns/spinner"
"io/ioutil"
"net/http"
- "net/http/cookiejar"
"net/url"
- "runtime"
"sort"
"strings"
"time"
)
-var cursor = []string{"\r⣷ 😸", "\r⣯ 😹", "\r⣟ 😺", "\r⡿ 😻", "\r⢿ 😼", "\r⣻ 😽", "\r⣽ 😾", "\r⣾ 😻"}
-
-func init() {
- if runtime.GOOS == "windows" {
- cursor = []string{"|", "/", "-", "\\"}
+func findSessionCookie(cookies []*http.Cookie) string {
+ var sessionKey string = ""
+ for _, cookie := range cookies {
+ if cookie.Name == "sessionkey" {
+ sessionKey = cookie.Value
+ break
+ }
}
+ return sessionKey
+}
+
+// Login logs in a user based on provided request and returns http client and session key
+func Login(r *Request) (string, error) {
+ params := make(url.Values)
+ params.Add("command", "login")
+ params.Add("username", r.Config.ActiveProfile.Username)
+ params.Add("password", r.Config.ActiveProfile.Password)
+ params.Add("domain", r.Config.ActiveProfile.Domain)
+ params.Add("response", "json")
+
+ url, _ := url.Parse(r.Config.ActiveProfile.URL)
+ if sessionKey := findSessionCookie(r.Client().Jar.Cookies(url)); sessionKey != "" {
+ return sessionKey, nil
+ }
+
+ spinner := r.Config.StartSpinner("trying to log in...")
+ resp, err := r.Client().PostForm(url.String(), params)
+ r.Config.StopSpinner(spinner)
+
+ if err != nil {
+ return "", errors.New("failed to authenticate with the CloudStack server, please check the settings: " + err.Error())
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ e := errors.New("failed to authenticate, please check the credentials")
+ if err != nil {
+ e = errors.New("failed to authenticate due to " + err.Error())
+ }
+ return "", e
+ }
+
+ return findSessionCookie(resp.Cookies()), nil
}
func encodeRequestParams(params url.Values) string {
@@ -69,45 +101,6 @@
return buf.String()
}
-// Login logs in a user based on provided request and returns http client and session key
-func Login(r *Request) (*http.Client, string, error) {
- params := make(url.Values)
- params.Add("command", "login")
- params.Add("username", r.Config.ActiveProfile.Username)
- params.Add("password", r.Config.ActiveProfile.Password)
- params.Add("domain", r.Config.ActiveProfile.Domain)
- params.Add("response", "json")
-
- jar, _ := cookiejar.New(nil)
- client := &http.Client{
- Jar: jar,
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: !r.Config.Core.VerifyCert},
- },
- }
-
- sessionKey := ""
- resp, err := client.PostForm(r.Config.ActiveProfile.URL, params)
- if err != nil {
- return client, sessionKey, errors.New("failed to authenticate with the CloudStack server, please check the settings: " + err.Error())
- }
- if resp.StatusCode != http.StatusOK {
- e := errors.New("failed to authenticate, please check the credentials")
- if err != nil {
- e = errors.New("failed to authenticate due to " + err.Error())
- }
- return client, sessionKey, e
- }
-
- for _, cookie := range resp.Cookies() {
- if cookie.Name == "sessionkey" {
- sessionKey = cookie.Value
- break
- }
- }
- return client, sessionKey, nil
-}
-
func getResponseData(data map[string]interface{}) map[string]interface{} {
for k := range data {
if strings.HasSuffix(k, "response") {
@@ -120,17 +113,14 @@
func pollAsyncJob(r *Request, jobID string) (map[string]interface{}, error) {
for timeout := float64(r.Config.Core.Timeout); timeout > 0.0; {
startTime := time.Now()
- s := spinner.New(cursor, 200*time.Millisecond)
- s.Color("blue", "bold")
- s.Suffix = " polling for async API job result"
- s.Start()
+ spinner := r.Config.StartSpinner("polling for async API result")
queryResult, queryError := NewAPIRequest(r, "queryAsyncJobResult", []string{"jobid=" + jobID}, false)
diff := time.Duration(1*time.Second).Nanoseconds() - time.Now().Sub(startTime).Nanoseconds()
if diff > 0 {
time.Sleep(time.Duration(diff) * time.Nanosecond)
}
- s.Stop()
timeout = timeout - time.Now().Sub(startTime).Seconds()
+ r.Config.StopSpinner(spinner)
if queryError != nil {
return queryResult, queryError
}
@@ -161,7 +151,6 @@
}
params.Add("response", "json")
- var client *http.Client
var encodedParams string
var err error
@@ -172,12 +161,6 @@
if len(apiKey) > 0 {
params.Add("apiKey", apiKey)
}
-
- client = &http.Client{
- Transport: &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: !r.Config.Core.VerifyCert},
- },
- }
encodedParams = encodeRequestParams(params)
mac := hmac.New(sha1.New, []byte(secretKey))
@@ -185,8 +168,7 @@
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
encodedParams = encodedParams + fmt.Sprintf("&signature=%s", url.QueryEscape(signature))
} else if len(r.Config.ActiveProfile.Username) > 0 && len(r.Config.ActiveProfile.Password) > 0 {
- var sessionKey string
- client, sessionKey, err = Login(r)
+ sessionKey, err := Login(r)
if err != nil {
return nil, err
}
@@ -197,8 +179,7 @@
return nil, errors.New("failed to authenticate to make API call")
}
- client.Timeout = time.Duration(time.Duration(r.Config.Core.Timeout) * time.Second)
- response, err := client.Get(fmt.Sprintf("%s?%s", r.Config.ActiveProfile.URL, encodedParams))
+ response, err := r.Client().Get(fmt.Sprintf("%s?%s", r.Config.ActiveProfile.URL, encodedParams))
if err != nil {
return nil, err
}
diff --git a/cmd/request.go b/cmd/request.go
index 45b006a..179080d 100644
--- a/cmd/request.go
+++ b/cmd/request.go
@@ -19,23 +19,25 @@
import (
"cloudmonkey/config"
- "github.com/chzyer/readline"
+ "net/http"
)
// Request describes a command request
type Request struct {
Command *Command
Config *config.Config
- Shell *readline.Instance
Args []string
}
+func (r *Request) Client() *http.Client {
+ return r.Config.ActiveProfile.Client
+}
+
// NewRequest creates a new request from a command
-func NewRequest(cmd *Command, cfg *config.Config, shell *readline.Instance, args []string) *Request {
+func NewRequest(cmd *Command, cfg *config.Config, args []string) *Request {
return &Request{
Command: cmd,
Config: cfg,
- Shell: shell,
Args: args,
}
}
diff --git a/cmd/set.go b/cmd/set.go
index a43c104..a2eeb9a 100644
--- a/cmd/set.go
+++ b/cmd/set.go
@@ -49,15 +49,13 @@
value := strings.Join(r.Args[1:], " ")
r.Config.UpdateConfig(subCommand, value)
- if subCommand == "profile" && r.Shell != nil {
+ if subCommand == "profile" && r.Config.HasShell {
fmt.Println("Loaded server profile:", r.Config.Core.ProfileName)
fmt.Println("Url: ", r.Config.ActiveProfile.URL)
fmt.Println("Username: ", r.Config.ActiveProfile.Username)
fmt.Println("Domain: ", r.Config.ActiveProfile.Domain)
fmt.Println("API Key: ", r.Config.ActiveProfile.APIKey)
fmt.Println()
-
- r.Shell.SetPrompt(r.Config.GetPrompt())
}
return nil
},
diff --git a/cmd/sync.go b/cmd/sync.go
index 160aa3f..a6d0289 100644
--- a/cmd/sync.go
+++ b/cmd/sync.go
@@ -26,7 +26,9 @@
Name: "sync",
Help: "Discovers and updates APIs",
Handle: func(r *Request) error {
+ spinner := r.Config.StartSpinner("discovering APIs, please wait...")
response, err := NewAPIRequest(r, "listApis", []string{"listall=true"}, false)
+ r.Config.StopSpinner(spinner)
if err != nil {
return err
}
diff --git a/config/cache.go b/config/cache.go
index 88c2480..216cf02 100644
--- a/config/cache.go
+++ b/config/cache.go
@@ -107,7 +107,7 @@
for _, node := range apiList {
api, valid := node.(map[string]interface{})
if !valid {
- fmt.Println("Errro, moving on 🍌")
+ fmt.Println("Error, moving on...")
continue
}
apiName := api["name"].(string)
diff --git a/config/config.go b/config/config.go
index 5d203c0..0a87dd5 100644
--- a/config/config.go
+++ b/config/config.go
@@ -18,12 +18,16 @@
package config
import (
+ "crypto/tls"
"fmt"
"github.com/mitchellh/go-homedir"
"gopkg.in/ini.v1"
+ "net/http"
+ "net/http/cookiejar"
"os"
"path"
"strconv"
+ "time"
)
// Output formats
@@ -46,6 +50,7 @@
Domain string `ini:"domain"`
APIKey string `ini:"apikey"`
SecretKey string `ini:"secretkey"`
+ Client *http.Client
}
// Core block describes common options for the CLI
@@ -65,6 +70,7 @@
HistoryFile string
CacheFile string
LogFile string
+ HasShell bool
Core *Core
ActiveProfile *ServerProfile
}
@@ -110,6 +116,7 @@
CacheFile: path.Join(configDir, "cache"),
HistoryFile: path.Join(configDir, "history"),
LogFile: path.Join(configDir, "log"),
+ HasShell: false,
Core: &defaultCoreConfig,
ActiveProfile: &defaultProfile,
}
@@ -122,8 +129,19 @@
return profiles
}
-func reloadConfig(cfg *Config) *Config {
+func newHttpClient(cfg *Config) *http.Client {
+ jar, _ := cookiejar.New(nil)
+ client := &http.Client{
+ Jar: jar,
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: !cfg.Core.VerifyCert},
+ },
+ }
+ client.Timeout = time.Duration(time.Duration(cfg.Core.Timeout) * time.Second)
+ return client
+}
+func reloadConfig(cfg *Config) *Config {
if _, err := os.Stat(cfg.Dir); err != nil {
os.Mkdir(cfg.Dir, 0700)
}
@@ -193,6 +211,7 @@
profiles = append(profiles, profile.Name())
}
+ cfg.ActiveProfile.Client = newHttpClient(cfg)
return cfg
}
@@ -203,8 +222,9 @@
c.Core.Prompt = value
case "asyncblock":
c.Core.AsyncBlock = value == "true"
- case "output":
case "display":
+ fallthrough
+ case "output":
c.Core.Output = value
case "timeout":
intValue, _ := strconv.Atoi(value)
diff --git a/config/spinner.go b/config/spinner.go
new file mode 100644
index 0000000..732032c
--- /dev/null
+++ b/config/spinner.go
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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 config
+
+import (
+ "github.com/briandowns/spinner"
+ "runtime"
+ "time"
+)
+
+var cursor = []string{"\r⣷ 😸", "\r⣯ 😹", "\r⣟ 😺", "\r⡿ 😻", "\r⢿ 😼", "\r⣻ 😽", "\r⣽ 😾", "\r⣾ 😻"}
+
+func init() {
+ if runtime.GOOS == "windows" {
+ cursor = []string{"|", "/", "-", "\\"}
+ }
+}
+
+// StartSpinner starts and returns a waiting cursor that the CLI can use
+func (c *Config) StartSpinner(suffix string) *spinner.Spinner {
+ if !c.HasShell {
+ return nil
+ }
+ waiter := spinner.New(cursor, 200*time.Millisecond)
+ waiter.Color("blue", "bold")
+ waiter.Suffix = " " + suffix
+ waiter.Start()
+ return waiter
+}
+
+func (c *Config) StopSpinner(waiter *spinner.Spinner) {
+ if waiter != nil {
+ waiter.Stop()
+ }
+}