package main import ( "errors" "flag" "fmt" "os" "path" "path/filepath" "strings" "github.com/containers/podman/v4/pkg/systemd/parser" "github.com/containers/podman/v4/pkg/systemd/quadlet" ) // This commandline app is the systemd generator (system and user, // decided by the name of the binary). // Generators run at very early startup, so must work in a very // limited environment (e.g. no /var, /home, or syslog). See: // https://www.freedesktop.org/software/systemd/man/systemd.generator.html#Notes%20about%20writing%20generators // for more details. var ( verboseFlag bool // True if -v passed isUser bool // True if run as quadlet-user-generator executable ) var ( // data saved between logToKmsg calls noKmsg = false kmsgFile *os.File ) // We log directly to /dev/kmsg, because that is the only way to get information out // of the generator into the system logs. func logToKmsg(s string) bool { if noKmsg { return false } if kmsgFile == nil { f, err := os.OpenFile("/dev/kmsg", os.O_WRONLY, 0644) if err != nil { noKmsg = true return false } kmsgFile = f } if _, err := kmsgFile.Write([]byte(s)); err != nil { kmsgFile.Close() kmsgFile = nil return false } return true } func Logf(format string, a ...interface{}) { s := fmt.Sprintf(format, a...) line := fmt.Sprintf("quadlet-generator[%d]: %s", os.Getpid(), s) if !logToKmsg(line) { // If we can't log, print to stderr fmt.Fprintf(os.Stderr, "%s\n", line) os.Stderr.Sync() } } var debugEnabled = false func enableDebug() { debugEnabled = true } func Debugf(format string, a ...interface{}) { if debugEnabled { Logf(format, a...) } } // This returns the directories where we read quadlet .container and .volumes from // For system generators these are in /usr/share/containers/systemd (for distro files) // and /etc/containers/systemd (for sysadmin files). // For user generators these live in $XDG_CONFIG_HOME/containers/systemd func getUnitDirs(user bool) []string { // Allow overdiding source dir, this is mainly for the CI tests unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS") if len(unitDirsEnv) > 0 { return strings.Split(unitDirsEnv, ":") } dirs := make([]string, 0) if user { if configDir, err := os.UserConfigDir(); err == nil { dirs = append(dirs, path.Join(configDir, "containers/systemd")) } } else { dirs = append(dirs, quadlet.UnitDirAdmin) dirs = append(dirs, quadlet.UnitDirDistro) } return dirs } func loadUnitsFromDir(sourcePath string, units map[string]*parser.UnitFile) { files, err := os.ReadDir(sourcePath) if err != nil { if !errors.Is(err, os.ErrNotExist) { Logf("Can't read \"%s\": %s", sourcePath, err) } return } for _, file := range files { name := file.Name() if units[name] == nil && (strings.HasSuffix(name, ".container") || strings.HasSuffix(name, ".volume")) { path := path.Join(sourcePath, name) Debugf("Loading source unit file %s", path) if f, err := parser.ParseUnitFile(path); err != nil { Logf("Error loading '%s', ignoring: %s", path, err) } else { units[name] = f } } } } func generateServiceFile(service *parser.UnitFile) error { Debugf("writing '%s'", service.Path) service.PrependComment("", "Automatically generated by quadlet-generator", "") f, err := os.Create(service.Path) if err != nil { return err } defer f.Close() err = service.Write(f) if err != nil { return err } err = f.Sync() if err != nil { return err } return nil } // This parses the `Install` group of the unit file and creates the required // symlinks to get systemd to start the newly generated file as needed. // In a traditional setup this is done by "systemctl enable", but that doesn't // work for auto-generated files like these. func enableServiceFile(outputPath string, service *parser.UnitFile) { symlinks := make([]string, 0) aliases := service.LookupAllStrv(quadlet.InstallGroup, "Alias") for _, alias := range aliases { symlinks = append(symlinks, filepath.Clean(alias)) } wantedBy := service.LookupAllStrv(quadlet.InstallGroup, "WantedBy") for _, wantedByUnit := range wantedBy { // Only allow filenames, not paths if !strings.Contains(wantedByUnit, "/") { symlinks = append(symlinks, fmt.Sprintf("%s.wants/%s", wantedByUnit, service.Filename)) } } requiredBy := service.LookupAllStrv(quadlet.InstallGroup, "RequiredBy") for _, requiredByUnit := range requiredBy { // Only allow filenames, not paths if !strings.Contains(requiredByUnit, "/") { symlinks = append(symlinks, fmt.Sprintf("%s.requires/%s", requiredByUnit, service.Filename)) } } for _, symlinkRel := range symlinks { target, err := filepath.Rel(path.Dir(symlinkRel), service.Filename) if err != nil { Logf("Can't create symlink %s: %s", symlinkRel, err) continue } symlinkPath := path.Join(outputPath, symlinkRel) symlinkDir := path.Dir(symlinkPath) err = os.MkdirAll(symlinkDir, os.ModePerm) if err != nil { Logf("Can't create dir %s: %s", symlinkDir, err) continue } Debugf("Creating symlink %s -> %s", symlinkPath, target) _ = os.Remove(symlinkPath) // overwrite existing symlinks err = os.Symlink(target, symlinkPath) if err != nil { Logf("Failed creating symlink %s: %s", symlinkPath, err) } } } func main() { prgname := path.Base(os.Args[0]) isUser = strings.Contains(prgname, "user") flag.Parse() if verboseFlag { enableDebug() } if flag.NArg() < 1 { Logf("Missing output directory argument") os.Exit(1) } outputPath := flag.Arg(0) Debugf("Starting quadlet-generator, output to: %s", outputPath) sourcePaths := getUnitDirs(isUser) units := make(map[string]*parser.UnitFile) for _, d := range sourcePaths { loadUnitsFromDir(d, units) } err := os.MkdirAll(outputPath, os.ModePerm) if err != nil { Logf("Can't create dir %s: %s", outputPath, err) os.Exit(1) } for name, unit := range units { var service *parser.UnitFile var err error switch { case strings.HasSuffix(name, ".container"): service, err = quadlet.ConvertContainer(unit, isUser) case strings.HasSuffix(name, ".volume"): service, err = quadlet.ConvertVolume(unit, name) default: Logf("Unsupported file type '%s'", name) continue } if err != nil { Logf("Error converting '%s', ignoring: %s", name, err) } else { service.Path = path.Join(outputPath, service.Filename) if err := generateServiceFile(service); err != nil { Logf("Error writing '%s'o: %s", service.Path, err) } enableServiceFile(outputPath, service) } } } func init() { flag.BoolVar(&verboseFlag, "v", false, "Print debug information") }