github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/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 "io/ioutil" 11 "net/url" 12 "os" 13 "path/filepath" 14 15 "github.com/containers/common/pkg/config" 16 "github.com/sirupsen/logrus" 17 ) 18 19 /* 20 If this file gets too nuts, we can perhaps use existing go code 21 to create ignition files. At this point, the file is so simple 22 that I chose to use structs and not import any code as I was 23 concerned (unsubstantiated) about too much bloat coming in. 24 25 https://github.com/openshift/machine-config-operator/blob/master/pkg/server/server.go 26 */ 27 28 // Convenience function to convert int to ptr 29 func intToPtr(i int) *int { 30 return &i 31 } 32 33 // Convenience function to convert string to ptr 34 func strToPtr(s string) *string { 35 return &s 36 } 37 38 // Convenience function to convert bool to ptr 39 func boolToPtr(b bool) *bool { 40 return &b 41 } 42 43 func getNodeUsr(usrName string) NodeUser { 44 return NodeUser{Name: &usrName} 45 } 46 47 func getNodeGrp(grpName string) NodeGroup { 48 return NodeGroup{Name: &grpName} 49 } 50 51 type DynamicIgnition struct { 52 Name string 53 Key string 54 TimeZone string 55 UID int 56 VMName string 57 WritePath string 58 } 59 60 // NewIgnitionFile 61 func NewIgnitionFile(ign DynamicIgnition) error { 62 if len(ign.Name) < 1 { 63 ign.Name = DefaultIgnitionUserName 64 } 65 ignVersion := Ignition{ 66 Version: "3.2.0", 67 } 68 ignPassword := Passwd{ 69 Users: []PasswdUser{ 70 { 71 Name: ign.Name, 72 SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(ign.Key)}, 73 // Set the UID of the core user inside the machine 74 UID: intToPtr(ign.UID), 75 }, 76 { 77 Name: "root", 78 SSHAuthorizedKeys: []SSHAuthorizedKey{SSHAuthorizedKey(ign.Key)}, 79 }, 80 }, 81 } 82 83 ignStorage := Storage{ 84 Directories: getDirs(ign.Name), 85 Files: getFiles(ign.Name), 86 Links: getLinks(ign.Name), 87 } 88 89 // Add or set the time zone for the machine 90 if len(ign.TimeZone) > 0 { 91 var ( 92 err error 93 tz string 94 ) 95 // local means the same as the host 96 // lookup where it is pointing to on the host 97 if ign.TimeZone == "local" { 98 tz, err = getLocalTimeZone() 99 if err != nil { 100 return err 101 } 102 } else { 103 tz = ign.TimeZone 104 } 105 tzLink := Link{ 106 Node: Node{ 107 Group: getNodeGrp("root"), 108 Path: "/etc/localtime", 109 Overwrite: boolToPtr(false), 110 User: getNodeUsr("root"), 111 }, 112 LinkEmbedded1: LinkEmbedded1{ 113 Hard: boolToPtr(false), 114 Target: filepath.Join("/usr/share/zoneinfo", tz), 115 }, 116 } 117 ignStorage.Links = append(ignStorage.Links, tzLink) 118 } 119 120 // ready is a unit file that sets up the virtual serial device 121 // where when the VM is done configuring, it will send an ack 122 // so a listening host knows it can being interacting with it 123 ready := `[Unit] 124 Requires=dev-virtio\\x2dports-%s.device 125 After=remove-moby.service sshd.socket sshd.service 126 OnFailure=emergency.target 127 OnFailureJobMode=isolate 128 [Service] 129 Type=oneshot 130 RemainAfterExit=yes 131 ExecStart=/bin/sh -c '/usr/bin/echo Ready >/dev/%s' 132 [Install] 133 RequiredBy=default.target 134 ` 135 deMoby := `[Unit] 136 Description=Remove moby-engine 137 # Run once for the machine 138 After=systemd-machine-id-commit.service 139 Before=zincati.service 140 ConditionPathExists=!/var/lib/%N.stamp 141 142 [Service] 143 Type=oneshot 144 RemainAfterExit=yes 145 ExecStart=/usr/bin/rpm-ostree override remove moby-engine 146 ExecStart=/usr/bin/rpm-ostree ex apply-live --allow-replacement 147 ExecStartPost=/bin/touch /var/lib/%N.stamp 148 149 [Install] 150 WantedBy=default.target 151 ` 152 // This service gets environment variables that are provided 153 // through qemu fw_cfg and then sets them into systemd/system.conf.d, 154 // profile.d and environment.d files 155 // 156 // Currently, it is used for propagating 157 // proxy settings e.g. HTTP_PROXY and others, on a start avoiding 158 // a need of re-creating/re-initiating a VM 159 envset := `[Unit] 160 Description=Environment setter from QEMU FW_CFG 161 [Service] 162 Type=oneshot 163 RemainAfterExit=yes 164 Environment=FWCFGRAW=/sys/firmware/qemu_fw_cfg/by_name/opt/com.coreos/environment/raw 165 Environment=SYSTEMD_CONF=/etc/systemd/system.conf.d/default-env.conf 166 Environment=ENVD_CONF=/etc/environment.d/default-env.conf 167 Environment=PROFILE_CONF=/etc/profile.d/default-env.sh 168 ExecStart=/usr/bin/bash -c '/usr/bin/test -f ${FWCFGRAW} &&\ 169 echo "[Manager]\n#Got from QEMU FW_CFG\nDefaultEnvironment=$(/usr/bin/base64 -d ${FWCFGRAW} | sed -e "s+|+ +g")\n" > ${SYSTEMD_CONF} ||\ 170 echo "[Manager]\n#Got nothing from QEMU FW_CFG\n#DefaultEnvironment=\n" > ${SYSTEMD_CONF}' 171 ExecStart=/usr/bin/bash -c '/usr/bin/test -f ${FWCFGRAW} && (\ 172 echo "#Got from QEMU FW_CFG"> ${ENVD_CONF};\ 173 IFS="|";\ 174 for iprxy in $(/usr/bin/base64 -d ${FWCFGRAW}); do\ 175 echo "$iprxy" >> ${ENVD_CONF}; done ) || \ 176 echo "#Got nothing from QEMU FW_CFG"> ${ENVD_CONF}' 177 ExecStart=/usr/bin/bash -c '/usr/bin/test -f ${FWCFGRAW} && (\ 178 echo "#Got from QEMU FW_CFG"> ${PROFILE_CONF};\ 179 IFS="|";\ 180 for iprxy in $(/usr/bin/base64 -d ${FWCFGRAW}); do\ 181 echo "export $iprxy" >> ${PROFILE_CONF}; done ) || \ 182 echo "#Got nothing from QEMU FW_CFG"> ${PROFILE_CONF}' 183 ExecStartPost=/usr/bin/systemctl daemon-reload 184 [Install] 185 WantedBy=sysinit.target 186 ` 187 _ = ready 188 ignSystemd := Systemd{ 189 Units: []Unit{ 190 { 191 Enabled: boolToPtr(true), 192 Name: "podman.socket", 193 }, 194 { 195 Enabled: boolToPtr(true), 196 Name: "ready.service", 197 Contents: strToPtr(fmt.Sprintf(ready, "vport1p1", "vport1p1")), 198 }, 199 { 200 Enabled: boolToPtr(false), 201 Name: "docker.service", 202 Mask: boolToPtr(true), 203 }, 204 { 205 Enabled: boolToPtr(false), 206 Name: "docker.socket", 207 Mask: boolToPtr(true), 208 }, 209 { 210 Enabled: boolToPtr(true), 211 Name: "remove-moby.service", 212 Contents: &deMoby, 213 }, 214 { 215 Enabled: boolToPtr(true), 216 Name: "envset-fwcfg.service", 217 Contents: &envset, 218 }, 219 }} 220 ignConfig := Config{ 221 Ignition: ignVersion, 222 Passwd: ignPassword, 223 Storage: ignStorage, 224 Systemd: ignSystemd, 225 } 226 b, err := json.Marshal(ignConfig) 227 if err != nil { 228 return err 229 } 230 return ioutil.WriteFile(ign.WritePath, b, 0644) 231 } 232 233 func getDirs(usrName string) []Directory { 234 // Ignition has a bug/feature? where if you make a series of dirs 235 // in one swoop, then the leading dirs are creates as root. 236 newDirs := []string{ 237 "/home/" + usrName + "/.config", 238 "/home/" + usrName + "/.config/containers", 239 "/home/" + usrName + "/.config/systemd", 240 "/home/" + usrName + "/.config/systemd/user", 241 "/home/" + usrName + "/.config/systemd/user/default.target.wants", 242 } 243 var ( 244 dirs = make([]Directory, len(newDirs)) 245 ) 246 for i, d := range newDirs { 247 newDir := Directory{ 248 Node: Node{ 249 Group: getNodeGrp(usrName), 250 Path: d, 251 User: getNodeUsr(usrName), 252 }, 253 DirectoryEmbedded1: DirectoryEmbedded1{Mode: intToPtr(0755)}, 254 } 255 dirs[i] = newDir 256 } 257 258 // Issue #11489: make sure that we can inject a custom registries.conf 259 // file on the system level to force a single search registry. 260 // The remote client does not yet support prompting for short-name 261 // resolution, so we enforce a single search registry (i.e., docker.io) 262 // as a workaround. 263 dirs = append(dirs, Directory{ 264 Node: Node{ 265 Group: getNodeGrp("root"), 266 Path: "/etc/containers/registries.conf.d", 267 User: getNodeUsr("root"), 268 }, 269 DirectoryEmbedded1: DirectoryEmbedded1{Mode: intToPtr(0755)}, 270 }) 271 272 // The directory is used by envset-fwcfg.service 273 // for propagating environment variables that got 274 // from a host 275 dirs = append(dirs, Directory{ 276 Node: Node{ 277 Group: getNodeGrp("root"), 278 Path: "/etc/systemd/system.conf.d", 279 User: getNodeUsr("root"), 280 }, 281 DirectoryEmbedded1: DirectoryEmbedded1{Mode: intToPtr(0755)}, 282 }, Directory{ 283 Node: Node{ 284 Group: getNodeGrp("root"), 285 Path: "/etc/environment.d", 286 User: getNodeUsr("root"), 287 }, 288 DirectoryEmbedded1: DirectoryEmbedded1{Mode: intToPtr(0755)}, 289 }) 290 291 return dirs 292 } 293 294 func getFiles(usrName string) []File { 295 files := make([]File, 0) 296 297 lingerExample := `[Unit] 298 Description=A systemd user unit demo 299 After=network-online.target 300 Wants=network-online.target podman.socket 301 [Service] 302 ExecStart=/usr/bin/sleep infinity 303 ` 304 containers := `[containers] 305 netns="bridge" 306 ` 307 // Set deprecated machine_enabled until podman package on fcos is 308 // current enough to no longer require it 309 rootContainers := `[engine] 310 machine_enabled=true 311 ` 312 313 delegateConf := `[Service] 314 Delegate=memory pids cpu io 315 ` 316 subUID := `%s:100000:1000000` 317 318 // Add a fake systemd service to get the user socket rolling 319 files = append(files, File{ 320 Node: Node{ 321 Group: getNodeGrp(usrName), 322 Path: "/home/" + usrName + "/.config/systemd/user/linger-example.service", 323 User: getNodeUsr(usrName), 324 }, 325 FileEmbedded1: FileEmbedded1{ 326 Append: nil, 327 Contents: Resource{ 328 Source: encodeDataURLPtr(lingerExample), 329 }, 330 Mode: intToPtr(0744), 331 }, 332 }) 333 334 // Set containers.conf up for core user to use cni networks 335 // by default 336 files = append(files, File{ 337 Node: Node{ 338 Group: getNodeGrp(usrName), 339 Path: "/home/" + usrName + "/.config/containers/containers.conf", 340 User: getNodeUsr(usrName), 341 }, 342 FileEmbedded1: FileEmbedded1{ 343 Append: nil, 344 Contents: Resource{ 345 Source: encodeDataURLPtr(containers), 346 }, 347 Mode: intToPtr(0744), 348 }, 349 }) 350 351 // Setup /etc/subuid and /etc/subgid 352 for _, sub := range []string{"/etc/subuid", "/etc/subgid"} { 353 files = append(files, File{ 354 Node: Node{ 355 Group: getNodeGrp("root"), 356 Path: sub, 357 User: getNodeUsr("root"), 358 Overwrite: boolToPtr(true), 359 }, 360 FileEmbedded1: FileEmbedded1{ 361 Append: nil, 362 Contents: Resource{ 363 Source: encodeDataURLPtr(fmt.Sprintf(subUID, usrName)), 364 }, 365 Mode: intToPtr(0744), 366 }, 367 }) 368 } 369 370 // Set delegate.conf so cpu,io subsystem is delegated to non-root users as well for cgroupv2 371 // by default 372 files = append(files, File{ 373 Node: Node{ 374 Group: getNodeGrp("root"), 375 Path: "/etc/systemd/system/user@.service.d/delegate.conf", 376 User: getNodeUsr("root"), 377 }, 378 FileEmbedded1: FileEmbedded1{ 379 Append: nil, 380 Contents: Resource{ 381 Source: encodeDataURLPtr(delegateConf), 382 }, 383 Mode: intToPtr(0644), 384 }, 385 }) 386 387 // Add a file into linger 388 files = append(files, File{ 389 Node: Node{ 390 Group: getNodeGrp(usrName), 391 Path: "/var/lib/systemd/linger/core", 392 User: getNodeUsr(usrName), 393 }, 394 FileEmbedded1: FileEmbedded1{Mode: intToPtr(0644)}, 395 }) 396 397 // Set deprecated machine_enabled to true to indicate we're in a VM 398 files = append(files, File{ 399 Node: Node{ 400 Group: getNodeGrp("root"), 401 Path: "/etc/containers/containers.conf", 402 User: getNodeUsr("root"), 403 }, 404 FileEmbedded1: FileEmbedded1{ 405 Append: nil, 406 Contents: Resource{ 407 Source: encodeDataURLPtr(rootContainers), 408 }, 409 Mode: intToPtr(0644), 410 }, 411 }) 412 413 // Set machine marker file to indicate podman is in a qemu based machine 414 files = append(files, File{ 415 Node: Node{ 416 Group: getNodeGrp("root"), 417 Path: "/etc/containers/podman-machine", 418 User: getNodeUsr("root"), 419 }, 420 FileEmbedded1: FileEmbedded1{ 421 Append: nil, 422 Contents: Resource{ 423 Source: encodeDataURLPtr("qemu\n"), 424 }, 425 Mode: intToPtr(0644), 426 }, 427 }) 428 429 // Issue #11489: make sure that we can inject a custom registries.conf 430 // file on the system level to force a single search registry. 431 // The remote client does not yet support prompting for short-name 432 // resolution, so we enforce a single search registry (i.e., docker.io) 433 // as a workaround. 434 files = append(files, File{ 435 Node: Node{ 436 Group: getNodeGrp("root"), 437 Path: "/etc/containers/registries.conf.d/999-podman-machine.conf", 438 User: getNodeUsr("root"), 439 }, 440 FileEmbedded1: FileEmbedded1{ 441 Append: nil, 442 Contents: Resource{ 443 Source: encodeDataURLPtr("unqualified-search-registries=[\"docker.io\"]\n"), 444 }, 445 Mode: intToPtr(0644), 446 }, 447 }) 448 449 files = append(files, File{ 450 Node: Node{ 451 Path: "/etc/tmpfiles.d/podman-docker.conf", 452 }, 453 FileEmbedded1: FileEmbedded1{ 454 Append: nil, 455 // Create a symlink from the docker socket to the podman socket. 456 // Taken from https://github.com/containers/podman/blob/main/contrib/systemd/system/podman-docker.conf 457 Contents: Resource{ 458 Source: encodeDataURLPtr("L+ /run/docker.sock - - - - /run/podman/podman.sock\n"), 459 }, 460 Mode: intToPtr(0644), 461 }, 462 }) 463 464 setDockerHost := `export DOCKER_HOST="unix://$(podman info -f "{{.Host.RemoteSocket.Path}}")" 465 ` 466 467 files = append(files, File{ 468 Node: Node{ 469 Group: getNodeGrp("root"), 470 Path: "/etc/profile.d/docker-host.sh", 471 User: getNodeUsr("root"), 472 }, 473 FileEmbedded1: FileEmbedded1{ 474 Append: nil, 475 Contents: Resource{ 476 Source: encodeDataURLPtr(setDockerHost), 477 }, 478 Mode: intToPtr(0644), 479 }, 480 }) 481 482 // get certs for current user 483 userHome, err := os.UserHomeDir() 484 if err != nil { 485 logrus.Warnf("Unable to copy certs via ignition %s", err.Error()) 486 return files 487 } 488 489 certFiles := getCerts(filepath.Join(userHome, ".config/containers/certs.d"), true) 490 files = append(files, certFiles...) 491 492 certFiles = getCerts(filepath.Join(userHome, ".config/docker/certs.d"), true) 493 files = append(files, certFiles...) 494 495 if sslCertFile, ok := os.LookupEnv("SSL_CERT_FILE"); ok { 496 if _, err := os.Stat(sslCertFile); err == nil { 497 certFiles = getCerts(sslCertFile, false) 498 files = append(files, certFiles...) 499 500 if len(certFiles) > 0 { 501 setSSLCertFile := fmt.Sprintf("export %s=%s", "SSL_CERT_FILE", filepath.Join("/etc/containers/certs.d", filepath.Base(sslCertFile))) 502 files = append(files, File{ 503 Node: Node{ 504 Group: getNodeGrp("root"), 505 Path: "/etc/profile.d/ssl_cert_file.sh", 506 User: getNodeUsr("root"), 507 }, 508 FileEmbedded1: FileEmbedded1{ 509 Append: nil, 510 Contents: Resource{ 511 Source: encodeDataURLPtr(setSSLCertFile), 512 }, 513 Mode: intToPtr(0644), 514 }, 515 }) 516 } 517 } 518 } 519 520 return files 521 } 522 523 func getCerts(certsDir string, isDir bool) []File { 524 var ( 525 files []File 526 ) 527 528 if isDir { 529 err := filepath.WalkDir(certsDir, func(path string, d fs.DirEntry, err error) error { 530 if err == nil && !d.IsDir() { 531 certPath, err := filepath.Rel(certsDir, path) 532 if err != nil { 533 logrus.Warnf("%s", err) 534 return nil 535 } 536 537 file, err := prepareCertFile(filepath.Join(certsDir, certPath), certPath) 538 if err == nil { 539 files = append(files, file) 540 } 541 } 542 543 return nil 544 }) 545 if err != nil { 546 if !os.IsNotExist(err) { 547 logrus.Warnf("Unable to copy certs via ignition, error while reading certs from %s: %s", certsDir, err.Error()) 548 } 549 } 550 } else { 551 fileName := filepath.Base(certsDir) 552 file, err := prepareCertFile(certsDir, fileName) 553 if err == nil { 554 files = append(files, file) 555 } 556 } 557 558 return files 559 } 560 561 func prepareCertFile(path string, name string) (File, error) { 562 b, err := ioutil.ReadFile(path) 563 if err != nil { 564 logrus.Warnf("Unable to read cert file %s", err.Error()) 565 return File{}, err 566 } 567 568 targetPath := filepath.Join("/etc/containers/certs.d", name) 569 570 logrus.Debugf("Copying cert file from '%s' to '%s'.", path, targetPath) 571 572 file := File{ 573 Node: Node{ 574 Group: getNodeGrp("root"), 575 Path: targetPath, 576 User: getNodeUsr("root"), 577 }, 578 FileEmbedded1: FileEmbedded1{ 579 Append: nil, 580 Contents: Resource{ 581 Source: encodeDataURLPtr(string(b)), 582 }, 583 Mode: intToPtr(0644), 584 }, 585 } 586 return file, nil 587 } 588 589 func GetProxyVariables() map[string]string { 590 proxyOpts := make(map[string]string) 591 for _, variable := range config.ProxyEnv { 592 if value, ok := os.LookupEnv(variable); ok { 593 proxyOpts[variable] = value 594 } 595 } 596 return proxyOpts 597 } 598 599 func getLinks(usrName string) []Link { 600 return []Link{{ 601 Node: Node{ 602 Group: getNodeGrp(usrName), 603 Path: "/home/" + usrName + "/.config/systemd/user/default.target.wants/linger-example.service", 604 User: getNodeUsr(usrName), 605 }, 606 LinkEmbedded1: LinkEmbedded1{ 607 Hard: boolToPtr(false), 608 Target: "/home/" + usrName + "/.config/systemd/user/linger-example.service", 609 }, 610 }, { 611 Node: Node{ 612 Group: getNodeGrp("root"), 613 Path: "/usr/local/bin/docker", 614 Overwrite: boolToPtr(true), 615 User: getNodeUsr("root"), 616 }, 617 LinkEmbedded1: LinkEmbedded1{ 618 Hard: boolToPtr(false), 619 Target: "/usr/bin/podman", 620 }, 621 }} 622 } 623 624 func encodeDataURLPtr(contents string) *string { 625 return strToPtr(fmt.Sprintf("data:,%s", url.PathEscape(contents))) 626 }