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  }