func writeAppReaper(p *stage1commontypes.Pod, appName string) error { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("%s Reaper", appName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "StopWhenUnneeded", "yes"), unit.NewUnitOption("Unit", "Wants", "shutdown.service"), unit.NewUnitOption("Unit", "After", "shutdown.service"), unit.NewUnitOption("Unit", "Conflicts", "exit.target"), unit.NewUnitOption("Unit", "Conflicts", "halt.target"), unit.NewUnitOption("Unit", "Conflicts", "poweroff.target"), unit.NewUnitOption("Service", "RemainAfterExit", "yes"), unit.NewUnitOption("Service", "ExecStop", fmt.Sprintf("/reaper.sh %s", appName)), } unitsPath := filepath.Join(common.Stage1RootfsPath(p.Root), UnitsDir) file, err := os.OpenFile(filepath.Join(unitsPath, fmt.Sprintf("reaper-%s.service", appName)), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create service unit file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return errwrap.Wrap(errors.New("failed to write service unit file"), err) } return nil }
// WriteDefaultTarget writes the default.target unit file // which is responsible for bringing up the applications func WriteDefaultTarget(p *stage1commontypes.Pod) error { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", "rkt apps target"), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), } for i := range p.Manifest.Apps { ra := &p.Manifest.Apps[i] serviceName := ServiceUnitName(ra.Name) opts = append(opts, unit.NewUnitOption("Unit", "After", serviceName)) opts = append(opts, unit.NewUnitOption("Unit", "Wants", serviceName)) } unitsPath := filepath.Join(common.Stage1RootfsPath(p.Root), UnitsDir) file, err := os.OpenFile(filepath.Join(unitsPath, "default.target"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return err } return nil }
// WritePrepareAppTemplate writes service unit files for preparing the pod's applications func WritePrepareAppTemplate(p *stage1commontypes.Pod) error { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", "Prepare minimum environment for chrooted applications"), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "OnFailureJobMode", "fail"), unit.NewUnitOption("Unit", "Requires", "systemd-journald.service"), unit.NewUnitOption("Unit", "After", "systemd-journald.service"), unit.NewUnitOption("Service", "Type", "oneshot"), unit.NewUnitOption("Service", "Restart", "no"), unit.NewUnitOption("Service", "ExecStart", "/prepare-app %I"), unit.NewUnitOption("Service", "User", "0"), unit.NewUnitOption("Service", "Group", "0"), unit.NewUnitOption("Service", "CapabilityBoundingSet", "CAP_SYS_ADMIN CAP_DAC_OVERRIDE"), } unitsPath := filepath.Join(common.Stage1RootfsPath(p.Root), UnitsDir) file, err := os.OpenFile(filepath.Join(unitsPath, "[email protected]"), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create service unit file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return errwrap.Wrap(errors.New("failed to write service unit file"), err) } return nil }
func GenerateNetworkInterfaceUnits(unitsPath string, netDescriptions []netDescriber) error { for i, netDescription := range netDescriptions { ifName := fmt.Sprintf(networking.IfNamePattern, i) netAddress := net.IPNet{ IP: netDescription.GuestIP(), Mask: net.IPMask(netDescription.Mask()), } address := netAddress.String() mac, err := generateMacAddress() if err != nil { return err } opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Network configuration for device: %v", ifName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Service", "Type", "oneshot"), unit.NewUnitOption("Service", "RemainAfterExit", "true"), unit.NewUnitOption("Service", "ExecStartPre", downInterfaceCommand(ifName)), unit.NewUnitOption("Service", "ExecStartPre", setMacCommand(ifName, mac.String())), unit.NewUnitOption("Service", "ExecStartPre", upInterfaceCommand(ifName)), unit.NewUnitOption("Service", "ExecStart", addAddressCommand(address, ifName)), unit.NewUnitOption("Install", "RequiredBy", "default.target"), } for _, route := range netDescription.Routes() { gw := route.GW if gw == nil { gw = netDescription.Gateway() } opts = append( opts, unit.NewUnitOption( "Service", "ExecStartPost", addRouteCommand(route.Dst.String(), gw.String()), ), ) } unitName := fmt.Sprintf("interface-%s", ifName) + ".service" unitBytes, err := ioutil.ReadAll(unit.Serialize(opts)) if err != nil { return errwrap.Wrap(fmt.Errorf("failed to serialize network unit file to bytes %q", unitName), err) } err = ioutil.WriteFile(filepath.Join(unitsPath, unitName), unitBytes, 0644) if err != nil { return errwrap.Wrap(fmt.Errorf("failed to create network unit file %q", unitName), err) } log.Printf("network unit created: %q in %q (iface=%q, addr=%q)", unitName, unitsPath, ifName, address) } return nil }
// ValidateOptions ensures that a set of UnitOptions is valid; if not, an error // is returned detailing the issue encountered. If there are several problems // with a set of options, only the first is returned. func ValidateOptions(opts []*schema.UnitOption) error { uf := schema.MapSchemaUnitOptionsToUnitFile(opts) // Sanity check using go-systemd's deserializer, which will do things // like check for excessive line lengths _, err := gsunit.Deserialize(gsunit.Serialize(uf.Options)) if err != nil { return err } j := &job.Job{ Unit: *uf, } conflicts := pkg.NewUnsafeSet(j.Conflicts()...) replaces := pkg.NewUnsafeSet(j.Replaces()...) peers := pkg.NewUnsafeSet(j.Peers()...) for _, peer := range peers.Values() { for _, conflict := range conflicts.Values() { matched, _ := path.Match(conflict, peer) if matched { return fmt.Errorf("unresolvable requirements: peer %q matches conflict %q", peer, conflict) } } for _, replace := range replaces.Values() { matched, _ := path.Match(replace, peer) if matched { return fmt.Errorf("unresolvable requirements: peer %q matches replace %q", peer, replace) } } } hasPeers := peers.Length() != 0 hasConflicts := conflicts.Length() != 0 hasReplaces := replaces.Length() != 0 _, hasReqTarget := j.RequiredTarget() u := &job.Unit{ Unit: *uf, } isGlobal := u.IsGlobal() switch { case hasReqTarget && hasPeers: return errors.New("MachineID cannot be used with Peers") case hasReqTarget && hasConflicts: return errors.New("MachineID cannot be used with Conflicts") case hasReqTarget && isGlobal: return errors.New("MachineID cannot be used with Global") case hasReqTarget && hasReplaces: return errors.New("MachineID cannot be used with Replaces") case isGlobal && hasPeers: return errors.New("Global cannot be used with Peers") case isGlobal && hasReplaces: return errors.New("Global cannot be used with Replaces") case hasConflicts && hasReplaces: return errors.New("Conflicts cannot be used with Replaces") } return nil }
func writeUnit(opts []*unit.UnitOption, unitPath string) error { unitBytes, err := ioutil.ReadAll(unit.Serialize(opts)) if err != nil { return errwrap.Wrap(fmt.Errorf("failed to serialize mount unit file to bytes %q", unitPath), err) } err = ioutil.WriteFile(unitPath, unitBytes, 0644) if err != nil { return errwrap.Wrap(fmt.Errorf("failed to create mount unit file %q", unitPath), err) } return nil }
// WriteUnit writes a systemd unit in the given path with the given unit options // if no previous error occured. func (uw *UnitWriter) WriteUnit(path string, errmsg string, opts ...*unit.UnitOption) { if uw.err != nil { return } file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { uw.err = errwrap.Wrap(errors.New(errmsg), err) return } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { uw.err = errwrap.Wrap(errors.New(errmsg), err) } }
func startUnit(name, tag string, myunit []*unit.UnitOption) (err error) { dir, err := ioutil.TempDir("", "flitter-builder") if err != nil { return err } defer func() { os.RemoveAll(dir) }() byteslicedunitreader := unit.Serialize(myunit) byteslicedunit, err := ioutil.ReadAll(byteslicedunitreader) if err != nil { return err } err = ioutil.WriteFile(dir+"/"+name+"@.service", byteslicedunit, 0666) if err != nil { return err } command := []string{"-endpoint", *etcdhost, "start", dir + "/" + name + "@" + tag + ".service"} output.WriteData("$ fleetctl " + strings.Join(command, " ")) output.WriteData("Waiting for fleetctl...") cmd := exec.Command("/opt/fleet/fleetctl", command...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Start() if err != nil { return err } err = cmd.Wait() if err != nil { return err } output.WriteData("done") return err }
func writeShutdownService(p *stage1commontypes.Pod) error { flavor, systemdVersion, err := GetFlavor(p) if err != nil { return err } opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", "Pod shutdown"), unit.NewUnitOption("Unit", "AllowIsolate", "true"), unit.NewUnitOption("Unit", "StopWhenUnneeded", "yes"), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Service", "RemainAfterExit", "yes"), } shutdownVerb := "exit" // systemd <v227 doesn't allow the "exit" verb when running as PID 1, so // use "halt". // If systemdVersion is 0 it means it couldn't be guessed, assume it's new // enough for "systemctl exit". // This can happen, for example, when building rkt with: // // ./configure --with-stage1-flavors=src --with-stage1-systemd-version=master // // The patches for the "exit" verb are backported to the "coreos" flavor, so // don't rely on the systemd version on the "coreos" flavor. if flavor != "coreos" && systemdVersion != 0 && systemdVersion < 227 { shutdownVerb = "halt" } opts = append(opts, unit.NewUnitOption("Service", "ExecStop", fmt.Sprintf("/usr/bin/systemctl --force %s", shutdownVerb))) unitsPath := filepath.Join(common.Stage1RootfsPath(p.Root), UnitsDir) file, err := os.OpenFile(filepath.Join(unitsPath, "shutdown.service"), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create unit file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return errwrap.Wrap(errors.New("failed to write unit file"), err) } return nil }
// preparePod will: // // 1. Invoke 'rkt prepare' to prepare the pod, and get the rkt pod uuid. // 2. Creates the unit file and save it under systemdUnitDir. // // On success, it will return a string that represents name of the unit file // and a boolean that indicates if the unit file needs to be reloaded (whether // the file is already existed). func (r *runtime) preparePod(pod *api.Pod) (string, bool, error) { cmds := []string{"prepare", "--quiet", "--pod-manifest"} // Generate the pod manifest from the pod spec. manifest, err := r.makePodManifest(pod) if err != nil { return "", false, err } manifestFile, err := ioutil.TempFile("", "manifest") if err != nil { return "", false, err } defer func() { manifestFile.Close() if err := os.Remove(manifestFile.Name()); err != nil { glog.Warningf("rkt: Cannot remove temp manifest file %q: %v", manifestFile.Name(), err) } }() data, err := json.Marshal(manifest) if err != nil { return "", false, err } // Since File.Write returns error if the written length is less than len(data), // so check error is enough for us. if _, err := manifestFile.Write(data); err != nil { return "", false, err } cmds = append(cmds, manifestFile.Name()) output, err := r.runCommand(cmds...) if err != nil { return "", false, err } if len(output) != 1 { return "", false, fmt.Errorf("cannot get uuid from 'rkt prepare'") } uuid := output[0] glog.V(4).Infof("'rkt prepare' returns %q.", uuid) p := r.apiPodToruntimePod(uuid, pod) b, err := json.Marshal(p) if err != nil { return "", false, err } runPrepared := fmt.Sprintf("%s run-prepared --private-net=%v %s", r.rktBinAbsPath, !pod.Spec.HostNetwork, uuid) units := []*unit.UnitOption{ newUnitOption(unitKubernetesSection, unitRktID, uuid), newUnitOption(unitKubernetesSection, unitPodName, string(b)), newUnitOption("Service", "ExecStart", runPrepared), } // Save the unit file under systemd's service directory. // TODO(yifan) Garbage collect 'dead' service files. needReload := false unitName := makePodServiceFileName(pod.UID) if _, err := os.Stat(path.Join(systemdServiceDir, unitName)); err == nil { needReload = true } unitFile, err := os.Create(path.Join(systemdServiceDir, unitName)) if err != nil { return "", false, err } defer unitFile.Close() _, err = io.Copy(unitFile, unit.Serialize(units)) if err != nil { return "", false, err } return unitName, needReload, nil }
func (u *UnitFile) Bytes() []byte { b, _ := ioutil.ReadAll(unit.Serialize(u.Options)) return b }
// preparePod will: // // 1. Invoke 'rkt prepare' to prepare the pod, and get the rkt pod uuid. // 2. Create the unit file and save it under systemdUnitDir. // // On success, it will return a string that represents name of the unit file // and the runtime pod. func (r *runtime) preparePod(pod *api.Pod, pullSecrets []api.Secret) (string, *kubecontainer.Pod, error) { // Generate the pod manifest from the pod spec. manifest, err := r.makePodManifest(pod, pullSecrets) if err != nil { return "", nil, err } manifestFile, err := ioutil.TempFile("", fmt.Sprintf("manifest-%s-", pod.Name)) if err != nil { return "", nil, err } defer func() { manifestFile.Close() if err := os.Remove(manifestFile.Name()); err != nil { glog.Warningf("rkt: Cannot remove temp manifest file %q: %v", manifestFile.Name(), err) } }() data, err := json.Marshal(manifest) if err != nil { return "", nil, err } // Since File.Write returns error if the written length is less than len(data), // so check error is enough for us. if _, err := manifestFile.Write(data); err != nil { return "", nil, err } // Run 'rkt prepare' to get the rkt UUID. cmds := []string{"prepare", "--quiet", "--pod-manifest", manifestFile.Name()} if r.config.Stage1Image != "" { cmds = append(cmds, "--stage1-image", r.config.Stage1Image) } output, err := r.runCommand(cmds...) if err != nil { return "", nil, err } if len(output) != 1 { return "", nil, fmt.Errorf("invalid output from 'rkt prepare': %v", output) } uuid := output[0] glog.V(4).Infof("'rkt prepare' returns %q", uuid) // Create systemd service file for the rkt pod. runtimePod := apiPodToRuntimePod(uuid, pod) b, err := json.Marshal(runtimePod) if err != nil { return "", nil, err } var runPrepared string if pod.Spec.HostNetwork { runPrepared = fmt.Sprintf("%s run-prepared --mds-register=false %s", r.rktBinAbsPath, uuid) } else { runPrepared = fmt.Sprintf("%s run-prepared --mds-register=false --private-net %s", r.rktBinAbsPath, uuid) } // TODO handle pod.Spec.HostPID // TODO handle pod.Spec.HostIPC units := []*unit.UnitOption{ newUnitOption(unitKubernetesSection, unitRktID, uuid), newUnitOption(unitKubernetesSection, unitPodName, string(b)), // This makes the service show up for 'systemctl list-units' even if it exits successfully. newUnitOption("Service", "RemainAfterExit", "true"), newUnitOption("Service", "ExecStart", runPrepared), // This enables graceful stop. newUnitOption("Service", "KillMode", "mixed"), } // Check if there's old rkt pod corresponding to the same pod, if so, update the restart count. var restartCount int var needReload bool serviceName := makePodServiceFileName(pod.UID) if _, err := os.Stat(serviceFilePath(serviceName)); err == nil { // Service file already exists, that means the pod is being restarted. needReload = true _, info, err := r.readServiceFile(serviceName) if err != nil { glog.Warningf("rkt: Cannot get old pod's info from service file %q: (%v), will ignore it", serviceName, err) restartCount = 0 } else { restartCount = info.restartCount + 1 } } units = append(units, newUnitOption(unitKubernetesSection, unitRestartCount, strconv.Itoa(restartCount))) glog.V(4).Infof("rkt: Creating service file %q for pod %q", serviceName, kubeletUtil.FormatPodName(pod)) serviceFile, err := os.Create(serviceFilePath(serviceName)) if err != nil { return "", nil, err } defer serviceFile.Close() _, err = io.Copy(serviceFile, unit.Serialize(units)) if err != nil { return "", nil, err } if needReload { if err := r.systemd.Reload(); err != nil { return "", nil, err } } return serviceName, runtimePod, nil }
// preparePod will: // // 1. Invoke 'rkt prepare' to prepare the pod, and get the rkt pod uuid. // 2. Create the unit file and save it under systemdUnitDir. // // On success, it will return a string that represents name of the unit file // and the runtime pod. func (r *Runtime) preparePod(pod *api.Pod, pullSecrets []api.Secret) (string, *kubecontainer.Pod, error) { // Generate the pod manifest from the pod spec. manifest, err := r.makePodManifest(pod, pullSecrets) if err != nil { return "", nil, err } manifestFile, err := ioutil.TempFile("", fmt.Sprintf("manifest-%s-", pod.Name)) if err != nil { return "", nil, err } defer func() { manifestFile.Close() if err := os.Remove(manifestFile.Name()); err != nil { glog.Warningf("rkt: Cannot remove temp manifest file %q: %v", manifestFile.Name(), err) } }() data, err := json.Marshal(manifest) if err != nil { return "", nil, err } glog.V(4).Infof("Generating pod manifest for pod %q: %v", format.Pod(pod), string(data)) // Since File.Write returns error if the written length is less than len(data), // so check error is enough for us. if _, err := manifestFile.Write(data); err != nil { return "", nil, err } // Run 'rkt prepare' to get the rkt UUID. cmds := []string{"prepare", "--quiet", "--pod-manifest", manifestFile.Name()} if r.config.Stage1Image != "" { cmds = append(cmds, "--stage1-image", r.config.Stage1Image) } output, err := r.runCommand(cmds...) if err != nil { return "", nil, err } if len(output) != 1 { return "", nil, fmt.Errorf("invalid output from 'rkt prepare': %v", output) } uuid := output[0] glog.V(4).Infof("'rkt prepare' returns %q", uuid) // Create systemd service file for the rkt pod. runPrepared, err := r.generateRunCommand(pod, uuid) if err != nil { return "", nil, fmt.Errorf("failed to generate 'rkt run-prepared' command: %v", err) } // TODO handle pod.Spec.HostPID // TODO handle pod.Spec.HostIPC units := []*unit.UnitOption{ // This makes the service show up for 'systemctl list-units' even if it exits successfully. newUnitOption("Service", "RemainAfterExit", "true"), newUnitOption("Service", "ExecStart", runPrepared), // This enables graceful stop. newUnitOption("Service", "KillMode", "mixed"), } // Check if there's old rkt pod corresponding to the same pod, if so, update the restart count. var needReload bool serviceName := makePodServiceFileName(pod.UID) if _, err := os.Stat(serviceFilePath(serviceName)); err == nil { // Service file already exists, that means the pod is being restarted. needReload = true } glog.V(4).Infof("rkt: Creating service file %q for pod %q", serviceName, format.Pod(pod)) serviceFile, err := os.Create(serviceFilePath(serviceName)) if err != nil { return "", nil, err } if _, err := io.Copy(serviceFile, unit.Serialize(units)); err != nil { return "", nil, err } serviceFile.Close() if needReload { if err := r.systemd.Reload(); err != nil { return "", nil, err } } return serviceName, apiPodToruntimePod(uuid, pod), nil }
func (uw *UnitWriter) AppUnit( ra *schema.RuntimeApp, binPath, privateUsers string, insecureOptions Stage1InsecureOptions, opts ...*unit.UnitOption, ) { if uw.err != nil { return } flavor, systemdVersion, err := GetFlavor(uw.p) if err != nil { uw.err = errwrap.Wrap(errors.New("unable to determine stage1 flavor"), err) return } app := ra.App appName := ra.Name imgName := uw.p.AppNameToImageName(appName) if len(app.Exec) == 0 { uw.err = fmt.Errorf(`image %q has an empty "exec" (try --exec=BINARY)`, imgName) return } env := app.Environment env.Set("AC_APP_NAME", appName.String()) if uw.p.MetadataServiceURL != "" { env.Set("AC_METADATA_URL", uw.p.MetadataServiceURL) } envFilePath := EnvFilePath(uw.p.Root, appName) uidRange := user.NewBlankUidRange() if err := uidRange.Deserialize([]byte(privateUsers)); err != nil { uw.err = err return } if err := common.WriteEnvFile(env, uidRange, envFilePath); err != nil { uw.err = errwrap.Wrap(errors.New("unable to write environment file for systemd"), err) return } u, g, err := parseUserGroup(uw.p, ra, uidRange) if err != nil { uw.err = err return } if err := generateSysusers(uw.p, ra, u, g, uidRange); err != nil { uw.err = errwrap.Wrap(errors.New("unable to generate sysusers"), err) return } var supplementaryGroups []string for _, g := range app.SupplementaryGIDs { supplementaryGroups = append(supplementaryGroups, strconv.Itoa(g)) } capabilitiesStr, err := getAppCapabilities(app.Isolators) if err != nil { uw.err = err return } execStart := append([]string{binPath}, app.Exec[1:]...) execStartString := quoteExec(execStart) opts = append(opts, []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v", appName, imgName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "Wants", fmt.Sprintf("reaper-%s.service", appName)), unit.NewUnitOption("Service", "Restart", "no"), unit.NewUnitOption("Service", "ExecStart", execStartString), unit.NewUnitOption("Service", "RootDirectory", common.RelAppRootfsPath(appName)), // MountFlags=shared creates a new mount namespace and (as unintuitive // as it might seem) makes sure the mount is slave+shared. unit.NewUnitOption("Service", "MountFlags", "shared"), unit.NewUnitOption("Service", "WorkingDirectory", app.WorkingDirectory), unit.NewUnitOption("Service", "EnvironmentFile", RelEnvFilePath(appName)), unit.NewUnitOption("Service", "User", strconv.Itoa(u)), unit.NewUnitOption("Service", "Group", strconv.Itoa(g)), // This helps working around a race // (https://github.com/systemd/systemd/issues/2913) that causes the // systemd unit name not getting written to the journal if the unit is // short-lived and runs as non-root. unit.NewUnitOption("Service", "SyslogIdentifier", appName.String()), }...) if len(supplementaryGroups) > 0 { opts = appendOptionsList(opts, "Service", "SupplementaryGroups", "", supplementaryGroups) } if supportsNotify(uw.p, appName.String()) { opts = append(opts, unit.NewUnitOption("Service", "Type", "notify")) } if !insecureOptions.DisableCapabilities { opts = append(opts, unit.NewUnitOption("Service", "CapabilityBoundingSet", strings.Join(capabilitiesStr, " "))) } noNewPrivileges := getAppNoNewPrivileges(app.Isolators) // Apply seccomp isolator, if any and not opt-ing out; // see https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter= if !insecureOptions.DisableSeccomp { var forceNoNewPrivileges bool unprivileged := (u != 0) opts, forceNoNewPrivileges, err = getSeccompFilter(opts, uw.p, unprivileged, app.Isolators) if err != nil { uw.err = err return } // Seccomp filters require NoNewPrivileges for unprivileged apps, that may override // manifest annotation. if forceNoNewPrivileges { noNewPrivileges = true } } opts = append(opts, unit.NewUnitOption("Service", "NoNewPrivileges", strconv.FormatBool(noNewPrivileges))) if ra.ReadOnlyRootFS { opts = append(opts, unit.NewUnitOption("Service", "ReadOnlyDirectories", common.RelAppRootfsPath(appName))) } // TODO(tmrts): Extract this logic into a utility function. vols := make(map[types.ACName]types.Volume) for _, v := range uw.p.Manifest.Volumes { vols[v.Name] = v } absRoot, err := filepath.Abs(uw.p.Root) // Absolute path to the pod's rootfs. if err != nil { uw.err = err return } appRootfs := common.AppRootfsPath(absRoot, appName) rwDirs := []string{} imageManifest := uw.p.Images[appName.String()] mounts := GenerateMounts(ra, vols, imageManifest) for _, m := range mounts { mntPath, err := EvaluateSymlinksInsideApp(appRootfs, m.Path) if err != nil { uw.err = err return } if !IsMountReadOnly(vols[m.Volume], app.MountPoints) { rwDirs = append(rwDirs, filepath.Join(common.RelAppRootfsPath(appName), mntPath)) } } if len(rwDirs) > 0 { opts = appendOptionsList(opts, "Service", "ReadWriteDirectories", "", rwDirs) } // Restrict access to sensitive paths (eg. procfs and sysfs entries). if !insecureOptions.DisablePaths { opts = protectKernelTunables(opts, appName, systemdVersion) } // Generate default device policy for the app, as well as the list of allowed devices. // For kvm flavor, devices are VM-specific and restricting them is not strictly needed. if !insecureOptions.DisablePaths && flavor != "kvm" { opts = append(opts, unit.NewUnitOption("Service", "DevicePolicy", "closed")) deviceAllows, err := generateDeviceAllows(common.Stage1RootfsPath(absRoot), appName, app.MountPoints, mounts, vols, uidRange) if err != nil { uw.err = err return } for _, dev := range deviceAllows { opts = append(opts, unit.NewUnitOption("Service", "DeviceAllow", dev)) } } // When an app fails, we shut down the pod opts = append(opts, unit.NewUnitOption("Unit", "OnFailure", "halt.target")) for _, eh := range app.EventHandlers { var typ string switch eh.Name { case "pre-start": typ = "ExecStartPre" case "post-stop": typ = "ExecStopPost" default: uw.err = fmt.Errorf("unrecognized eventHandler: %v", eh.Name) return } exec := quoteExec(eh.Exec) opts = append(opts, unit.NewUnitOption("Service", typ, exec)) } // Some pre-start jobs take a long time, set the timeout to 0 opts = append(opts, unit.NewUnitOption("Service", "TimeoutStartSec", "0")) var saPorts []types.Port for _, p := range app.Ports { if p.SocketActivated { saPorts = append(saPorts, p) } } doWithIsolator := func(isolator string, f func() error) bool { ok, err := cgroup.IsIsolatorSupported(isolator) if err != nil { uw.err = err return true } if !ok { fmt.Fprintf(os.Stderr, "warning: resource/%s isolator set but support disabled in the kernel, skipping\n", isolator) } if err := f(); err != nil { uw.err = err return true } return false } exit := false for _, i := range app.Isolators { if exit { return } switch v := i.Value().(type) { case *types.ResourceMemory: exit = doWithIsolator("memory", func() error { if v.Limit() == nil { return nil } opts = append(opts, unit.NewUnitOption("Service", "MemoryLimit", strconv.Itoa(int(v.Limit().Value())))) return nil }) case *types.ResourceCPU: exit = doWithIsolator("cpu", func() error { if v.Limit() == nil { return nil } if v.Limit().Value() > resource.MaxMilliValue { return fmt.Errorf("cpu limit exceeds the maximum millivalue: %v", v.Limit().String()) } quota := strconv.Itoa(int(v.Limit().MilliValue()/10)) + "%" opts = append(opts, unit.NewUnitOption("Service", "CPUQuota", quota)) return nil }) } } if len(saPorts) > 0 { sockopts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v %s", appName, imgName, "socket-activated ports")), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Socket", "BindIPv6Only", "both"), unit.NewUnitOption("Socket", "Service", ServiceUnitName(appName)), } for _, sap := range saPorts { var proto string switch sap.Protocol { case "tcp": proto = "ListenStream" case "udp": proto = "ListenDatagram" default: uw.err = fmt.Errorf("unrecognized protocol: %v", sap.Protocol) return } // We find the host port for the pod's port and use that in the // socket unit file. // This is so because systemd inside the pod will match based on // the socket port number, and since the socket was created on the // host, it will have the host port number. port := findHostPort(*uw.p.Manifest, sap.Name) if port == 0 { log.Printf("warning: no --port option for socket-activated port %q, assuming port %d as specified in the manifest", sap.Name, sap.Port) port = sap.Port } sockopts = append(sockopts, unit.NewUnitOption("Socket", proto, fmt.Sprintf("%v", port))) } file, err := os.OpenFile(SocketUnitPath(uw.p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { uw.err = errwrap.Wrap(errors.New("failed to create socket file"), err) return } defer file.Close() if _, err = io.Copy(file, unit.Serialize(sockopts)); err != nil { uw.err = errwrap.Wrap(errors.New("failed to write socket unit file"), err) return } if err = os.Symlink(path.Join("..", SocketUnitName(appName)), SocketWantPath(uw.p.Root, appName)); err != nil { uw.err = errwrap.Wrap(errors.New("failed to link socket want"), err) return } opts = append(opts, unit.NewUnitOption("Unit", "Requires", SocketUnitName(appName))) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", InstantiatedPrepareAppUnitName(appName))) opts = append(opts, unit.NewUnitOption("Unit", "After", InstantiatedPrepareAppUnitName(appName))) opts = append(opts, unit.NewUnitOption("Unit", "Requires", "sysusers.service")) opts = append(opts, unit.NewUnitOption("Unit", "After", "sysusers.service")) uw.WriteUnit(ServiceUnitPath(uw.p.Root, appName), "failed to create service unit file", opts...) uw.Activate(ServiceUnitName(appName), ServiceWantPath(uw.p.Root, appName)) }
// appToSystemd transforms the provided RuntimeApp+ImageManifest into systemd units func appToSystemd(p *stage1commontypes.Pod, ra *schema.RuntimeApp, interactive bool, flavor string, privateUsers string) error { app := ra.App appName := ra.Name image, ok := p.Images[appName.String()] if !ok { // This is impossible as we have updated the map in LoadPod(). panic(fmt.Sprintf("No images for app %q", ra.Name.String())) } imgName := image.Name if len(app.Exec) == 0 { return fmt.Errorf(`image %q has an empty "exec" (try --exec=BINARY)`, imgName) } workDir := "/" if app.WorkingDirectory != "" { workDir = app.WorkingDirectory } env := app.Environment env.Set("AC_APP_NAME", appName.String()) if p.MetadataServiceURL != "" { env.Set("AC_METADATA_URL", p.MetadataServiceURL) } if err := writeEnvFile(p, env, appName, privateUsers); err != nil { return errwrap.Wrap(errors.New("unable to write environment file"), err) } // This is a partial implementation for app.User and app.Group: // For now, only numeric ids (and the string "root") are supported. var uid, gid int var err error if app.User == "root" { uid = 0 } else { uid, err = strconv.Atoi(app.User) if err != nil { return fmt.Errorf("non-numerical user id not supported yet") } } if app.Group == "root" { gid = 0 } else { gid, err = strconv.Atoi(app.Group) if err != nil { return fmt.Errorf("non-numerical group id not supported yet") } } execWrap := []string{"/appexec", common.RelAppRootfsPath(appName), workDir, RelEnvFilePath(appName), strconv.Itoa(uid), generateGidArg(gid, app.SupplementaryGIDs)} execStart := quoteExec(append(execWrap, app.Exec...)) opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v", appName, imgName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "Wants", fmt.Sprintf("reaper-%s.service", appName)), unit.NewUnitOption("Service", "Restart", "no"), unit.NewUnitOption("Service", "ExecStart", execStart), unit.NewUnitOption("Service", "User", "0"), unit.NewUnitOption("Service", "Group", "0"), } if interactive { opts = append(opts, unit.NewUnitOption("Service", "StandardInput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "tty")) } else { opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "SyslogIdentifier", filepath.Base(app.Exec[0]))) } // When an app fails, we shut down the pod opts = append(opts, unit.NewUnitOption("Unit", "OnFailure", "halt.target")) for _, eh := range app.EventHandlers { var typ string switch eh.Name { case "pre-start": typ = "ExecStartPre" case "post-stop": typ = "ExecStopPost" default: return fmt.Errorf("unrecognized eventHandler: %v", eh.Name) } exec := quoteExec(append(execWrap, eh.Exec...)) opts = append(opts, unit.NewUnitOption("Service", typ, exec)) } // Some pre-start jobs take a long time, set the timeout to 0 opts = append(opts, unit.NewUnitOption("Service", "TimeoutStartSec", "0")) var saPorts []types.Port for _, p := range app.Ports { if p.SocketActivated { saPorts = append(saPorts, p) } } for _, i := range app.Isolators { switch v := i.Value().(type) { case *types.ResourceMemory: opts, err = cgroup.MaybeAddIsolator(opts, "memory", v.Limit()) if err != nil { return err } case *types.ResourceCPU: opts, err = cgroup.MaybeAddIsolator(opts, "cpu", v.Limit()) if err != nil { return err } } } if len(saPorts) > 0 { sockopts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v %s", appName, imgName, "socket-activated ports")), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Socket", "BindIPv6Only", "both"), unit.NewUnitOption("Socket", "Service", ServiceUnitName(appName)), } for _, sap := range saPorts { var proto string switch sap.Protocol { case "tcp": proto = "ListenStream" case "udp": proto = "ListenDatagram" default: return fmt.Errorf("unrecognized protocol: %v", sap.Protocol) } sockopts = append(sockopts, unit.NewUnitOption("Socket", proto, fmt.Sprintf("%v", sap.Port))) } file, err := os.OpenFile(SocketUnitPath(p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create socket file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(sockopts)); err != nil { return errwrap.Wrap(errors.New("failed to write socket unit file"), err) } if err = os.Symlink(path.Join("..", SocketUnitName(appName)), SocketWantPath(p.Root, appName)); err != nil { return errwrap.Wrap(errors.New("failed to link socket want"), err) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", SocketUnitName(appName))) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", InstantiatedPrepareAppUnitName(appName))) opts = append(opts, unit.NewUnitOption("Unit", "After", InstantiatedPrepareAppUnitName(appName))) file, err := os.OpenFile(ServiceUnitPath(p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create service unit file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return errwrap.Wrap(errors.New("failed to write service unit file"), err) } if err = os.Symlink(path.Join("..", ServiceUnitName(appName)), ServiceWantPath(p.Root, appName)); err != nil { return errwrap.Wrap(errors.New("failed to link service want"), err) } if flavor == "kvm" { // bind mount all shared volumes from /mnt/volumeName (we don't use mechanism for bind-mounting given by nspawn) err := AppToSystemdMountUnits(common.Stage1RootfsPath(p.Root), appName, p.Manifest.Volumes, ra, UnitsDir) if err != nil { return errwrap.Wrap(errors.New("failed to prepare mount units"), err) } } if err = writeAppReaper(p, appName.String()); err != nil { return errwrap.Wrap(fmt.Errorf("failed to write app %q reaper service", appName), err) } return nil }
// appToSystemd transforms the provided RuntimeApp+ImageManifest into systemd units func appToSystemd(p *stage1commontypes.Pod, ra *schema.RuntimeApp, interactive bool, flavor string, privateUsers string) error { app := ra.App appName := ra.Name imgName := p.AppNameToImageName(appName) if len(app.Exec) == 0 { return fmt.Errorf(`image %q has an empty "exec" (try --exec=BINARY)`, imgName) } workDir := "/" if app.WorkingDirectory != "" { workDir = app.WorkingDirectory } env := app.Environment env.Set("AC_APP_NAME", appName.String()) if p.MetadataServiceURL != "" { env.Set("AC_METADATA_URL", p.MetadataServiceURL) } envFilePath := EnvFilePath(p.Root, appName) uidRange := user.NewBlankUidRange() if err := uidRange.Deserialize([]byte(privateUsers)); err != nil { return err } if err := writeEnvFile(p, env, appName, uidRange, '\n', envFilePath); err != nil { return errwrap.Wrap(errors.New("unable to write environment file for systemd"), err) } u, g, err := parseUserGroup(p, ra, uidRange) if err != nil { return err } if err := generateSysusers(p, ra, u, g, uidRange); err != nil { return errwrap.Wrap(errors.New("unable to generate sysusers"), err) } binPath, err := findBinPath(p, appName, *app, workDir, app.Exec[0]) if err != nil { return err } var supplementaryGroups []string for _, g := range app.SupplementaryGIDs { supplementaryGroups = append(supplementaryGroups, strconv.Itoa(g)) } capabilitiesStr, err := getAppCapabilities(app.Isolators) if err != nil { return err } noNewPrivileges := getAppNoNewPrivileges(app.Isolators) execStart := append([]string{binPath}, app.Exec[1:]...) execStartString := quoteExec(execStart) opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v", appName, imgName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "Wants", fmt.Sprintf("reaper-%s.service", appName)), unit.NewUnitOption("Service", "Restart", "no"), unit.NewUnitOption("Service", "ExecStart", execStartString), unit.NewUnitOption("Service", "RootDirectory", common.RelAppRootfsPath(appName)), // MountFlags=shared creates a new mount namespace and (as unintuitive // as it might seem) makes sure the mount is slave+shared. unit.NewUnitOption("Service", "MountFlags", "shared"), unit.NewUnitOption("Service", "WorkingDirectory", workDir), unit.NewUnitOption("Service", "EnvironmentFile", RelEnvFilePath(appName)), unit.NewUnitOption("Service", "User", strconv.Itoa(u)), unit.NewUnitOption("Service", "Group", strconv.Itoa(g)), unit.NewUnitOption("Service", "SupplementaryGroups", strings.Join(supplementaryGroups, " ")), unit.NewUnitOption("Service", "CapabilityBoundingSet", strings.Join(capabilitiesStr, " ")), unit.NewUnitOption("Service", "NoNewPrivileges", strconv.FormatBool(noNewPrivileges)), // This helps working around a race // (https://github.com/systemd/systemd/issues/2913) that causes the // systemd unit name not getting written to the journal if the unit is // short-lived and runs as non-root. unit.NewUnitOption("Service", "SyslogIdentifier", appName.String()), } // Restrict access to sensitive paths (eg. procfs) opts = protectSystemFiles(opts, appName) if ra.ReadOnlyRootFS { opts = append(opts, unit.NewUnitOption("Service", "ReadOnlyDirectories", common.RelAppRootfsPath(appName))) } // TODO(tmrts): Extract this logic into a utility function. vols := make(map[types.ACName]types.Volume) for _, v := range p.Manifest.Volumes { vols[v.Name] = v } absRoot, err := filepath.Abs(p.Root) // Absolute path to the pod's rootfs. if err != nil { return err } appRootfs := common.AppRootfsPath(absRoot, appName) rwDirs := []string{} imageManifest := p.Images[appName.String()] for _, m := range GenerateMounts(ra, vols, imageManifest) { mntPath, err := EvaluateSymlinksInsideApp(appRootfs, m.Path) if err != nil { return err } if !IsMountReadOnly(vols[m.Volume], app.MountPoints) { rwDirs = append(rwDirs, filepath.Join(common.RelAppRootfsPath(appName), mntPath)) } } opts = append(opts, unit.NewUnitOption("Service", "ReadWriteDirectories", strings.Join(rwDirs, " "))) if interactive { opts = append(opts, unit.NewUnitOption("Service", "StandardInput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "tty")) } else { opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "journal+console")) } // When an app fails, we shut down the pod opts = append(opts, unit.NewUnitOption("Unit", "OnFailure", "halt.target")) for _, eh := range app.EventHandlers { var typ string switch eh.Name { case "pre-start": typ = "ExecStartPre" case "post-stop": typ = "ExecStopPost" default: return fmt.Errorf("unrecognized eventHandler: %v", eh.Name) } exec := quoteExec(eh.Exec) opts = append(opts, unit.NewUnitOption("Service", typ, exec)) } // Some pre-start jobs take a long time, set the timeout to 0 opts = append(opts, unit.NewUnitOption("Service", "TimeoutStartSec", "0")) var saPorts []types.Port for _, p := range app.Ports { if p.SocketActivated { saPorts = append(saPorts, p) } } for _, i := range app.Isolators { switch v := i.Value().(type) { case *types.ResourceMemory: opts, err = cgroup.MaybeAddIsolator(opts, "memory", v.Limit()) if err != nil { return err } case *types.ResourceCPU: opts, err = cgroup.MaybeAddIsolator(opts, "cpu", v.Limit()) if err != nil { return err } } } if len(saPorts) > 0 { sockopts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v %s", appName, imgName, "socket-activated ports")), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Socket", "BindIPv6Only", "both"), unit.NewUnitOption("Socket", "Service", ServiceUnitName(appName)), } for _, sap := range saPorts { var proto string switch sap.Protocol { case "tcp": proto = "ListenStream" case "udp": proto = "ListenDatagram" default: return fmt.Errorf("unrecognized protocol: %v", sap.Protocol) } // We find the host port for the pod's port and use that in the // socket unit file. // This is so because systemd inside the pod will match based on // the socket port number, and since the socket was created on the // host, it will have the host port number. port := findHostPort(*p.Manifest, sap.Name) if port == 0 { log.Printf("warning: no --port option for socket-activated port %q, assuming port %d as specified in the manifest", sap.Name, sap.Port) port = sap.Port } sockopts = append(sockopts, unit.NewUnitOption("Socket", proto, fmt.Sprintf("%v", port))) } file, err := os.OpenFile(SocketUnitPath(p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create socket file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(sockopts)); err != nil { return errwrap.Wrap(errors.New("failed to write socket unit file"), err) } if err = os.Symlink(path.Join("..", SocketUnitName(appName)), SocketWantPath(p.Root, appName)); err != nil { return errwrap.Wrap(errors.New("failed to link socket want"), err) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", SocketUnitName(appName))) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", InstantiatedPrepareAppUnitName(appName))) opts = append(opts, unit.NewUnitOption("Unit", "After", InstantiatedPrepareAppUnitName(appName))) opts = append(opts, unit.NewUnitOption("Unit", "Requires", "sysusers.service")) opts = append(opts, unit.NewUnitOption("Unit", "After", "sysusers.service")) file, err := os.OpenFile(ServiceUnitPath(p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create service unit file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return errwrap.Wrap(errors.New("failed to write service unit file"), err) } if err = os.Symlink(path.Join("..", ServiceUnitName(appName)), ServiceWantPath(p.Root, appName)); err != nil { return errwrap.Wrap(errors.New("failed to link service want"), err) } if err = writeAppReaper(p, appName.String(), common.RelAppRootfsPath(appName), binPath); err != nil { return errwrap.Wrap(fmt.Errorf("failed to write app %q reaper service", appName), err) } return nil }
// appToSystemd transforms the provided RuntimeApp+ImageManifest into systemd units func appToSystemd(p *stage1commontypes.Pod, ra *schema.RuntimeApp, interactive bool, flavor string, privateUsers string) error { app := ra.App appName := ra.Name imgName := p.AppNameToImageName(appName) if len(app.Exec) == 0 { return fmt.Errorf(`image %q has an empty "exec" (try --exec=BINARY)`, imgName) } workDir := "/" if app.WorkingDirectory != "" { workDir = app.WorkingDirectory } env := app.Environment env.Set("AC_APP_NAME", appName.String()) if p.MetadataServiceURL != "" { env.Set("AC_METADATA_URL", p.MetadataServiceURL) } if err := writeEnvFile(p, env, appName, privateUsers); err != nil { return errwrap.Wrap(errors.New("unable to write environment file"), err) } var _uid, gid int var err error uidRange := uid.NewBlankUidRange() if err := uidRange.Deserialize([]byte(privateUsers)); err != nil { return errwrap.Wrap(errors.New("unable to deserialize uid range"), err) } if strings.HasPrefix(app.User, "/") { var stat syscall.Stat_t if err = syscall.Lstat(filepath.Join(common.AppRootfsPath(p.Root, appName), app.User), &stat); err != nil { return errwrap.Wrap(fmt.Errorf("unable to get uid from file %q", app.User), err) } uidReal, _, err := uidRange.UnshiftRange(stat.Uid, 0) if err != nil { return errwrap.Wrap(errors.New("unable to determine real uid"), err) } _uid = int(uidReal) } else { _uid, err = strconv.Atoi(app.User) if err != nil { _uid, err = passwd.LookupUidFromFile(app.User, filepath.Join(common.AppRootfsPath(p.Root, appName), "etc/passwd")) if err != nil { return errwrap.Wrap(fmt.Errorf("cannot lookup user %q", app.User), err) } } } if strings.HasPrefix(app.Group, "/") { var stat syscall.Stat_t if err = syscall.Lstat(filepath.Join(common.AppRootfsPath(p.Root, appName), app.Group), &stat); err != nil { return errwrap.Wrap(fmt.Errorf("unable to get gid from file %q", app.Group), err) } _, gidReal, err := uidRange.UnshiftRange(0, stat.Gid) if err != nil { return errwrap.Wrap(errors.New("unable to determine real gid"), err) } gid = int(gidReal) } else { gid, err = strconv.Atoi(app.Group) if err != nil { gid, err = group.LookupGidFromFile(app.Group, filepath.Join(common.AppRootfsPath(p.Root, appName), "etc/group")) if err != nil { return errwrap.Wrap(fmt.Errorf("cannot lookup group %q", app.Group), err) } } } execWrap := []string{"/appexec", common.RelAppRootfsPath(appName), workDir, RelEnvFilePath(appName), strconv.Itoa(_uid), generateGidArg(gid, app.SupplementaryGIDs), "--"} execStart := quoteExec(append(execWrap, app.Exec...)) opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v", appName, imgName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "Wants", fmt.Sprintf("reaper-%s.service", appName)), unit.NewUnitOption("Service", "Restart", "no"), unit.NewUnitOption("Service", "ExecStart", execStart), unit.NewUnitOption("Service", "User", "0"), unit.NewUnitOption("Service", "Group", "0"), } if interactive { opts = append(opts, unit.NewUnitOption("Service", "StandardInput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "tty")) } else { opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "SyslogIdentifier", filepath.Base(app.Exec[0]))) } // When an app fails, we shut down the pod opts = append(opts, unit.NewUnitOption("Unit", "OnFailure", "halt.target")) for _, eh := range app.EventHandlers { var typ string switch eh.Name { case "pre-start": typ = "ExecStartPre" case "post-stop": typ = "ExecStopPost" default: return fmt.Errorf("unrecognized eventHandler: %v", eh.Name) } exec := quoteExec(append(execWrap, eh.Exec...)) opts = append(opts, unit.NewUnitOption("Service", typ, exec)) } // Some pre-start jobs take a long time, set the timeout to 0 opts = append(opts, unit.NewUnitOption("Service", "TimeoutStartSec", "0")) var saPorts []types.Port for _, p := range app.Ports { if p.SocketActivated { saPorts = append(saPorts, p) } } for _, i := range app.Isolators { switch v := i.Value().(type) { case *types.ResourceMemory: opts, err = cgroup.MaybeAddIsolator(opts, "memory", v.Limit()) if err != nil { return err } case *types.ResourceCPU: opts, err = cgroup.MaybeAddIsolator(opts, "cpu", v.Limit()) if err != nil { return err } } } if len(saPorts) > 0 { sockopts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v %s", appName, imgName, "socket-activated ports")), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Socket", "BindIPv6Only", "both"), unit.NewUnitOption("Socket", "Service", ServiceUnitName(appName)), } for _, sap := range saPorts { var proto string switch sap.Protocol { case "tcp": proto = "ListenStream" case "udp": proto = "ListenDatagram" default: return fmt.Errorf("unrecognized protocol: %v", sap.Protocol) } // We find the host port for the pod's port and use that in the // socket unit file. // This is so because systemd inside the pod will match based on // the socket port number, and since the socket was created on the // host, it will have the host port number. port := findHostPort(*p.Manifest, sap.Name) if port == 0 { log.Printf("warning: no --port option for socket-activated port %q, assuming port %d as specified in the manifest", sap.Name, sap.Port) port = sap.Port } sockopts = append(sockopts, unit.NewUnitOption("Socket", proto, fmt.Sprintf("%v", port))) } file, err := os.OpenFile(SocketUnitPath(p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create socket file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(sockopts)); err != nil { return errwrap.Wrap(errors.New("failed to write socket unit file"), err) } if err = os.Symlink(path.Join("..", SocketUnitName(appName)), SocketWantPath(p.Root, appName)); err != nil { return errwrap.Wrap(errors.New("failed to link socket want"), err) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", SocketUnitName(appName))) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", InstantiatedPrepareAppUnitName(appName))) opts = append(opts, unit.NewUnitOption("Unit", "After", InstantiatedPrepareAppUnitName(appName))) file, err := os.OpenFile(ServiceUnitPath(p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return errwrap.Wrap(errors.New("failed to create service unit file"), err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return errwrap.Wrap(errors.New("failed to write service unit file"), err) } if err = os.Symlink(path.Join("..", ServiceUnitName(appName)), ServiceWantPath(p.Root, appName)); err != nil { return errwrap.Wrap(errors.New("failed to link service want"), err) } if flavor == "kvm" { // bind mount all shared volumes from /mnt/volumeName (we don't use mechanism for bind-mounting given by nspawn) err := AppToSystemdMountUnits(common.Stage1RootfsPath(p.Root), appName, p.Manifest.Volumes, ra, UnitsDir) if err != nil { return errwrap.Wrap(errors.New("failed to prepare mount units"), err) } } if err = writeAppReaper(p, appName.String()); err != nil { return errwrap.Wrap(fmt.Errorf("failed to write app %q reaper service", appName), err) } return nil }
func promptSystemd() { if !systemd.IsRunningSystemd() { log.Debugf("not running systemd") return } log.Debug("connecting to systemd") conn, err := sddbus.New() if err != nil { log.Errore(err, "connect to systemd") return } defer conn.Close() log.Debug("connected") props, err := conn.GetUnitProperties("acmetool-redirector.service") if err != nil { log.Errore(err, "systemd GetUnitProperties") return } if props["LoadState"].(string) != "not-found" { log.Info("acmetool-redirector.service unit already installed, skipping") return } r, err := interaction.Auto.Prompt(&interaction.Challenge{ Title: "Install Redirector as systemd Service?", Body: `Would you like acmetool to automatically install the redirector as a systemd service? The service name will be acmetool-redirector.`, ResponseType: interaction.RTYesNo, UniqueID: "acmetool-quickstart-install-redirector-systemd", }) log.Fatale(err, "interaction") if r.Cancelled { return } username, err := determineAppropriateUsername() if err != nil { log.Errore(err, "determine appropriate username") return } f, err := os.OpenFile("/etc/systemd/system/acmetool-redirector.service", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) if err != nil { log.Errore(err, "acmetool-redirector.service unit file already exists?") return } defer f.Close() rdr := sdunit.Serialize([]*sdunit.UnitOption{ sdunit.NewUnitOption("Unit", "Description", "acmetool HTTP redirector"), sdunit.NewUnitOption("Service", "Type", "notify"), sdunit.NewUnitOption("Service", "ExecStart", exepath.Abs+` redirector --service.uid=`+username), sdunit.NewUnitOption("Service", "Restart", "always"), sdunit.NewUnitOption("Service", "RestartSec", "30"), sdunit.NewUnitOption("Install", "WantedBy", "multi-user.target"), }) _, err = io.Copy(f, rdr) if err != nil { log.Errore(err, "cannot write unit file") return } f.Close() err = conn.Reload() // softfail log.Warne(err, "systemctl daemon-reload failed") _, _, err = conn.EnableUnitFiles([]string{"acmetool-redirector.service"}, false, false) log.Errore(err, "failed to enable unit acmetool-redirector.service") _, err = conn.StartUnit("acmetool-redirector.service", "replace", nil) log.Errore(err, "failed to start acmetool-redirector") resultStr := "The acmetool-redirector service was successfully started." if err != nil { resultStr = "The acmetool-redirector service WAS NOT successfully started. You may have a web server listening on port 80. You will need to troubleshoot this yourself." } _, err = interaction.Auto.Prompt(&interaction.Challenge{ Title: "systemd Service Installation Complete", Body: fmt.Sprintf(`acmetool-redirector has been installed as a systemd service. %s`, resultStr), UniqueID: "acmetool-quickstart-complete", }) log.Errore(err, "interaction") }