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 }