github.com/vmware/govmomi@v0.51.0/cli/vm/vnc.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 vm
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"reflect"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"github.com/vmware/govmomi/cli"
    19  	"github.com/vmware/govmomi/cli/flags"
    20  	"github.com/vmware/govmomi/object"
    21  	"github.com/vmware/govmomi/property"
    22  	"github.com/vmware/govmomi/vim25"
    23  	"github.com/vmware/govmomi/vim25/mo"
    24  	"github.com/vmware/govmomi/vim25/types"
    25  )
    26  
    27  type intRange struct {
    28  	low, high int
    29  }
    30  
    31  var intRangeRegexp = regexp.MustCompile("^([0-9]+)-([0-9]+)$")
    32  
    33  func (i *intRange) Set(s string) error {
    34  	m := intRangeRegexp.FindStringSubmatch(s)
    35  	if m == nil {
    36  		return fmt.Errorf("invalid range: %s", s)
    37  	}
    38  
    39  	low, err := strconv.Atoi(m[1])
    40  	if err != nil {
    41  		return fmt.Errorf("couldn't convert to integer: %v", err)
    42  	}
    43  
    44  	high, err := strconv.Atoi(m[2])
    45  	if err != nil {
    46  		return fmt.Errorf("couldn't convert to integer: %v", err)
    47  	}
    48  
    49  	if low > high {
    50  		return fmt.Errorf("invalid range: low > high")
    51  	}
    52  
    53  	i.low = low
    54  	i.high = high
    55  	return nil
    56  }
    57  
    58  func (i *intRange) String() string {
    59  	return fmt.Sprintf("%d-%d", i.low, i.high)
    60  }
    61  
    62  type vnc struct {
    63  	*flags.SearchFlag
    64  
    65  	Enable    bool
    66  	Disable   bool
    67  	Port      int
    68  	PortRange intRange
    69  	Password  string
    70  }
    71  
    72  func init() {
    73  	cmd := &vnc{}
    74  	err := cmd.PortRange.Set("5900-5999")
    75  	if err != nil {
    76  		fmt.Printf("Error setting port range %v", err)
    77  	}
    78  	cli.Register("vm.vnc", cmd)
    79  }
    80  
    81  func (cmd *vnc) Register(ctx context.Context, f *flag.FlagSet) {
    82  	cmd.SearchFlag, ctx = flags.NewSearchFlag(ctx, flags.SearchVirtualMachines)
    83  	cmd.SearchFlag.Register(ctx, f)
    84  
    85  	f.BoolVar(&cmd.Enable, "enable", false, "Enable VNC")
    86  	f.BoolVar(&cmd.Disable, "disable", false, "Disable VNC")
    87  	f.IntVar(&cmd.Port, "port", -1, "VNC port (-1 for auto-select)")
    88  	f.Var(&cmd.PortRange, "port-range", "VNC port auto-select range")
    89  	f.StringVar(&cmd.Password, "password", "", "VNC password")
    90  }
    91  
    92  func (cmd *vnc) Process(ctx context.Context) error {
    93  	if err := cmd.SearchFlag.Process(ctx); err != nil {
    94  		return err
    95  	}
    96  	// Either may be true or none may be true.
    97  	if cmd.Enable && cmd.Disable {
    98  		return flag.ErrHelp
    99  	}
   100  
   101  	return nil
   102  }
   103  
   104  func (cmd *vnc) Usage() string {
   105  	return "VM..."
   106  }
   107  
   108  func (cmd *vnc) Description() string {
   109  	return `Enable or disable VNC for VM.
   110  
   111  Port numbers are automatically chosen if not specified.
   112  
   113  If neither -enable or -disable is specified, the current state is returned.
   114  
   115  Examples:
   116    govc vm.vnc -enable -password 1234 $vm | awk '{print $2}' | xargs open`
   117  }
   118  
   119  func (cmd *vnc) Run(ctx context.Context, f *flag.FlagSet) error {
   120  	vms, err := cmd.loadVMs(f.Args())
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	// Actuate settings in VMs
   126  	for _, vm := range vms {
   127  		switch {
   128  		case cmd.Enable:
   129  			err = vm.enable(cmd.Port, cmd.Password)
   130  			if err != nil {
   131  				return err
   132  			}
   133  		case cmd.Disable:
   134  			err = vm.disable()
   135  			if err != nil {
   136  				return err
   137  			}
   138  		}
   139  	}
   140  
   141  	// Reconfigure VMs to reflect updates
   142  	for _, vm := range vms {
   143  		err = vm.reconfigure()
   144  		if err != nil {
   145  			return err
   146  		}
   147  	}
   148  
   149  	return cmd.WriteResult(vncResult(vms))
   150  }
   151  
   152  func (cmd *vnc) loadVMs(args []string) ([]*vncVM, error) {
   153  	c, err := cmd.Client()
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	vms, err := cmd.VirtualMachines(args)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	var vncVMs []*vncVM
   164  	for _, vm := range vms {
   165  		v, err := newVNCVM(c, vm)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  		vncVMs = append(vncVMs, v)
   170  	}
   171  
   172  	// Assign vncHosts to vncVMs
   173  	hosts := make(map[string]*vncHost)
   174  	for _, vm := range vncVMs {
   175  		if h, ok := hosts[vm.hostReference().Value]; ok {
   176  			vm.host = h
   177  			continue
   178  		}
   179  
   180  		hs := object.NewHostSystem(c, vm.hostReference())
   181  		h, err := newVNCHost(c, hs, cmd.PortRange.low, cmd.PortRange.high)
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  
   186  		hosts[vm.hostReference().Value] = h
   187  		vm.host = h
   188  	}
   189  
   190  	return vncVMs, nil
   191  }
   192  
   193  type vncVM struct {
   194  	c    *vim25.Client
   195  	vm   *object.VirtualMachine
   196  	mvm  mo.VirtualMachine
   197  	host *vncHost
   198  
   199  	curOptions vncOptions
   200  	newOptions vncOptions
   201  }
   202  
   203  func newVNCVM(c *vim25.Client, vm *object.VirtualMachine) (*vncVM, error) {
   204  	v := &vncVM{
   205  		c:  c,
   206  		vm: vm,
   207  	}
   208  
   209  	virtualMachineProperties := []string{
   210  		"name",
   211  		"config.extraConfig",
   212  		"runtime.host",
   213  	}
   214  
   215  	pc := property.DefaultCollector(c)
   216  	ctx := context.TODO()
   217  	err := pc.RetrieveOne(ctx, vm.Reference(), virtualMachineProperties, &v.mvm)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  	v.curOptions = vncOptionsFromExtraConfig(v.mvm.Config.ExtraConfig)
   223  	v.newOptions = vncOptionsFromExtraConfig(v.mvm.Config.ExtraConfig)
   224  
   225  	return v, nil
   226  }
   227  
   228  func (v *vncVM) hostReference() types.ManagedObjectReference {
   229  	return *v.mvm.Runtime.Host
   230  }
   231  
   232  func (v *vncVM) enable(port int, password string) error {
   233  	v.newOptions["enabled"] = "true"
   234  	v.newOptions["port"] = fmt.Sprintf("%d", port)
   235  	v.newOptions["password"] = password
   236  
   237  	// Find port if auto-select
   238  	if port == -1 {
   239  		// Reuse port if If VM already has a port, reuse it.
   240  		// Otherwise, find unused VNC port on host.
   241  		if p, ok := v.curOptions["port"]; ok && p != "" {
   242  			v.newOptions["port"] = p
   243  		} else {
   244  			port, err := v.host.popUnusedPort()
   245  			if err != nil {
   246  				return err
   247  			}
   248  			v.newOptions["port"] = fmt.Sprintf("%d", port)
   249  		}
   250  	}
   251  	return nil
   252  }
   253  
   254  func (v *vncVM) disable() error {
   255  	v.newOptions["enabled"] = "false"
   256  	v.newOptions["port"] = ""
   257  	v.newOptions["password"] = ""
   258  	return nil
   259  }
   260  
   261  func (v *vncVM) reconfigure() error {
   262  	if reflect.DeepEqual(v.curOptions, v.newOptions) {
   263  		// No changes to settings
   264  		return nil
   265  	}
   266  
   267  	spec := types.VirtualMachineConfigSpec{
   268  		ExtraConfig: v.newOptions.ToExtraConfig(),
   269  	}
   270  
   271  	ctx := context.TODO()
   272  	task, err := v.vm.Reconfigure(ctx, spec)
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	return task.Wait(ctx)
   278  }
   279  
   280  func (v *vncVM) uri() (string, error) {
   281  	ip, err := v.host.managementIP()
   282  	if err != nil {
   283  		return "", err
   284  	}
   285  
   286  	uri := fmt.Sprintf("vnc://:%s@%s:%s",
   287  		v.newOptions["password"],
   288  		ip,
   289  		v.newOptions["port"])
   290  
   291  	return uri, nil
   292  }
   293  
   294  func (v *vncVM) write(w io.Writer) error {
   295  	if strings.EqualFold(v.newOptions["enabled"], "true") {
   296  		uri, err := v.uri()
   297  		if err != nil {
   298  			return err
   299  		}
   300  		fmt.Printf("%s: %s\n", v.mvm.Name, uri)
   301  	} else {
   302  		fmt.Printf("%s: disabled\n", v.mvm.Name)
   303  	}
   304  	return nil
   305  }
   306  
   307  type vncHost struct {
   308  	c     *vim25.Client
   309  	host  *object.HostSystem
   310  	ports map[int]struct{}
   311  	ip    string // This field is populated by `managementIP`
   312  }
   313  
   314  func newVNCHost(c *vim25.Client, host *object.HostSystem, low, high int) (*vncHost, error) {
   315  	ports := make(map[int]struct{})
   316  	for i := low; i <= high; i++ {
   317  		ports[i] = struct{}{}
   318  	}
   319  
   320  	used, err := loadUsedPorts(c, host.Reference())
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  
   325  	// Remove used ports from range
   326  	for _, u := range used {
   327  		delete(ports, u)
   328  	}
   329  
   330  	h := &vncHost{
   331  		c:     c,
   332  		host:  host,
   333  		ports: ports,
   334  	}
   335  
   336  	return h, nil
   337  }
   338  
   339  func loadUsedPorts(c *vim25.Client, host types.ManagedObjectReference) ([]int, error) {
   340  	ctx := context.TODO()
   341  	ospec := types.ObjectSpec{
   342  		Obj: host,
   343  		SelectSet: []types.BaseSelectionSpec{
   344  			&types.TraversalSpec{
   345  				Type: "HostSystem",
   346  				Path: "vm",
   347  				Skip: types.NewBool(false),
   348  			},
   349  		},
   350  		Skip: types.NewBool(false),
   351  	}
   352  
   353  	pspec := types.PropertySpec{
   354  		Type:    "VirtualMachine",
   355  		PathSet: []string{"config.extraConfig"},
   356  	}
   357  
   358  	req := types.RetrieveProperties{
   359  		This: c.ServiceContent.PropertyCollector,
   360  		SpecSet: []types.PropertyFilterSpec{
   361  			{
   362  				ObjectSet: []types.ObjectSpec{ospec},
   363  				PropSet:   []types.PropertySpec{pspec},
   364  			},
   365  		},
   366  	}
   367  
   368  	var vms []mo.VirtualMachine
   369  	err := mo.RetrievePropertiesForRequest(ctx, c, req, &vms)
   370  	if err != nil {
   371  		return nil, err
   372  	}
   373  
   374  	var ports []int
   375  	for _, vm := range vms {
   376  		if vm.Config == nil || vm.Config.ExtraConfig == nil {
   377  			continue
   378  		}
   379  
   380  		options := vncOptionsFromExtraConfig(vm.Config.ExtraConfig)
   381  		if ps, ok := options["port"]; ok && ps != "" {
   382  			pi, err := strconv.Atoi(ps)
   383  			if err == nil {
   384  				ports = append(ports, pi)
   385  			}
   386  		}
   387  	}
   388  
   389  	return ports, nil
   390  }
   391  
   392  func (h *vncHost) popUnusedPort() (int, error) {
   393  	if len(h.ports) == 0 {
   394  		return 0, fmt.Errorf("no unused ports in range")
   395  	}
   396  
   397  	// Return first port we get when iterating
   398  	var port int
   399  	for port = range h.ports {
   400  		break
   401  	}
   402  	delete(h.ports, port)
   403  	return port, nil
   404  }
   405  
   406  func (h *vncHost) managementIP() (string, error) {
   407  	ctx := context.TODO()
   408  	if h.ip != "" {
   409  		return h.ip, nil
   410  	}
   411  
   412  	ips, err := h.host.ManagementIPs(ctx)
   413  	if err != nil {
   414  		return "", err
   415  	}
   416  
   417  	if len(ips) > 0 {
   418  		h.ip = ips[0].String()
   419  	} else {
   420  		h.ip = "<unknown>"
   421  	}
   422  
   423  	return h.ip, nil
   424  }
   425  
   426  type vncResult []*vncVM
   427  
   428  func (vms vncResult) MarshalJSON() ([]byte, error) {
   429  	out := make(map[string]string)
   430  	for _, vm := range vms {
   431  		uri, err := vm.uri()
   432  		if err != nil {
   433  			return nil, err
   434  		}
   435  		out[vm.mvm.Name] = uri
   436  	}
   437  	return json.Marshal(out)
   438  }
   439  
   440  func (vms vncResult) Write(w io.Writer) error {
   441  	for _, vm := range vms {
   442  		err := vm.write(w)
   443  		if err != nil {
   444  			return err
   445  		}
   446  	}
   447  
   448  	return nil
   449  }
   450  
   451  type vncOptions map[string]string
   452  
   453  var vncPrefix = "RemoteDisplay.vnc."
   454  
   455  func vncOptionsFromExtraConfig(ov []types.BaseOptionValue) vncOptions {
   456  	vo := make(vncOptions)
   457  	for _, b := range ov {
   458  		o := b.GetOptionValue()
   459  		if strings.HasPrefix(o.Key, vncPrefix) {
   460  			key := o.Key[len(vncPrefix):]
   461  			if key != "key" {
   462  				vo[key] = o.Value.(string)
   463  			}
   464  		}
   465  	}
   466  	return vo
   467  }
   468  
   469  func (vo vncOptions) ToExtraConfig() []types.BaseOptionValue {
   470  	ov := make([]types.BaseOptionValue, 0)
   471  	for k, v := range vo {
   472  		key := vncPrefix + k
   473  		value := v
   474  
   475  		o := types.OptionValue{
   476  			Key:   key,
   477  			Value: &value, // Pass pointer to avoid omitempty
   478  		}
   479  
   480  		ov = append(ov, &o)
   481  	}
   482  
   483  	// Don't know how to deal with the key option, set it to be empty...
   484  	o := types.OptionValue{
   485  		Key:   vncPrefix + "key",
   486  		Value: new(string), // Pass pointer to avoid omitempty
   487  	}
   488  
   489  	ov = append(ov, &o)
   490  
   491  	return ov
   492  }