github.com/vmware/govmomi@v0.51.0/cli/flags/client.go (about) 1 // © Broadcom. All Rights Reserved. 2 // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. 3 // SPDX-License-Identifier: Apache-2.0 4 5 package flags 6 7 import ( 8 "context" 9 "crypto/tls" 10 "errors" 11 "flag" 12 "fmt" 13 "net/url" 14 "os" 15 "os/signal" 16 "path/filepath" 17 "strings" 18 "syscall" 19 "time" 20 21 "github.com/vmware/govmomi/cns" 22 "github.com/vmware/govmomi/pbm" 23 "github.com/vmware/govmomi/session" 24 "github.com/vmware/govmomi/session/cache" 25 "github.com/vmware/govmomi/session/keepalive" 26 "github.com/vmware/govmomi/vapi/rest" 27 "github.com/vmware/govmomi/vim25" 28 "github.com/vmware/govmomi/vim25/soap" 29 ) 30 31 const ( 32 envURL = "GOVC_URL" 33 envUsername = "GOVC_USERNAME" 34 envPassword = "GOVC_PASSWORD" 35 envCertificate = "GOVC_CERTIFICATE" 36 envPrivateKey = "GOVC_PRIVATE_KEY" 37 envInsecure = "GOVC_INSECURE" 38 envPersist = "GOVC_PERSIST_SESSION" 39 envMinAPIVersion = "GOVC_MIN_API_VERSION" 40 envVimNamespace = "GOVC_VIM_NAMESPACE" 41 envVimVersion = "GOVC_VIM_VERSION" 42 envTLSCaCerts = "GOVC_TLS_CA_CERTS" 43 envTLSKnownHosts = "GOVC_TLS_KNOWN_HOSTS" 44 ) 45 46 const cDescr = "ESX or vCenter URL" 47 48 type ClientFlag struct { 49 common 50 51 *DebugFlag 52 53 username string 54 password string 55 cert string 56 key string 57 persist bool 58 vimNamespace string 59 vimVersion string 60 tlsCaCerts string 61 tlsKnownHosts string 62 client *vim25.Client 63 restClient *rest.Client 64 Session cache.Session 65 } 66 67 var ( 68 home = os.Getenv("GOVMOMI_HOME") 69 clientFlagKey = flagKey("client") 70 ) 71 72 func init() { 73 if home == "" { 74 home = filepath.Join(os.Getenv("HOME"), ".govmomi") 75 } 76 } 77 78 func NewClientFlag(ctx context.Context) (*ClientFlag, context.Context) { 79 if v := ctx.Value(clientFlagKey); v != nil { 80 return v.(*ClientFlag), ctx 81 } 82 83 v := &ClientFlag{} 84 v.DebugFlag, ctx = NewDebugFlag(ctx) 85 ctx = context.WithValue(ctx, clientFlagKey, v) 86 return v, ctx 87 } 88 89 func (flag *ClientFlag) String() string { 90 url := flag.Session.Endpoint() 91 if url == nil { 92 return "" 93 } 94 95 return url.String() 96 } 97 98 func (flag *ClientFlag) Set(s string) error { 99 var err error 100 101 flag.Session.URL, err = soap.ParseURL(s) 102 103 return err 104 } 105 106 func (flag *ClientFlag) Register(ctx context.Context, f *flag.FlagSet) { 107 flag.RegisterOnce(func() { 108 flag.DebugFlag.Register(ctx, f) 109 110 { 111 flag.Set(os.Getenv(envURL)) 112 usage := fmt.Sprintf("%s [%s]", cDescr, envURL) 113 f.Var(flag, "u", usage) 114 } 115 116 { 117 flag.username = os.Getenv(envUsername) 118 flag.password = os.Getenv(envPassword) 119 } 120 121 { 122 value := os.Getenv(envCertificate) 123 usage := fmt.Sprintf("Certificate [%s]", envCertificate) 124 f.StringVar(&flag.cert, "cert", value, usage) 125 } 126 127 { 128 value := os.Getenv(envPrivateKey) 129 usage := fmt.Sprintf("Private key [%s]", envPrivateKey) 130 f.StringVar(&flag.key, "key", value, usage) 131 } 132 133 { 134 insecure := false 135 switch env := strings.ToLower(os.Getenv(envInsecure)); env { 136 case "1", "true": 137 insecure = true 138 } 139 140 usage := fmt.Sprintf("Skip verification of server certificate [%s]", envInsecure) 141 f.BoolVar(&flag.Session.Insecure, "k", insecure, usage) 142 } 143 144 { 145 persist := true 146 switch env := strings.ToLower(os.Getenv(envPersist)); env { 147 case "0", "false": 148 persist = false 149 } 150 151 usage := fmt.Sprintf("Persist session to disk [%s]", envPersist) 152 f.BoolVar(&flag.persist, "persist-session", persist, usage) 153 } 154 155 { 156 value := os.Getenv(envVimNamespace) 157 if value == "" { 158 value = vim25.Namespace 159 } 160 usage := fmt.Sprintf("Vim namespace [%s]", envVimNamespace) 161 f.StringVar(&flag.vimNamespace, "vim-namespace", value, usage) 162 } 163 164 { 165 value := os.Getenv(envVimVersion) 166 if value == "" { 167 value = vim25.Version 168 } 169 usage := fmt.Sprintf("Vim version [%s]", envVimVersion) 170 f.StringVar(&flag.vimVersion, "vim-version", value, usage) 171 } 172 173 { 174 value := os.Getenv(envTLSCaCerts) 175 usage := fmt.Sprintf("TLS CA certificates file [%s]", envTLSCaCerts) 176 f.StringVar(&flag.tlsCaCerts, "tls-ca-certs", value, usage) 177 } 178 179 { 180 value := os.Getenv(envTLSKnownHosts) 181 usage := fmt.Sprintf("TLS known hosts file [%s]", envTLSKnownHosts) 182 f.StringVar(&flag.tlsKnownHosts, "tls-known-hosts", value, usage) 183 } 184 }) 185 } 186 187 func (flag *ClientFlag) Process(ctx context.Context) error { 188 return flag.ProcessOnce(func() error { 189 err := flag.DebugFlag.Process(ctx) 190 if err != nil { 191 return err 192 } 193 194 if flag.Session.URL == nil { 195 return errors.New("specify an " + cDescr) 196 } 197 198 if !flag.persist { 199 flag.Session.Passthrough = true 200 } 201 202 flag.username, err = session.Secret(flag.username) 203 if err != nil { 204 return err 205 } 206 flag.password, err = session.Secret(flag.password) 207 if err != nil { 208 return err 209 } 210 211 // Override username if set 212 if flag.username != "" { 213 var password string 214 var ok bool 215 216 if flag.Session.URL.User != nil { 217 password, ok = flag.Session.URL.User.Password() 218 } 219 220 if ok { 221 flag.Session.URL.User = url.UserPassword(flag.username, password) 222 } else { 223 flag.Session.URL.User = url.User(flag.username) 224 } 225 } 226 227 // Override password if set 228 if flag.password != "" { 229 var username string 230 231 if flag.Session.URL.User != nil { 232 username = flag.Session.URL.User.Username() 233 } 234 235 flag.Session.URL.User = url.UserPassword(username, flag.password) 236 } 237 238 return nil 239 }) 240 } 241 242 func (flag *ClientFlag) ConfigureTLS(sc *soap.Client) error { 243 if flag.cert != "" { 244 cert, err := tls.LoadX509KeyPair(flag.cert, flag.key) 245 if err != nil { 246 return fmt.Errorf("%s=%q %s=%q: %s", envCertificate, flag.cert, envPrivateKey, flag.key, err) 247 } 248 249 sc.SetCertificate(cert) 250 } 251 252 // Set namespace and version 253 sc.Namespace = "urn:" + flag.vimNamespace 254 sc.Version = flag.vimVersion 255 256 sc.UserAgent = fmt.Sprintf("govc/%s", strings.TrimPrefix(BuildVersion, "v")) 257 258 if err := flag.SetRootCAs(sc); err != nil { 259 return err 260 } 261 262 if err := sc.LoadThumbprints(flag.tlsKnownHosts); err != nil { 263 return err 264 } 265 266 t := sc.DefaultTransport() 267 var err error 268 269 value := os.Getenv("GOVC_TLS_HANDSHAKE_TIMEOUT") 270 if value != "" { 271 t.TLSHandshakeTimeout, err = time.ParseDuration(value) 272 if err != nil { 273 return err 274 } 275 } 276 277 sc.UseJSON(os.Getenv("GOVC_VI_JSON") != "") 278 279 return nil 280 } 281 282 func (flag *ClientFlag) SetRootCAs(c *soap.Client) error { 283 if flag.tlsCaCerts != "" { 284 return c.SetRootCAs(flag.tlsCaCerts) 285 } 286 return nil 287 } 288 289 func (flag *ClientFlag) RoundTripper(c *soap.Client) soap.RoundTripper { 290 // Retry twice when a temporary I/O error occurs. 291 // This means a maximum of 3 attempts. 292 rt := vim25.Retry(c, vim25.RetryTemporaryNetworkError, 3) 293 294 switch { 295 case flag.dump: 296 rt = &dump{roundTripper: rt} 297 case flag.verbose: 298 rt = &verbose{roundTripper: rt} 299 } 300 301 return rt 302 } 303 304 func (flag *ClientFlag) Client() (*vim25.Client, error) { 305 if flag.client != nil { 306 return flag.client, nil 307 } 308 309 c := new(vim25.Client) 310 err := flag.Session.Login(context.Background(), c, flag.ConfigureTLS) 311 if err != nil { 312 return nil, err 313 } 314 315 if flag.vimVersion == "" || flag.vimVersion == "-" { 316 err = c.UseServiceVersion() 317 if err != nil { 318 return nil, err 319 } 320 } 321 322 c.RoundTripper = flag.RoundTripper(c.Client) 323 flag.client = c 324 325 return flag.client, nil 326 } 327 328 func (flag *ClientFlag) RestClient() (*rest.Client, error) { 329 if flag.restClient != nil { 330 return flag.restClient, nil 331 } 332 333 c := new(rest.Client) 334 335 err := flag.Session.Login(context.Background(), c, flag.ConfigureTLS) 336 if err != nil { 337 return nil, err 338 } 339 340 flag.restClient = c 341 return flag.restClient, nil 342 } 343 344 func (flag *ClientFlag) PbmClient() (*pbm.Client, error) { 345 vc, err := flag.Client() 346 if err != nil { 347 return nil, err 348 } 349 c, err := pbm.NewClient(context.Background(), vc) 350 if err != nil { 351 return nil, err 352 } 353 354 c.RoundTripper = flag.RoundTripper(c.Client) 355 356 return c, nil 357 } 358 359 func (flag *ClientFlag) CnsClient() (*cns.Client, error) { 360 vc, err := flag.Client() 361 if err != nil { 362 return nil, err 363 } 364 365 c, err := cns.NewClient(context.Background(), vc) 366 if err != nil { 367 return nil, err 368 } 369 370 c.RoundTripper = flag.RoundTripper(c.Client) 371 372 return c, nil 373 } 374 375 func (flag *ClientFlag) KeepAlive(client cache.Client) { 376 switch c := client.(type) { 377 case *vim25.Client: 378 keepalive.NewHandlerSOAP(c, 0, nil).Start() 379 case *rest.Client: 380 keepalive.NewHandlerREST(c, 0, nil).Start() 381 default: 382 panic(fmt.Sprintf("unsupported client type=%T", client)) 383 } 384 } 385 386 func (flag *ClientFlag) Logout(ctx context.Context) error { 387 if flag.client != nil { 388 _ = flag.Session.Logout(ctx, flag.client) 389 } 390 391 if flag.restClient != nil { 392 _ = flag.Session.Logout(ctx, flag.restClient) 393 } 394 395 return nil 396 } 397 398 // Environ returns the govc environment variables for this connection 399 func (flag *ClientFlag) Environ(extra bool) []string { 400 var env []string 401 add := func(k, v string) { 402 env = append(env, fmt.Sprintf("%s=%s", k, v)) 403 } 404 405 u := *flag.Session.URL 406 if u.User != nil { 407 add(envUsername, u.User.Username()) 408 409 if p, ok := u.User.Password(); ok { 410 add(envPassword, p) 411 } 412 413 u.User = nil 414 } 415 416 if u.Path == vim25.Path { 417 u.Path = "" 418 } 419 u.Fragment = "" 420 u.RawQuery = "" 421 422 add(envURL, strings.TrimPrefix(u.String(), "https://")) 423 424 keys := []string{ 425 envCertificate, 426 envPrivateKey, 427 envInsecure, 428 envPersist, 429 envMinAPIVersion, 430 envVimNamespace, 431 envVimVersion, 432 } 433 434 for _, k := range keys { 435 if v := os.Getenv(k); v != "" { 436 add(k, v) 437 } 438 } 439 440 if extra { 441 add("GOVC_URL_SCHEME", flag.Session.URL.Scheme) 442 443 v := strings.SplitN(u.Host, ":", 2) 444 add("GOVC_URL_HOST", v[0]) 445 if len(v) == 2 { 446 add("GOVC_URL_PORT", v[1]) 447 } 448 449 add("GOVC_URL_PATH", flag.Session.URL.Path) 450 451 if f := flag.Session.URL.Fragment; f != "" { 452 add("GOVC_URL_FRAGMENT", f) 453 } 454 455 if q := flag.Session.URL.RawQuery; q != "" { 456 add("GOVC_URL_QUERY", q) 457 } 458 } 459 460 return env 461 } 462 463 // WithCancel calls the given function, returning when complete or canceled via SIGINT. 464 func (flag *ClientFlag) WithCancel(ctx context.Context, f func(context.Context) error) error { 465 sig := make(chan os.Signal, 1) 466 signal.Notify(sig, syscall.SIGINT) 467 468 wctx, cancel := context.WithCancel(ctx) 469 defer cancel() 470 471 done := make(chan bool) 472 var werr error 473 474 go func() { 475 defer close(done) 476 werr = f(wctx) 477 }() 478 479 select { 480 case <-sig: 481 cancel() 482 <-done // Wait for f() to complete 483 case <-done: 484 } 485 486 return werr 487 }