/* Copyright (c) 2014-2023 VMware, Inc. All Rights Reserved. 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 flags import ( "context" "crypto/tls" "errors" "flag" "fmt" "net/url" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/vmware/govmomi/cns" "github.com/vmware/govmomi/pbm" "github.com/vmware/govmomi/session" "github.com/vmware/govmomi/session/cache" "github.com/vmware/govmomi/session/keepalive" "github.com/vmware/govmomi/vapi/rest" "github.com/vmware/govmomi/vim25" "github.com/vmware/govmomi/vim25/soap" ) const ( envURL = "GOVC_URL" envUsername = "GOVC_USERNAME" envPassword = "GOVC_PASSWORD" envCertificate = "GOVC_CERTIFICATE" envPrivateKey = "GOVC_PRIVATE_KEY" envInsecure = "GOVC_INSECURE" envPersist = "GOVC_PERSIST_SESSION" envMinAPIVersion = "GOVC_MIN_API_VERSION" envVimNamespace = "GOVC_VIM_NAMESPACE" envVimVersion = "GOVC_VIM_VERSION" envTLSCaCerts = "GOVC_TLS_CA_CERTS" envTLSKnownHosts = "GOVC_TLS_KNOWN_HOSTS" ) const cDescr = "ESX or vCenter URL" type ClientFlag struct { common *DebugFlag username string password string cert string key string persist bool vimNamespace string vimVersion string tlsCaCerts string tlsKnownHosts string client *vim25.Client restClient *rest.Client Session cache.Session } var ( home = os.Getenv("GOVMOMI_HOME") clientFlagKey = flagKey("client") ) func init() { if home == "" { home = filepath.Join(os.Getenv("HOME"), ".govmomi") } } func NewClientFlag(ctx context.Context) (*ClientFlag, context.Context) { if v := ctx.Value(clientFlagKey); v != nil { return v.(*ClientFlag), ctx } v := &ClientFlag{} v.DebugFlag, ctx = NewDebugFlag(ctx) ctx = context.WithValue(ctx, clientFlagKey, v) return v, ctx } func (flag *ClientFlag) String() string { url := flag.Session.Endpoint() if url == nil { return "" } return url.String() } func (flag *ClientFlag) Set(s string) error { var err error flag.Session.URL, err = soap.ParseURL(s) return err } func (flag *ClientFlag) Register(ctx context.Context, f *flag.FlagSet) { flag.RegisterOnce(func() { flag.DebugFlag.Register(ctx, f) { flag.Set(os.Getenv(envURL)) usage := fmt.Sprintf("%s [%s]", cDescr, envURL) f.Var(flag, "u", usage) } { flag.username = os.Getenv(envUsername) flag.password = os.Getenv(envPassword) } { value := os.Getenv(envCertificate) usage := fmt.Sprintf("Certificate [%s]", envCertificate) f.StringVar(&flag.cert, "cert", value, usage) } { value := os.Getenv(envPrivateKey) usage := fmt.Sprintf("Private key [%s]", envPrivateKey) f.StringVar(&flag.key, "key", value, usage) } { insecure := false switch env := strings.ToLower(os.Getenv(envInsecure)); env { case "1", "true": insecure = true } usage := fmt.Sprintf("Skip verification of server certificate [%s]", envInsecure) f.BoolVar(&flag.Session.Insecure, "k", insecure, usage) } { persist := true switch env := strings.ToLower(os.Getenv(envPersist)); env { case "0", "false": persist = false } usage := fmt.Sprintf("Persist session to disk [%s]", envPersist) f.BoolVar(&flag.persist, "persist-session", persist, usage) } { value := os.Getenv(envVimNamespace) if value == "" { value = vim25.Namespace } usage := fmt.Sprintf("Vim namespace [%s]", envVimNamespace) f.StringVar(&flag.vimNamespace, "vim-namespace", value, usage) } { value := os.Getenv(envVimVersion) if value == "" { value = vim25.Version } usage := fmt.Sprintf("Vim version [%s]", envVimVersion) f.StringVar(&flag.vimVersion, "vim-version", value, usage) } { value := os.Getenv(envTLSCaCerts) usage := fmt.Sprintf("TLS CA certificates file [%s]", envTLSCaCerts) f.StringVar(&flag.tlsCaCerts, "tls-ca-certs", value, usage) } { value := os.Getenv(envTLSKnownHosts) usage := fmt.Sprintf("TLS known hosts file [%s]", envTLSKnownHosts) f.StringVar(&flag.tlsKnownHosts, "tls-known-hosts", value, usage) } }) } func (flag *ClientFlag) Process(ctx context.Context) error { return flag.ProcessOnce(func() error { err := flag.DebugFlag.Process(ctx) if err != nil { return err } if flag.Session.URL == nil { return errors.New("specify an " + cDescr) } if !flag.persist { flag.Session.Passthrough = true } flag.username, err = session.Secret(flag.username) if err != nil { return err } flag.password, err = session.Secret(flag.password) if err != nil { return err } // Override username if set if flag.username != "" { var password string var ok bool if flag.Session.URL.User != nil { password, ok = flag.Session.URL.User.Password() } if ok { flag.Session.URL.User = url.UserPassword(flag.username, password) } else { flag.Session.URL.User = url.User(flag.username) } } // Override password if set if flag.password != "" { var username string if flag.Session.URL.User != nil { username = flag.Session.URL.User.Username() } flag.Session.URL.User = url.UserPassword(username, flag.password) } return nil }) } func (flag *ClientFlag) ConfigureTLS(sc *soap.Client) error { if flag.cert != "" { cert, err := tls.LoadX509KeyPair(flag.cert, flag.key) if err != nil { return fmt.Errorf("%s=%q %s=%q: %s", envCertificate, flag.cert, envPrivateKey, flag.key, err) } sc.SetCertificate(cert) } // Set namespace and version sc.Namespace = "urn:" + flag.vimNamespace sc.Version = flag.vimVersion sc.UserAgent = fmt.Sprintf("govc/%s", strings.TrimPrefix(BuildVersion, "v")) if err := flag.SetRootCAs(sc); err != nil { return err } if err := sc.LoadThumbprints(flag.tlsKnownHosts); err != nil { return err } t := sc.DefaultTransport() var err error value := os.Getenv("GOVC_TLS_HANDSHAKE_TIMEOUT") if value != "" { t.TLSHandshakeTimeout, err = time.ParseDuration(value) if err != nil { return err } } sc.UseJSON(os.Getenv("GOVC_VI_JSON") != "") return nil } func (flag *ClientFlag) SetRootCAs(c *soap.Client) error { if flag.tlsCaCerts != "" { return c.SetRootCAs(flag.tlsCaCerts) } return nil } func (flag *ClientFlag) RoundTripper(c *soap.Client) soap.RoundTripper { // Retry twice when a temporary I/O error occurs. // This means a maximum of 3 attempts. rt := vim25.Retry(c, vim25.RetryTemporaryNetworkError, 3) switch { case flag.dump: rt = &dump{roundTripper: rt} case flag.verbose: rt = &verbose{roundTripper: rt} } return rt } func (flag *ClientFlag) Client() (*vim25.Client, error) { if flag.client != nil { return flag.client, nil } c := new(vim25.Client) err := flag.Session.Login(context.Background(), c, flag.ConfigureTLS) if err != nil { return nil, err } if flag.vimVersion == "" || flag.vimVersion == "-" { err = c.UseServiceVersion() if err != nil { return nil, err } } c.RoundTripper = flag.RoundTripper(c.Client) flag.client = c return flag.client, nil } func (flag *ClientFlag) RestClient() (*rest.Client, error) { if flag.restClient != nil { return flag.restClient, nil } c := new(rest.Client) err := flag.Session.Login(context.Background(), c, flag.ConfigureTLS) if err != nil { return nil, err } flag.restClient = c return flag.restClient, nil } func (flag *ClientFlag) PbmClient() (*pbm.Client, error) { vc, err := flag.Client() if err != nil { return nil, err } c, err := pbm.NewClient(context.Background(), vc) if err != nil { return nil, err } c.RoundTripper = flag.RoundTripper(c.Client) return c, nil } func (flag *ClientFlag) CnsClient() (*cns.Client, error) { vc, err := flag.Client() if err != nil { return nil, err } c, err := cns.NewClient(context.Background(), vc) if err != nil { return nil, err } c.RoundTripper = flag.RoundTripper(c.Client) return c, nil } func (flag *ClientFlag) KeepAlive(client cache.Client) { switch c := client.(type) { case *vim25.Client: keepalive.NewHandlerSOAP(c, 0, nil).Start() case *rest.Client: keepalive.NewHandlerREST(c, 0, nil).Start() default: panic(fmt.Sprintf("unsupported client type=%T", client)) } } func (flag *ClientFlag) Logout(ctx context.Context) error { if flag.client != nil { _ = flag.Session.Logout(ctx, flag.client) } if flag.restClient != nil { _ = flag.Session.Logout(ctx, flag.restClient) } return nil } // Environ returns the govc environment variables for this connection func (flag *ClientFlag) Environ(extra bool) []string { var env []string add := func(k, v string) { env = append(env, fmt.Sprintf("%s=%s", k, v)) } u := *flag.Session.URL if u.User != nil { add(envUsername, u.User.Username()) if p, ok := u.User.Password(); ok { add(envPassword, p) } u.User = nil } if u.Path == vim25.Path { u.Path = "" } u.Fragment = "" u.RawQuery = "" add(envURL, strings.TrimPrefix(u.String(), "https://")) keys := []string{ envCertificate, envPrivateKey, envInsecure, envPersist, envMinAPIVersion, envVimNamespace, envVimVersion, } for _, k := range keys { if v := os.Getenv(k); v != "" { add(k, v) } } if extra { add("GOVC_URL_SCHEME", flag.Session.URL.Scheme) v := strings.SplitN(u.Host, ":", 2) add("GOVC_URL_HOST", v[0]) if len(v) == 2 { add("GOVC_URL_PORT", v[1]) } add("GOVC_URL_PATH", flag.Session.URL.Path) if f := flag.Session.URL.Fragment; f != "" { add("GOVC_URL_FRAGMENT", f) } if q := flag.Session.URL.RawQuery; q != "" { add("GOVC_URL_QUERY", q) } } return env } // WithCancel calls the given function, returning when complete or canceled via SIGINT. func (flag *ClientFlag) WithCancel(ctx context.Context, f func(context.Context) error) error { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT) wctx, cancel := context.WithCancel(ctx) defer cancel() done := make(chan bool) var werr error go func() { defer close(done) werr = f(wctx) }() select { case <-sig: cancel() <-done // Wait for f() to complete case <-done: } return werr }