github.com/containers/podman/v4@v4.9.4/pkg/machine/applehv/machine.go (about) 1 //go:build darwin 2 // +build darwin 3 4 package applehv 5 6 import ( 7 "context" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "io/fs" 12 "net" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "strconv" 17 "strings" 18 "syscall" 19 "time" 20 21 "github.com/containers/common/pkg/config" 22 gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types" 23 "github.com/containers/podman/v4/pkg/machine" 24 "github.com/containers/podman/v4/pkg/machine/define" 25 "github.com/containers/podman/v4/pkg/strongunits" 26 "github.com/containers/podman/v4/pkg/util" 27 "github.com/containers/podman/v4/utils" 28 "github.com/containers/storage/pkg/lockfile" 29 vfConfig "github.com/crc-org/vfkit/pkg/config" 30 vfRest "github.com/crc-org/vfkit/pkg/rest" 31 "github.com/docker/go-units" 32 "github.com/sirupsen/logrus" 33 ) 34 35 var ( 36 // vmtype refers to qemu (vs libvirt, krun, etc). 37 vmtype = machine.AppleHvVirt 38 ) 39 40 const ( 41 dockerSock = "/var/run/docker.sock" 42 dockerConnectTimeout = 5 * time.Second 43 apiUpTimeout = 20 * time.Second 44 ) 45 46 // VfkitHelper describes the use of vfkit: cmdline and endpoint 47 type VfkitHelper struct { 48 LogLevel logrus.Level 49 Endpoint string 50 VfkitBinaryPath *define.VMFile 51 VirtualMachine *vfConfig.VirtualMachine 52 } 53 54 // appleHVReadyUnit is a unit file that sets up the virtual serial device 55 // where when the VM is done configuring, it will send an ack 56 // so a listening host knows it can begin interacting with it 57 const appleHVReadyUnit = `[Unit] 58 Requires=dev-virtio\\x2dports-%s.device 59 After=remove-moby.service sshd.socket sshd.service 60 OnFailure=emergency.target 61 OnFailureJobMode=isolate 62 [Service] 63 Type=oneshot 64 RemainAfterExit=yes 65 ExecStart=/bin/sh -c '/usr/bin/echo Ready | socat - VSOCK-CONNECT:2:1025' 66 [Install] 67 RequiredBy=default.target 68 ` 69 70 type MacMachine struct { 71 // ConfigPath is the fully qualified path to the configuration file 72 ConfigPath define.VMFile 73 // HostUser contains info about host user 74 machine.HostUser 75 // ImageConfig describes the bootable image 76 machine.ImageConfig 77 // Mounts is the list of remote filesystems to mount 78 Mounts []machine.Mount 79 // Name of VM 80 Name string 81 // ReadySocket tells host when vm is booted 82 ReadySocket define.VMFile 83 // ResourceConfig is physical attrs of the VM 84 machine.ResourceConfig 85 // SSHConfig for accessing the remote vm 86 machine.SSHConfig 87 // Starting tells us whether the machine is running or if we have just dialed it to start it 88 Starting bool 89 // Created contains the original created time instead of querying the file mod time 90 Created time.Time 91 // LastUp contains the last recorded uptime 92 LastUp time.Time 93 // The VFKit endpoint where we can interact with the VM 94 Vfkit VfkitHelper 95 LogPath define.VMFile 96 GvProxyPid define.VMFile 97 GvProxySock define.VMFile 98 99 // Used at runtime for serializing write operations 100 lock *lockfile.LockFile 101 } 102 103 // setGVProxyInfo sets the VM's gvproxy pid and socket files 104 func (m *MacMachine) setGVProxyInfo(runtimeDir string) error { 105 gvProxyPid, err := define.NewMachineFile(filepath.Join(runtimeDir, "gvproxy.pid"), nil) 106 if err != nil { 107 return err 108 } 109 m.GvProxyPid = *gvProxyPid 110 111 return machine.SetSocket(&m.GvProxySock, filepath.Join(runtimeDir, "gvproxy.sock"), nil) 112 } 113 114 // setVfkitInfo stores the default devices, sets the vfkit endpoint, and 115 // locates/stores the path to the binary 116 func (m *MacMachine) setVfkitInfo(cfg *config.Config, readySocket define.VMFile) error { 117 defaultDevices, err := getDefaultDevices(m.ImagePath.GetPath(), m.LogPath.GetPath(), readySocket.GetPath()) 118 if err != nil { 119 return err 120 } 121 // Store VFKit stuffs 122 vfkitPath, err := cfg.FindHelperBinary("vfkit", false) 123 if err != nil { 124 return err 125 } 126 vfkitBinaryPath, err := define.NewMachineFile(vfkitPath, nil) 127 if err != nil { 128 return err 129 } 130 131 m.Vfkit.VirtualMachine.Devices = defaultDevices 132 randPort, err := utils.GetRandomPort() 133 if err != nil { 134 return err 135 } 136 137 m.Vfkit.Endpoint = localhostURI + ":" + strconv.Itoa(randPort) 138 m.Vfkit.VfkitBinaryPath = vfkitBinaryPath 139 140 return nil 141 } 142 143 // addMountsToVM converts the volumes passed through the CLI to virtio-fs mounts 144 // and adds them to the machine 145 func (m *MacMachine) addMountsToVM(opts machine.InitOptions, virtiofsMnts *[]machine.VirtIoFs) error { 146 var mounts []machine.Mount 147 for _, volume := range opts.Volumes { 148 source, target, _, readOnly, err := machine.ParseVolumeFromPath(volume) 149 if err != nil { 150 return err 151 } 152 mnt := machine.NewVirtIoFsMount(source, target, readOnly) 153 *virtiofsMnts = append(*virtiofsMnts, mnt) 154 mounts = append(mounts, mnt.ToMount()) 155 } 156 m.Mounts = mounts 157 158 return nil 159 } 160 161 func (m *MacMachine) Init(opts machine.InitOptions) (bool, error) { 162 var ( 163 key string 164 virtiofsMnts []machine.VirtIoFs 165 err error 166 ) 167 168 // cleanup half-baked files if init fails at any point 169 callbackFuncs := machine.InitCleanup() 170 defer callbackFuncs.CleanIfErr(&err) 171 go callbackFuncs.CleanOnSignal() 172 173 callbackFuncs.Add(m.ConfigPath.Delete) 174 dataDir, err := machine.GetDataDir(machine.AppleHvVirt) 175 if err != nil { 176 return false, err 177 } 178 cfg, err := config.Default() 179 if err != nil { 180 return false, err 181 } 182 183 dl, err := VirtualizationProvider().NewDownload(m.Name) 184 if err != nil { 185 return false, err 186 } 187 188 imagePath, strm, err := dl.AcquireVMImage(opts.ImagePath) 189 if err != nil { 190 return false, err 191 } 192 callbackFuncs.Add(imagePath.Delete) 193 194 // Set the values for imagePath and strm 195 m.ImagePath = *imagePath 196 m.ImageStream = strm.String() 197 198 logPath, err := define.NewMachineFile(filepath.Join(dataDir, fmt.Sprintf("%s.log", m.Name)), nil) 199 if err != nil { 200 return false, err 201 } 202 callbackFuncs.Add(logPath.Delete) 203 204 m.LogPath = *logPath 205 runtimeDir, err := m.getRuntimeDir() 206 if err != nil { 207 return false, err 208 } 209 210 if err := machine.SetSocket(&m.ReadySocket, machine.ReadySocketPath(runtimeDir, m.Name), nil); err != nil { 211 return false, err 212 } 213 214 if err = m.setGVProxyInfo(runtimeDir); err != nil { 215 return false, err 216 } 217 218 if err = m.setVfkitInfo(cfg, m.ReadySocket); err != nil { 219 return false, err 220 } 221 222 m.IdentityPath = util.GetIdentityPath(m.Name) 223 m.Rootful = opts.Rootful 224 m.RemoteUsername = opts.Username 225 226 m.UID = os.Getuid() 227 228 sshPort, err := utils.GetRandomPort() 229 if err != nil { 230 return false, err 231 } 232 m.Port = sshPort 233 234 if err = m.addMountsToVM(opts, &virtiofsMnts); err != nil { 235 return false, err 236 } 237 238 err = machine.AddSSHConnectionsToPodmanSocket( 239 m.UID, 240 m.Port, 241 m.IdentityPath, 242 m.Name, 243 m.RemoteUsername, 244 opts, 245 ) 246 if err != nil { 247 return false, err 248 } 249 callbackFuncs.Add(m.removeSystemConnections) 250 251 logrus.Debugf("resizing disk to %d GiB", opts.DiskSize) 252 if err = m.resizeDisk(strongunits.GiB(opts.DiskSize)); err != nil { 253 return false, err 254 } 255 256 if err = m.writeConfig(); err != nil { 257 return false, err 258 } 259 260 if len(opts.IgnitionPath) < 1 { 261 key, err = machine.CreateSSHKeys(m.IdentityPath) 262 if err != nil { 263 return false, err 264 } 265 callbackFuncs.Add(m.removeSSHKeys) 266 } 267 268 builder := machine.NewIgnitionBuilder(machine.DynamicIgnition{ 269 Name: opts.Username, 270 Key: key, 271 VMName: m.Name, 272 VMType: machine.AppleHvVirt, 273 TimeZone: opts.TimeZone, 274 WritePath: m.IgnitionFile.GetPath(), 275 UID: m.UID, 276 Rootful: m.Rootful, 277 }) 278 279 if len(opts.IgnitionPath) > 0 { 280 return false, builder.BuildWithIgnitionFile(opts.IgnitionPath) 281 } 282 283 if err := builder.GenerateIgnitionConfig(); err != nil { 284 return false, err 285 } 286 287 builder.WithUnit(machine.Unit{ 288 Enabled: machine.BoolToPtr(true), 289 Name: "ready.service", 290 Contents: machine.StrToPtr(fmt.Sprintf(appleHVReadyUnit, "vsock")), 291 }) 292 builder.WithUnit(generateSystemDFilesForVirtiofsMounts(virtiofsMnts)...) 293 294 // TODO Ignition stuff goes here 295 err = builder.Build() 296 callbackFuncs.Add(m.IgnitionFile.Delete) 297 298 return err == nil, err 299 } 300 301 func (m *MacMachine) removeSSHKeys() error { 302 if err := os.Remove(fmt.Sprintf("%s.pub", m.IdentityPath)); err != nil { 303 logrus.Error(err) 304 } 305 return os.Remove(m.IdentityPath) 306 } 307 308 func (m *MacMachine) removeSystemConnections() error { 309 return machine.RemoveConnections(m.Name, fmt.Sprintf("%s-root", m.Name)) 310 } 311 312 func (m *MacMachine) Inspect() (*machine.InspectInfo, error) { 313 vmState, err := m.Vfkit.state() 314 if err != nil { 315 return nil, err 316 } 317 318 podmanSocket, err := m.forwardSocketPath() 319 if err != nil { 320 return nil, err 321 } 322 323 ii := machine.InspectInfo{ 324 ConfigPath: m.ConfigPath, 325 ConnectionInfo: machine.ConnectionConfig{ 326 PodmanSocket: podmanSocket, 327 PodmanPipe: nil, 328 }, 329 Created: m.Created, 330 Image: machine.ImageConfig{ 331 IgnitionFile: m.IgnitionFile, 332 ImageStream: m.ImageStream, 333 ImagePath: m.ImagePath, 334 }, 335 LastUp: m.LastUp, 336 Name: m.Name, 337 Resources: machine.ResourceConfig{ 338 CPUs: m.CPUs, 339 DiskSize: m.DiskSize, 340 Memory: m.Memory, 341 }, 342 SSHConfig: m.SSHConfig, 343 State: vmState, 344 Rootful: m.Rootful, 345 } 346 return &ii, nil 347 } 348 349 // collectFilesToDestroy retrieves the files that will be destroyed by `Remove` 350 func (m *MacMachine) collectFilesToDestroy(opts machine.RemoveOptions) []string { 351 files := []string{} 352 if !opts.SaveKeys { 353 files = append(files, m.IdentityPath, m.IdentityPath+".pub") 354 } 355 if !opts.SaveIgnition { 356 files = append(files, m.IgnitionFile.GetPath()) 357 } 358 359 if !opts.SaveImage { 360 files = append(files, m.ImagePath.GetPath()) 361 } 362 363 files = append(files, m.ConfigPath.GetPath()) 364 return files 365 } 366 367 func (m *MacMachine) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { 368 var ( 369 files []string 370 ) 371 372 m.lock.Lock() 373 defer m.lock.Unlock() 374 375 vmState, err := m.Vfkit.state() 376 if err != nil { 377 return "", nil, err 378 } 379 380 if vmState == machine.Running { 381 if !opts.Force { 382 return "", nil, &machine.ErrVMRunningCannotDestroyed{Name: m.Name} 383 } 384 if err := m.Vfkit.stop(true, true); err != nil { 385 return "", nil, err 386 } 387 defer func() { 388 if err := machine.CleanupGVProxy(m.GvProxyPid); err != nil { 389 logrus.Error(err) 390 } 391 }() 392 } 393 394 files = m.collectFilesToDestroy(opts) 395 396 confirmationMessage := "\nThe following files will be deleted:\n\n" 397 for _, msg := range files { 398 confirmationMessage += msg + "\n" 399 } 400 401 confirmationMessage += "\n" 402 return confirmationMessage, func() error { 403 machine.RemoveFilesAndConnections(files, m.Name, m.Name+"-root") 404 // TODO We will need something like this for applehv too i think 405 /* 406 // Remove the HVSOCK for networking 407 if err := m.NetworkHVSock.Remove(); err != nil { 408 logrus.Errorf("unable to remove registry entry for %s: %q", m.NetworkHVSock.KeyName, err) 409 } 410 411 // Remove the HVSOCK for events 412 if err := m.ReadyHVSock.Remove(); err != nil { 413 logrus.Errorf("unable to remove registry entry for %s: %q", m.NetworkHVSock.KeyName, err) 414 } 415 */ 416 return nil 417 }, nil 418 } 419 420 func (m *MacMachine) writeConfig() error { 421 b, err := json.MarshalIndent(m, "", " ") 422 if err != nil { 423 return err 424 } 425 return os.WriteFile(m.ConfigPath.Path, b, 0644) 426 } 427 428 func (m *MacMachine) setRootful(rootful bool) error { 429 if err := machine.SetRootful(rootful, m.Name, m.Name+"-root"); err != nil { 430 return err 431 } 432 433 m.HostUser.Modified = true 434 return nil 435 } 436 437 func (m *MacMachine) Set(name string, opts machine.SetOptions) ([]error, error) { 438 var setErrors []error 439 440 m.lock.Lock() 441 defer m.lock.Unlock() 442 443 vmState, err := m.State(false) 444 if err != nil { 445 return nil, err 446 } 447 if vmState != machine.Stopped { 448 return nil, machine.ErrWrongState 449 } 450 if cpus := opts.CPUs; cpus != nil { 451 m.CPUs = *cpus 452 } 453 if mem := opts.Memory; mem != nil { 454 m.Memory = *mem 455 } 456 if newSize := opts.DiskSize; newSize != nil { 457 if *newSize < m.DiskSize { 458 setErrors = append(setErrors, errors.New("new disk size smaller than existing disk size: cannot shrink disk size")) 459 } else { 460 m.DiskSize = *newSize 461 if err := m.resizeDisk(strongunits.GiB(*opts.DiskSize)); err != nil { 462 setErrors = append(setErrors, err) 463 } 464 } 465 } 466 if opts.USBs != nil { 467 setErrors = append(setErrors, errors.New("changing USBs not supported for applehv machines")) 468 } 469 470 if opts.Rootful != nil && m.Rootful != *opts.Rootful { 471 if err := m.setRootful(*opts.Rootful); err != nil { 472 setErrors = append(setErrors, fmt.Errorf("failed to set rootful option: %w", err)) 473 } else { 474 m.Rootful = *opts.Rootful 475 } 476 } 477 478 // Write the machine config to the filesystem 479 err = m.writeConfig() 480 setErrors = append(setErrors, err) 481 switch len(setErrors) { 482 case 0: 483 return setErrors, nil 484 case 1: 485 return nil, setErrors[0] 486 default: 487 // Number of errors is 2 or more 488 lastErr := setErrors[len(setErrors)-1] 489 return setErrors[:len(setErrors)-1], lastErr 490 } 491 } 492 493 func (m *MacMachine) SSH(name string, opts machine.SSHOptions) error { 494 st, err := m.State(false) 495 if err != nil { 496 return err 497 } 498 if st != machine.Running { 499 return fmt.Errorf("vm %q is not running", m.Name) 500 } 501 username := opts.Username 502 if username == "" { 503 username = m.RemoteUsername 504 } 505 return machine.CommonSSH(username, m.IdentityPath, m.Name, m.Port, opts.Args) 506 } 507 508 // deleteIgnitionSocket retrieves the ignition socket, deletes it, and returns a 509 // pointer to the `VMFile` 510 func (m *MacMachine) deleteIgnitionSocket() (*define.VMFile, error) { 511 ignitionSocket, err := m.getIgnitionSock() 512 if err != nil { 513 return nil, err 514 } 515 if err := ignitionSocket.Delete(); err != nil { 516 return nil, err 517 } 518 return ignitionSocket, nil 519 } 520 521 // getIgnitionVsockDeviceAsCLI retrieves the ignition vsock device and converts 522 // it to a cmdline format 523 func getIgnitionVsockDeviceAsCLI(ignitionSocketPath string) ([]string, error) { 524 ignitionVsockDevice, err := getIgnitionVsockDevice(ignitionSocketPath) 525 if err != nil { 526 return nil, err 527 } 528 // Convert the device into cli args 529 ignitionVsockDeviceCLI, err := ignitionVsockDevice.ToCmdLine() 530 if err != nil { 531 return nil, err 532 } 533 return ignitionVsockDeviceCLI, nil 534 } 535 536 // getDebugDevicesCMDArgs retrieves the debug devices and converts them to a 537 // cmdline format 538 func getDebugDevicesCMDArgs() ([]string, error) { 539 args := []string{} 540 debugDevices, err := getDebugDevices() 541 if err != nil { 542 return nil, err 543 } 544 for _, debugDevice := range debugDevices { 545 debugCli, err := debugDevice.ToCmdLine() 546 if err != nil { 547 return nil, err 548 } 549 args = append(args, debugCli...) 550 } 551 return args, nil 552 } 553 554 // getVfKitEndpointCMDArgs converts the vfkit endpoint to a cmdline format 555 func getVfKitEndpointCMDArgs(endpoint string) ([]string, error) { 556 restEndpoint, err := vfRest.NewEndpoint(endpoint) 557 if err != nil { 558 return nil, err 559 } 560 return restEndpoint.ToCmdLine() 561 } 562 563 // addVolumesToVfKit adds the VM's mounts to vfkit's devices 564 func (m *MacMachine) addVolumesToVfKit() error { 565 for _, vol := range m.Mounts { 566 virtfsDevice, err := vfConfig.VirtioFsNew(vol.Source, vol.Tag) 567 if err != nil { 568 return err 569 } 570 m.Vfkit.VirtualMachine.Devices = append(m.Vfkit.VirtualMachine.Devices, virtfsDevice) 571 } 572 return nil 573 } 574 575 func (m *MacMachine) Start(name string, opts machine.StartOptions) error { 576 var ignitionSocket *define.VMFile 577 578 m.lock.Lock() 579 defer m.lock.Unlock() 580 581 st, err := m.State(false) 582 if err != nil { 583 return err 584 } 585 586 if st == machine.Running { 587 return machine.ErrVMAlreadyRunning 588 } 589 590 // TODO handle returns from startHostNetworking 591 forwardSock, forwardState, err := m.startHostNetworking() 592 if err != nil { 593 return err 594 } 595 596 // Add networking 597 netDevice, err := vfConfig.VirtioNetNew("5a:94:ef:e4:0c:ee") 598 if err != nil { 599 return err 600 } 601 // Set user networking with gvproxy 602 netDevice.SetUnixSocketPath(m.GvProxySock.GetPath()) 603 604 m.Vfkit.VirtualMachine.Devices = append(m.Vfkit.VirtualMachine.Devices, netDevice) 605 606 if err := m.addVolumesToVfKit(); err != nil { 607 return err 608 } 609 610 // To start the VM, we need to call vfkit 611 612 logrus.Debugf("vfkit path is: %s", m.Vfkit.VfkitBinaryPath.Path) 613 cmd, err := m.Vfkit.VirtualMachine.Cmd(m.Vfkit.VfkitBinaryPath.Path) 614 if err != nil { 615 return err 616 } 617 618 vfkitEndpointArgs, err := getVfKitEndpointCMDArgs(m.Vfkit.Endpoint) 619 if err != nil { 620 return err 621 } 622 cmd.Args = append(cmd.Args, vfkitEndpointArgs...) 623 624 firstBoot, err := m.isFirstBoot() 625 if err != nil { 626 return err 627 } 628 629 if firstBoot { 630 // If this is the first boot of the vm, we need to add the vsock 631 // device to vfkit so we can inject the ignition file 632 ignitionSocket, err = m.deleteIgnitionSocket() 633 if err != nil { 634 return err 635 } 636 637 ignitionVsockDeviceCLI, err := getIgnitionVsockDeviceAsCLI(ignitionSocket.GetPath()) 638 if err != nil { 639 return err 640 } 641 cmd.Args = append(cmd.Args, ignitionVsockDeviceCLI...) 642 } 643 644 if logrus.IsLevelEnabled(logrus.DebugLevel) { 645 debugDevArgs, err := getDebugDevicesCMDArgs() 646 if err != nil { 647 return err 648 } 649 cmd.Args = append(cmd.Args, debugDevArgs...) 650 cmd.Args = append(cmd.Args, "--gui") // add command line switch to pop the gui open 651 } 652 653 readSocketBaseDir := filepath.Dir(m.ReadySocket.GetPath()) 654 if err := os.MkdirAll(readSocketBaseDir, 0755); err != nil { 655 return err 656 } 657 658 if firstBoot { 659 logrus.Debug("first boot detected") 660 logrus.Debugf("serving ignition file over %s", ignitionSocket.GetPath()) 661 go func() { 662 if err := m.serveIgnitionOverSock(ignitionSocket); err != nil { 663 logrus.Error(err) 664 } 665 logrus.Debug("ignition vsock server exited") 666 }() 667 } 668 669 if err := m.ReadySocket.Delete(); err != nil { 670 return err 671 } 672 673 logrus.Debugf("listening for ready on: %s", m.ReadySocket.GetPath()) 674 readyListen, err := net.Listen("unix", m.ReadySocket.GetPath()) 675 if err != nil { 676 return err 677 } 678 679 logrus.Debug("waiting for ready notification") 680 readyChan := make(chan error) 681 go machine.ListenAndWaitOnSocket(readyChan, readyListen) 682 683 if err := cmd.Start(); err != nil { 684 return err 685 } 686 687 processErrChan := make(chan error) 688 ctx, cancel := context.WithCancel(context.Background()) 689 defer cancel() 690 go func() { 691 defer close(processErrChan) 692 for { 693 select { 694 case <-ctx.Done(): 695 return 696 default: 697 } 698 if err := checkProcessRunning("vfkit", cmd.Process.Pid); err != nil { 699 processErrChan <- err 700 return 701 } 702 // lets poll status every half second 703 time.Sleep(500 * time.Millisecond) 704 } 705 }() 706 707 // wait for either socket or to be ready or process to have exited 708 select { 709 case err := <-processErrChan: 710 if err != nil { 711 return err 712 } 713 case err := <-readyChan: 714 if err != nil { 715 return err 716 } 717 } 718 719 logrus.Debug("ready notification received") 720 machine.WaitAPIAndPrintInfo( 721 forwardState, 722 m.Name, 723 findClaimHelper(), 724 forwardSock, 725 opts.NoInfo, 726 m.isIncompatible(), 727 m.Rootful, 728 ) 729 730 // update the podman/docker socket service if the host user has been modified at all (UID or Rootful) 731 if m.HostUser.Modified { 732 if machine.UpdatePodmanDockerSockService(m, name, m.UID, m.Rootful) == nil { 733 // Reset modification state if there are no errors, otherwise ignore errors 734 // which are already logged 735 m.HostUser.Modified = false 736 _ = m.writeConfig() 737 } 738 } 739 740 return nil 741 } 742 743 func (m *MacMachine) State(_ bool) (machine.Status, error) { 744 vmStatus, err := m.Vfkit.state() 745 if err != nil { 746 return "", err 747 } 748 return vmStatus, nil 749 } 750 751 func (m *MacMachine) Stop(name string, opts machine.StopOptions) error { 752 m.lock.Lock() 753 defer m.lock.Unlock() 754 755 vmState, err := m.State(false) 756 if err != nil { 757 return err 758 } 759 760 if vmState != machine.Running { 761 return nil 762 } 763 764 defer func() { 765 if err := machine.CleanupGVProxy(m.GvProxyPid); err != nil { 766 logrus.Error(err) 767 } 768 }() 769 if err := m.Vfkit.stop(false, true); err != nil { 770 return err 771 } 772 773 // keep track of last up 774 m.LastUp = time.Now() 775 return m.writeConfig() 776 } 777 778 // getVMConfigPath is a simple wrapper for getting the fully-qualified 779 // path of the vm json config file. It should be used to get conformity 780 func getVMConfigPath(configDir, vmName string) string { 781 return filepath.Join(configDir, fmt.Sprintf("%s.json", vmName)) 782 } 783 784 func (m *MacMachine) loadFromFile() (*MacMachine, error) { 785 if len(m.Name) < 1 { 786 return nil, errors.New("encountered machine with no name") 787 } 788 789 jsonPath, err := m.jsonConfigPath() 790 if err != nil { 791 return nil, err 792 } 793 794 mm, err := loadMacMachineFromJSON(jsonPath) 795 if err != nil { 796 return nil, err 797 } 798 799 lock, err := machine.GetLock(mm.Name, vmtype) 800 if err != nil { 801 return nil, err 802 } 803 mm.lock = lock 804 805 return mm, nil 806 } 807 808 func loadMacMachineFromJSON(fqConfigPath string) (*MacMachine, error) { 809 b, err := os.ReadFile(fqConfigPath) 810 if err != nil { 811 if errors.Is(err, fs.ErrNotExist) { 812 name := strings.TrimSuffix(filepath.Base(fqConfigPath), ".json") 813 return nil, fmt.Errorf("%s: %w", name, machine.ErrNoSuchVM) 814 } 815 return nil, err 816 } 817 mm := new(MacMachine) 818 if err := json.Unmarshal(b, mm); err != nil { 819 return nil, err 820 } 821 return mm, nil 822 } 823 824 func (m *MacMachine) jsonConfigPath() (string, error) { 825 configDir, err := machine.GetConfDir(machine.AppleHvVirt) 826 if err != nil { 827 return "", err 828 } 829 return getVMConfigPath(configDir, m.Name), nil 830 } 831 832 func getVMInfos() ([]*machine.ListResponse, error) { 833 vmConfigDir, err := machine.GetConfDir(vmtype) 834 if err != nil { 835 return nil, err 836 } 837 838 var listed []*machine.ListResponse 839 840 if err = filepath.WalkDir(vmConfigDir, func(path string, d fs.DirEntry, err error) error { 841 vm := new(MacMachine) 842 if strings.HasSuffix(d.Name(), ".json") { 843 fullPath := filepath.Join(vmConfigDir, d.Name()) 844 b, err := os.ReadFile(fullPath) 845 if err != nil { 846 return err 847 } 848 err = json.Unmarshal(b, vm) 849 if err != nil { 850 return err 851 } 852 listEntry := new(machine.ListResponse) 853 854 listEntry.Name = vm.Name 855 listEntry.Stream = vm.ImageStream 856 listEntry.VMType = machine.AppleHvVirt.String() 857 listEntry.CPUs = vm.CPUs 858 listEntry.Memory = vm.Memory * units.MiB 859 listEntry.DiskSize = vm.DiskSize * units.GiB 860 listEntry.Port = vm.Port 861 listEntry.RemoteUsername = vm.RemoteUsername 862 listEntry.IdentityPath = vm.IdentityPath 863 listEntry.CreatedAt = vm.Created 864 listEntry.Starting = vm.Starting 865 866 if listEntry.CreatedAt.IsZero() { 867 listEntry.CreatedAt = time.Now() 868 vm.Created = time.Now() 869 if err := vm.writeConfig(); err != nil { 870 return err 871 } 872 } 873 874 vmState, err := vm.State(false) 875 if err != nil { 876 return err 877 } 878 listEntry.Running = vmState == machine.Running 879 listEntry.LastUp = vm.LastUp 880 881 listed = append(listed, listEntry) 882 } 883 return nil 884 }); err != nil { 885 return nil, err 886 } 887 return listed, err 888 } 889 890 // setupStartHostNetworkingCmd generates the cmd that will be used to start the 891 // host networking. Includes the ssh port, gvproxy pid file, gvproxy socket, and 892 // a debug flag depending on the logrus log level 893 func (m *MacMachine) setupStartHostNetworkingCmd() (gvproxy.GvproxyCommand, string, machine.APIForwardingState) { 894 cmd := gvproxy.NewGvproxyCommand() 895 cmd.SSHPort = m.Port 896 cmd.PidFile = m.GvProxyPid.GetPath() 897 cmd.AddVfkitSocket(fmt.Sprintf("unixgram://%s", m.GvProxySock.GetPath())) 898 cmd.Debug = logrus.IsLevelEnabled(logrus.DebugLevel) 899 900 cmd, forwardSock, state := m.setupAPIForwarding(cmd) 901 if cmd.Debug { 902 logrus.Debug(cmd.ToCmdline()) 903 } 904 905 return cmd, forwardSock, state 906 } 907 908 func (m *MacMachine) startHostNetworking() (string, machine.APIForwardingState, error) { 909 var ( 910 forwardSock string 911 state machine.APIForwardingState 912 ) 913 914 // TODO This should probably be added to startHostNetworking everywhere 915 // GvProxy does not clean up after itself 916 if err := m.GvProxySock.Delete(); err != nil { 917 b, err := m.GvProxyPid.Read() 918 if err != nil { 919 return "", machine.NoForwarding, err 920 } 921 pid, err := strconv.Atoi(string(b)) 922 if err != nil { 923 return "", 0, err 924 } 925 gvProcess, err := os.FindProcess(pid) 926 if err != nil { 927 return "", 0, err 928 } 929 // shoot it with a signal 0 and see if it is active 930 err = gvProcess.Signal(syscall.Signal(0)) 931 if err == nil { 932 return "", 0, fmt.Errorf("gvproxy process %s already running", string(b)) 933 } 934 if err := m.GvProxySock.Delete(); err != nil { 935 return "", 0, err 936 } 937 } 938 cfg, err := config.Default() 939 if err != nil { 940 return "", machine.NoForwarding, err 941 } 942 943 gvproxyBinary, err := cfg.FindHelperBinary("gvproxy", false) 944 if err != nil { 945 return "", 0, err 946 } 947 948 logrus.Debugf("gvproxy binary being used: %s", gvproxyBinary) 949 950 cmd, forwardSock, state := m.setupStartHostNetworkingCmd() 951 c := cmd.Cmd(gvproxyBinary) 952 if err := c.Start(); err != nil { 953 return "", 0, fmt.Errorf("unable to execute: %q: %w", cmd.ToCmdline(), err) 954 } 955 956 // We need to wait and make sure gvproxy is in fact running 957 // before continuing 958 for i := 0; i < 10; i++ { 959 _, err := os.Stat(m.GvProxySock.GetPath()) 960 if err == nil { 961 break 962 } 963 if err := checkProcessRunning("gvproxy", c.Process.Pid); err != nil { 964 // gvproxy is no longer running 965 return "", 0, err 966 } 967 logrus.Debugf("gvproxy unixgram socket %q not found: %v", m.GvProxySock.GetPath(), err) 968 // Sleep for 1/2 second 969 time.Sleep(500 * time.Millisecond) 970 } 971 if err != nil { 972 // I guess we would also check the pidfile and look to see if it is running 973 // to? 974 return "", 0, fmt.Errorf("unable to verify gvproxy is running") 975 } 976 return forwardSock, state, nil 977 } 978 979 // checkProcessRunning checks non blocking if the pid exited 980 // returns nil if process is running otherwise an error if not 981 func checkProcessRunning(processName string, pid int) error { 982 var status syscall.WaitStatus 983 pid, err := syscall.Wait4(pid, &status, syscall.WNOHANG, nil) 984 if err != nil { 985 return fmt.Errorf("failed to read %s process status: %w", processName, err) 986 } 987 if pid > 0 { 988 // child exited 989 return fmt.Errorf("%s exited unexpectedly with exit code %d", processName, status.ExitStatus()) 990 } 991 return nil 992 } 993 994 func (m *MacMachine) setupAPIForwarding(cmd gvproxy.GvproxyCommand) (gvproxy.GvproxyCommand, string, machine.APIForwardingState) { 995 socket, err := m.forwardSocketPath() 996 if err != nil { 997 return cmd, "", machine.NoForwarding 998 } 999 1000 destSock := fmt.Sprintf("/run/user/%d/podman/podman.sock", m.UID) 1001 forwardUser := m.RemoteUsername 1002 1003 if m.Rootful { 1004 destSock = "/run/podman/podman.sock" 1005 forwardUser = "root" 1006 } 1007 1008 cmd.AddForwardSock(socket.GetPath()) 1009 cmd.AddForwardDest(destSock) 1010 cmd.AddForwardUser(forwardUser) 1011 cmd.AddForwardIdentity(m.IdentityPath) 1012 1013 link, err := m.userGlobalSocketLink() 1014 if err != nil { 1015 return cmd, socket.GetPath(), machine.MachineLocal 1016 } 1017 1018 if !dockerClaimSupported() { 1019 return cmd, socket.GetPath(), machine.ClaimUnsupported 1020 } 1021 1022 if !dockerClaimHelperInstalled() { 1023 return cmd, socket.GetPath(), machine.NotInstalled 1024 } 1025 1026 if !alreadyLinked(socket.GetPath(), link) { 1027 if checkSockInUse(link) { 1028 return cmd, socket.GetPath(), machine.MachineLocal 1029 } 1030 1031 _ = os.Remove(link) 1032 if err = os.Symlink(socket.GetPath(), link); err != nil { 1033 logrus.Warnf("could not create user global API forwarding link: %s", err.Error()) 1034 return cmd, socket.GetPath(), machine.MachineLocal 1035 } 1036 } 1037 1038 if !alreadyLinked(link, dockerSock) { 1039 if checkSockInUse(dockerSock) { 1040 return cmd, socket.GetPath(), machine.MachineLocal 1041 } 1042 1043 if !claimDockerSock() { 1044 logrus.Warn("podman helper is installed, but was not able to claim the global docker sock") 1045 return cmd, socket.GetPath(), machine.MachineLocal 1046 } 1047 } 1048 1049 return cmd, socket.GetPath(), machine.MachineLocal 1050 1051 } 1052 1053 func (m *MacMachine) dockerSock() (string, error) { 1054 dd, err := machine.GetDataDir(machine.AppleHvVirt) 1055 if err != nil { 1056 return "", err 1057 } 1058 return filepath.Join(dd, "podman.sock"), nil 1059 } 1060 1061 func (m *MacMachine) forwardSocketPath() (*define.VMFile, error) { 1062 sockName := "podman.sock" 1063 path, err := machine.GetDataDir(machine.AppleHvVirt) 1064 if err != nil { 1065 return nil, fmt.Errorf("Resolving data dir: %s", err.Error()) 1066 } 1067 return define.NewMachineFile(filepath.Join(path, sockName), &sockName) 1068 } 1069 1070 // resizeDisk uses os truncate to resize (only larger) a raw disk. the input size 1071 // is assumed GiB 1072 func (m *MacMachine) resizeDisk(newSize strongunits.GiB) error { 1073 if uint64(newSize) < m.DiskSize { 1074 // TODO this error needs to be changed to the common error. would do now but the PR for the common 1075 // error has not merged 1076 return fmt.Errorf("invalid disk size %d: new disk must be larger than %dGB", newSize, m.DiskSize) 1077 } 1078 logrus.Debugf("resizing %s to %d bytes", m.ImagePath.GetPath(), newSize.ToBytes()) 1079 // seems like os.truncate() is not very performant with really large files 1080 // so exec'ing out to the command truncate 1081 size := fmt.Sprintf("%dG", newSize) 1082 c := exec.Command("truncate", "-s", size, m.ImagePath.GetPath()) 1083 if logrus.IsLevelEnabled(logrus.DebugLevel) { 1084 c.Stderr = os.Stderr 1085 c.Stdout = os.Stdout 1086 } 1087 return c.Run() 1088 } 1089 1090 // isFirstBoot returns a bool reflecting if the machine has been booted before 1091 func (m *MacMachine) isFirstBoot() (bool, error) { 1092 never, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") 1093 if err != nil { 1094 return false, err 1095 } 1096 return m.LastUp == never, nil 1097 } 1098 1099 func (m *MacMachine) getIgnitionSock() (*define.VMFile, error) { 1100 dataDir, err := machine.GetDataDir(machine.AppleHvVirt) 1101 if err != nil { 1102 return nil, err 1103 } 1104 if err := os.MkdirAll(dataDir, 0755); err != nil { 1105 if !errors.Is(err, os.ErrExist) { 1106 return nil, err 1107 } 1108 } 1109 return define.NewMachineFile(filepath.Join(dataDir, ignitionSocketName), nil) 1110 } 1111 1112 func (m *MacMachine) getRuntimeDir() (string, error) { 1113 tmpDir, ok := os.LookupEnv("TMPDIR") 1114 if !ok { 1115 tmpDir = "/tmp" 1116 } 1117 rtd := filepath.Join(tmpDir, "podman") 1118 logrus.Debugf("creating runtimeDir: %s", rtd) 1119 if err := os.MkdirAll(rtd, 0755); err != nil { 1120 return "", err 1121 } 1122 1123 return rtd, nil 1124 } 1125 1126 func (m *MacMachine) userGlobalSocketLink() (string, error) { 1127 path, err := machine.GetDataDir(machine.AppleHvVirt) 1128 if err != nil { 1129 logrus.Errorf("Resolving data dir: %s", err.Error()) 1130 return "", err 1131 } 1132 // User global socket is located in parent directory of machine dirs (one per user) 1133 return filepath.Join(filepath.Dir(path), "podman.sock"), err 1134 } 1135 1136 func (m *MacMachine) isIncompatible() bool { 1137 return m.UID == -1 1138 } 1139 1140 func generateSystemDFilesForVirtiofsMounts(mounts []machine.VirtIoFs) []machine.Unit { 1141 // mounting in fcos with virtiofs is a bit of a dance. we need a unit file for the mount, a unit file 1142 // for automatic mounting on boot, and a "preparatory" service file that disables FCOS security, performs 1143 // the mkdir of the mount point, and then re-enables security. This must be done for each mount. 1144 1145 var unitFiles []machine.Unit 1146 for _, mnt := range mounts { 1147 // Here we are looping the mounts and for each mount, we are adding two unit files 1148 // for virtiofs. One unit file is the mount itself and the second is to automount it 1149 // on boot. 1150 autoMountUnit := `[Automount] 1151 Where=%s 1152 [Install] 1153 WantedBy=multi-user.target 1154 1155 [Unit] 1156 Description=Mount virtiofs volume %s 1157 ` 1158 mountUnit := `[Mount] 1159 What=%s 1160 Where=%s 1161 Type=virtiofs 1162 1163 [Install] 1164 WantedBy=multi-user.target 1165 ` 1166 virtiofsAutomount := machine.Unit{ 1167 Enabled: machine.BoolToPtr(true), 1168 Name: fmt.Sprintf("%s.automount", mnt.Tag), 1169 Contents: machine.StrToPtr(fmt.Sprintf(autoMountUnit, mnt.Target, mnt.Target)), 1170 } 1171 virtiofsMount := machine.Unit{ 1172 Enabled: machine.BoolToPtr(true), 1173 Name: fmt.Sprintf("%s.mount", mnt.Tag), 1174 Contents: machine.StrToPtr(fmt.Sprintf(mountUnit, mnt.Tag, mnt.Target)), 1175 } 1176 1177 // This "unit" simulates something like systemctl enable virtiofs-mount-prepare@ 1178 enablePrep := machine.Unit{ 1179 Enabled: machine.BoolToPtr(true), 1180 Name: fmt.Sprintf("virtiofs-mount-prepare@%s.service", mnt.Tag), 1181 } 1182 1183 unitFiles = append(unitFiles, virtiofsAutomount, virtiofsMount, enablePrep) 1184 } 1185 1186 // mount prep is a way to workaround the FCOS limitation of creating directories 1187 // at the rootfs / and then mounting to them. 1188 mountPrep := ` 1189 [Unit] 1190 Description=Allow virtios to mount to / 1191 DefaultDependencies=no 1192 ConditionPathExists=!%f 1193 1194 [Service] 1195 Type=oneshot 1196 ExecStartPre=chattr -i / 1197 ExecStart=mkdir -p '%f' 1198 ExecStopPost=chattr +i / 1199 1200 [Install] 1201 WantedBy=remote-fs.target 1202 ` 1203 virtioFSChattr := machine.Unit{ 1204 Contents: machine.StrToPtr(mountPrep), 1205 Name: "virtiofs-mount-prepare@.service", 1206 } 1207 unitFiles = append(unitFiles, virtioFSChattr) 1208 1209 return unitFiles 1210 }