climc.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package entry
  15. import (
  16. "context"
  17. "encoding/json"
  18. "fmt"
  19. "io/ioutil"
  20. "net/http"
  21. "net/url"
  22. "os"
  23. "path/filepath"
  24. "strings"
  25. "sync"
  26. "time"
  27. "github.com/c-bata/go-prompt"
  28. "github.com/cheggaaa/pb/v3"
  29. "golang.org/x/net/http/httpproxy"
  30. "yunion.io/x/log"
  31. "yunion.io/x/pkg/errors"
  32. "yunion.io/x/pkg/util/httputils"
  33. "yunion.io/x/pkg/util/version"
  34. "yunion.io/x/structarg"
  35. "yunion.io/x/onecloud/cmd/climc/promputils"
  36. "yunion.io/x/onecloud/cmd/climc/shell"
  37. "yunion.io/x/onecloud/pkg/mcclient"
  38. )
  39. type BaseOptions struct {
  40. Debug bool `help:"Show debug information"`
  41. Version bool `help:"Show version"`
  42. Timeout int `default:"600" help:"Number of seconds to wait for a response"`
  43. Insecure bool `default:"$YUNION_INSECURE|false" help:"Allow skip server cert verification if URL is https" short-token:"k"`
  44. CertFile string `default:"$YUNION_CERT_FILE" help:"certificate file"`
  45. KeyFile string `default:"$YUNION_KEY_FILE" help:"private key file"`
  46. Completion string `default:"" help:"Generate climc auto complete script" choices:"bash|zsh"`
  47. UseCachedToken bool `default:"$YUNION_USE_CACHED_TOKEN|false" help:"Use cached token"`
  48. OsUsername string `default:"$OS_USERNAME" help:"Username, defaults to env[OS_USERNAME]"`
  49. OsPassword string `default:"$OS_PASSWORD" help:"Password, defaults to env[OS_PASSWORD]"`
  50. // OsProjectId string `default:"$OS_PROJECT_ID" help:"Proejct ID, defaults to env[OS_PROJECT_ID]"`
  51. OsProjectName string `default:"$OS_PROJECT_NAME" help:"Project name, defaults to env[OS_PROJECT_NAME]"`
  52. OsProjectDomain string `default:"$OS_PROJECT_DOMAIN" help:"Domain name of project, defaults to env[OS_PROJECT_DOMAIN]"`
  53. OsDomainName string `default:"$OS_DOMAIN_NAME" help:"Domain name, defaults to env[OS_DOMAIN_NAME]"`
  54. OsAccessKey string `default:"$OS_ACCESS_KEY" help:"ak/sk access key, defaults to env[OS_ACCESS_KEY]"`
  55. OsSecretKey string `default:"$OS_SECRET_KEY" help:"ak/s secret, defaults to env[OS_SECRET_KEY]"`
  56. OsAuthToken string `default:"$OS_AUTH_TOKEN" help:"token authenticate, defaults to env[OS_AUTH_TOKEN]"`
  57. OsAuthURL string `default:"$OS_AUTH_URL" help:"Defaults to env[OS_AUTH_URL]"`
  58. OsRegionName string `default:"$OS_REGION_NAME" help:"Defaults to env[OS_REGION_NAME]"`
  59. OsZoneName string `default:"$OS_ZONE_NAME" help:"Defaults to env[OS_ZONE_NAME]"`
  60. OsEndpointType string `default:"$OS_ENDPOINT_TYPE|internalURL" help:"Defaults to env[OS_ENDPOINT_TYPE] or internalURL" choices:"publicURL|internalURL|adminURL|apigateway"`
  61. // ApiVersion string `default:"$API_VERSION" help:"override default modules service api version"`
  62. OutputFormat string `default:"$CLIMC_OUTPUT_FORMAT|table" choices:"table|kv|json|yaml|flatten-table|flatten-kv" help:"output format"`
  63. ParallelRun int `help:"run in parallel to stess test the performance of server"`
  64. SUBCOMMAND string `help:"climc subcommand" subcommand:"true"`
  65. }
  66. func getSubcommandsParser() (*structarg.ArgumentParser, error) {
  67. var (
  68. prog = "climc"
  69. desc = `Command-line interface to the API server.`
  70. )
  71. parse, e := structarg.NewArgumentParserWithHelp(&BaseOptions{},
  72. prog,
  73. desc,
  74. `See "climc help COMMAND" for help on a specific command.`)
  75. if e != nil {
  76. return nil, e
  77. }
  78. subcmd := parse.GetSubcommand()
  79. if subcmd == nil {
  80. return nil, fmt.Errorf("No subcommand argument")
  81. }
  82. promptRootCmd := promputils.InitRootCmd(prog, desc, parse.GetOptArgs(), parse.GetPosArgs())
  83. var errs []error
  84. for _, v := range shell.CommandTable {
  85. _par, e := subcmd.AddSubParserWithHelp(v.Options, v.Command, v.Desc, v.Callback)
  86. if e != nil {
  87. errs = append(errs, e)
  88. continue
  89. }
  90. promputils.AppendCommand(promptRootCmd, v.Command, v.Desc)
  91. cmd := v.Command
  92. for _, v := range _par.GetOptArgs() {
  93. text := v.String()
  94. text = strings.TrimLeft(text, "[<")
  95. text = strings.TrimRight(text, "]>")
  96. promputils.AppendOpt(cmd, text, v.HelpString(""), v)
  97. }
  98. for _, v := range _par.GetPosArgs() {
  99. text := v.String()
  100. text = strings.TrimLeft(text, "[<")
  101. text = strings.TrimRight(text, "]>")
  102. promputils.AppendPos(cmd, text, v.HelpString(""), v)
  103. }
  104. }
  105. if len(errs) > 0 {
  106. return nil, errors.NewAggregate(errs)
  107. }
  108. return parse, nil
  109. }
  110. func showErrorAndExit(e error) {
  111. fmt.Fprintf(os.Stderr, "%s", e)
  112. fmt.Fprintln(os.Stderr)
  113. os.Exit(1)
  114. }
  115. func newClientSession(options *BaseOptions) (*mcclient.ClientSession, error) {
  116. if len(options.OsAuthURL) == 0 {
  117. return nil, fmt.Errorf("Missing OS_AUTH_URL")
  118. }
  119. if len(options.OsUsername) == 0 && len(options.OsAccessKey) == 0 && len(options.OsAuthToken) == 0 {
  120. return nil, fmt.Errorf("Missing OS_USERNAME or OS_ACCESS_KEY or OS_AUTH_TOKEN")
  121. }
  122. if len(options.OsUsername) > 0 && len(options.OsPassword) == 0 {
  123. return nil, fmt.Errorf("Missing OS_PASSWORD")
  124. }
  125. if len(options.OsAccessKey) > 0 && len(options.OsSecretKey) == 0 {
  126. return nil, fmt.Errorf("Missing OS_SECRET_KEY")
  127. }
  128. logLevel := "info"
  129. if options.Debug {
  130. logLevel = "debug"
  131. }
  132. log.SetLogLevelByString(log.Logger(), logLevel)
  133. client := mcclient.NewClient(options.OsAuthURL,
  134. options.Timeout,
  135. options.Debug,
  136. options.Insecure,
  137. options.CertFile,
  138. options.KeyFile)
  139. cfg := &httpproxy.Config{
  140. HTTPProxy: os.Getenv("HTTP_PROXY"),
  141. HTTPSProxy: os.Getenv("HTTPS_PROXY"),
  142. NoProxy: os.Getenv("NO_PROXY"),
  143. }
  144. cfgProxyFunc := cfg.ProxyFunc()
  145. proxyFunc := func(req *http.Request) (*url.URL, error) {
  146. return cfgProxyFunc(req.URL)
  147. }
  148. httputils.SetClientProxyFunc(client.GetClient(), proxyFunc)
  149. var cacheToken mcclient.TokenCredential
  150. authUrlAlter := strings.Replace(options.OsAuthURL, "/", "", -1)
  151. authUrlAlter = strings.Replace(authUrlAlter, ":", "", -1)
  152. tokenCachePath := filepath.Join(os.TempDir(), fmt.Sprintf("OS_AUTH_CACHE_TOKEN-%s-%s-%s-%s", authUrlAlter, options.OsUsername, options.OsDomainName, options.OsProjectName))
  153. if options.UseCachedToken {
  154. cacheFile, err := os.Open(tokenCachePath)
  155. if err == nil && cacheFile != nil {
  156. fileInfo, _ := cacheFile.Stat()
  157. dur, err := time.ParseDuration("-24h")
  158. if fileInfo != nil && err == nil && fileInfo.ModTime().After(time.Now().Add(dur)) {
  159. bytesToken, err := ioutil.ReadAll(cacheFile)
  160. if err == nil {
  161. token := client.NewAuthTokenCredential()
  162. err := json.Unmarshal(bytesToken, token)
  163. if err != nil {
  164. fmt.Printf("Unmarshal token error:%s", err)
  165. } else if token.IsValid() {
  166. cacheToken = token
  167. }
  168. }
  169. cacheFile.Close()
  170. }
  171. }
  172. }
  173. if cacheToken == nil {
  174. var token mcclient.TokenCredential
  175. var err error
  176. if len(options.OsAuthToken) > 0 {
  177. token, err = client.AuthenticateToken(options.OsAuthToken, options.OsProjectName,
  178. options.OsProjectDomain,
  179. mcclient.AuthSourceCli)
  180. } else if len(options.OsAccessKey) > 0 {
  181. token, err = client.AuthenticateByAccessKey(options.OsAccessKey,
  182. options.OsSecretKey, mcclient.AuthSourceCli)
  183. } else {
  184. token, err = client.AuthenticateWithSource(options.OsUsername,
  185. options.OsPassword,
  186. options.OsDomainName,
  187. options.OsProjectName,
  188. options.OsProjectDomain,
  189. mcclient.AuthSourceCli)
  190. }
  191. if err != nil {
  192. return nil, err
  193. }
  194. cacheToken = token
  195. bytesCacheToken, err := json.Marshal(cacheToken)
  196. if err != nil {
  197. fmt.Printf("Marshal token error:%s", err)
  198. } else {
  199. fo, err := os.Create(tokenCachePath)
  200. if err != nil {
  201. fmt.Printf("Save token cache fail: %s", err)
  202. } else {
  203. fo.Write(bytesCacheToken)
  204. fo.Close()
  205. }
  206. }
  207. }
  208. session := client.NewSession(
  209. context.Background(),
  210. options.OsRegionName,
  211. options.OsZoneName,
  212. options.OsEndpointType,
  213. cacheToken,
  214. )
  215. return session, nil
  216. }
  217. func enterInteractiveMode(
  218. parser *structarg.ArgumentParser,
  219. sessionFactory func() *mcclient.ClientSession,
  220. ) {
  221. promputils.InitEnv(parser, sessionFactory())
  222. defer fmt.Println("Bye!")
  223. p := prompt.New(
  224. promputils.Executor,
  225. promputils.Completer,
  226. prompt.OptionPrefix("climc> "),
  227. prompt.OptionTitle("Climc, a Command Line Interface to Manage Clouds"),
  228. prompt.OptionMaxSuggestion(16),
  229. )
  230. p.Run()
  231. }
  232. func executeSubcommand(
  233. subcmd *structarg.SubcommandArgument,
  234. subparser *structarg.ArgumentParser,
  235. options *BaseOptions,
  236. sessionFactory func() *mcclient.ClientSession,
  237. parallel int,
  238. ) {
  239. suboptions := subparser.Options()
  240. if subparser.IsHelpSet() {
  241. helpStr, err := subcmd.SubHelpString(options.SUBCOMMAND)
  242. if err != nil {
  243. showErrorAndExit(err)
  244. return
  245. }
  246. fmt.Println(helpStr)
  247. return
  248. }
  249. if parallel <= 1 {
  250. sess := sessionFactory()
  251. err := subcmd.Invoke(sess, suboptions)
  252. if err != nil {
  253. showErrorAndExit(err)
  254. return
  255. }
  256. } else {
  257. fmt.Println("Authenticating...")
  258. bar := pb.StartNew(parallel)
  259. sess := make([]*mcclient.ClientSession, parallel)
  260. for i := 0; i < parallel; i++ {
  261. sess[i] = sessionFactory()
  262. bar.Increment()
  263. }
  264. bar.Finish()
  265. fmt.Println("Tokens are ready, start to request ...")
  266. start := time.Now()
  267. var wg sync.WaitGroup
  268. var errs []error
  269. for i := 0; i < parallel; i++ {
  270. wg.Add(1)
  271. s := sess[i]
  272. go func() {
  273. defer wg.Done()
  274. err := subcmd.Invoke(s, suboptions)
  275. if err != nil {
  276. errs = append(errs, err)
  277. }
  278. }()
  279. }
  280. wg.Wait()
  281. if len(errs) > 0 {
  282. showErrorAndExit(errors.NewAggregate(errs))
  283. return
  284. }
  285. diff := time.Now().Sub(start)
  286. fmt.Printf("cost: %f seconds %f qps\n", diff.Seconds(), float64(parallel)/diff.Seconds())
  287. }
  288. }
  289. func ClimcMain() {
  290. parser, e := getSubcommandsParser()
  291. if e != nil {
  292. showErrorAndExit(e)
  293. return
  294. }
  295. e = parser.ParseArgs(os.Args[1:], false)
  296. options := parser.Options().(*BaseOptions)
  297. if len(options.Completion) > 0 {
  298. completeScript := promputils.GenerateAutoCompleteCmds(promputils.GetRootCmd(), options.Completion)
  299. if len(completeScript) > 0 {
  300. fmt.Printf("%s", completeScript)
  301. }
  302. return
  303. }
  304. if parser.IsHelpSet() {
  305. return
  306. }
  307. if options.Version {
  308. fmt.Printf("Yunion API client version:\n %s\n", version.GetJsonString())
  309. return
  310. }
  311. shell.OutputFormat(options.OutputFormat)
  312. ensureSessionFactory := func() *mcclient.ClientSession {
  313. session, err := newClientSession(options)
  314. if err != nil {
  315. showErrorAndExit(err)
  316. return nil
  317. }
  318. return session
  319. }
  320. // enter interactive mode when not enough argument and SUBCOMMAND is empty
  321. if _, ok := e.(*structarg.NotEnoughArgumentsError); ok && options.SUBCOMMAND == "" {
  322. enterInteractiveMode(parser, ensureSessionFactory)
  323. return
  324. }
  325. subcmd := parser.GetSubcommand()
  326. subparser := subcmd.GetSubParser()
  327. if e != nil {
  328. if subparser != nil {
  329. fmt.Print(subparser.Usage())
  330. } else {
  331. fmt.Print(parser.Usage())
  332. }
  333. showErrorAndExit(e)
  334. return
  335. }
  336. // execute subcommand in non-interactive mode
  337. executeSubcommand(subcmd, subparser, options, ensureSessionFactory, options.ParallelRun)
  338. }