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 }