github.com/containers/podman/v4@v4.9.4/pkg/machine/hyperv/machine.go (about) 1 //go:build windows 2 // +build windows 3 4 package hyperv 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io/fs" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "strings" 16 "time" 17 18 "github.com/Microsoft/go-winio" 19 "github.com/containers/common/pkg/config" 20 gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" 21 "github.com/containers/libhvee/pkg/hypervctl" 22 "github.com/containers/podman/v4/pkg/machine" 23 "github.com/containers/podman/v4/pkg/machine/define" 24 "github.com/containers/podman/v4/pkg/strongunits" 25 "github.com/containers/podman/v4/pkg/util" 26 "github.com/containers/podman/v4/utils" 27 "github.com/containers/storage/pkg/lockfile" 28 psutil "github.com/shirou/gopsutil/v3/process" 29 "github.com/sirupsen/logrus" 30 ) 31 32 var ( 33 // vmtype refers to qemu (vs libvirt, krun, etc). 34 vmtype = machine.HyperVVirt 35 ) 36 37 const ( 38 // Some of this will need to change when we are closer to having 39 // working code. 40 VolumeTypeVirtfs = "virtfs" 41 MountType9p = "9p" 42 dockerSockPath = "/var/run/docker.sock" 43 dockerConnectTimeout = 5 * time.Second 44 apiUpTimeout = 20 * time.Second 45 ) 46 47 // hyperVReadyUnit is a unit file that sets up the virtual serial device 48 // where when the VM is done configuring, it will send an ack 49 // so a listening host knows it can begin interacting with it 50 // 51 // VSOCK-CONNECT:2 <- shortcut to connect to the hostvm 52 const hyperVReadyUnit = `[Unit] 53 After=remove-moby.service sshd.socket sshd.service 54 After=systemd-user-sessions.service 55 OnFailure=emergency.target 56 OnFailureJobMode=isolate 57 [Service] 58 Type=oneshot 59 RemainAfterExit=yes 60 ExecStart=/bin/sh -c '/usr/bin/echo Ready | socat - VSOCK-CONNECT:2:%d' 61 [Install] 62 RequiredBy=default.target 63 ` 64 65 // hyperVVsockNetUnit is a systemd unit file that calls the vm helper utility 66 // needed to take traffic from a network vsock0 device to the actual vsock 67 // and onto the host 68 const hyperVVsockNetUnit = ` 69 [Unit] 70 Description=vsock_network 71 After=NetworkManager.service 72 73 [Service] 74 ExecStart=/usr/libexec/podman/gvforwarder -preexisting -iface vsock0 -url vsock://2:%d/connect 75 ExecStartPost=/usr/bin/nmcli c up vsock0 76 77 [Install] 78 WantedBy=multi-user.target 79 ` 80 81 const hyperVVsockNMConnection = ` 82 [connection] 83 id=vsock0 84 type=tun 85 interface-name=vsock0 86 87 [tun] 88 mode=2 89 90 [802-3-ethernet] 91 cloned-mac-address=5A:94:EF:E4:0C:EE 92 93 [ipv4] 94 method=auto 95 96 [proxy] 97 ` 98 99 type HyperVMachine struct { 100 // ConfigPath is the fully qualified path to the configuration file 101 ConfigPath define.VMFile 102 // HostUser contains info about host user 103 machine.HostUser 104 // ImageConfig describes the bootable image 105 machine.ImageConfig 106 // Mounts is the list of remote filesystems to mount 107 Mounts []machine.Mount 108 // Name of VM 109 Name string 110 // NetworkVSock is for the user networking 111 NetworkHVSock HVSockRegistryEntry 112 // ReadySocket tells host when vm is booted 113 ReadyHVSock HVSockRegistryEntry 114 // ResourceConfig is physical attrs of the VM 115 machine.ResourceConfig 116 // SSHConfig for accessing the remote vm 117 machine.SSHConfig 118 // Starting tells us whether the machine is running or if we have just dialed it to start it 119 Starting bool 120 // Created contains the original created time instead of querying the file mod time 121 Created time.Time 122 // LastUp contains the last recorded uptime 123 LastUp time.Time 124 // GVProxy will write its PID here 125 GvProxyPid define.VMFile 126 // MountVsocks contains the currently-active vsocks, mapped to the 127 // directory they should be mounted on. 128 MountVsocks map[string]uint64 129 // Used at runtime for serializing write operations 130 lock *lockfile.LockFile 131 } 132 133 // addNetworkAndReadySocketsToRegistry adds the Network and Ready sockets to the 134 // Windows registry 135 func (m *HyperVMachine) addNetworkAndReadySocketsToRegistry() error { 136 networkHVSock, err := NewHVSockRegistryEntry(m.Name, Network) 137 if err != nil { 138 return err 139 } 140 eventHVSocket, err := NewHVSockRegistryEntry(m.Name, Events) 141 if err != nil { 142 return err 143 } 144 m.NetworkHVSock = *networkHVSock 145 m.ReadyHVSock = *eventHVSocket 146 return nil 147 } 148 149 // readAndSplitIgnition reads the ignition file and splits it into key:value pairs 150 func (m *HyperVMachine) readAndSplitIgnition() error { 151 ignFile, err := m.IgnitionFile.Read() 152 if err != nil { 153 return err 154 } 155 reader := bytes.NewReader(ignFile) 156 157 vm, err := hypervctl.NewVirtualMachineManager().GetMachine(m.Name) 158 if err != nil { 159 return err 160 } 161 return vm.SplitAndAddIgnition("ignition.config.", reader) 162 } 163 164 func (m *HyperVMachine) Init(opts machine.InitOptions) (bool, error) { 165 var ( 166 key string 167 err error 168 ) 169 170 // cleanup half-baked files if init fails at any point 171 callbackFuncs := machine.InitCleanup() 172 defer callbackFuncs.CleanIfErr(&err) 173 go callbackFuncs.CleanOnSignal() 174 175 callbackFuncs.Add(m.ImagePath.Delete) 176 callbackFuncs.Add(m.ConfigPath.Delete) 177 callbackFuncs.Add(m.unregisterMachine) 178 179 // Parsing here is confusing. 180 // Basically, we have two paths: a source path, on the Windows machine, 181 // with all that entails (drive letter, backslash separator, etc) and a 182 // dest path, in the Linux machine, normal Unix semantics. They are 183 // separated by a : character, with source path first, dest path second. 184 // So we split on :, first two parts are guaranteed to be Windows (the 185 // drive letter and file path), next one is Linux. Options, when we get 186 // around to those, would be another : after that. 187 // TODO: Need to support options here 188 for _, mount := range opts.Volumes { 189 newMount := machine.Mount{} 190 191 splitMount := strings.Split(mount, ":") 192 if len(splitMount) < 3 { 193 return false, fmt.Errorf("volumes must be specified as source:destination and must be absolute") 194 } 195 newMount.Target = splitMount[2] 196 newMount.Source = strings.Join(splitMount[:2], ":") 197 if len(splitMount) > 3 { 198 return false, fmt.Errorf("volume options are not presently supported on Hyper-V") 199 } 200 201 m.Mounts = append(m.Mounts, newMount) 202 } 203 204 if err = m.addNetworkAndReadySocketsToRegistry(); err != nil { 205 return false, err 206 } 207 callbackFuncs.Add(func() error { 208 m.removeNetworkAndReadySocketsFromRegistry() 209 return nil 210 }) 211 212 m.IdentityPath = util.GetIdentityPath(m.Name) 213 214 if m.UID == 0 { 215 m.UID = 1000 216 } 217 218 sshPort, err := utils.GetRandomPort() 219 if err != nil { 220 return false, err 221 } 222 m.Port = sshPort 223 224 m.RemoteUsername = opts.Username 225 err = machine.AddSSHConnectionsToPodmanSocket( 226 m.UID, 227 m.Port, 228 m.IdentityPath, 229 m.Name, 230 m.RemoteUsername, 231 opts, 232 ) 233 if err != nil { 234 return false, err 235 } 236 callbackFuncs.Add(m.removeSystemConnections) 237 238 if len(opts.IgnitionPath) < 1 { 239 key, err = machine.CreateSSHKeys(m.IdentityPath) 240 if err != nil { 241 return false, err 242 } 243 callbackFuncs.Add(m.removeSSHKeys) 244 } 245 246 m.ResourceConfig = machine.ResourceConfig{ 247 CPUs: opts.CPUS, 248 DiskSize: opts.DiskSize, 249 Memory: opts.Memory, 250 } 251 m.Rootful = opts.Rootful 252 253 builder := machine.NewIgnitionBuilder(machine.DynamicIgnition{ 254 Name: m.RemoteUsername, 255 Key: key, 256 VMName: m.Name, 257 VMType: machine.HyperVVirt, 258 TimeZone: opts.TimeZone, 259 WritePath: m.IgnitionFile.GetPath(), 260 UID: m.UID, 261 Rootful: m.Rootful, 262 }) 263 264 // If the user provides an ignition file, we need to 265 // copy it into the conf dir 266 if len(opts.IgnitionPath) > 0 { 267 return false, builder.BuildWithIgnitionFile(opts.IgnitionPath) 268 } 269 callbackFuncs.Add(m.IgnitionFile.Delete) 270 271 if err := m.writeConfig(); err != nil { 272 return false, err 273 } 274 275 if err := builder.GenerateIgnitionConfig(); err != nil { 276 return false, err 277 } 278 279 builder.WithUnit(machine.Unit{ 280 Enabled: machine.BoolToPtr(true), 281 Name: "ready.service", 282 Contents: machine.StrToPtr(fmt.Sprintf(hyperVReadyUnit, m.ReadyHVSock.Port)), 283 }) 284 285 builder.WithUnit(machine.Unit{ 286 Contents: machine.StrToPtr(fmt.Sprintf(hyperVVsockNetUnit, m.NetworkHVSock.Port)), 287 Enabled: machine.BoolToPtr(true), 288 Name: "vsock-network.service", 289 }) 290 291 builder.WithFile(machine.File{ 292 Node: machine.Node{ 293 Path: "/etc/NetworkManager/system-connections/vsock0.nmconnection", 294 }, 295 FileEmbedded1: machine.FileEmbedded1{ 296 Append: nil, 297 Contents: machine.Resource{ 298 Source: machine.EncodeDataURLPtr(hyperVVsockNMConnection), 299 }, 300 Mode: machine.IntToPtr(0600), 301 }, 302 }) 303 304 if err := builder.Build(); err != nil { 305 return false, err 306 } 307 308 if err = m.resizeDisk(strongunits.GiB(opts.DiskSize)); err != nil { 309 return false, err 310 } 311 // The ignition file has been written. We now need to 312 // read it so that we can put it into key-value pairs 313 err = m.readAndSplitIgnition() 314 return err == nil, err 315 } 316 317 func (m *HyperVMachine) unregisterMachine() error { 318 vmm := hypervctl.NewVirtualMachineManager() 319 vm, err := vmm.GetMachine(m.Name) 320 if err != nil { 321 logrus.Error(err) 322 } 323 return vm.Remove("") 324 } 325 326 func (m *HyperVMachine) removeSSHKeys() error { 327 if err := os.Remove(fmt.Sprintf("%s.pub", m.IdentityPath)); err != nil { 328 logrus.Error(err) 329 } 330 return os.Remove(m.IdentityPath) 331 } 332 333 func (m *HyperVMachine) removeSystemConnections() error { 334 return machine.RemoveConnections(m.Name, fmt.Sprintf("%s-root", m.Name)) 335 } 336 337 func (m *HyperVMachine) Inspect() (*machine.InspectInfo, error) { 338 vm, err := hypervctl.NewVirtualMachineManager().GetMachine(m.Name) 339 if err != nil { 340 return nil, err 341 } 342 343 cfg, err := vm.GetConfig(m.ImagePath.GetPath()) 344 if err != nil { 345 return nil, err 346 } 347 348 vmState, err := stateConversion(vm.State()) 349 if err != nil { 350 return nil, err 351 } 352 353 podmanSocket, err := m.forwardSocketPath() 354 if err != nil { 355 return nil, err 356 } 357 machinePipe := machine.ToDist(m.Name) 358 podmanPipe := &define.VMFile{Path: `\\.\pipe\` + machinePipe} 359 360 return &machine.InspectInfo{ 361 ConfigPath: m.ConfigPath, 362 ConnectionInfo: machine.ConnectionConfig{ 363 PodmanSocket: podmanSocket, 364 PodmanPipe: podmanPipe, 365 }, 366 Created: m.Created, 367 Image: machine.ImageConfig{ 368 IgnitionFile: m.IgnitionFile, 369 ImageStream: "", 370 ImagePath: m.ImagePath, 371 }, 372 LastUp: m.LastUp, 373 Name: m.Name, 374 Resources: machine.ResourceConfig{ 375 CPUs: uint64(cfg.Hardware.CPUs), 376 DiskSize: 0, 377 Memory: cfg.Hardware.Memory, 378 }, 379 SSHConfig: m.SSHConfig, 380 State: string(vmState), 381 Rootful: m.Rootful, 382 }, nil 383 } 384 385 // collectFilesToDestroy retrieves the files that will be destroyed by `Remove` 386 func (m *HyperVMachine) collectFilesToDestroy(opts machine.RemoveOptions, diskPath *string) []string { 387 files := []string{} 388 389 if !opts.SaveKeys { 390 files = append(files, m.IdentityPath, m.IdentityPath+".pub") 391 } 392 if !opts.SaveIgnition { 393 files = append(files, m.IgnitionFile.GetPath()) 394 } 395 396 if !opts.SaveImage { 397 *diskPath = m.ImagePath.GetPath() 398 files = append(files, *diskPath) 399 } 400 401 files = append(files, m.ConfigPath.GetPath()) 402 return files 403 } 404 405 // removeNetworkAndReadySocketsFromRegistry removes the Network and Ready sockets 406 // from the Windows Registry 407 func (m *HyperVMachine) removeNetworkAndReadySocketsFromRegistry() { 408 // Remove the HVSOCK for networking 409 if err := m.NetworkHVSock.Remove(); err != nil { 410 logrus.Errorf("unable to remove registry entry for %s: %q", m.NetworkHVSock.KeyName, err) 411 } 412 413 // Remove the HVSOCK for events 414 if err := m.ReadyHVSock.Remove(); err != nil { 415 logrus.Errorf("unable to remove registry entry for %s: %q", m.ReadyHVSock.KeyName, err) 416 } 417 } 418 419 func (m *HyperVMachine) Remove(_ string, opts machine.RemoveOptions) (string, func() error, error) { 420 var ( 421 files []string 422 diskPath string 423 ) 424 m.lock.Lock() 425 defer m.lock.Unlock() 426 427 vmm := hypervctl.NewVirtualMachineManager() 428 vm, err := vmm.GetMachine(m.Name) 429 if err != nil { 430 return "", nil, fmt.Errorf("getting virtual machine: %w", err) 431 } 432 // In hyperv, they call running 'enabled' 433 if vm.State() == hypervctl.Enabled { 434 if !opts.Force { 435 return "", nil, &machine.ErrVMRunningCannotDestroyed{Name: m.Name} 436 } 437 // force stop bc we are destroying 438 if err := vm.StopWithForce(); err != nil { 439 return "", nil, fmt.Errorf("stopping virtual machine: %w", err) 440 } 441 442 // Update state on the VM by pulling its info again 443 vm, err = vmm.GetMachine(m.Name) 444 if err != nil { 445 return "", nil, fmt.Errorf("getting VM: %w", err) 446 } 447 } 448 449 // Tear down vsocks 450 if err := m.removeShares(); err != nil { 451 logrus.Errorf("Error removing vsock: %w", err) 452 } 453 454 // Collect all the files that need to be destroyed 455 files = m.collectFilesToDestroy(opts, &diskPath) 456 457 confirmationMessage := "\nThe following files will be deleted:\n\n" 458 for _, msg := range files { 459 confirmationMessage += msg + "\n" 460 } 461 462 confirmationMessage += "\n" 463 return confirmationMessage, func() error { 464 machine.RemoveFilesAndConnections(files, m.Name, m.Name+"-root") 465 m.removeNetworkAndReadySocketsFromRegistry() 466 if err := vm.Remove(""); err != nil { 467 return fmt.Errorf("removing virtual machine: %w", err) 468 } 469 return nil 470 }, nil 471 } 472 473 func (m *HyperVMachine) Set(name string, opts machine.SetOptions) ([]error, error) { 474 var ( 475 cpuChanged, memoryChanged bool 476 setErrors []error 477 ) 478 479 m.lock.Lock() 480 defer m.lock.Unlock() 481 482 vmm := hypervctl.NewVirtualMachineManager() 483 // Considering this a hard return if we cannot lookup the machine 484 vm, err := vmm.GetMachine(m.Name) 485 if err != nil { 486 return setErrors, fmt.Errorf("getting machine: %w", err) 487 } 488 if vm.State() != hypervctl.Disabled { 489 return nil, errors.New("unable to change settings unless vm is stopped") 490 } 491 492 if opts.Rootful != nil && m.Rootful != *opts.Rootful { 493 if err := m.setRootful(*opts.Rootful); err != nil { 494 setErrors = append(setErrors, fmt.Errorf("failed to set rootful option: %w", err)) 495 } else { 496 m.Rootful = *opts.Rootful 497 } 498 } 499 if opts.DiskSize != nil && m.DiskSize != *opts.DiskSize { 500 newDiskSize := strongunits.GiB(*opts.DiskSize) 501 if err := m.resizeDisk(newDiskSize); err != nil { 502 setErrors = append(setErrors, err) 503 } 504 } 505 if opts.CPUs != nil && m.CPUs != *opts.CPUs { 506 m.CPUs = *opts.CPUs 507 cpuChanged = true 508 } 509 if opts.Memory != nil && m.Memory != *opts.Memory { 510 m.Memory = *opts.Memory 511 memoryChanged = true 512 } 513 514 if opts.USBs != nil { 515 setErrors = append(setErrors, errors.New("changing USBs not supported for hyperv machines")) 516 } 517 518 if cpuChanged || memoryChanged { 519 err := vm.UpdateProcessorMemSettings(func(ps *hypervctl.ProcessorSettings) { 520 if cpuChanged { 521 ps.VirtualQuantity = m.CPUs 522 } 523 }, func(ms *hypervctl.MemorySettings) { 524 if memoryChanged { 525 ms.DynamicMemoryEnabled = false 526 ms.VirtualQuantity = m.Memory 527 ms.Limit = m.Memory 528 ms.Reservation = m.Memory 529 } 530 }) 531 if err != nil { 532 setErrors = append(setErrors, fmt.Errorf("setting CPU and Memory for VM: %w", err)) 533 } 534 } 535 536 if len(setErrors) > 0 { 537 return setErrors, setErrors[0] 538 } 539 540 // Write the new JSON out 541 // considering this a hard return if we cannot write the JSON file. 542 return setErrors, m.writeConfig() 543 } 544 545 func (m *HyperVMachine) SSH(name string, opts machine.SSHOptions) error { 546 state, err := m.State(false) 547 if err != nil { 548 return err 549 } 550 if state != machine.Running { 551 return fmt.Errorf("vm %q is not running", m.Name) 552 } 553 554 username := opts.Username 555 if username == "" { 556 username = m.RemoteUsername 557 } 558 return machine.CommonSSH(username, m.IdentityPath, m.Name, m.Port, opts.Args) 559 } 560 561 func (m *HyperVMachine) Start(name string, opts machine.StartOptions) error { 562 m.lock.Lock() 563 defer m.lock.Unlock() 564 565 // Start 9p shares 566 shares, err := m.createShares() 567 if err != nil { 568 return err 569 } 570 m.MountVsocks = shares 571 if err := m.writeConfig(); err != nil { 572 return err 573 } 574 575 vmm := hypervctl.NewVirtualMachineManager() 576 vm, err := vmm.GetMachine(m.Name) 577 if err != nil { 578 return err 579 } 580 if vm.State() != hypervctl.Disabled { 581 return hypervctl.ErrMachineStateInvalid 582 } 583 gvproxyPid, _, _, err := m.startHostNetworking() 584 if err != nil { 585 return fmt.Errorf("unable to start host networking: %q", err) 586 } 587 588 // The "starting" status from hyper v is a very small windows and not really 589 // the same as what we want. so modeling starting behaviour after qemu 590 m.Starting = true 591 if err := m.writeConfig(); err != nil { 592 return fmt.Errorf("writing JSON file: %w", err) 593 } 594 595 if err := vm.Start(); err != nil { 596 return err 597 } 598 // Wait on notification from the guest 599 if err := m.ReadyHVSock.Listen(); err != nil { 600 return err 601 } 602 603 // set starting back false now that we are running 604 m.Starting = false 605 606 if m.HostUser.Modified { 607 if machine.UpdatePodmanDockerSockService(m, name, m.UID, m.Rootful) == nil { 608 // Reset modification state if there are no errors, otherwise ignore errors 609 // which are already logged 610 m.HostUser.Modified = false 611 } 612 } 613 winProxyOpts := machine.WinProxyOpts{ 614 Name: m.Name, 615 IdentityPath: m.IdentityPath, 616 Port: m.Port, 617 RemoteUsername: m.RemoteUsername, 618 Rootful: m.Rootful, 619 VMType: vmtype, 620 } 621 machine.LaunchWinProxy(winProxyOpts, opts.NoInfo) 622 623 // Write the config with updated starting status and hostuser modification 624 if err := m.writeConfig(); err != nil { 625 return err 626 } 627 628 // Check if gvproxy is still running. 629 // Do this *after* we write config, so we have still recorded that the 630 // VM is actually running - to ensure that stopping the machine works as 631 // expected. 632 _, err = psutil.NewProcess(gvproxyPid) 633 if err != nil { 634 return fmt.Errorf("gvproxy appears to have stopped (PID %d): %w", gvproxyPid, err) 635 } 636 637 // Finalize starting shares after we are confident gvproxy is still alive. 638 if err := m.startShares(); err != nil { 639 return err 640 } 641 642 return nil 643 } 644 645 func (m *HyperVMachine) State(_ bool) (machine.Status, error) { 646 vmm := hypervctl.NewVirtualMachineManager() 647 vm, err := vmm.GetMachine(m.Name) 648 if err != nil { 649 return "", err 650 } 651 if vm.IsStarting() { 652 return machine.Starting, nil 653 } 654 if vm.State() == hypervctl.Enabled { 655 return machine.Running, nil 656 } 657 // Following QEMU pattern here where only three 658 // states seem valid 659 return machine.Stopped, nil 660 } 661 662 func (m *HyperVMachine) Stop(name string, opts machine.StopOptions) error { 663 m.lock.Lock() 664 defer m.lock.Unlock() 665 666 vmm := hypervctl.NewVirtualMachineManager() 667 vm, err := vmm.GetMachine(m.Name) 668 if err != nil { 669 return fmt.Errorf("getting virtual machine: %w", err) 670 } 671 vmState := vm.State() 672 if vm.State() == hypervctl.Disabled { 673 return nil 674 } 675 if vmState != hypervctl.Enabled { // more states could be provided as well 676 return hypervctl.ErrMachineStateInvalid 677 } 678 679 if err := machine.CleanupGVProxy(m.GvProxyPid); err != nil { 680 logrus.Error(err) 681 } 682 683 if err := machine.StopWinProxy(m.Name, vmtype); err != nil { 684 fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error()) 685 } 686 687 if err := vm.Stop(); err != nil { 688 return fmt.Errorf("stopping virtual machine: %w", err) 689 } 690 691 // keep track of last up 692 m.LastUp = time.Now() 693 return m.writeConfig() 694 } 695 696 func (m *HyperVMachine) jsonConfigPath() (string, error) { 697 configDir, err := machine.GetConfDir(machine.HyperVVirt) 698 if err != nil { 699 return "", err 700 } 701 return getVMConfigPath(configDir, m.Name), nil 702 } 703 704 func (m *HyperVMachine) loadFromFile() (*HyperVMachine, error) { 705 if len(m.Name) < 1 { 706 return nil, errors.New("encountered machine with no name") 707 } 708 709 jsonPath, err := m.jsonConfigPath() 710 if err != nil { 711 return nil, err 712 } 713 mm := HyperVMachine{} 714 715 if err := mm.loadHyperVMachineFromJSON(jsonPath); err != nil { 716 if errors.Is(err, machine.ErrNoSuchVM) { 717 return nil, &machine.ErrVMDoesNotExist{Name: m.Name} 718 } 719 return nil, err 720 } 721 722 lock, err := machine.GetLock(mm.Name, vmtype) 723 if err != nil { 724 return nil, err 725 } 726 mm.lock = lock 727 728 vmm := hypervctl.NewVirtualMachineManager() 729 vm, err := vmm.GetMachine(m.Name) 730 if err != nil { 731 return nil, err 732 } 733 734 cfg, err := vm.GetConfig(mm.ImagePath.GetPath()) 735 if err != nil { 736 return nil, err 737 } 738 739 // If the machine is on, we can get what it is actually using 740 if cfg.Hardware.CPUs > 0 { 741 mm.CPUs = uint64(cfg.Hardware.CPUs) 742 } 743 // Same for memory 744 if cfg.Hardware.Memory > 0 { 745 mm.Memory = uint64(cfg.Hardware.Memory) 746 } 747 748 return &mm, nil 749 } 750 751 // getVMConfigPath is a simple wrapper for getting the fully-qualified 752 // path of the vm json config file. It should be used to get conformity 753 func getVMConfigPath(configDir, vmName string) string { 754 return filepath.Join(configDir, fmt.Sprintf("%s.json", vmName)) 755 } 756 757 func (m *HyperVMachine) loadHyperVMachineFromJSON(fqConfigPath string) error { 758 b, err := os.ReadFile(fqConfigPath) 759 if err != nil { 760 if errors.Is(err, fs.ErrNotExist) { 761 return machine.ErrNoSuchVM 762 } 763 return err 764 } 765 return json.Unmarshal(b, m) 766 } 767 768 func (m *HyperVMachine) startHostNetworking() (int32, string, machine.APIForwardingState, error) { 769 var ( 770 forwardSock string 771 state machine.APIForwardingState 772 ) 773 cfg, err := config.Default() 774 if err != nil { 775 return -1, "", machine.NoForwarding, err 776 } 777 778 executable, err := os.Executable() 779 if err != nil { 780 return -1, "", 0, fmt.Errorf("unable to locate executable: %w", err) 781 } 782 783 gvproxyBinary, err := cfg.FindHelperBinary("gvproxy.exe", false) 784 if err != nil { 785 return -1, "", 0, err 786 } 787 788 cmd := gvproxy.NewGvproxyCommand() 789 cmd.SSHPort = m.Port 790 cmd.AddEndpoint(fmt.Sprintf("vsock://%s", m.NetworkHVSock.KeyName)) 791 cmd.PidFile = m.GvProxyPid.GetPath() 792 793 cmd, forwardSock, state = m.setupAPIForwarding(cmd) 794 if logrus.IsLevelEnabled(logrus.DebugLevel) { 795 cmd.Debug = true 796 } 797 798 c := cmd.Cmd(gvproxyBinary) 799 800 if logrus.IsLevelEnabled(logrus.DebugLevel) { 801 if err := logCommandToFile(c, "gvproxy.log"); err != nil { 802 return -1, "", 0, err 803 } 804 } 805 806 logrus.Debugf("Starting gvproxy with command: %s %v", gvproxyBinary, c.Args) 807 808 if err := c.Start(); err != nil { 809 return -1, "", 0, fmt.Errorf("unable to execute: %s: %w", cmd.ToCmdline(), err) 810 } 811 812 logrus.Debugf("Got gvproxy PID as %d", c.Process.Pid) 813 814 if len(m.MountVsocks) == 0 { 815 return int32(c.Process.Pid), forwardSock, state, nil 816 } 817 818 // Start the 9p server in the background 819 args := []string{} 820 if logrus.IsLevelEnabled(logrus.DebugLevel) { 821 args = append(args, "--log-level=debug") 822 } 823 args = append(args, "machine", "server9p") 824 for dir, vsock := range m.MountVsocks { 825 for _, mount := range m.Mounts { 826 if mount.Target == dir { 827 args = append(args, "--serve", fmt.Sprintf("%s:%s", mount.Source, winio.VsockServiceID(uint32(vsock)).String())) 828 break 829 } 830 } 831 } 832 args = append(args, fmt.Sprintf("%d", c.Process.Pid)) 833 834 logrus.Debugf("Going to start 9p server using command: %s %v", executable, args) 835 836 fsCmd := exec.Command(executable, args...) 837 838 if logrus.IsLevelEnabled(logrus.DebugLevel) { 839 if err := logCommandToFile(fsCmd, "podman-machine-server9.log"); err != nil { 840 return -1, "", 0, err 841 } 842 } 843 844 if err := fsCmd.Start(); err != nil { 845 return -1, "", 0, fmt.Errorf("unable to execute: %s %v: %w", executable, args, err) 846 } 847 848 logrus.Infof("Started podman 9p server as PID %d", fsCmd.Process.Pid) 849 850 return int32(c.Process.Pid), forwardSock, state, nil 851 } 852 853 func logCommandToFile(c *exec.Cmd, filename string) error { 854 dir, err := machine.GetDataDir(machine.HyperVVirt) 855 if err != nil { 856 return fmt.Errorf("obtain machine dir: %w", err) 857 } 858 path := filepath.Join(dir, filename) 859 logrus.Infof("Going to log to %s", path) 860 log, err := os.Create(path) 861 if err != nil { 862 return fmt.Errorf("create log file: %w", err) 863 } 864 defer log.Close() 865 866 c.Stdout = log 867 c.Stderr = log 868 869 return nil 870 } 871 872 func (m *HyperVMachine) setupAPIForwarding(cmd gvproxy.GvproxyCommand) (gvproxy.GvproxyCommand, string, machine.APIForwardingState) { 873 socket, err := m.forwardSocketPath() 874 if err != nil { 875 return cmd, "", machine.NoForwarding 876 } 877 878 destSock := fmt.Sprintf("/run/user/%d/podman/podman.sock", m.UID) 879 forwardUser := m.RemoteUsername 880 881 if m.Rootful { 882 destSock = "/run/podman/podman.sock" 883 forwardUser = "root" 884 } 885 886 cmd.AddForwardSock(socket.GetPath()) 887 cmd.AddForwardDest(destSock) 888 cmd.AddForwardUser(forwardUser) 889 cmd.AddForwardIdentity(m.IdentityPath) 890 891 return cmd, "", machine.MachineLocal 892 } 893 894 func (m *HyperVMachine) dockerSock() (string, error) { 895 dd, err := machine.GetDataDir(machine.HyperVVirt) 896 if err != nil { 897 return "", err 898 } 899 return filepath.Join(dd, "podman.sock"), nil 900 } 901 902 func (m *HyperVMachine) forwardSocketPath() (*define.VMFile, error) { 903 sockName := "podman.sock" 904 path, err := machine.GetDataDir(machine.HyperVVirt) 905 if err != nil { 906 return nil, fmt.Errorf("Resolving data dir: %s", err.Error()) 907 } 908 return define.NewMachineFile(filepath.Join(path, sockName), &sockName) 909 } 910 911 func (m *HyperVMachine) writeConfig() error { 912 // Write the JSON file 913 return machine.WriteConfig(m.ConfigPath.Path, m) 914 } 915 916 func (m *HyperVMachine) setRootful(rootful bool) error { 917 if err := machine.SetRootful(rootful, m.Name, m.Name+"-root"); err != nil { 918 return err 919 } 920 921 m.HostUser.Modified = true 922 return nil 923 } 924 925 func (m *HyperVMachine) resizeDisk(newSize strongunits.GiB) error { 926 if m.DiskSize > uint64(newSize) { 927 return &machine.ErrNewDiskSizeTooSmall{OldSize: strongunits.ToGiB(strongunits.B(m.DiskSize)), NewSize: newSize} 928 } 929 resize := exec.Command("powershell", []string{"-command", fmt.Sprintf("Resize-VHD %s %d", m.ImagePath.GetPath(), newSize.ToBytes())}...) 930 resize.Stdout = os.Stdout 931 resize.Stderr = os.Stderr 932 if err := resize.Run(); err != nil { 933 return fmt.Errorf("resizing image: %q", err) 934 } 935 return nil 936 } 937 938 func (m *HyperVMachine) isStarting() bool { 939 return m.Starting 940 } 941 942 func (m *HyperVMachine) createShares() (_ map[string]uint64, defErr error) { 943 toReturn := make(map[string]uint64) 944 945 for _, mount := range m.Mounts { 946 var vsock *HVSockRegistryEntry 947 948 vsockNum, ok := m.MountVsocks[mount.Target] 949 if ok { 950 // Ignore errors here, we'll just try and recreate the 951 // vsock below. 952 testVsock, err := LoadHVSockRegistryEntry(vsockNum) 953 if err == nil { 954 vsock = testVsock 955 } 956 } 957 if vsock == nil { 958 testVsock, err := NewHVSockRegistryEntry(m.Name, Fileserver) 959 if err != nil { 960 return nil, err 961 } 962 defer func() { 963 if defErr != nil { 964 if err := testVsock.Remove(); err != nil { 965 logrus.Errorf("Removing vsock: %v", err) 966 } 967 } 968 }() 969 vsock = testVsock 970 } 971 972 logrus.Debugf("Going to share directory %s via 9p on vsock %d", mount.Source, vsock.Port) 973 974 toReturn[mount.Target] = vsock.Port 975 } 976 977 return toReturn, nil 978 } 979 980 func (m *HyperVMachine) removeShares() error { 981 var removalErr error 982 983 for _, mount := range m.Mounts { 984 vsockNum, ok := m.MountVsocks[mount.Target] 985 if !ok { 986 // Mount doesn't have a valid vsock, no need to tear down 987 continue 988 } 989 990 vsock, err := LoadHVSockRegistryEntry(vsockNum) 991 if err != nil { 992 logrus.Debugf("Vsock %d for mountpoint %s does not have a valid registry entry, skipping removal", vsockNum, mount.Target) 993 continue 994 } 995 996 if err := vsock.Remove(); err != nil { 997 if removalErr != nil { 998 logrus.Errorf("Error removing vsock: %w", removalErr) 999 } 1000 removalErr = fmt.Errorf("removing vsock %d for mountpoint %s: %w", vsockNum, mount.Target, err) 1001 } 1002 } 1003 1004 return removalErr 1005 } 1006 1007 func (m *HyperVMachine) startShares() error { 1008 for mountpoint, sockNum := range m.MountVsocks { 1009 args := []string{"-q", "--", "sudo", "podman"} 1010 if logrus.IsLevelEnabled(logrus.DebugLevel) { 1011 args = append(args, "--log-level=debug") 1012 } 1013 args = append(args, "machine", "client9p", fmt.Sprintf("%d", sockNum), mountpoint) 1014 1015 sshOpts := machine.SSHOptions{ 1016 Args: args, 1017 } 1018 1019 if err := m.SSH(m.Name, sshOpts); err != nil { 1020 return err 1021 } 1022 } 1023 1024 return nil 1025 }