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  }