github.com/vmware/govmomi@v0.43.0/govc/vm/vnc.go (about)

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