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  }