podman/pkg/systemd/generate/systemdgen.go

209 lines
6.6 KiB
Go

package generate
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
"time"
"github.com/containers/libpod/version"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// ContainerInfo contains data required for generating a container's systemd
// unit file.
type ContainerInfo struct {
// ServiceName of the systemd service.
ServiceName string
// Name or ID of the container.
ContainerName string
// InfraContainer of the pod.
InfraContainer string
// StopTimeout sets the timeout Podman waits before killing the container
// during service stop.
StopTimeout int
// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
RestartPolicy string
// PIDFile of the service. Required for forking services. Must point to the
// PID of the associated conmon process.
PIDFile string
// GenerateTimestamp, if set the generated unit file has a time stamp.
GenerateTimestamp bool
// BoundToServices are the services this service binds to. Note that this
// service runs after them.
BoundToServices []string
// RequiredServices are services this service requires. Note that this
// service runs before them.
RequiredServices []string
// PodmanVersion for the header. Will be set internally. Will be auto-filled
// if left empty.
PodmanVersion string
// Executable is the path to the podman executable. Will be auto-filled if
// left empty.
Executable string
// TimeStamp at the time of creating the unit file. Will be set internally.
TimeStamp string
// New controls if a new container is created or if an existing one is started.
New bool
// CreateCommand is the full command plus arguments of the process the
// container has been created with.
CreateCommand []string
// RunCommand is a post-processed variant of CreateCommand and used for
// the ExecStart field in generic unit files.
RunCommand string
}
var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"}
// validateRestartPolicy checks that the user-provided policy is valid.
func validateRestartPolicy(restart string) error {
for _, i := range restartPolicies {
if i == restart {
return nil
}
}
return errors.Errorf("%s is not a valid restart policy", restart)
}
const containerTemplate = `# {{.ServiceName}}.service
# autogenerated by Podman {{.PodmanVersion}}
{{- if .TimeStamp}}
# {{.TimeStamp}}
{{- end}}
[Unit]
Description=Podman {{.ServiceName}}.service
Documentation=man:podman-generate-systemd(1)
Wants=network.target
After=network-online.target
{{- if .BoundToServices}}
RefuseManualStart=yes
RefuseManualStop=yes
BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
{{- end}}
{{- if .RequiredServices}}
Requires={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
{{- end}}
[Service]
Restart={{.RestartPolicy}}
{{- if .New}}
ExecStartPre=/usr/bin/rm -f /%t/%n-pid /%t/%n-cid
ExecStart={{.RunCommand}}
ExecStop={{.Executable}} stop --ignore --cidfile /%t/%n-cid {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}
ExecStopPost={{.Executable}} rm --ignore -f --cidfile /%t/%n-cid
PIDFile=/%t/%n-pid
{{- else}}
ExecStart={{.Executable}} start {{.ContainerName}}
ExecStop={{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerName}}
PIDFile={{.PIDFile}}
{{- end}}
KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target`
// Options include different options to control the unit file generation.
type Options struct {
// When set, generate service files in the current working directory and
// return the paths to these files instead of returning all contents in one
// big string.
Files bool
// New controls if a new container is created or if an existing one is started.
New bool
}
// CreateContainerSystemdUnit creates a systemd unit file for a container.
func CreateContainerSystemdUnit(info *ContainerInfo, opts Options) (string, error) {
if err := validateRestartPolicy(info.RestartPolicy); err != nil {
return "", err
}
// Make sure the executable is set.
if info.Executable == "" {
executable, err := os.Executable()
if err != nil {
executable = "/usr/bin/podman"
logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
}
info.Executable = executable
}
// Assemble the ExecStart command when creating a new container.
//
// Note that we cannot catch all corner cases here such that users
// *must* manually check the generated files. A container might have
// been created via a Python script, which would certainly yield an
// invalid `info.CreateCommand`. Hence, we're doing a best effort unit
// generation and don't try aiming at completeness.
if opts.New {
// The create command must at least have three arguments:
// /usr/bin/podman run $IMAGE
index := 2
if info.CreateCommand[1] == "container" {
index = 3
}
if len(info.CreateCommand) < index+1 {
return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand)
}
// We're hard-coding the first five arguments and append the
// CreateCommand with a stripped command and subcomand.
command := []string{
info.Executable,
"run",
"--conmon-pidfile", "/%t/%n-pid",
"--cidfile", "/%t/%n-cid",
"--cgroups=no-conmon",
}
command = append(command, info.CreateCommand[index:]...)
info.RunCommand = strings.Join(command, " ")
info.New = true
}
if info.PodmanVersion == "" {
info.PodmanVersion = version.Version
}
if info.GenerateTimestamp {
info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
}
// Sort the slices to assure a deterministic output.
sort.Strings(info.RequiredServices)
sort.Strings(info.BoundToServices)
// Generate the template and compile it.
templ, err := template.New("systemd_service_file").Parse(containerTemplate)
if err != nil {
return "", errors.Wrap(err, "error parsing systemd service template")
}
var buf bytes.Buffer
if err := templ.Execute(&buf, info); err != nil {
return "", err
}
if !opts.Files {
return buf.String(), nil
}
buf.WriteByte('\n')
cwd, err := os.Getwd()
if err != nil {
return "", errors.Wrap(err, "error getting current working directory")
}
path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName))
if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil {
return "", errors.Wrap(err, "error generating systemd unit")
}
return path, nil
}