github.com/containers/podman/v4@v4.9.4/pkg/machine/ignition.go (about) 1 //go:build amd64 || arm64 2 // +build amd64 arm64 3 4 package machine 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io/fs" 10 "net/url" 11 "os" 12 "path/filepath" 13 "strings" 14 15 "github.com/containers/common/libnetwork/etchosts" 16 "github.com/containers/common/pkg/config" 17 "github.com/containers/podman/v4/pkg/machine/define" 18 "github.com/containers/podman/v4/pkg/systemd/parser" 19 "github.com/sirupsen/logrus" 20 ) 21 22 /* 23 If this file gets too nuts, we can perhaps use existing go code 24 to create ignition files. At this point, the file is so simple 25 that I chose to use structs and not import any code as I was 26 concerned (unsubstantiated) about too much bloat coming in. 27 28 https://github.com/openshift/machine-config-operator/blob/master/pkg/server/server.go 29 */ 30 31 const ( 32 UserCertsTargetPath = "/etc/containers/certs.d" 33 PodmanDockerTmpConfPath = "/etc/tmpfiles.d/podman-docker.conf" 34 ) 35 36 // Convenience function to convert int to ptr 37 func IntToPtr(i int) *int { 38 return &i 39 } 40 41 // Convenience function to convert string to ptr 42 func StrToPtr(s string) *string { 43 return &s 44 } 45 46 // Convenience function to convert bool to ptr 47 func BoolToPtr(b bool) *bool { 48 return &b 49 } 50 51 func GetNodeUsr(usrName string) NodeUser { 52 return NodeUser{Name: &usrName} 53 } 54 55 func GetNodeGrp(grpName string) NodeGroup { 56 return NodeGroup{Name: &grpName} 57 } 58 59 type DynamicIgnition struct { 60 Name string 61 Key string 62 TimeZone string 63 UID int 64 VMName string 65 VMType VMType 66 WritePath string 67 Cfg Config 68 Rootful bool 69 NetRecover bool 70 } 71 72 func (ign *DynamicIgnition) Write() error { 73 b, err := json.Marshal(ign.Cfg) 74 if err != nil { 75 return err 76 } 77 return os.WriteFile(ign.WritePath, b, 0644) 78 } 79 80 func (ign *DynamicIgnition) getUsers() []PasswdUser { 81 var ( 82 users []PasswdUser 83 ) 84 85 isCoreUser := ign.Name == DefaultIgnitionUserName 86 87 // if we are not using the 'core' user, we need to tell ignition to 88 // not add it 89 if !isCoreUser { 90 coreUser := PasswdUser{ 91 Name: DefaultIgnitionUserName, 92 ShouldExist: BoolToPtr(false), 93 } 94 users = append(users, coreUser) 95 } 96 97 // Adding the user 98 user := PasswdUser{ 99 Name: ign.Name, 100 SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(ign.Key)}, 101 UID: IntToPtr(ign.UID), 102 } 103 104 // If we are not using the core user, we need to make the user part 105 // of the following groups 106 if !isCoreUser { 107 user.Groups = []Group{ 108 Group("sudo"), 109 Group("adm"), 110 Group("wheel"), 111 Group("systemd-journal")} 112 } 113 114 // set root SSH key 115 root := PasswdUser{ 116 Name: "root", 117 SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(ign.Key)}, 118 } 119 // add them all in 120 users = append(users, user, root) 121 122 return users 123 } 124 125 // GenerateIgnitionConfig 126 func (ign *DynamicIgnition) GenerateIgnitionConfig() error { 127 if len(ign.Name) < 1 { 128 ign.Name = DefaultIgnitionUserName 129 } 130 ignVersion := Ignition{ 131 Version: "3.2.0", 132 } 133 ignPassword := Passwd{ 134 Users: ign.getUsers(), 135 } 136 137 ignStorage := Storage{ 138 Directories: getDirs(ign.Name), 139 Files: getFiles(ign.Name, ign.UID, ign.Rootful, ign.VMType, ign.NetRecover), 140 Links: getLinks(ign.Name), 141 } 142 143 // Add or set the time zone for the machine 144 if len(ign.TimeZone) > 0 { 145 var ( 146 err error 147 tz string 148 ) 149 // local means the same as the host 150 // look up where it is pointing to on the host 151 if ign.TimeZone == "local" { 152 tz, err = getLocalTimeZone() 153 if err != nil { 154 return err 155 } 156 } else { 157 tz = ign.TimeZone 158 } 159 tzLink := Link{ 160 Node: Node{ 161 Group: GetNodeGrp("root"), 162 Path: "/etc/localtime", 163 Overwrite: BoolToPtr(false), 164 User: GetNodeUsr("root"), 165 }, 166 LinkEmbedded1: LinkEmbedded1{ 167 Hard: BoolToPtr(false), 168 // We always want this value in unix form (/path/to/something) because this is being 169 // set in the machine OS (always Linux). However, filepath.join on windows will use a "\\" 170 // separator; therefore we use ToSlash to convert the path to unix style 171 Target: filepath.ToSlash(filepath.Join("/usr/share/zoneinfo", tz)), 172 }, 173 } 174 ignStorage.Links = append(ignStorage.Links, tzLink) 175 } 176 177 deMoby := `[Unit] 178 Description=Remove moby-engine 179 # Run once for the machine 180 After=systemd-machine-id-commit.service 181 Before=zincati.service 182 ConditionPathExists=!/var/lib/%N.stamp 183 184 [Service] 185 Type=oneshot 186 RemainAfterExit=yes 187 ExecStart=/usr/bin/rpm-ostree override remove moby-engine 188 ExecStart=/usr/bin/rpm-ostree ex apply-live --allow-replacement 189 ExecStartPost=/bin/touch /var/lib/%N.stamp 190 191 [Install] 192 WantedBy=default.target 193 ` 194 // This service gets environment variables that are provided 195 // through qemu fw_cfg and then sets them into systemd/system.conf.d, 196 // profile.d and environment.d files 197 // 198 // Currently, it is used for propagating 199 // proxy settings e.g. HTTP_PROXY and others, on a start avoiding 200 // a need of re-creating/re-initiating a VM 201 envset := `[Unit] 202 Description=Environment setter from QEMU FW_CFG 203 [Service] 204 Type=oneshot 205 RemainAfterExit=yes 206 Environment=FWCFGRAW=/sys/firmware/qemu_fw_cfg/by_name/opt/com.coreos/environment/raw 207 Environment=SYSTEMD_CONF=/etc/systemd/system.conf.d/default-env.conf 208 Environment=ENVD_CONF=/etc/environment.d/default-env.conf 209 Environment=PROFILE_CONF=/etc/profile.d/default-env.sh 210 ExecStart=/usr/bin/bash -c '/usr/bin/test -f ${FWCFGRAW} &&\ 211 echo "[Manager]\n#Got from QEMU FW_CFG\nDefaultEnvironment=$(/usr/bin/base64 -d ${FWCFGRAW} | sed -e "s+|+ +g")\n" > ${SYSTEMD_CONF} ||\ 212 echo "[Manager]\n#Got nothing from QEMU FW_CFG\n#DefaultEnvironment=\n" > ${SYSTEMD_CONF}' 213 ExecStart=/usr/bin/bash -c '/usr/bin/test -f ${FWCFGRAW} && (\ 214 echo "#Got from QEMU FW_CFG"> ${ENVD_CONF};\ 215 IFS="|";\ 216 for iprxy in $(/usr/bin/base64 -d ${FWCFGRAW}); do\ 217 echo "$iprxy" >> ${ENVD_CONF}; done ) || \ 218 echo "#Got nothing from QEMU FW_CFG"> ${ENVD_CONF}' 219 ExecStart=/usr/bin/bash -c '/usr/bin/test -f ${FWCFGRAW} && (\ 220 echo "#Got from QEMU FW_CFG"> ${PROFILE_CONF};\ 221 IFS="|";\ 222 for iprxy in $(/usr/bin/base64 -d ${FWCFGRAW}); do\ 223 echo "export $iprxy" >> ${PROFILE_CONF}; done ) || \ 224 echo "#Got nothing from QEMU FW_CFG"> ${PROFILE_CONF}' 225 ExecStartPost=/usr/bin/systemctl daemon-reload 226 [Install] 227 WantedBy=sysinit.target 228 ` 229 ignSystemd := Systemd{ 230 Units: []Unit{ 231 { 232 Enabled: BoolToPtr(true), 233 Name: "podman.socket", 234 }, 235 { 236 Enabled: BoolToPtr(false), 237 Name: "docker.service", 238 Mask: BoolToPtr(true), 239 }, 240 { 241 Enabled: BoolToPtr(false), 242 Name: "docker.socket", 243 Mask: BoolToPtr(true), 244 }, 245 { 246 Enabled: BoolToPtr(true), 247 Name: "remove-moby.service", 248 Contents: &deMoby, 249 }, 250 { 251 // Disable auto-updating of fcos images 252 // https://github.com/containers/podman/issues/20122 253 Enabled: BoolToPtr(false), 254 Name: "zincati.service", 255 }, 256 }} 257 258 // Only qemu has the qemu firmware environment setting 259 if ign.VMType == QemuVirt { 260 qemuUnit := Unit{ 261 Enabled: BoolToPtr(true), 262 Name: "envset-fwcfg.service", 263 Contents: &envset, 264 } 265 ignSystemd.Units = append(ignSystemd.Units, qemuUnit) 266 } 267 268 if ign.NetRecover { 269 contents, err := GetNetRecoveryUnitFile().ToString() 270 if err != nil { 271 return err 272 } 273 274 recoveryUnit := Unit{ 275 Enabled: BoolToPtr(true), 276 Name: "net-health-recovery.service", 277 Contents: &contents, 278 } 279 ignSystemd.Units = append(ignSystemd.Units, recoveryUnit) 280 } 281 282 // Only after all checks are done 283 // it's ready create the ingConfig 284 ign.Cfg = Config{ 285 Ignition: ignVersion, 286 Passwd: ignPassword, 287 Storage: ignStorage, 288 Systemd: ignSystemd, 289 } 290 return nil 291 } 292 293 func getDirs(usrName string) []Directory { 294 // Ignition has a bug/feature? where if you make a series of dirs 295 // in one swoop, then the leading dirs are creates as root. 296 newDirs := []string{ 297 "/home/" + usrName + "/.config", 298 "/home/" + usrName + "/.config/containers", 299 "/home/" + usrName + "/.config/systemd", 300 "/home/" + usrName + "/.config/systemd/user", 301 "/home/" + usrName + "/.config/systemd/user/default.target.wants", 302 } 303 var ( 304 dirs = make([]Directory, len(newDirs)) 305 ) 306 for i, d := range newDirs { 307 newDir := Directory{ 308 Node: Node{ 309 Group: GetNodeGrp(usrName), 310 Path: d, 311 User: GetNodeUsr(usrName), 312 }, 313 DirectoryEmbedded1: DirectoryEmbedded1{Mode: IntToPtr(0755)}, 314 } 315 dirs[i] = newDir 316 } 317 318 // Issue #11489: make sure that we can inject a custom registries.conf 319 // file on the system level to force a single search registry. 320 // The remote client does not yet support prompting for short-name 321 // resolution, so we enforce a single search registry (i.e., docker.io) 322 // as a workaround. 323 dirs = append(dirs, Directory{ 324 Node: Node{ 325 Group: GetNodeGrp("root"), 326 Path: "/etc/containers/registries.conf.d", 327 User: GetNodeUsr("root"), 328 }, 329 DirectoryEmbedded1: DirectoryEmbedded1{Mode: IntToPtr(0755)}, 330 }) 331 332 // The directory is used by envset-fwcfg.service 333 // for propagating environment variables that got 334 // from a host 335 dirs = append(dirs, Directory{ 336 Node: Node{ 337 Group: GetNodeGrp("root"), 338 Path: "/etc/systemd/system.conf.d", 339 User: GetNodeUsr("root"), 340 }, 341 DirectoryEmbedded1: DirectoryEmbedded1{Mode: IntToPtr(0755)}, 342 }, Directory{ 343 Node: Node{ 344 Group: GetNodeGrp("root"), 345 Path: "/etc/environment.d", 346 User: GetNodeUsr("root"), 347 }, 348 DirectoryEmbedded1: DirectoryEmbedded1{Mode: IntToPtr(0755)}, 349 }) 350 351 return dirs 352 } 353 354 //nolint:unparam // matches signature in 5.x 355 func getFiles(usrName string, uid int, rootful bool, vmtype VMType, netRecover bool) []File { 356 files := make([]File, 0) 357 358 lingerExample := `[Unit] 359 Description=A systemd user unit demo 360 After=network-online.target 361 Wants=network-online.target podman.socket 362 [Service] 363 ExecStart=/usr/bin/sleep infinity 364 ` 365 containers := `[containers] 366 netns="bridge" 367 pids_limit=0 368 ` 369 // Set deprecated machine_enabled until podman package on fcos is 370 // current enough to no longer require it 371 rootContainers := `[engine] 372 machine_enabled=true 373 ` 374 375 delegateConf := `[Service] 376 Delegate=memory pids cpu io 377 ` 378 // Prevent subUID from clashing with actual UID 379 subUID := 100000 380 subUIDs := 1000000 381 if uid >= subUID && uid < (subUID+subUIDs) { 382 subUID = uid + 1 383 } 384 etcSubUID := fmt.Sprintf(`%s:%d:%d`, usrName, subUID, subUIDs) 385 386 // Add a fake systemd service to get the user socket rolling 387 files = append(files, File{ 388 Node: Node{ 389 Group: GetNodeGrp(usrName), 390 Path: "/home/" + usrName + "/.config/systemd/user/linger-example.service", 391 User: GetNodeUsr(usrName), 392 }, 393 FileEmbedded1: FileEmbedded1{ 394 Append: nil, 395 Contents: Resource{ 396 Source: EncodeDataURLPtr(lingerExample), 397 }, 398 Mode: IntToPtr(0744), 399 }, 400 }) 401 402 // Set containers.conf up for core user to use networks 403 // by default 404 files = append(files, File{ 405 Node: Node{ 406 Group: GetNodeGrp(usrName), 407 Path: "/home/" + usrName + "/.config/containers/containers.conf", 408 User: GetNodeUsr(usrName), 409 }, 410 FileEmbedded1: FileEmbedded1{ 411 Append: nil, 412 Contents: Resource{ 413 Source: EncodeDataURLPtr(containers), 414 }, 415 Mode: IntToPtr(0744), 416 }, 417 }) 418 // Set up /etc/subuid and /etc/subgid 419 for _, sub := range []string{"/etc/subuid", "/etc/subgid"} { 420 files = append(files, File{ 421 Node: Node{ 422 Group: GetNodeGrp("root"), 423 Path: sub, 424 User: GetNodeUsr("root"), 425 Overwrite: BoolToPtr(true), 426 }, 427 FileEmbedded1: FileEmbedded1{ 428 Append: nil, 429 Contents: Resource{ 430 Source: EncodeDataURLPtr(etcSubUID), 431 }, 432 Mode: IntToPtr(0744), 433 }, 434 }) 435 } 436 437 // Set delegate.conf so cpu,io subsystem is delegated to non-root users as well for cgroupv2 438 // by default 439 files = append(files, File{ 440 Node: Node{ 441 Group: GetNodeGrp("root"), 442 Path: "/etc/systemd/system/user@.service.d/delegate.conf", 443 User: GetNodeUsr("root"), 444 }, 445 FileEmbedded1: FileEmbedded1{ 446 Append: nil, 447 Contents: Resource{ 448 Source: EncodeDataURLPtr(delegateConf), 449 }, 450 Mode: IntToPtr(0644), 451 }, 452 }) 453 454 // Add a file into linger 455 files = append(files, File{ 456 Node: Node{ 457 Group: GetNodeGrp(usrName), 458 Path: "/var/lib/systemd/linger/core", 459 User: GetNodeUsr(usrName), 460 }, 461 FileEmbedded1: FileEmbedded1{Mode: IntToPtr(0644)}, 462 }) 463 464 // Set deprecated machine_enabled to true to indicate we're in a VM 465 files = append(files, File{ 466 Node: Node{ 467 Group: GetNodeGrp("root"), 468 Path: "/etc/containers/containers.conf", 469 User: GetNodeUsr("root"), 470 }, 471 FileEmbedded1: FileEmbedded1{ 472 Append: nil, 473 Contents: Resource{ 474 Source: EncodeDataURLPtr(rootContainers), 475 }, 476 Mode: IntToPtr(0644), 477 }, 478 }) 479 480 // Set machine marker file to indicate podman is in a qemu based machine 481 files = append(files, File{ 482 Node: Node{ 483 Group: GetNodeGrp("root"), 484 Path: "/etc/containers/podman-machine", 485 User: GetNodeUsr("root"), 486 }, 487 FileEmbedded1: FileEmbedded1{ 488 Append: nil, 489 Contents: Resource{ 490 // TODO this should be fixed for all vmtypes 491 Source: EncodeDataURLPtr("qemu\n"), 492 }, 493 Mode: IntToPtr(0644), 494 }, 495 }) 496 497 // Increase the number of inotify instances. 498 files = append(files, File{ 499 Node: Node{ 500 Group: GetNodeGrp("root"), 501 Path: "/etc/sysctl.d/10-inotify-instances.conf", 502 User: GetNodeUsr("root"), 503 }, 504 FileEmbedded1: FileEmbedded1{ 505 Append: nil, 506 Contents: Resource{ 507 Source: EncodeDataURLPtr("fs.inotify.max_user_instances=524288\n"), 508 }, 509 Mode: IntToPtr(0644), 510 }, 511 }) 512 513 // Issue #11489: make sure that we can inject a custom registries.conf 514 // file on the system level to force a single search registry. 515 // The remote client does not yet support prompting for short-name 516 // resolution, so we enforce a single search registry (i.e., docker.io) 517 // as a workaround. 518 files = append(files, File{ 519 Node: Node{ 520 Group: GetNodeGrp("root"), 521 Path: "/etc/containers/registries.conf.d/999-podman-machine.conf", 522 User: GetNodeUsr("root"), 523 }, 524 FileEmbedded1: FileEmbedded1{ 525 Append: nil, 526 Contents: Resource{ 527 Source: EncodeDataURLPtr("unqualified-search-registries=[\"docker.io\"]\n"), 528 }, 529 Mode: IntToPtr(0644), 530 }, 531 }) 532 533 files = append(files, File{ 534 Node: Node{ 535 Path: PodmanDockerTmpConfPath, 536 }, 537 FileEmbedded1: FileEmbedded1{ 538 Append: nil, 539 // Create a symlink from the docker socket to the podman socket. 540 Contents: Resource{ 541 Source: EncodeDataURLPtr(GetPodmanDockerTmpConfig(uid, rootful, true)), 542 }, 543 Mode: IntToPtr(0644), 544 }, 545 }) 546 547 setDockerHost := `export DOCKER_HOST="unix://$(podman info -f "{{.Host.RemoteSocket.Path}}")" 548 ` 549 550 files = append(files, File{ 551 Node: Node{ 552 Group: GetNodeGrp("root"), 553 Path: "/etc/profile.d/docker-host.sh", 554 User: GetNodeUsr("root"), 555 }, 556 FileEmbedded1: FileEmbedded1{ 557 Append: nil, 558 Contents: Resource{ 559 Source: EncodeDataURLPtr(setDockerHost), 560 }, 561 Mode: IntToPtr(0644), 562 }, 563 }) 564 565 // get certs for current user 566 userHome, err := os.UserHomeDir() 567 if err != nil { 568 logrus.Warnf("Unable to copy certs via ignition %s", err.Error()) 569 return files 570 } 571 572 certFiles := getCerts(filepath.Join(userHome, ".config/containers/certs.d"), true) 573 files = append(files, certFiles...) 574 575 certFiles = getCerts(filepath.Join(userHome, ".config/docker/certs.d"), true) 576 files = append(files, certFiles...) 577 578 if sslCertFile, ok := os.LookupEnv("SSL_CERT_FILE"); ok { 579 if _, err := os.Stat(sslCertFile); err == nil { 580 certFiles = getCerts(sslCertFile, false) 581 files = append(files, certFiles...) 582 } else { 583 logrus.Warnf("Invalid path in SSL_CERT_FILE: %q", err) 584 } 585 } 586 587 if sslCertDir, ok := os.LookupEnv("SSL_CERT_DIR"); ok { 588 if _, err := os.Stat(sslCertDir); err == nil { 589 certFiles = getCerts(sslCertDir, true) 590 files = append(files, certFiles...) 591 } else { 592 logrus.Warnf("Invalid path in SSL_CERT_DIR: %q", err) 593 } 594 } 595 596 files = append(files, File{ 597 Node: Node{ 598 User: GetNodeUsr("root"), 599 Group: GetNodeGrp("root"), 600 Path: "/etc/chrony.conf", 601 }, 602 FileEmbedded1: FileEmbedded1{ 603 Append: []Resource{{ 604 Source: EncodeDataURLPtr("\nconfdir /etc/chrony.d\n"), 605 }}, 606 }, 607 }) 608 609 // Issue #11541: allow Chrony to update the system time when it has drifted 610 // far from NTP time. 611 files = append(files, File{ 612 Node: Node{ 613 User: GetNodeUsr("root"), 614 Group: GetNodeGrp("root"), 615 Path: "/etc/chrony.d/50-podman-makestep.conf", 616 }, 617 FileEmbedded1: FileEmbedded1{ 618 Contents: Resource{ 619 Source: EncodeDataURLPtr("makestep 1 -1\n"), 620 }, 621 }, 622 }) 623 624 // Only necessary for qemu on mac 625 if netRecover { 626 files = append(files, File{ 627 Node: Node{ 628 User: GetNodeUsr("root"), 629 Group: GetNodeGrp("root"), 630 Path: "/usr/local/bin/net-health-recovery.sh", 631 }, 632 FileEmbedded1: FileEmbedded1{ 633 Mode: IntToPtr(0755), 634 Contents: Resource{ 635 Source: EncodeDataURLPtr(GetNetRecoveryFile()), 636 }, 637 }, 638 }) 639 } 640 641 return files 642 } 643 644 func getCerts(certsDir string, isDir bool) []File { 645 var ( 646 files []File 647 ) 648 649 if isDir { 650 err := filepath.WalkDir(certsDir, func(path string, d fs.DirEntry, err error) error { 651 if err == nil && !d.IsDir() { 652 certPath, err := filepath.Rel(certsDir, path) 653 if err != nil { 654 logrus.Warnf("%s", err) 655 return nil 656 } 657 658 file, err := prepareCertFile(filepath.Join(certsDir, certPath), certPath) 659 if err == nil { 660 files = append(files, file) 661 } 662 } 663 664 return nil 665 }) 666 if err != nil { 667 if !os.IsNotExist(err) { 668 logrus.Warnf("Unable to copy certs via ignition, error while reading certs from %s: %s", certsDir, err.Error()) 669 } 670 } 671 } else { 672 fileName := filepath.Base(certsDir) 673 file, err := prepareCertFile(certsDir, fileName) 674 if err == nil { 675 files = append(files, file) 676 } 677 } 678 679 return files 680 } 681 682 func prepareCertFile(path string, name string) (File, error) { 683 b, err := os.ReadFile(path) 684 if err != nil { 685 logrus.Warnf("Unable to read cert file %v", err) 686 return File{}, err 687 } 688 689 targetPath := filepath.Join(UserCertsTargetPath, name) 690 691 logrus.Debugf("Copying cert file from '%s' to '%s'.", path, targetPath) 692 693 file := File{ 694 Node: Node{ 695 Group: GetNodeGrp("root"), 696 Path: targetPath, 697 User: GetNodeUsr("root"), 698 }, 699 FileEmbedded1: FileEmbedded1{ 700 Append: nil, 701 Contents: Resource{ 702 Source: EncodeDataURLPtr(string(b)), 703 }, 704 Mode: IntToPtr(0644), 705 }, 706 } 707 return file, nil 708 } 709 710 func GetProxyVariables() map[string]string { 711 proxyOpts := make(map[string]string) 712 for _, variable := range config.ProxyEnv { 713 if value, ok := os.LookupEnv(variable); ok { 714 if value == "" { 715 continue 716 } 717 718 v := strings.ReplaceAll(value, "127.0.0.1", etchosts.HostContainersInternal) 719 v = strings.ReplaceAll(v, "localhost", etchosts.HostContainersInternal) 720 proxyOpts[variable] = v 721 } 722 } 723 return proxyOpts 724 } 725 726 func getLinks(usrName string) []Link { 727 return []Link{{ 728 Node: Node{ 729 Group: GetNodeGrp(usrName), 730 Path: "/home/" + usrName + "/.config/systemd/user/default.target.wants/linger-example.service", 731 User: GetNodeUsr(usrName), 732 }, 733 LinkEmbedded1: LinkEmbedded1{ 734 Hard: BoolToPtr(false), 735 Target: "/home/" + usrName + "/.config/systemd/user/linger-example.service", 736 }, 737 }, { 738 Node: Node{ 739 Group: GetNodeGrp("root"), 740 Path: "/usr/local/bin/docker", 741 Overwrite: BoolToPtr(true), 742 User: GetNodeUsr("root"), 743 }, 744 LinkEmbedded1: LinkEmbedded1{ 745 Hard: BoolToPtr(false), 746 Target: "/usr/bin/podman", 747 }, 748 }} 749 } 750 751 func EncodeDataURLPtr(contents string) *string { 752 return StrToPtr(fmt.Sprintf("data:,%s", url.PathEscape(contents))) 753 } 754 755 func GetPodmanDockerTmpConfig(uid int, rootful bool, newline bool) string { 756 // Derived from https://github.com/containers/podman/blob/main/contrib/systemd/system/podman-docker.conf 757 podmanSock := "/run/podman/podman.sock" 758 if !rootful { 759 podmanSock = fmt.Sprintf("/run/user/%d/podman/podman.sock", uid) 760 } 761 suffix := "" 762 if newline { 763 suffix = "\n" 764 } 765 766 return fmt.Sprintf("L+ /run/docker.sock - - - - %s%s", podmanSock, suffix) 767 } 768 769 // SetIgnitionFile creates a new Machine File for the machine's ignition file 770 // and assignes the handle to `loc` 771 func SetIgnitionFile(loc *define.VMFile, vmtype VMType, vmName string) error { 772 vmConfigDir, err := GetConfDir(vmtype) 773 if err != nil { 774 return err 775 } 776 777 ignitionFile, err := define.NewMachineFile(filepath.Join(vmConfigDir, vmName+".ign"), nil) 778 if err != nil { 779 return err 780 } 781 782 *loc = *ignitionFile 783 return nil 784 } 785 786 type IgnitionBuilder struct { 787 dynamicIgnition DynamicIgnition 788 units []Unit 789 } 790 791 // NewIgnitionBuilder generates a new IgnitionBuilder type using the 792 // base `DynamicIgnition` object 793 func NewIgnitionBuilder(dynamicIgnition DynamicIgnition) IgnitionBuilder { 794 return IgnitionBuilder{ 795 dynamicIgnition, 796 []Unit{}, 797 } 798 } 799 800 // GenerateIgnitionConfig generates the ignition config 801 func (i *IgnitionBuilder) GenerateIgnitionConfig() error { 802 return i.dynamicIgnition.GenerateIgnitionConfig() 803 } 804 805 // WithUnit adds systemd units to the internal `DynamicIgnition` config 806 func (i *IgnitionBuilder) WithUnit(units ...Unit) { 807 i.dynamicIgnition.Cfg.Systemd.Units = append(i.dynamicIgnition.Cfg.Systemd.Units, units...) 808 } 809 810 // WithFile adds storage files to the internal `DynamicIgnition` config 811 func (i *IgnitionBuilder) WithFile(files ...File) { 812 i.dynamicIgnition.Cfg.Storage.Files = append(i.dynamicIgnition.Cfg.Storage.Files, files...) 813 } 814 815 // BuildWithIgnitionFile copies the provided ignition file into the internal 816 // `DynamicIgnition` write path 817 func (i *IgnitionBuilder) BuildWithIgnitionFile(ignPath string) error { 818 inputIgnition, err := os.ReadFile(ignPath) 819 if err != nil { 820 return err 821 } 822 823 return os.WriteFile(i.dynamicIgnition.WritePath, inputIgnition, 0644) 824 } 825 826 // Build writes the internal `DynamicIgnition` config to its write path 827 func (i *IgnitionBuilder) Build() error { 828 return i.dynamicIgnition.Write() 829 } 830 831 func GetNetRecoveryFile() string { 832 return `#!/bin/bash 833 # Verify network health, and bounce the network device if host connectivity 834 # is lost. This is a temporary workaround for a known rare qemu/virtio issue 835 # that affects some systems 836 837 sleep 120 # allow time for network setup on initial boot 838 while true; do 839 sleep 30 840 curl -s -o /dev/null --max-time 30 http://192.168.127.1/health 841 if [ "$?" != "0" ]; then 842 echo "bouncing nic due to loss of connectivity with host" 843 ifconfig enp0s1 down; ifconfig enp0s1 up 844 fi 845 done 846 ` 847 } 848 849 func GetNetRecoveryUnitFile() *parser.UnitFile { 850 recoveryUnit := parser.NewUnitFile() 851 recoveryUnit.Add("Unit", "Description", "Verifies health of network and recovers if necessary") 852 recoveryUnit.Add("Unit", "After", "sshd.socket sshd.service") 853 recoveryUnit.Add("Service", "ExecStart", "/usr/local/bin/net-health-recovery.sh") 854 recoveryUnit.Add("Service", "StandardOutput", "journal") 855 recoveryUnit.Add("Service", "StandardError", "journal") 856 recoveryUnit.Add("Service", "StandardInput", "null") 857 recoveryUnit.Add("Install", "WantedBy", "default.target") 858 859 return recoveryUnit 860 }